From be670ec0bbc4cf3f320f292f34efea3838ad71cd Mon Sep 17 00:00:00 2001 From: Robotic-Brain Date: Fri, 17 Jan 2025 05:42:08 +0100 Subject: [PATCH] Fix unchecked calloc bug in network.c At line 636 sport will always be non-NULL because it has already been checked in line 608 using early return on error. Meanwhile result has never been checked against NULL and is subsequently used in line 696. Probable cause of the bug is inadequate copy-and-pasting of code. I assume this changes it to the intended behavior. --- .github/ISSUE_TEMPLATE/bug_report.md | 61 + .github/ISSUE_TEMPLATE/feature_request.md | 17 + .github/workflows/ci.yml | 196 + .gitignore | 5 + AUTHORS | 17 + CMakeLists.txt | 160 + CODE_OF_CONDUCT.md | 54 + CONTRIBUTING.md | 148 + LICENSE | 26 + README.md | 138 + cmake/FindLibatomic.cmake | 22 + cmake/FindLibev.cmake | 38 + cmake/FindLz4.cmake | 21 + cmake/FindPandoc.cmake | 38 + cmake/FindPdflatex.cmake | 58 + cmake/FindRst2man.cmake | 30 + cmake/FindSystemd.cmake | 26 + cmake/FindTHREAD.cmake | 23 + cmake/FindZstd.cmake | 21 + contrib/grafana/README.md | 26 + contrib/grafana/dashboard.json | 1695 ++++++ contrib/shell_comp/pgagroal_comp.bash | 62 + contrib/shell_comp/pgagroal_comp.zsh | 91 + contrib/valgrind/README.md | 22 + contrib/valgrind/pgagroal.supp | 268 + doc/ADMIN.md | 87 + doc/ARCHITECTURE.md | 313 + doc/CLI.md | 549 ++ doc/CMakeLists.txt | 185 + doc/CONFIGURATION.md | 216 + doc/DEVELOPERS.md | 354 ++ doc/DISTRIBUTIONS.md | 59 + doc/FAILOVER.md | 42 + doc/GETTING_STARTED.md | 298 + doc/PERFORMANCE.md | 63 + doc/PIPELINES.md | 112 + doc/RPM.md | 35 + doc/SECURITY.md | 72 + doc/VAULT.md | 91 + doc/etc/pgagroal.conf | 16 + doc/etc/pgagroal.service | 21 + doc/etc/pgagroal.socket | 13 + doc/etc/pgagroal_hba.conf | 4 + doc/etc/pgagroal_vault.conf | 12 + doc/images/perf-extended.png | Bin 0 -> 26392 bytes doc/images/perf-prepared.png | Bin 0 -> 27976 bytes doc/images/perf-readonly.png | Bin 0 -> 26643 bytes doc/images/perf-simple.png | Bin 0 -> 28110 bytes doc/man/pgagroal-admin.1.rst | 76 + doc/man/pgagroal-cli.1.rst | 111 + doc/man/pgagroal-vault.1.rst | 50 + doc/man/pgagroal.1.rst | 64 + doc/man/pgagroal.conf.5.rst | 222 + doc/man/pgagroal_databases.conf.5.rst | 58 + doc/man/pgagroal_hba.conf.5.rst | 58 + doc/man/pgagroal_vault.conf.5.rst | 128 + doc/manual/01-introduction.md | 34 + doc/manual/02-installation.md | 185 + doc/manual/97-acknowledgement.md | 50 + doc/manual/98-licenses.md | 78 + doc/manual/99-references.md | 72 + doc/manual/advanced/00-frontpage.md | 13 + doc/manual/advanced/01-preface.md | 12 + doc/manual/advanced/02-introduction.md | 34 + doc/manual/advanced/03-installation.md | 172 + doc/manual/advanced/04-configuration.md | 193 + doc/manual/advanced/05-prefill.md | 59 + doc/manual/advanced/06-remote_management.md | 66 + doc/manual/advanced/07-split_security.md | 45 + doc/manual/advanced/08-tls.md | 66 + doc/manual/advanced/09-vault.md | 167 + doc/manual/advanced/10-prometheus.md | 31 + doc/manual/advanced/97-acknowledgement.md | 50 + doc/manual/advanced/98-licenses.md | 78 + doc/manual/advanced/99-references.md | 73 + doc/manual/dev-00-head.md | 14 + doc/manual/dev-01-git.md | 101 + doc/manual/dev-02-architecture.md | 315 + doc/manual/dev-03-rpm.md | 37 + doc/manual/user-00-head.md | 14 + doc/manual/user-01-quickstart.md | 233 + doc/manual/user-02-configuration.md | 193 + doc/manual/user-04-tutorials.md | 3 + doc/manual/user-10-cli.md | 582 ++ doc/manual/user-11-prometheus.md | 261 + doc/manual/user-12-vault.md | 93 + doc/tutorial/01_install.md | 235 + doc/tutorial/02_prefill.md | 78 + doc/tutorial/03_remote_management.md | 80 + doc/tutorial/04_prometheus.md | 103 + doc/tutorial/05_split_security.md | 60 + doc/tutorial/06_tls.md | 122 + doc/tutorial/07_vault.md | 152 + pgagroal.spec | 147 + src/CMakeLists.txt | 363 ++ src/admin.c | 937 +++ src/cli.c | 1912 ++++++ src/include/aes.h | 93 + src/include/art.h | 176 + src/include/bzip2_compression.h | 62 + src/include/configuration.h | 500 ++ src/include/connection.h | 156 + src/include/deque.h | 271 + src/include/gzip_compression.h | 62 + src/include/json.h | 204 + src/include/logging.h | 189 + src/include/lz4_compression.h | 64 + src/include/management.h | 483 ++ src/include/memory.h | 70 + src/include/message.h | 428 ++ src/include/network.h | 187 + src/include/pgagroal.h | 592 ++ src/include/pipeline.h | 89 + src/include/pool.h | 170 + src/include/prometheus.h | 357 ++ src/include/remote.h | 51 + src/include/security.h | 148 + src/include/server.h | 102 + src/include/shmem.h | 72 + src/include/status.h | 66 + src/include/tracker.h | 106 + src/include/utils.h | 593 ++ src/include/value.h | 172 + src/include/worker.h | 78 + src/include/zstandard_compression.h | 62 + src/libpgagroal/aes.c | 362 ++ src/libpgagroal/art.c | 1668 +++++ src/libpgagroal/bzip2_compression.c | 124 + src/libpgagroal/configuration.c | 6061 +++++++++++++++++++ src/libpgagroal/connection.c | 911 +++ src/libpgagroal/deque.c | 847 +++ src/libpgagroal/gzip_compression.c | 208 + src/libpgagroal/json.c | 650 ++ src/libpgagroal/logging.c | 531 ++ src/libpgagroal/lz4_compression.c | 103 + src/libpgagroal/management.c | 1576 +++++ src/libpgagroal/memory.c | 138 + src/libpgagroal/message.c | 1518 +++++ src/libpgagroal/network.c | 712 +++ src/libpgagroal/pipeline_perf.c | 296 + src/libpgagroal/pipeline_session.c | 624 ++ src/libpgagroal/pipeline_transaction.c | 677 +++ src/libpgagroal/pool.c | 1597 +++++ src/libpgagroal/prometheus.c | 3009 +++++++++ src/libpgagroal/remote.c | 132 + src/libpgagroal/security.c | 5631 +++++++++++++++++ src/libpgagroal/server.c | 421 ++ src/libpgagroal/shmem.c | 114 + src/libpgagroal/status.c | 295 + src/libpgagroal/tracker.c | 189 + src/libpgagroal/utils.c | 1304 ++++ src/libpgagroal/value.c | 549 ++ src/libpgagroal/worker.c | 332 + src/libpgagroal/zstandard_compression.c | 114 + src/main.c | 2718 +++++++++ src/vault.c | 895 +++ uncrustify.cfg | 3398 +++++++++++ uncrustify.sh | 13 + 158 files changed, 57405 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 AUTHORS create mode 100644 CMakeLists.txt create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmake/FindLibatomic.cmake create mode 100644 cmake/FindLibev.cmake create mode 100644 cmake/FindLz4.cmake create mode 100644 cmake/FindPandoc.cmake create mode 100644 cmake/FindPdflatex.cmake create mode 100644 cmake/FindRst2man.cmake create mode 100644 cmake/FindSystemd.cmake create mode 100644 cmake/FindTHREAD.cmake create mode 100644 cmake/FindZstd.cmake create mode 100644 contrib/grafana/README.md create mode 100644 contrib/grafana/dashboard.json create mode 100644 contrib/shell_comp/pgagroal_comp.bash create mode 100644 contrib/shell_comp/pgagroal_comp.zsh create mode 100644 contrib/valgrind/README.md create mode 100644 contrib/valgrind/pgagroal.supp create mode 100644 doc/ADMIN.md create mode 100644 doc/ARCHITECTURE.md create mode 100644 doc/CLI.md create mode 100644 doc/CMakeLists.txt create mode 100644 doc/CONFIGURATION.md create mode 100644 doc/DEVELOPERS.md create mode 100644 doc/DISTRIBUTIONS.md create mode 100644 doc/FAILOVER.md create mode 100644 doc/GETTING_STARTED.md create mode 100644 doc/PERFORMANCE.md create mode 100644 doc/PIPELINES.md create mode 100644 doc/RPM.md create mode 100644 doc/SECURITY.md create mode 100644 doc/VAULT.md create mode 100644 doc/etc/pgagroal.conf create mode 100644 doc/etc/pgagroal.service create mode 100644 doc/etc/pgagroal.socket create mode 100644 doc/etc/pgagroal_hba.conf create mode 100644 doc/etc/pgagroal_vault.conf create mode 100644 doc/images/perf-extended.png create mode 100644 doc/images/perf-prepared.png create mode 100644 doc/images/perf-readonly.png create mode 100644 doc/images/perf-simple.png create mode 100644 doc/man/pgagroal-admin.1.rst create mode 100644 doc/man/pgagroal-cli.1.rst create mode 100644 doc/man/pgagroal-vault.1.rst create mode 100644 doc/man/pgagroal.1.rst create mode 100644 doc/man/pgagroal.conf.5.rst create mode 100644 doc/man/pgagroal_databases.conf.5.rst create mode 100644 doc/man/pgagroal_hba.conf.5.rst create mode 100644 doc/man/pgagroal_vault.conf.5.rst create mode 100644 doc/manual/01-introduction.md create mode 100644 doc/manual/02-installation.md create mode 100644 doc/manual/97-acknowledgement.md create mode 100644 doc/manual/98-licenses.md create mode 100644 doc/manual/99-references.md create mode 100644 doc/manual/advanced/00-frontpage.md create mode 100644 doc/manual/advanced/01-preface.md create mode 100644 doc/manual/advanced/02-introduction.md create mode 100644 doc/manual/advanced/03-installation.md create mode 100644 doc/manual/advanced/04-configuration.md create mode 100644 doc/manual/advanced/05-prefill.md create mode 100644 doc/manual/advanced/06-remote_management.md create mode 100644 doc/manual/advanced/07-split_security.md create mode 100644 doc/manual/advanced/08-tls.md create mode 100644 doc/manual/advanced/09-vault.md create mode 100644 doc/manual/advanced/10-prometheus.md create mode 100644 doc/manual/advanced/97-acknowledgement.md create mode 100644 doc/manual/advanced/98-licenses.md create mode 100644 doc/manual/advanced/99-references.md create mode 100644 doc/manual/dev-00-head.md create mode 100644 doc/manual/dev-01-git.md create mode 100644 doc/manual/dev-02-architecture.md create mode 100644 doc/manual/dev-03-rpm.md create mode 100644 doc/manual/user-00-head.md create mode 100644 doc/manual/user-01-quickstart.md create mode 100644 doc/manual/user-02-configuration.md create mode 100644 doc/manual/user-04-tutorials.md create mode 100644 doc/manual/user-10-cli.md create mode 100644 doc/manual/user-11-prometheus.md create mode 100644 doc/manual/user-12-vault.md create mode 100644 doc/tutorial/01_install.md create mode 100644 doc/tutorial/02_prefill.md create mode 100644 doc/tutorial/03_remote_management.md create mode 100644 doc/tutorial/04_prometheus.md create mode 100644 doc/tutorial/05_split_security.md create mode 100644 doc/tutorial/06_tls.md create mode 100644 doc/tutorial/07_vault.md create mode 100644 pgagroal.spec create mode 100644 src/CMakeLists.txt create mode 100644 src/admin.c create mode 100644 src/cli.c create mode 100644 src/include/aes.h create mode 100644 src/include/art.h create mode 100644 src/include/bzip2_compression.h create mode 100644 src/include/configuration.h create mode 100644 src/include/connection.h create mode 100644 src/include/deque.h create mode 100644 src/include/gzip_compression.h create mode 100644 src/include/json.h create mode 100644 src/include/logging.h create mode 100644 src/include/lz4_compression.h create mode 100644 src/include/management.h create mode 100644 src/include/memory.h create mode 100644 src/include/message.h create mode 100644 src/include/network.h create mode 100644 src/include/pgagroal.h create mode 100644 src/include/pipeline.h create mode 100644 src/include/pool.h create mode 100644 src/include/prometheus.h create mode 100644 src/include/remote.h create mode 100644 src/include/security.h create mode 100644 src/include/server.h create mode 100644 src/include/shmem.h create mode 100644 src/include/status.h create mode 100644 src/include/tracker.h create mode 100644 src/include/utils.h create mode 100644 src/include/value.h create mode 100644 src/include/worker.h create mode 100644 src/include/zstandard_compression.h create mode 100644 src/libpgagroal/aes.c create mode 100644 src/libpgagroal/art.c create mode 100644 src/libpgagroal/bzip2_compression.c create mode 100644 src/libpgagroal/configuration.c create mode 100644 src/libpgagroal/connection.c create mode 100644 src/libpgagroal/deque.c create mode 100644 src/libpgagroal/gzip_compression.c create mode 100644 src/libpgagroal/json.c create mode 100644 src/libpgagroal/logging.c create mode 100644 src/libpgagroal/lz4_compression.c create mode 100644 src/libpgagroal/management.c create mode 100644 src/libpgagroal/memory.c create mode 100644 src/libpgagroal/message.c create mode 100644 src/libpgagroal/network.c create mode 100644 src/libpgagroal/pipeline_perf.c create mode 100644 src/libpgagroal/pipeline_session.c create mode 100644 src/libpgagroal/pipeline_transaction.c create mode 100644 src/libpgagroal/pool.c create mode 100644 src/libpgagroal/prometheus.c create mode 100644 src/libpgagroal/remote.c create mode 100644 src/libpgagroal/security.c create mode 100644 src/libpgagroal/server.c create mode 100644 src/libpgagroal/shmem.c create mode 100644 src/libpgagroal/status.c create mode 100644 src/libpgagroal/tracker.c create mode 100644 src/libpgagroal/utils.c create mode 100644 src/libpgagroal/value.c create mode 100644 src/libpgagroal/worker.c create mode 100644 src/libpgagroal/zstandard_compression.c create mode 100644 src/main.c create mode 100644 src/vault.c create mode 100644 uncrustify.cfg create mode 100755 uncrustify.sh diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..10f6ea02 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,61 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** + +A clear and concise description of what the bug is. + +**To Reproduce** + +Steps to reproduce the behavior. + +**Version** + +What is the version of pgagroal ? + +**PostgreSQL** + +What is the version of PostgreSQL ? + +**libev** + +What is the version of libev ? + +**OpenSSL** + +What is the version of OpenSSL ? + +**Access method** + +Which security access method is used (trust, password, md5, scram-sha-256) ? + +**OS** + +Which Operating System (OS) is used ? + +**ulimit** + +What is the output from `ulimit -a` ? + +**Configuration** + +Can you provide the configuration pgagroal ? + +* pgagroal.conf +* pgagroal_hba.conf +* pgagroal_databases.conf +* pgagroal_users.conf + +**Debug logs** + +Can you provide any debug logs (`log_level = debug5`) of the issue ? + +**Tip** + +Use \`\`\` before and after the text to keep the output as is. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..88c97df7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: feature +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Additional information** +Any additional information you can provide, like an overall design description diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..a1078612 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,196 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build-linux: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Add PostgreSQL apt repository + run: | + sudo sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' + sudo wget --quiet --output-document /etc/apt/trusted.gpg.d/apt.postgresql.org.asc https://www.postgresql.org/media/keys/ACCC4CF8.asc + - name: Update system + run: sudo apt update + - name: Install libev + run: sudo apt install -y libev4 libev-dev + - name: Install systemd + run: sudo apt install -y libsystemd-dev + - name: Install rst2man + run: sudo apt install -y python3-docutils + - name: Install zstd + run: sudo apt install -y libzstd-dev + - name: Install lz4 + run: sudo apt install -y liblz4-dev + - name: Install bzip2 + run: sudo apt install -y libbz2-dev + - name: Install graphviz + run: sudo apt install graphviz + - name: Install doxygen + run: sudo apt install doxygen + - name: Install clang + run: sudo apt install -y clang + - name: Install PostgreSQL + run: sudo apt install -y postgresql + - name: Start postgres + run: | + version=$(pg_config --version | grep -Eo "[0-9]{1,2}" | head -1) + sudo -u postgres /usr/lib/postgresql/${version}/bin/pg_ctl start -D /etc/postgresql/${version}/main/ + - name: GCC/mkdir + run: mkdir build + working-directory: /home/runner/work/pgagroal/pgagroal/ + - name: GCC/cmake + run: sudo apt install cmake && export CC=/usr/bin/gcc && cmake -DCMAKE_BUILD_TYPE=Debug .. + working-directory: /home/runner/work/pgagroal/pgagroal/build/ + - name: GCC/make + run: make + working-directory: /home/runner/work/pgagroal/pgagroal/build/ + - name: GCC/Run pgagroal & confirm pgagroal is running + run: | + sudo mkdir -p /etc/pgagroal + sudo cp ../../doc/etc/*.conf /etc/pgagroal + ./pgagroal >> /dev/null 2>&1 & + pid=$! + sleep 5 + ./pgagroal-cli ping + working-directory: /home/runner/work/pgagroal/pgagroal/build/src/ + - name: GCC/Stop pgagroal & postgres + run: | + ./pgagroal-cli shutdown + version=$(pg_config --version | grep -Eo "[0-9]{1,2}" | head -1) + sudo -u postgres /usr/lib/postgresql/${version}/bin/pg_ctl stop -D /etc/postgresql/${version}/main/ + working-directory: /home/runner/work/pgagroal/pgagroal/build/src/ + - name: rm -Rf + run: rm -Rf build/ + working-directory: /home/runner/work/pgagroal/pgagroal/ + - name: Start postgres + run: | + version=$(pg_config --version | grep -Eo "[0-9]{1,2}" | head -1) + sudo -u postgres /usr/lib/postgresql/${version}/bin/pg_ctl start -D /etc/postgresql/${version}/main/ + - name: CLANG/mkdir + run: mkdir build + working-directory: /home/runner/work/pgagroal/pgagroal/ + - name: CLANG/cmake + run: export CC=/usr/bin/clang && cmake -DCMAKE_BUILD_TYPE=Debug .. + working-directory: /home/runner/work/pgagroal/pgagroal/build/ + - name: CLANG/make + run: make + working-directory: /home/runner/work/pgagroal/pgagroal/build/ + - name: CLANG/Run pgagroal & confirm pgagroal is running + run: | + sudo mkdir -p /etc/pgagroal + sudo cp ../../doc/etc/*.conf /etc/pgagroal + ./pgagroal >> /dev/null 2>&1 & + pid=$! + sleep 5 + ./pgagroal-cli ping + working-directory: /home/runner/work/pgagroal/pgagroal/build/src/ + - name: CLANG/Stop pgagroal & postgres + run: | + ./pgagroal-cli shutdown + version=$(pg_config --version | grep -Eo "[0-9]{1,2}" | head -1) + sudo -u postgres /usr/lib/postgresql/${version}/bin/pg_ctl stop -D /etc/postgresql/${version}/main/ + working-directory: /home/runner/work/pgagroal/pgagroal/build/src/ + + + + build-macos: + + runs-on: macos-latest + + steps: + - uses: actions/checkout@v3 + - name: Install Homebrew + run: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" + - name: Update system + run: brew update + - name: Install openssl + run: brew install openssl + - name: Install libev + run: brew install libev + - name: Install zstd + run: brew install zstd + - name: Install lz4 + run: brew install lz4 + - name: Install bzip2 + run: brew install bzip2 + - name: Install rst2man + run: brew install docutils + - name: Install graphviz + run: brew install graphviz + - name: Install doxygen + run: brew install doxygen + - name: Install clang + run: brew install llvm + - name: Install PostgreSQL + run: | + latest_pg=$(brew search postgresql | grep postgresql@ | tail -n 1) + brew install ${latest_pg} || true # `|| true` prevents install errors from breaking the run + - name: Start postgres + run: | + installed_pg=$(brew search postgresql | grep postgresql@ | tail -n 1) + brew services start ${installed_pg} + - name: GCC/mkdir + run: mkdir build + working-directory: /Users/runner/work/pgagroal/pgagroal/ + - name: GCC/cmake + run: export CC=/usr/bin/gcc && export OPENSSL_ROOT_DIR=`brew --prefix openssl` && cmake -DCMAKE_BUILD_TYPE=Debug .. + working-directory: /Users/runner/work/pgagroal/pgagroal/build/ + - name: GCC/make + run: make + working-directory: /Users/runner/work/pgagroal/pgagroal/build/ + - name: GCC/Run pgagroal & confirm pgagroal is running + run: | + sudo mkdir -p /etc/pgagroal + sudo cp ../../doc/etc/*.conf /etc/pgagroal + ./pgagroal >> /dev/null 2>&1 & + pid=$! + sleep 5 + ./pgagroal-cli ping + working-directory: /Users/runner/work/pgagroal/pgagroal/build/src/ + - name: GCC/Stop pgagroal & postgres + run: | + ./pgagroal-cli shutdown + installed_pg=$(brew search postgresql | grep postgresql@ | tail -n 1) + brew services stop ${installed_pg} + working-directory: /Users/runner/work/pgagroal/pgagroal/build/src/ + - name: rm -Rf + run: rm -Rf build/ + working-directory: /Users/runner/work/pgagroal/pgagroal/ + - name: Start postgres + run: | + installed_pg=$(brew search postgresql | grep postgresql@ | tail -n 1) + brew services start ${installed_pg} + - name: CLANG/mkdir + run: mkdir build + working-directory: /Users/runner/work/pgagroal/pgagroal/ + - name: CLANG/cmake + run: export CC=/usr/bin/clang && export OPENSSL_ROOT_DIR=`brew --prefix openssl` && cmake -DCMAKE_BUILD_TYPE=Debug .. + working-directory: /Users/runner/work/pgagroal/pgagroal/build/ + - name: CLANG/make + run: make + working-directory: /Users/runner/work/pgagroal/pgagroal/build/ + - name: CLANG/Run pgagroal & confirm pgagroal is running + run: | + sudo mkdir -p /etc/pgagroal + sudo cp ../../doc/etc/*.conf /etc/pgagroal + ./pgagroal >> /dev/null 2>&1 & + pid=$! + sleep 5 + ./pgagroal-cli ping + working-directory: /Users/runner/work/pgagroal/pgagroal/build/src/ + - name: CLANG/Stop pgagroal & postgres + run: | + ./pgagroal-cli shutdown + installed_pg=$(brew search postgresql | grep postgresql@ | tail -n 1) + brew services stop ${installed_pg} + working-directory: /Users/runner/work/pgagroal/pgagroal/build/src/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a462a369 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.bundle/ +.cache/ +_site/ +build/ +vendor/ diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..037b861f --- /dev/null +++ b/AUTHORS @@ -0,0 +1,17 @@ +pgagroal was created by the following authors: + +Jesper Pedersen +David Fetter +Will Leinweber +Junduo Dong +Luca Ferrari +Nikita Bugrovsky +Lawrence Wu +Yongting You <2010youy01@gmail.com> +Ashutosh Sharma +Henrique de Carvalho +Yihe Lu +Eugenio Gigante +Haoran Zhang +Mohanad Khaled +Christian Englert diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..7f7eb03e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,160 @@ +cmake_minimum_required(VERSION 3.14.0) + +set(VERSION_MAJOR "2") +set(VERSION_MINOR "0") +set(VERSION_PATCH "0") +set(VERSION_STRING ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}) + +# +# Avoid source tree pollution +# +set(CMAKE_DISABLE_SOURCE_CHANGES ON) +set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) + +If(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) + message(FATAL_ERROR "In-source builds are not permitted. Make a separate folder for building:\nmkdir build; cd build; cmake ..\nBefore that, remove the files already created:\nrm -rf CMakeCache.txt CMakeFiles") +endif(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) + +project(pgagroal VERSION ${VERSION_STRING} LANGUAGES C) + +set(CPACK_PACKAGE_VERSION_MAJOR ${VERSION_MAJOR}) +set(CPACK_PACKAGE_VERSION_MINOR ${VERSION_MINOR}) +set(CPACK_PACKAGE_VERSION_PATCH ${VERSION_PATCH}) +set(CPACK_SOURCE_GENERATOR "TGZ") +set(CPACK_SOURCE_PACKAGE_FILE_NAME + "${CMAKE_PROJECT_NAME}-${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}") +set(CPACK_SOURCE_IGNORE_FILES + "/build/;/.git/;/.github/;/*.patch;/.bundle/;/_site/;/vendor/;~$;${CPACK_SOURCE_IGNORE_FILES}") +include(CPack) + +set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +message(STATUS "pgagroal ${VERSION_STRING}") + +set(generation TRUE) + +include(CheckCCompilerFlag) +include(CheckCSourceCompiles) +include(CheckLinkerFlag) +include(FindPackageHandleStandardArgs) +include(GNUInstallDirs) + +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build, options are: Debug Release Performance" FORCE) +endif () + +message(STATUS "Build type is ${CMAKE_BUILD_TYPE}") +message(STATUS "System is ${CMAKE_SYSTEM_NAME}") + +set(SUPPORTED_COMPILERS "GNU" "Clang" "AppleClang") + +# Check for a supported compiler +if (NOT CMAKE_C_COMPILER_ID IN_LIST SUPPORTED_COMPILERS) + message(FATAL_ERROR "Unsupported compiler ${CMAKE_C_COMPILER_ID}. Supported compilers are: ${SUPPORTED_COMPILERS}") +endif () + +CHECK_C_COMPILER_FLAG("-std=c17" COMPILER_SUPPORTS_C17) +if(NOT COMPILER_SUPPORTS_C17) + message(FATAL_ERROR "The compiler ${CMAKE_C_COMPILER} has no C17 support. Please use a different C compiler.") +endif() + +find_package(ZLIB) +if (ZLIB_FOUND) + message(STATUS "zlib found") +else () + message(FATAL_ERROR "zlib needed") +endif() + +find_package(BZip2) +if (BZIP2_FOUND) + message(STATUS "bzip2 found") +else () + message(FATAL_ERROR "bzip2 needed") +endif() + +find_package(Zstd) +if (ZSTD_FOUND) + message(STATUS "zstd found") +else () + message(FATAL_ERROR "zstd needed") +endif() + +find_package(Lz4) +if (LZ4_FOUND) + message(STATUS "lz4 found") +else () + message(FATAL_ERROR "lz4 needed") +endif() + +find_package(Libev 4.11) +if (LIBEV_FOUND) + message(STATUS "libev found") +else () + message(FATAL_ERROR "libev needed") +endif() + +find_package(OpenSSL) +if (OPENSSL_FOUND) + message(STATUS "OpenSSL found") +else () + message(FATAL_ERROR "OpenSSL needed") +endif() + +find_package(Rst2man) +if (RST2MAN_FOUND) + message(STATUS "rst2man found") +else () + message(FATAL_ERROR "rst2man needed") +endif() + +find_package(THREAD) +if (THREAD_FOUND) + message(STATUS "pthread found") +else () + message(FATAL_ERROR "pthread needed") +endif() + +find_package(Pandoc) +if (PANDOC_FOUND) + message(STATUS "pandoc found") +else () + set(generation FALSE) + message(STATUS "pandoc needed. The generation process will be skipped.") +endif() + +find_package(Pdflatex) +if (PDFLATEX_FOUND) + message(STATUS "pdflatex found") +else () + set(generation FALSE) + message(STATUS "pdflatex needed. The generation process will be skipped.") +endif() + +find_package(Doxygen + REQUIRED dot) + +if (DOXYGEN_FOUND) + message(status "Doxygen found: ${DOXYGEN_EXECUTABLE}") +endif() + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + find_package(Libatomic) + if (LIBATOMIC_FOUND) + message(STATUS "libatomic found") + else () + message(FATAL_ERROR "libatomic needed") + endif() + + find_package(Systemd) + if (SYSTEMD_FOUND) + message(STATUS "systemd found") + else () + message(FATAL_ERROR "systemd needed") + endif() +endif() + +file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/src/") + +add_subdirectory(doc) +add_subdirectory(src) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..5d1b5d23 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,54 @@ +## pgagroal Community Code of Conduct + +### Contributor Code of Conduct + +As contributors and maintainers of the projects under the [pgagroal](https://github.com/agroal/pgagroal) repository, +and in the interest of fostering an open and welcoming community, we pledge to +respect all people who contribute through reporting issues, posting feature +requests, updating documentation, submitting pull requests or patches, and other +activities to any of the projects under the pgagroal umbrella. + +We are committed to making participation in these projects a harassment-free experience for +everyone, regardless of level of experience, gender, gender identity and expression, +sexual orientation, disability, personal appearance, body size, race, ethnicity, age, +religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery. +* Personal attacks. +* Trolling or insulting/derogatory comments. +* Public or private harassment. +* Publishing other's private information, such as physical or electronic addresses, without explicit permission. +* Other unethical or unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are not +aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers +commit themselves to fairly and consistently applying these principles to every aspect +of managing their project. Project maintainers who do not follow or enforce the Code of +Conduct may be permanently removed from the project team or teams if they are in multiple +projects. + +This code of conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a maintainer of the project, Jesper Pedersen . + +### pgagroal Events Code of Conduct + +pgagroal events are working conferences intended for professional networking and collaboration in the +pgagroal community. Attendees are expected to behave according to professional standards and in accordance +with their employer's policies on appropriate workplace behavior. + +While at pgagroal events or related social networking opportunities, attendees should not engage in +discriminatory or offensive speech or actions regarding gender, sexuality, race, or religion. Speakers should +be especially aware of these concerns. + +The maintainers of the pgagroal project do not condone any statements by speakers contrary to these standards. +The maintainers of the pgagroal project reserves the right to deny entrance and/or eject from an event +(without refund) any individual found to be engaging in discriminatory or offensive speech or actions. + +Please bring any concerns to the immediate attention of the pgagroal maintainers. + +This Code of Conduct was adapted from the [CNCF Community Code of Conduct v1.0](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..22e74785 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,148 @@ +# Contributing guide + +**Want to contribute? Great!** + +All contributions are more than welcome ! This includes bug reports, bug fixes, enhancements, features, questions, ideas, +and documentation. + +This document will hopefully help you contribute to pgagroal. + +* [Legal](#legal) +* [Reporting an issue](#reporting-an-issue) +* [Setup your build environment](#setup-your-build-environment) +* [Building the master branch](#building-the-master-branch) +* [Before you contribute](#before-you-contribute) +* [Code reviews](#code-reviews) +* [Coding Guidelines](#coding-guidelines) +* [Discuss a Feature](#discuss-a-feature) +* [Development](#development) +* [Code Style](#code-style) + +## Legal + +All contributions to pgagroal are licensed under the [The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause). + +## Reporting an issue + +This project uses GitHub issues to manage the issues. Open an issue directly in GitHub. + +If you believe you found a bug, and it's likely possible, please indicate a way to reproduce it, what you are seeing and what you would expect to see. +Don't forget to indicate your pgagroal version. + +## Setup your build environment + +You can use the follow command, if you are using a [Fedora](https://getfedora.org/) based platform: + +``` +dnf install git gcc cmake make libev libev-devel openssl openssl-devel systemd systemd-devel python3-docutils +``` + +in order to get the necessary dependencies. + +## Building the master branch + +To build the `master` branch: + +``` +git clone https://github.com/agroal/pgagroal.git +cd pgagroal +mkdir build +cd build +cmake -DCMAKE_BUILD_TYPE=Debug .. +make +cd src +cp ../../doc/etc/*.conf . +./pgagroal -c pgagroal.conf -a pgagroal_hba.conf +``` + +and you will have a running instance. + +## Before you contribute + +To contribute, use GitHub Pull Requests, from your **own** fork. + +Also, make sure you have set up your Git authorship correctly: + +``` +git config --global user.name "Your Full Name" +git config --global user.email your.email@example.com +``` + +We use this information to acknowledge your contributions in release announcements. + +## Code reviews + +GitHub pull requests can be reviewed by all such that input can be given to the author(s). + +See [GitHub Pull Request Review Process](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/about-pull-request-reviews) +for more information. + +## Coding Guidelines + +* Discuss the feature +* Do development + + Follow the code style +* Commits should be atomic and semantic. Therefore, squash your pull request before submission and keep it rebased until merged + + If your feature has independent parts submit those as separate pull requests + +## Discuss a Feature + +You can discuss bug reports, enhancements and features in our [forum](https://github.com/agroal/pgagroal/discussions). + +Once there is an agreement on the development plan you can open an issue that will used for reference in the pull request. + +## Development + +You can follow this workflow for your development. + +Add your repository + +``` +git clone git@github.com:yourname/pgagroal.git +cd pgagroal +git remote add upstream https://github.com/agroal/pgagroal.git +``` + +Create a work branch + +``` +git checkout -b mywork master +``` + +During development + +``` +git commit -a -m "[#issue] My feature" +git push -f origin mywork +``` + +If you have more commits then squash them + +``` +git rebase -i HEAD~2 +git push -f origin mywork +``` + +If the `master` branch changes then + +``` +git fetch upstream +git rebase -i upstream/master +git push -f origin mywork +``` + +as all pull requests should be squashed and rebased. + +In your first pull request you need to add yourself to the `AUTHORS` file. + +## Code Style + +Please, follow the coding style of the project. + +You can use the [uncrustify](http://uncrustify.sourceforge.net/) tool to help with the formatting, by running + +``` +./uncrustify.sh +``` + +and verify the changes. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..de90d473 --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ + +Copyright (C) 2025 The pgagroal community + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or other +materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may +be used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..68661b96 --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +# pgagroal + +[**pgagroal**](https://github.com/agroal/pgagroal) is a high-performance protocol-native connection pool for [PostgreSQL](https://www.postgresql.org). + +Pronounced: p-g-a-gro-al, named after [A](https://www.visitportugal.com/en/content/praia-fluvial-do-agroal)[gro](https://www.google.com/maps/place/Agroal,+Portugal/@39.6775431,-8.4486056,14z/)[al](https://www.infatima.pt/en/nearby/sun-sea/fluvial-beaches/agroal/) in Portugal. + +## Features + +* High performance +* Connection pool +* Limit connections for users and databases +* Prefill support +* Remove idle connections +* Perform connection validation +* Enable / disable database access +* Graceful / fast shutdown +* Prometheus support +* Grafana 8 dashboard +* Remote management +* Authentication query support +* Failover support +* Transport Layer Security (TLS) v1.2+ support +* Daemon mode +* User vault + +See [Getting Started](./doc/GETTING_STARTED.md) on how to get started with [**pgagroal**](https://github.com/agroal/pgagroal). + +See [Configuration](./doc/CONFIGURATION.md) on how to configure [**pgagroal**](https://github.com/agroal/pgagroal). + +See [Performance](./doc/PERFORMANCE.md) for a performance run. + +## Overview + +[**pgagroal**](https://github.com/agroal/pgagroal) makes use of + +* Process model +* Shared memory model across processes +* [libev](http://software.schmorp.de/pkg/libev.html) for fast network interactions +* [Atomic operations](https://en.cppreference.com/w/c/atomic) are used to keep track of state +* The [PostgreSQL](https://www.postgresql.org) native protocol + [v3](https://www.postgresql.org/docs/11/protocol-message-formats.html) for its communication + +[**pgagroal**](https://github.com/agroal/pgagroal) will work with any [PostgreSQL](https://www.postgresql.org) compliant driver, for example +[pgjdbc](https://jdbc.postgresql.org/), [Npgsql](https://www.npgsql.org/) and [pq](https://github.com/lib/pq). + +See [Architecture](./doc/ARCHITECTURE.md) for the architecture of [**pgagroal**](https://github.com/agroal/pgagroal). + +## Tested platforms + +* [Fedora](https://getfedora.org/) 38+ +* [RHEL 9.x](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9) +* [Rocky Linux 9.x](https://rockylinux.org/) + +* [FreeBSD](https://www.freebsd.org/) +* [OpenBSD](http://www.openbsd.org/) + + +## Compiling from sources + +[**pgagroal**](https://github.com/agroal/pgagroal) can be compiled from sources, +after having installed all the required dependencies: + +* [gcc 8+](https://gcc.gnu.org) (C17) or [clang 8+](https://clang.llvm.org/) +* [cmake](https://cmake.org) +* [GNU make](https://www.gnu.org/software/make/) or BSD `make` +* [libev](http://software.schmorp.de/pkg/libev.html) +* [OpenSSL](http://www.openssl.org/) +* [rst2man](https://docutils.sourceforge.io/) +* [libatomic](https://gcc.gnu.org/wiki/Atomic) +* [Doxygen](https://doxygen.nl/index.html) +* [pdflatex](https://tug.org/texlive/) +* [systemd](https://www.freedesktop.org/wiki/Software/systemd/) (on Linux systems) +* [zlib](https://zlib.net) +* [zstd](http://www.zstd.net) +* [lz4](https://lz4.github.io/lz4/) +* [bzip2](http://sourceware.org/bzip2/) + + +See the [documentation about installing the required dependencies](doc/DISTRIBUTIONS.md). + + +### Release build + +The following commands will install [**pgagroal**](https://github.com/agroal/pgagroal) in the `/usr/local` hierarchy +and run the default configuration. + +```sh +git clone https://github.com/agroal/pgagroal.git +cd pgagroal +mkdir build +cd build +cmake -DCMAKE_INSTALL_PREFIX=/usr/local .. +make +sudo make install +/usr/local/bin/pgagroal -c /usr/local/etc/pgagroal/pgagroal.conf -a /usr/local/etc/pgagroal/pgagroal_hba.conf +``` + +See [RPM](./doc/RPM.md) for how to build a RPM of [**pgagroal**](https://github.com/agroal/pgagroal). + +### Debug build + +The following commands will create a `DEBUG` version of [**pgagroal**](https://github.com/agroal/pgagroal). + +```sh +git clone https://github.com/agroal/pgagroal.git +cd pgagroal +mkdir build +cd build +cmake -DCMAKE_BUILD_TYPE=Debug .. +make +cd src +cp ../../doc/etc/*.conf . +./pgagroal -c pgagroal.conf -a pgagroal_hba.conf +``` + +Remember to set the `log_level` configuration option to `debug5`. + +## Contributing + +Contributions to [**pgagroal**](https://github.com/agroal/pgagroal) are managed on [GitHub.com](https://github.com/agroal/pgagroal/) + +* [Ask a question](https://github.com/agroal/pgagroal/discussions) +* [Raise an issue](https://github.com/agroal/pgagroal/issues) +* [Feature request](https://github.com/agroal/pgagroal/issues) +* [Code submission](https://github.com/agroal/pgagroal/pulls) + +Contributions are most welcome ! + +Please, consult our [Code of Conduct](./CODE_OF_CONDUCT.md) policies for interacting in our +community. + +Consider giving the project a [star](https://github.com/agroal/pgagroal/stargazers) on +[GitHub](https://github.com/agroal/pgagroal/) if you find it useful. And, feel free to follow +the project on [Twitter](https://twitter.com/pgagroal/) as well. + +## License + +[BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) diff --git a/cmake/FindLibatomic.cmake b/cmake/FindLibatomic.cmake new file mode 100644 index 00000000..f2735169 --- /dev/null +++ b/cmake/FindLibatomic.cmake @@ -0,0 +1,22 @@ +# - Try to find libatomic +# Once done this will define +# LIBATOMIC_FOUND - System has libatomic +# LIBATOMIC_LIBRARY - The library needed to use libatomic + +FIND_LIBRARY(LIBATOMIC_LIBRARY NAMES atomic atomic.so.1 libatomic.so.1 + HINTS + /usr/local/lib64 + /usr/local/lib + /opt/local/lib64 + /opt/local/lib + /usr/lib64 + /usr/lib + /lib64 + /lib +) + +IF (LIBATOMIC_LIBRARY) + SET(LIBATOMIC_FOUND TRUE) +ELSE () + SET(LIBATOMIC_FOUND FALSE) +ENDIF () diff --git a/cmake/FindLibev.cmake b/cmake/FindLibev.cmake new file mode 100644 index 00000000..71e45082 --- /dev/null +++ b/cmake/FindLibev.cmake @@ -0,0 +1,38 @@ +# - Try to find libev +# Once done this will define +# LIBEV_FOUND - System has libev +# LIBEV_INCLUDE_DIRS - The libev include directories +# LIBEV_LIBRARIES - The libraries needed to use libev + +find_path(LIBEV_INCLUDE_DIR + NAMES ev.h +) +find_library(LIBEV_LIBRARY + NAMES ev +) + +if(LIBEV_INCLUDE_DIR) + file(STRINGS "${LIBEV_INCLUDE_DIR}/ev.h" + LIBEV_VERSION_MAJOR REGEX "^#define[ \t]+EV_VERSION_MAJOR[ \t]+[0-9]+") + file(STRINGS "${LIBEV_INCLUDE_DIR}/ev.h" + LIBEV_VERSION_MINOR REGEX "^#define[ \t]+EV_VERSION_MINOR[ \t]+[0-9]+") + string(REGEX REPLACE "[^0-9]+" "" LIBEV_VERSION_MAJOR "${LIBEV_VERSION_MAJOR}") + string(REGEX REPLACE "[^0-9]+" "" LIBEV_VERSION_MINOR "${LIBEV_VERSION_MINOR}") + set(LIBEV_VERSION "${LIBEV_VERSION_MAJOR}.${LIBEV_VERSION_MINOR}") + unset(LIBEV_VERSION_MINOR) + unset(LIBEV_VERSION_MAJOR) +endif() + +include(FindPackageHandleStandardArgs) +# handle the QUIETLY and REQUIRED arguments and set LIBEV_FOUND to TRUE +# if all listed variables are TRUE and the requested version matches. +find_package_handle_standard_args(Libev REQUIRED_VARS + LIBEV_LIBRARY LIBEV_INCLUDE_DIR + VERSION_VAR LIBEV_VERSION) + +if(LIBEV_FOUND) + set(LIBEV_LIBRARIES ${LIBEV_LIBRARY}) + set(LIBEV_INCLUDE_DIRS ${LIBEV_INCLUDE_DIR}) +endif() + +mark_as_advanced(LIBEV_INCLUDE_DIR LIBEV_LIBRARY) diff --git a/cmake/FindLz4.cmake b/cmake/FindLz4.cmake new file mode 100644 index 00000000..4b9e27f8 --- /dev/null +++ b/cmake/FindLz4.cmake @@ -0,0 +1,21 @@ +# +# LZ4 Support +# + +find_path(LZ4_INCLUDE_DIR + NAMES lz4.h +) +find_library(LZ4_LIBRARY + NAMES lz4 +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Lz4 DEFAULT_MSG + LZ4_LIBRARY LZ4_INCLUDE_DIR) + +if(LZ4_FOUND) + set(LZ4_LIBRARIES ${LZ4_LIBRARY}) + set(LZ4_INCLUDE_DIRS ${LZ4_INCLUDE_DIR}) +endif() + +mark_as_advanced(LZ4_INCLUDE_DIR LZ4_LIBRARY) diff --git a/cmake/FindPandoc.cmake b/cmake/FindPandoc.cmake new file mode 100644 index 00000000..b8707172 --- /dev/null +++ b/cmake/FindPandoc.cmake @@ -0,0 +1,38 @@ +# +# pandoc Support +# + +find_program(PANDOC_EXECUTABLE + NAMES pandoc +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Pandoc DEFAULT_MSG + PANDOC_EXECUTABLE) + +if(PANDOC_FOUND) + # check for eisvogel.latex + execute_process( + COMMAND ${PANDOC_EXECUTABLE} --version + OUTPUT_VARIABLE pandoc_output + RESULT_VARIABLE result + ) + string(REGEX MATCH "User data directory: ([^\n\r]*)" _ ${pandoc_output}) + + if (NOT CMAKE_MATCH_COUNT) + string(REGEX MATCH "Default user data directory: ([^ ]+)" _ ${pandoc_output}) + set(EISVOGEL_TEMPLATE_PATH "${CMAKE_MATCH_1}/templates/eisvogel.latex") + else() + set(EISVOGEL_TEMPLATE_PATH "${CMAKE_MATCH_1}/templates/eisvogel.latex") + endif() + + if(EXISTS "${EISVOGEL_TEMPLATE_PATH}") + message(STATUS "Found eisvogel template at ${EISVOGEL_TEMPLATE_PATH}") + else() + message(STATUS "eisvogel template not found at ${EISVOGEL_TEMPLATE_PATH}. The generation process will be skipped.") + set(generation FALSE) + endif() + +endif() + +mark_as_advanced(PANDOC_EXECUTABLE) \ No newline at end of file diff --git a/cmake/FindPdflatex.cmake b/cmake/FindPdflatex.cmake new file mode 100644 index 00000000..4ecdd54d --- /dev/null +++ b/cmake/FindPdflatex.cmake @@ -0,0 +1,58 @@ +# +# pdflatex Support +# + +find_program(PDFLATEX_EXECUTABLE + NAMES pdflatex +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Pdflatex DEFAULT_MSG + PDFLATEX_EXECUTABLE) + +if(PDFLATEX_FOUND) + + # check for kpsewhich + find_program(KPSEWHICH kpsewhich) + if(NOT KPSEWHICH) + set(generation FALSE) + message(STATUS "kpsewhich not found. The generation process will be skipped.") + return() + endif() + + # check for packages + set(check_next TRUE) + macro(check_latex_package PACKAGE_NAME) + if(check_next) + execute_process( + COMMAND ${KPSEWHICH} ${PACKAGE_NAME} + OUTPUT_VARIABLE PATH + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(NOT PATH) + set(generation FALSE) + message(STATUS "${PACKAGE_NAME} not found. The generation process will be skipped.") + set(check_next FALSE) # Disable further checks + else() + message(STATUS "${PACKAGE_NAME} found at: ${PATH}") + endif() + endif() + endmacro() + + # Use the macro to check for packages + check_latex_package("footnote.sty") + check_latex_package("footnotebackref.sty") + check_latex_package("pagecolor.sty") + check_latex_package("hardwrap.sty") + check_latex_package("mdframed.sty") + check_latex_package("sourcesanspro.sty") + check_latex_package("ly1enc.def") + check_latex_package("sourcecodepro.sty") + check_latex_package("titling.sty") + check_latex_package("csquotes.sty") + check_latex_package("zref-abspage.sty") + check_latex_package("needspace.sty") + +endif() + +mark_as_advanced(PDFLATEX_EXECUTABLE) \ No newline at end of file diff --git a/cmake/FindRst2man.cmake b/cmake/FindRst2man.cmake new file mode 100644 index 00000000..1ec29de7 --- /dev/null +++ b/cmake/FindRst2man.cmake @@ -0,0 +1,30 @@ +# RST2MAN_FOUND - true if the program was found +# RST2MAN_VERSION - version of rst2man +# RST2MAN_EXECUTABLE - path to the rst2man program + +find_program(RST2MAN_EXECUTABLE + NAMES rst2man rst2man.py rst2man-3 rst2man-3.py + DOC "The Python Docutils generator of Unix Manpages from reStructuredText" +) + +if (RST2MAN_EXECUTABLE) + # Get the version string + execute_process( + COMMAND ${RST2MAN_EXECUTABLE} --version + OUTPUT_VARIABLE rst2man_version_str + ) + # Expected format: rst2man (Docutils 0.13.1 [release], Python 2.7.15, on linux2) + string(REGEX REPLACE "^rst2man[\t ]+\\(Docutils[\t ]+([^\t ]*).*" "\\1" + RST2MAN_VERSION "${rst2man_version_str}") + unset(rst2man_version_str) +endif() + +# handle the QUIETLY and REQUIRED arguments and set RST2MAN_FOUND to TRUE +# if all listed variables are set +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Rst2man + REQUIRED_VARS RST2MAN_EXECUTABLE + VERSION_VAR RST2MAN_VERSION +) + +mark_as_advanced(RST2MAN_EXECUTABLE RST2MAN_VERSION) diff --git a/cmake/FindSystemd.cmake b/cmake/FindSystemd.cmake new file mode 100644 index 00000000..2aef5dbe --- /dev/null +++ b/cmake/FindSystemd.cmake @@ -0,0 +1,26 @@ +# - Try to find systemd +# Once done this will define +# SYSTEMD_FOUND - System has systemd +# SYSTEMD_INCLUDE_DIRS - The systemd include directories +# SYSTEMD_LIBRARIES - The libraries needed to use systemd + +find_path(SYSTEMD_INCLUDE_DIR + NAMES systemd/sd-daemon.h +) +find_library(SYSTEMD_LIBRARY + NAMES systemd +) + +include(FindPackageHandleStandardArgs) +# handle the QUIETLY and REQUIRED arguments and set SYSTEMD_FOUND to TRUE +# if all listed variables are TRUE and the requested version matches. +find_package_handle_standard_args(Systemd REQUIRED_VARS + SYSTEMD_LIBRARY SYSTEMD_INCLUDE_DIR + VERSION_VAR SYSTEMD_VERSION) + +if(SYSTEMD_FOUND) + set(SYSTEMD_LIBRARIES ${SYSTEMD_LIBRARY}) + set(SYSTEMD_INCLUDE_DIRS ${SYSTEMD_INCLUDE_DIR}) +endif() + +mark_as_advanced(SYSTEMD_INCLUDE_DIR SYSTEMD_LIBRARY) diff --git a/cmake/FindTHREAD.cmake b/cmake/FindTHREAD.cmake new file mode 100644 index 00000000..17c2acc8 --- /dev/null +++ b/cmake/FindTHREAD.cmake @@ -0,0 +1,23 @@ +# +# pthread support +# + +find_path(THREAD_INCLUDE_DIR + NAMES + pthread.h +) +find_library(THREAD_LIBRARY + NAMES + pthread +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(THREAD DEFAULT_MSG + THREAD_LIBRARY THREAD_INCLUDE_DIR) + +if(THREAD_FOUND) + set(THREAD_LIBRARIES ${THREAD_LIBRARY}) + set(THREAD_INCLUDE_DIRS ${THREAD_INCLUDE_DIR}) +endif() + +mark_as_advanced(THREAD_INCLUDE_DIR THREAD_LIBRARY) diff --git a/cmake/FindZstd.cmake b/cmake/FindZstd.cmake new file mode 100644 index 00000000..0277a160 --- /dev/null +++ b/cmake/FindZstd.cmake @@ -0,0 +1,21 @@ +# +# ZSTD support +# + +find_path(ZSTD_INCLUDE_DIR + NAMES zstd.h +) +find_library(ZSTD_LIBRARY + NAMES zstd +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Zstd REQUIRED_VARS + ZSTD_LIBRARY ZSTD_INCLUDE_DIR) + +if(ZSTD_FOUND) + set(ZSTD_LIBRARIES ${ZSTD_LIBRARY}) + set(ZSTD_INCLUDE_DIRS ${ZSTD_INCLUDE_DIR}) +endif() + +mark_as_advanced(ZSTD_INCLUDE_DIR ZSTD_LIBRARY) diff --git a/contrib/grafana/README.md b/contrib/grafana/README.md new file mode 100644 index 00000000..de8668d0 --- /dev/null +++ b/contrib/grafana/README.md @@ -0,0 +1,26 @@ +# Grafana dashboard for pgagroal + +## Getting Started + +### Step1: Configure the Prometheus + +1. Open the prometheus metric port like `8000` in `pgagroal.conf`. + +2. Add the pgagroal instance in Prometheus configuration file `prometheus.yml` like follows: + ``` + ... + scrape_configs: + - job_name: 'pgagroal' + static_configs: + - targets: ['localhost:8000'] + ... + ``` + +### Step2: Start the Grafana + +1. Integrate the Prometheus instance as a data source into Grafana. +2. Open Grafana web page, click + -> Import. + +3. Upload JSON file `contrib/grafana/dashboard.json` with Upload JSON file, then change the options if necessary. + +4. Click Import. Let's start using it! \ No newline at end of file diff --git a/contrib/grafana/dashboard.json b/contrib/grafana/dashboard.json new file mode 100644 index 00000000..18764a56 --- /dev/null +++ b/contrib/grafana/dashboard.json @@ -0,0 +1,1695 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 1, + "links": [], + "panels": [ + { + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 28, + "title": "Overview", + "type": "row" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "0": { + "index": 0, + "text": "Performance" + }, + "1": { + "index": 1, + "text": "Session" + }, + "2": { + "index": 2, + "text": "Transaction" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 1 + }, + "id": 54, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "8.0.2", + "targets": [ + { + "exemplar": true, + "expr": "pgagroal_pipeline_mode", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Pipeline mode", + "type": "stat" + }, + { + "datasource": null, + "description": "Active connections", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 0.8 + }, + { + "color": "dark-red", + "value": 0.9 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 5, + "x": 6, + "y": 1 + }, + "id": 30, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "text": {} + }, + "pluginVersion": "8.0.2", + "targets": [ + { + "exemplar": true, + "expr": "pgagroal_active_connections/pgagroal_max_connections", + "interval": "", + "legendFormat": "", + "refId": "active connections" + } + ], + "title": "Active connections", + "type": "gauge" + }, + { + "datasource": null, + "description": "Session time", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 13, + "x": 11, + "y": 1 + }, + "id": 32, + "options": { + "displayMode": "gradient", + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": false, + "text": {} + }, + "pluginVersion": "8.0.2", + "targets": [ + { + "exemplar": true, + "expr": "pgagroal_session_time_seconds_bucket", + "format": "heatmap", + "interval": "", + "legendFormat": "{{le}}", + "refId": "pgagroal_session_time_seconds_bucket" + } + ], + "title": "Session time", + "type": "bargauge" + }, + { + "datasource": null, + "description": "pgagroal state", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "0": { + "color": "red", + "index": 1, + "text": "DOWN" + }, + "1": { + "color": "green", + "index": 0, + "text": "UP" + }, + "2": { + "color": "yellow", + "index": 2, + "text": "GRACEFUL" + } + }, + "type": "value" + } + ], + "max": 1, + "min": 0, + "noValue": "UNKNOWN", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 0, + "y": 5 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "8.0.2", + "targets": [ + { + "exemplar": true, + "expr": "pgagroal_state", + "interval": "", + "legendFormat": "state", + "refId": "pgagroal_state" + } + ], + "title": "State", + "type": "stat" + }, + { + "datasource": null, + "description": "Failed servers", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 3, + "y": 5 + }, + "id": 20, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "8.0.2", + "targets": [ + { + "exemplar": true, + "expr": "pgagroal_failed_servers", + "interval": "", + "legendFormat": "", + "refId": "pgagroal_failed_servers" + } + ], + "title": "Failed servers", + "type": "stat" + }, + { + "datasource": null, + "description": "Connection state", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 11, + "x": 0, + "y": 9 + }, + "id": 26, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "pgagroal_active_connections", + "interval": "", + "legendFormat": "active connections", + "refId": "pgagroal_active_connections" + }, + { + "exemplar": true, + "expr": "pgagroal_total_connections", + "hide": false, + "interval": "", + "legendFormat": "total connections", + "refId": "pgagroal_total_connections" + }, + { + "exemplar": true, + "expr": "pgagroal_max_connections", + "hide": false, + "interval": "", + "legendFormat": "max connections", + "refId": "pgagroal_max_connections" + } + ], + "title": "Connection state", + "type": "timeseries" + }, + { + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 17 + }, + "id": 14, + "title": "Pool", + "type": "row" + }, + { + "datasource": null, + "description": "Authentication details", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 11, + "x": 0, + "y": 18 + }, + "id": 16, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "pgagroal_auth_user_success", + "interval": "", + "legendFormat": "success", + "refId": "pgagroal_auth_user_success" + }, + { + "exemplar": true, + "expr": "pgagroal_auth_user_error", + "hide": false, + "interval": "", + "legendFormat": "error", + "refId": "pgagroal_auth_user_error" + }, + { + "exemplar": true, + "expr": "pgagroal_auth_user_bad_password", + "hide": false, + "interval": "", + "legendFormat": "bad password", + "refId": "pgagroal_auth_user_bad_password" + } + ], + "title": "Authentication", + "type": "timeseries" + }, + { + "datasource": null, + "description": "The waiting time of clients", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 11, + "x": 11, + "y": 18 + }, + "id": 40, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "pgagroal_wait_time", + "interval": "", + "legendFormat": "", + "refId": "pgagroal_wait_time" + } + ], + "title": "Client waiting time", + "type": "timeseries" + }, + { + "datasource": null, + "description": "Only session and transaction modes are supported", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 11, + "x": 0, + "y": 26 + }, + "id": 42, + "interval": "1s", + "maxDataPoints": 20000, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "irate(pgagroal_query_count[1m])", + "interval": "", + "legendFormat": "", + "refId": "query rate" + } + ], + "title": "Query rate", + "type": "timeseries" + }, + { + "datasource": null, + "description": "Only session and transaction modes are supported", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 11, + "x": 11, + "y": 26 + }, + "id": 44, + "interval": "1s", + "maxDataPoints": 20000, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "irate(pgagroal_tx_count[1m])", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Transaction rate", + "type": "timeseries" + }, + { + "datasource": null, + "description": "Only session and transaction modes are supported", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 11, + "x": 0, + "y": 34 + }, + "id": 56, + "interval": "1s", + "maxDataPoints": 20000, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "sum(irate(pgagroal_connection_query_count{database!=\"\"}[5s])) by (database)", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Query rate per database", + "type": "timeseries" + }, + { + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 42 + }, + "id": 12, + "title": "Internal", + "type": "row" + }, + { + "datasource": null, + "description": "Only session and transaction modes are supported", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "Bps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 11, + "x": 0, + "y": 43 + }, + "id": 46, + "interval": "1s", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "irate(pgagroal_network_sent[1m])", + "instant": false, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Send traffic", + "type": "timeseries" + }, + { + "datasource": null, + "description": "Only session and transaction modes are supported", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "Bps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 11, + "x": 11, + "y": 43 + }, + "id": 48, + "interval": "1s", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "irate(pgagroal_network_received[1m])", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Receive traffic", + "type": "timeseries" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 11, + "x": 0, + "y": 51 + }, + "id": 50, + "interval": "1s", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "pgagroal_client_sockets", + "interval": "", + "legendFormat": "client sockets", + "refId": "client sockets" + }, + { + "exemplar": true, + "expr": "pgagroal_total_connections", + "hide": false, + "interval": "", + "legendFormat": "server sockets", + "refId": "server sockets" + }, + { + "exemplar": true, + "expr": "pgagroal_self_sockets", + "hide": false, + "interval": "", + "legendFormat": "self sockets", + "refId": "self sockets" + } + ], + "title": "Sockets used", + "type": "timeseries" + }, + { + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 59 + }, + "id": 10, + "title": "Connection", + "type": "row" + }, + { + "datasource": null, + "description": "Connection events", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 11, + "x": 0, + "y": 60 + }, + "id": 24, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "pgagroal_connection_error", + "interval": "", + "legendFormat": "error", + "refId": "pgagroal_connection_error" + }, + { + "exemplar": true, + "expr": "pgagroal_connection_kill", + "hide": false, + "interval": "", + "legendFormat": "kill", + "refId": "pgagroal_connection_kill" + }, + { + "exemplar": true, + "expr": "pgagroal_connection_remove", + "hide": false, + "interval": "", + "legendFormat": "remove", + "refId": "pgagroal_connection_remove" + }, + { + "exemplar": true, + "expr": "pgagroal_connection_timeout", + "hide": false, + "interval": "", + "legendFormat": "timeout", + "refId": "pgagroal_connection_timeout" + }, + { + "exemplar": true, + "expr": "pgagroal_connection_return", + "hide": false, + "interval": "", + "legendFormat": "return", + "refId": "pgagroal_connection_return" + }, + { + "exemplar": true, + "expr": "pgagroal_connection_invalid", + "hide": false, + "interval": "", + "legendFormat": "invalid", + "refId": "pgagroal_connection_invalid" + }, + { + "exemplar": true, + "expr": "pgagroal_connection_get", + "hide": false, + "interval": "", + "legendFormat": "get", + "refId": "pgagroal_connection_get" + }, + { + "exemplar": true, + "expr": "pgagroal_connection_idletimeout", + "hide": false, + "interval": "", + "legendFormat": "idletimeout", + "refId": "pgagroal_connection_idletimeout" + }, + { + "exemplar": true, + "expr": "pgagroal_connection_flush", + "hide": false, + "interval": "", + "legendFormat": "flush", + "refId": "pgagroal_connection_flush" + }, + { + "exemplar": true, + "expr": "pgagroal_connection_success", + "hide": false, + "interval": "", + "legendFormat": "success", + "refId": "pgagroal_connection_success" + } + ], + "title": "Connection events", + "type": "timeseries" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 11, + "y": 60 + }, + "id": 52, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "sum(pgagroal_connection) by (database) > 0", + "interval": "", + "legendFormat": "{{database}}", + "refId": "A" + } + ], + "title": "Connections per database", + "type": "timeseries" + }, + { + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 68 + }, + "id": 8, + "title": "Client", + "type": "row" + }, + { + "datasource": null, + "description": "Client state", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 11, + "x": 0, + "y": 69 + }, + "id": 38, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "pgagroal_client_active", + "interval": "", + "legendFormat": "active clients", + "refId": "pgagroal_client_active" + }, + { + "exemplar": true, + "expr": "pgagroal_client_wait", + "hide": false, + "interval": "", + "legendFormat": "waiting clients", + "refId": "pgagroal_client_wait" + } + ], + "title": "Client state", + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 77 + }, + "id": 6, + "panels": [], + "title": "Server", + "type": "row" + }, + { + "datasource": null, + "description": "Server connections", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 11, + "x": 0, + "y": 78 + }, + "id": 34, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "sum(pgagroal_connection) by (database) > 0", + "interval": "", + "legendFormat": "{{database}}", + "refId": "pgagroal_connection per database" + } + ], + "title": "Server connections", + "type": "timeseries" + }, + { + "datasource": null, + "description": "Server errors", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 11, + "y": 78 + }, + "id": 36, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "sum(pgagroal_server_error) by (name)", + "interval": "", + "legendFormat": "{{name}}", + "refId": "pgagroal_server_error" + } + ], + "title": "Server errors", + "type": "timeseries" + } + ], + "refresh": false, + "schemaVersion": 30, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "pgagroal dashboard", + "uid": "t_a1YcR7k", + "version": 1 +} \ No newline at end of file diff --git a/contrib/shell_comp/pgagroal_comp.bash b/contrib/shell_comp/pgagroal_comp.bash new file mode 100644 index 00000000..12b025e9 --- /dev/null +++ b/contrib/shell_comp/pgagroal_comp.bash @@ -0,0 +1,62 @@ +#/usr/bin/env bash + +# COMP_WORDS contains +# at index 0 the executable name (pgagroal-cli) +# at index 1 the command name (e.g., flush) +# at index 2, if required, the subcommand name (e.g., all) +pgagroal_cli_completions() +{ + + if [ "${#COMP_WORDS[@]}" == "2" ]; then + # main completion: the user has specified nothing at all + # or a single word, that is a command + COMPREPLY=($(compgen -W "flush ping enable disable shutdown status switch-to conf clear" "${COMP_WORDS[1]}")) + else + # the user has specified something else + # subcommand required? + case ${COMP_WORDS[1]} in + flush) + COMPREPLY+=($(compgen -W "gracefully idle all" "${COMP_WORDS[2]}")) + ;; + shutdown) + COMPREPLY+=($(compgen -W "gracefully immediate cancel" "${COMP_WORDS[2]}")) + ;; + clear) + COMPREPLY+=($(compgen -W "server prometheus" "${COMP_WORDS[2]}")) + ;; + conf) + COMPREPLY+=($(compgen -W "reload get set ls" "${COMP_WORDS[2]}")) + ;; + status) + COMPREPLY+=($(compgen -W "details" "${COMP_WORDS[2]}")) + esac + fi + + +} + + + +pgagroal_admin_completions() +{ + if [ "${#COMP_WORDS[@]}" == "2" ]; then + # main completion: the user has specified nothing at all + # or a single word, that is a command + COMPREPLY=($(compgen -W "master-key user" "${COMP_WORDS[1]}")) + else + # the user has specified something else + # subcommand required? + case ${COMP_WORDS[1]} in + user) + COMPREPLY+=($(compgen -W "add del edit ls" "${COMP_WORDS[2]}")) + ;; + esac + fi +} + + + + +# install the completion functions +complete -F pgagroal_cli_completions pgagroal-cli +complete -F pgagroal_admin_completions pgagroal-admin diff --git a/contrib/shell_comp/pgagroal_comp.zsh b/contrib/shell_comp/pgagroal_comp.zsh new file mode 100644 index 00000000..5b0ef704 --- /dev/null +++ b/contrib/shell_comp/pgagroal_comp.zsh @@ -0,0 +1,91 @@ +#compdef _pgagroal_cli pgagroal-cli +#compdef _pgagroal_admin pgagroal-admin + + +function _pgagroal_cli() +{ + local line + _arguments -C \ + "1: :(flush ping enable disable shutdown status switch-to conf clear)" \ + "*::arg:->args" + + case $line[1] in + flush) + _pgagroal_cli_flush + ;; + shutdown) + _pgagroal_cli_shutdown + ;; + clear) + _pgagroal_cli_clear + ;; + conf) + _pgagroal_cli_conf + ;; + status) + _pgagroal_cli_status + ;; + esac +} + +function _pgagroal_cli_flush() +{ + local line + _arguments -C \ + "1: :(gracefully idle all)" \ + "*::arg:->args" +} + +function _pgagroal_cli_conf() +{ + local line + _arguments -C \ + "1: :(reload get set ls)" \ + "*::arg:->args" +} + +function _pgagroal_cli_shutdown() +{ + local line + _arguments -C \ + "1: :(gracefully immediate cancel)" \ + "*::arg:->args" +} + +function _pgagroal_cli_clear() +{ + local line + _arguments -C \ + "1: :(server prometheus)" \ + "*::arg:->args" +} + + +function _pgagroal_admin() +{ + local line + _arguments -C \ + "1: :(master-key user)" \ + "*::arg:->args" + + case $line[1] in + user) + _pgagroal_admin_user + ;; + esac +} + +function _pgagroal_admin_user() +{ + _arguments -C \ + "1: :(add del edit ls)" \ + "*::arg:->args" +} + +function _pgagroal_cli_status() +{ + local line + _arguments -C \ + "1: :(details)" \ + "*::arg:->args" +} diff --git a/contrib/valgrind/README.md b/contrib/valgrind/README.md new file mode 100644 index 00000000..8ad70876 --- /dev/null +++ b/contrib/valgrind/README.md @@ -0,0 +1,22 @@ +# Valgrind + +The [Valgrind](https://valgrind.org/) tool suite provides a number of debugging and profiling tools that help you make your programs faster and more correct. The most popular and the default of these tools is called **Memcheck**. It can detect many memory-related errors that can lead to crashes and unpredictable behaviour. + +# Run memory management detection + +``` bash +valgrind --leak-check=full --show-leak-kinds=all --log-file=%p.log --trace-children=yes --track-origins=yes --read-var-info=yes ./pgagroal -c pgagroal.conf -a pgagroal_hba.conf +``` + +# Use suppressions rules + +``` bash +valgrind --suppressions=../../contrib/valgrind/pgagroal.supp --leak-check=full --show-leak-kinds=all --log-file=%p.log --trace-children=yes --track-origins=yes --read-var-info=yes ./pgagroal -c pgagroal.conf -a pgagroal_hba.conf +``` + +# Generate suppressions rules + +``` bash +valgrind --gen-suppressions=all --leak-check=full --show-leak-kinds=all --log-file=%p.log --trace-children=yes --track-origins=yes --read-var-info=yes ./pgagroal -c pgagroal.conf -a pgagroal_hba.conf +``` + diff --git a/contrib/valgrind/pgagroal.supp b/contrib/valgrind/pgagroal.supp new file mode 100644 index 00000000..15582074 --- /dev/null +++ b/contrib/valgrind/pgagroal.supp @@ -0,0 +1,268 @@ +# +# pgagroal rules +# + +{ + pgagroal_bind_host + Memcheck:Leak + match-leak-kinds: reachable + fun:calloc + fun:bind_host + fun:pgagroal_bind + fun:main +} + +{ + pgagroal_set_proc_title_calloc + Memcheck:Leak + match-leak-kinds: indirect + fun:calloc + fun:pgagroal_set_proc_title + fun:main +} + +{ + pgagroal_set_proc_title_malloc + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + fun:pgagroal_set_proc_title + fun:main +} + +# +# non-pgagroal rules +# + +{ + dl_open_malloc_inline + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + fun:UnknownInlinedFun + fun:_dl_new_object + fun:_dl_map_object_from_fd + fun:_dl_map_object + fun:dl_open_worker_begin + fun:_dl_catch_exception + fun:dl_open_worker + fun:_dl_catch_exception + fun:_dl_open + fun:dlopen_doit + fun:_dl_catch_exception + fun:_dl_catch_error +} + +{ + dl_open_calloc + Memcheck:Leak + match-leak-kinds: reachable + fun:calloc + fun:UnknownInlinedFun + fun:_dl_check_map_versions + fun:dl_open_worker_begin + fun:_dl_catch_exception + fun:dl_open_worker + fun:_dl_catch_exception + fun:_dl_open + fun:dlopen_doit + fun:_dl_catch_exception + fun:_dl_catch_error + fun:_dlerror_run + fun:UnknownInlinedFun + fun:dlopen@@GLIBC_2.34 +} + +{ + dl_open_calloc_inline + Memcheck:Leak + match-leak-kinds: reachable + fun:calloc + fun:UnknownInlinedFun + fun:_dl_new_object + fun:_dl_map_object_from_fd + fun:_dl_map_object + fun:dl_open_worker_begin + fun:_dl_catch_exception + fun:dl_open_worker + fun:_dl_catch_exception + fun:_dl_open + fun:dlopen_doit + fun:_dl_catch_exception + fun:_dl_catch_error +} + +{ + dl_open_worker_malloc + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + fun:malloc + fun:strdup + fun:_dl_map_object + fun:dl_open_worker_begin + fun:_dl_catch_exception + fun:dl_open_worker + fun:_dl_catch_exception + fun:_dl_open + fun:dlopen_doit + fun:_dl_catch_exception + fun:_dl_catch_error + fun:_dlerror_run +} + + +{ + ev_loop_timers_realloc + Memcheck:Leak + match-leak-kinds: reachable + fun:realloc + fun:ev_realloc + fun:feed_reverse + fun:timers_reify + fun:ev_run.cold + fun:ev_loop + fun:main +} + +{ + ev_io_realloc + Memcheck:Leak + match-leak-kinds: reachable + fun:realloc + fun:ev_realloc + fun:fd_change + fun:ev_io_start.cold + fun:evtimerfd_init + fun:ev_periodic_start.cold + fun:main +} + +{ + ev_io_management_realloc + Memcheck:Leak + match-leak-kinds: reachable + fun:realloc + fun:ev_realloc + fun:ev_io_start.cold + fun:start_management + fun:main +} + +{ + ev_io_uds_realloc + Memcheck:Leak + match-leak-kinds: reachable + fun:realloc + fun:ev_realloc + fun:ev_io_start.cold + fun:start_uds + fun:main +} + +{ + ev_io_metrics_realloc + Memcheck:Leak + match-leak-kinds: reachable + fun:realloc + fun:ev_realloc + fun:ev_io_start.cold + fun:start_metrics + fun:main +} + +{ + ev_fd_change_realloc + Memcheck:Leak + match-leak-kinds: reachable + fun:realloc + fun:ev_realloc + fun:fd_change + fun:ev_io_start.cold + fun:main +} + +{ + ev_periodics_start_realloc + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + fun:realloc + fun:ev_realloc + fun:ev_periodic_start.cold + fun:main +} + +{ + ev_loop_evpipe_realloc + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + fun:realloc + fun:ev_realloc + fun:ev_io_start.cold + fun:evpipe_init + fun:ev_signal_start.cold + fun:ev_default_loop + fun:main +} + +{ + ev_loop_epoll_realloc + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + fun:realloc + fun:ev_realloc + fun:epoll_init + fun:epoll_init.isra.0 + fun:loop_init + fun:ev_default_loop + fun:main +} + +{ + ev_run_feed_event_realloc + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + fun:realloc + fun:ev_realloc + fun:ev_feed_event.cold + fun:fd_event_nocheck + fun:fd_event + fun:epoll_poll + fun:ev_run + fun:ev_run + fun:ev_loop + fun:main +} + +{ + ev_invoke_pending_realloc + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + fun:add_client + fun:accept_main_cb + fun:ev_invoke_pending + fun:ev_run + fun:ev_run + fun:ev_loop + fun:main +} + +{ + ev_periodics_realloc + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + fun:realloc + fun:ev_realloc + fun:feed_reverse + fun:periodics_reify + fun:ev_run + fun:ev_run.cold + fun:ev_loop + fun:main +} \ No newline at end of file diff --git a/doc/ADMIN.md b/doc/ADMIN.md new file mode 100644 index 00000000..0582736f --- /dev/null +++ b/doc/ADMIN.md @@ -0,0 +1,87 @@ +# `pgagroal-admin` user guide + +`pgagroal-admin` is a command line interface to manage users known +to the [**pgagroal**](https://github.com/agroal/pgagroal) connection pooler. +The executable accepts a set of options, as well as a command to execute. +If no command is provided, the program will show the help screen. + +The `pgagroal-admin` utility has the following synopsis: + +``` +pgagroal-admin [ OPTIONS ] [ COMMAND ] +``` + + +## Options + +Available options are the following ones: + +``` + -f, --file FILE Set the path to a user file + -U, --user USER Set the user name + -P, --password PASSWORD Set the password for the user + -g, --generate Generate a password + -l, --length Password length + -V, --version Display version information + -?, --help Display help + +``` + +Options can be specified either in short or long form, in any position of the command line. + +The `-f` option is mandatory for every operation that involves user management. If no +user file is specified, `pgagroal-admin` will silently use the default one (`pgagroal_users.conf`). + +## Commands + +### user +The `user` command allows the management of the users known to the connection pooler. +The command accepts the following subcommands: +- `add` to add a new user to the system; +- `del` to remove an existing user from the system; +- `edit` to change the credentials of an existing user; +- `ls` to list all known users within the system. + +The command will edit the `pgagroal_users.conf` file or any file specified by means of the `-f` option flag. + +Unless the command is run with the `-U` and/or `-P` flags, the execution will be interactive. + +Examples: + +``` shell +pgagroal-admin user add -U simon -P secret +pgagroal-admin user del -U simon + +``` + +## master-key + +The `master-key` command allows the definition of a password to protect the vault of the users, +that is the "container" for users' credentials. + + +## Deprecated commands + +The following commands have been deprecated and will be removed +in later releases of [**pgagroal**](https://github.com/agroal/pgagroal). +For each command, this is the corresponding current mapping +to the working command: + +- `add-user` is now `user add`; +- `remove-user` is now `user del`; +- `update-user` is now `user edit`; +- `list-users` is now `user ls`. + +Whenever you use a deprecated command, the `pgagroal-admin` will print on standard error a warning message. +If you don't want to get any warning about deprecated commands, you +can redirect the `stderr` to `/dev/null` or any other location with: + +``` +pgagroal-admin user-add -U luca -P strongPassword 2>/dev/null +``` + + +## Shell completion + +There is a minimal shell completion support for `pgagroal-admin`. +See the [Install pgagroal](https://github.com/pgagroal/pgagroal/blob/main/doc/tutorial/01_install.md) for more details. diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md new file mode 100644 index 00000000..e1a8aee7 --- /dev/null +++ b/doc/ARCHITECTURE.md @@ -0,0 +1,313 @@ +# pgagroal architecture + +## Overview + +[**pgagroal**](https://github.com/agroal/pgagroal) use a process model (`fork()`), where each process handles one connection to [PostgreSQL](https://www.postgresql.org). +This was done such a potential crash on one connection won't take the entire pool down. + +The main process is defined in [main.c](../src/main.c). When a client connects it is processed in its own process, which +is handle in [worker.h](../src/include/worker.h) ([worker.c](../src/libpgagroal/worker.c)). + +Once the client disconnects the connection is put back in the pool, and the child process is terminated. + +## Shared memory + +A memory segment ([shmem.h](../src/include/shmem.h)) is shared among all processes which contains the [**pgagroal**](https://github.com/agroal/pgagroal) +state containing the configuration of the pool, the list of servers and the state of each connection. + +The configuration of [**pgagroal**](https://github.com/agroal/pgagroal) (`struct configuration`), the configuration of the servers (`struct server`) and +the state of each connection (`struct connection`) is initialized in this shared memory segment. +These structs are all defined in [pgagroal.h](../src/include/pgagroal.h). + +The shared memory segment is created using the `mmap()` call. + +## Atomic operations + +The [atomic operation library](https://en.cppreference.com/w/c/atomic) is used to define the state of each of the +connection, and move them around in the connection state diagram. The state diagram has the follow states + +| State name | Description | +|------------|-------------| +| `STATE_NOTINIT` | The connection has not been initialized | +| `STATE_INIT` | The connection is being initialized | +| `STATE_FREE` | The connection is free | +| `STATE_IN_USE` | The connection is in use | +| `STATE_GRACEFULLY` | The connection will be killed upon return to the pool | +| `STATE_FLUSH` | The connection is being flushed | +| `STATE_IDLE_CHECK` | The connection is being idle timeout checked | +| `STATE_MAX_CONNECTION_AGE` | The connection is being max connection age checked | +| `STATE_VALIDATION` | The connection is being validated | +| `STATE_REMOVE` | The connection is being removed | + +These state are defined in [pgagroal.h](../src/include/pgagroal.h). + +## Pool + +The [**pgagroal**](https://github.com/agroal/pgagroal) pool API is defined in [pool.h](../src/include/pool.h) ([pool.c](../src/libpgagroal/pool.c)). + +This API defines the functionality of the pool such as getting a connection from the pool, and returning it. +There is no ordering among processes, so a newly created process can obtain a connection before an older process. + +The pool operates on the `struct connection` data type defined in [pgagroal.h](../src/include/pgagroal.h). + +## Network and messages + +All communication is abstracted using the `struct message` data type defined in [message.h](../src/include/message.h). + +Reading and writing messages are handled in the [message.h](../src/include/message.h) ([message.c](../src/libpgagroal/message.c)) +files. + +Network operations are defined in [network.h](../src/include/network.h) ([network.c](../src/libpgagroal/network.c)). + +## Memory + +Each process uses a fixed memory block for its network communication, which is allocated upon startup of the worker. + +That way we don't have to allocate memory for each network message, and more importantly free it after end of use. + +The memory interface is defined in [memory.h](../src/include/memory.h) ([memory.c](../src/libpgagroal/memory.c)). + +## Management + +`pgagroal` has a management interface which defines the administrator abilities that can be performed when it is running. +This include for example taking a backup. The `pgagroal-cli` program is used for these operations ([cli.c](../src/cli.c)). + +The management interface is defined in [management.h](../src/include/management.h). The management interface +uses its own protocol which uses JSON as its foundation. + +### Write + +The client sends a single JSON string to the server, + +| Field | Type | Description | +| :------------ | :----- | :------------------------------ | +| `compression` | uint8 | The compression type | +| `encryption` | uint8 | The encryption type | +| `length` | uint32 | The length of the JSON document | +| `json` | String | The JSON document | + +The server sends a single JSON string to the client, + +| Field | Type | Description | +| :------------ | :----- | :------------------------------ | +| `compression` | uint8 | The compression type | +| `encryption` | uint8 | The encryption type | +| `length` | uint32 | The length of the JSON document | +| `json` | String | The JSON document | + +### Read + +The server sends a single JSON string to the client, + +| Field | Type | Description | +| :------------ | :----- | :------------------------------ | +| `compression` | uint8 | The compression type | +| `encryption` | uint8 | The encryption type | +| `length` | uint32 | The length of the JSON document | +| `json` | String | The JSON document | + +The client sends to the server a single JSON documents, + +| Field | Type | Description | +| :------------ | :----- | :------------------------------ | +| `compression` | uint8 | The compression type | +| `encryption` | uint8 | The encryption type | +| `length` | uint32 | The length of the JSON document | +| `json` | String | The JSON document | + +### Remote management + +The remote management functionality uses the same protocol as the standard management method. + +However, before the management packet is sent the client has to authenticate using SCRAM-SHA-256 using the +same message format that PostgreSQL uses, e.g. StartupMessage, AuthenticationSASL, AuthenticationSASLContinue, +AuthenticationSASLFinal and AuthenticationOk. The SSLRequest message is supported. + +The remote management interface is defined in [remote.h](../src/include/remote.h) ([remote.c](../src/libpgagroal/remote.c)). + +## libev usage + +[libev](http://software.schmorp.de/pkg/libev.html) is used to handle network interactions, which is "activated" +upon an `EV_READ` event. + +Each process has its own event loop, such that the process only gets notified when data related only to that process +is ready. The main loop handles the system wide "services" such as idle timeout checks and so on. + +## Pipeline + +[**pgagroal**](https://github.com/agroal/pgagroal) has the concept of a pipeline that defines how communication is routed from the client through [**pgagroal**](https://github.com/agroal/pgagroal) to +[PostgreSQL](https://www.postgresql.org). Likewise in the other direction. + +A pipeline is defined by + +```C +struct pipeline +{ + initialize initialize; + start start; + callback client; + callback server; + stop stop; + destroy destroy; + periodic periodic; +}; +``` + +in [pipeline.h](../src/include/pipeline.h). + +The functions in the pipeline are defined as + +| Function | Description | +|----------|-------------| +| `initialize` | Global initialization of the pipeline, may return a pointer to a shared memory segment | +| `start` | Called when the pipeline instance is started | +| `client` | Client to [**pgagroal**](https://github.com/agroal/pgagroal) communication | +| `server` | [PostgreSQL](https://www.postgresql.org) to [**pgagroal**](https://github.com/agroal/pgagroal) communication | +| `stop` | Called when the pipeline instance is stopped | +| `destroy` | Global destruction of the pipeline | +| `periodic` | Called periodic | + +The functions `start`, `client`, `server` and `stop` has access to the following information + +```C +struct worker_io +{ + struct ev_io io; /* The libev base type */ + int client_fd; /* The client descriptor */ + int server_fd; /* The server descriptor */ + int slot; /* The slot */ + SSL* client_ssl; /* The client SSL context */ + SSL* server_ssl; /* The server SSL context */ +}; +``` +defined in [worker.h](../src/include/worker.h). + +### Performance pipeline + +One of the goals for [**pgagroal**](https://github.com/agroal/pgagroal) is performance, so the performance pipeline will only look for the +[`Terminate`](https://www.postgresql.org/docs/11/protocol-message-formats.html) message from the client and act on that. +Likewise the performance pipeline will only look for `FATAL` errors from the server. This makes the pipeline very fast, since there +is a minimum overhead in the interaction. + +The pipeline is defined in [pipeline_perf.c](../src/libpgagroal/pipeline_perf.c) in the functions + +| Function | Description | +|----------|-------------| +| `performance_initialize` | Nothing | +| `performance_start` | Nothing | +| `performance_client` | Client to [**pgagroal**](https://github.com/agroal/pgagroal) communication | +| `performance_server` | [PostgreSQL](https://www.postgresql.org) to [**pgagroal**](https://github.com/agroal/pgagroal) communication | +| `performance_stop` | Nothing | +| `performance_destroy` | Nothing | +| `performance_periodic` | Nothing | + +### Session pipeline + +The session pipeline works like the performance pipeline with the exception that it checks if +a Transport Layer Security (TLS) transport should be used. + +The pipeline is defined in [pipeline_session.c](../src/libpgagroal/pipeline_session.c) in the functions + +| Function | Description | +|----------|-------------| +| `session_initialize` | Initialize memory segment if disconnect_client is active | +| `session_start` | Prepares the client segment if disconnect_client is active | +| `session_client` | Client to [**pgagroal**](https://github.com/agroal/pgagroal) communication | +| `session_server` | [PostgreSQL](https://www.postgresql.org) to [**pgagroal**](https://github.com/agroal/pgagroal) communication | +| `session_stop` | Updates the client segment if disconnect_client is active | +| `session_destroy` | Destroys memory segment if initialized | +| `session_periodic` | Checks if clients should be disconnected | + +### Transaction pipeline + +The transaction pipeline will return the connection to the server after each transaction. The pipeline supports +Transport Layer Security (TLS). + +The pipeline uses the [ReadyForQuery](https://www.postgresql.org/docs/current/protocol-message-formats.html) message +to check the status of the transaction, and therefore needs to maintain track of the message headers. + +The pipeline has a management interface in order to receive the socket descriptors from the parent process when a new +connection is added to the pool. The pool will retry if the client in question doesn't consider the socket descriptor valid. + +The pipeline is defined in [pipeline_transaction.c](../src/libpgagroal/pipeline_transaction.c) in the functions + +| Function | Description | +|----------|-------------| +| `transaction_initialize` | Nothing | +| `transaction_start` | Setup process variables and returns the connection to the pool | +| `transaction_client` | Client to [**pgagroal**](https://github.com/agroal/pgagroal) communication. Obtain connection if needed | +| `transaction_server` | [PostgreSQL](https://www.postgresql.org) to [**pgagroal**](https://github.com/agroal/pgagroal) communication. Keep track of message headers | +| `transaction_stop` | Return connection to the pool if needed. Possible rollback of active transaction | +| `transaction_destroy` | Nothing | +| `transaction_periodic` | Nothing | + +## Signals + +The main process of [**pgagroal**](https://github.com/agroal/pgagroal) supports the following signals `SIGTERM`, `SIGINT` and `SIGALRM` +as a mechanism for shutting down. The `SIGTRAP` signal will put [**pgagroal**](https://github.com/agroal/pgagroal) into graceful shutdown, meaning that +exisiting connections are allowed to finish their session. The `SIGABRT` is used to request a core dump (`abort()`). +The `SIGHUP` signal will trigger a reload of the configuration. + +The child processes support `SIGQUIT` as a mechanism to shutdown. This will not shutdown the pool itself. + +It should not be needed to use `SIGKILL` for [**pgagroal**](https://github.com/agroal/pgagroal). Please, consider using `SIGABRT` instead, and share the +core dump and debug logs with the [**pgagroal**](https://github.com/agroal/pgagroal) community. + +## Reload + +The `SIGHUP` signal will trigger a reload of the configuration. + +However, some configuration settings requires a full restart of [**pgagroal**](https://github.com/agroal/pgagroal) in order to take effect. These are + +* `hugepage` +* `libev` +* `log_path` +* `log_type` +* `max_connections` +* `pipeline` +* `unix_socket_dir` +* `pidfile` +* Limit rules defined by `pgagroal_databases.conf` +* TLS rules defined by server section + +The configuration can also be reloaded using `pgagroal-cli -c pgagroal.conf conf reload`. The command is only supported +over the local interface, and hence doesn't work remotely. + +## Prometheus + +pgagroal has support for [Prometheus](https://prometheus.io/) when the `metrics` port is specified. + +**Note:** It is crucial to carefully initialize Prometheus memory in any program files for example functions like `pgagroal_init_prometheus()` and `pgagroal_init_prometheus_cache()` should only be invoked if `metrics` is greater than 0. + +The module serves two endpoints + +* `/` - Overview of the functionality (`text/html`) +* `/metrics` - The metrics (`text/plain`) + +All other URLs will result in a 403 response. + +The metrics endpoint supports `Transfer-Encoding: chunked` to account for a large amount of data. + +The implementation is done in [prometheus.h](../src/include/prometheus.h) and +[prometheus.c](../src/libpgagroal/prometheus.c). + +## Failover support + +pgagroal can failover a PostgreSQL instance if clients can't write to it. + +This is done using an external script provided by the user. + +The implementation is done in [server.h](../src/include/server.h) and +[server.c](../src/libpgagroal/server.c). + +## Logging + +Simple logging implementation based on a `atomic_schar` lock. + +The implementation is done in [logging.h](../src/include/logging.h) and +[logging.c](../src/libpgagroal/logging.c). + +## Protocol + +The protocol interactions can be debugged using [Wireshark](https://www.wireshark.org/) or +[pgprtdbg](https://github.com/jesperpedersen/pgprtdbg). diff --git a/doc/CLI.md b/doc/CLI.md new file mode 100644 index 00000000..fc6854d2 --- /dev/null +++ b/doc/CLI.md @@ -0,0 +1,549 @@ +# `pgagroal-cli` user guide + +`pgagroal-cli` is a command line interface to interact with [**pgagroal**](https://github.com/agroal/pgagroal). +The executable accepts a set of options, as well as a command to execute. +If no command is provided, the program will show the help screen. + +The `pgagroal-cli` utility has the following synopsis: + +``` +pgagroal-cli [ OPTIONS ] [ COMMAND ] +``` + + +## Options + +Available options are the following ones: + +``` +-c, --config CONFIG_FILE Set the path to the pgagroal.conf file +-h, --host HOST Set the host name +-p, --port PORT Set the port number +-U, --user USERNAME Set the user name +-P, --password PASSWORD Set the password +-L, --logfile FILE Set the log file +-F, --format text|json Set the output format +-v, --verbose Output text string of result +-V, --version Display version information +-?, --help Display help + +``` + +Options can be specified either in short or long form, in any position of the command line. + +By default the command output, if any, is reported as text. It is possible to specify JSON as the output format, +and this is the suggested format if there is the need to automtically parse the command output, since the text format +could be subject to changes in future releases. For more information about the JSON output format, +please see the [JSON Output Format](#json-output-format) section. + +## Commands + +### flush +The `flush` command performs a connection flushing. +It accepts a *mode* to operate the actual flushing: +- `gracefully` (the default if not specified), flush connections when possible; +- `idle` to flush only connections in state *idle*; +- `all` to flush all the connections (**use with caution!**). + +The command accepts a database name, that if provided, restricts the scope of +`flush` only to connections related to such database. +If no database is provided, the `flush` command is operated against all databases. + + +Command + +``` +pgagroal-cli flush [gracefully|idle|all] [*|] +``` + +Examples + +``` +pgagroal-cli flush # pgagroal-cli flush gracefully '*' +pgagroal-cli flush idle # pgagroal-cli flush idle '*' +pgagroal-cli flush all # pgagroal-cli flush all '*' +pgagroal-cli flush pgbench # pgagroal-cli flush gracefully pgbench +``` + +### ping +The `ping` command checks if [**pgagroal**](https://github.com/agroal/pgagroal) is running. +In case of success, the command does not print anything on the standard output unless the `--verbose` flag is used. + +Command + +``` +pgagroal-cli ping +``` + +Example + +``` +pgagroal-cli ping --verbose # pgagroal-cli: Success (0) +pgagroal-cli ping # $? = 0 +``` + +In the case [**pgagroal**](https://github.com/agroal/pgagroal) is not running, a message is printed on the standard error and the exit status is set to a non-zero value: + +``` +pgagroal-cli ping # $? = 1 +Connection error on /tmp +``` + +### enable +Enables a database (or all databases). + +Command + +``` +pgagroal-cli enable [|*] +``` + +Example + +``` +pgagroal-cli enable +``` + +### disable +Disables a database (or all databases). + +Command + +``` +pgagroal-cli disable [|*] +``` + +Example + +``` +pgagroal-cli disable +``` + +### shutdown +The `shutdown` command is used to stop the connection pooler. +It supports the following operating modes: +- `gracefully` (the default) closes the pooler as soon as no active connections are running; +- `immediate` force an immediate stop. + +If the `gracefully` mode is requested, chances are the system will take some time to +perform the effective shutdown, and therefore it is possible to abort the request +issuing another `shutdown` command with the mode `cancel`. + + +Command + +``` +pgagroal-cli shutdown [gracefully|immediate|cancel] +``` + +Examples + +``` +pgagroal-cli shutdown # pgagroal-cli shutdown gracefully +... +pgagroal-cli shutdown cancel # stops the above command +``` + + +### status +The `status` command reports the current status of the [**pgagroal**](https://github.com/agroal/pgagroal) pooler. +Without any subcommand, `status` reports back a short set of information about the pooler. + +Command + +``` +pgagroal-cli status +``` + +Example + +``` +pgagroal-cli status + +``` + +With the `details` subcommand, a more verbose output is printed with a detail about every connection. + +Example + +``` +pgagroal-cli status details +``` + +### switch-to +Switch to another primary server. + +Command + +``` +pgagroal-cli switch-to +``` + +Example + +``` +pgagroal-cli switch-to replica +``` + +### conf +Manages the configuration of the running instance. +This command requires one subcommand, that can be: +- `reload` issue a reload of the configuration, applying at runtime any changes from the configuration files; +- `get` provides a configuration parameter value; +- `set` modifies a configuration parameter at runtime; +- `ls` prints where the configuration files are located. + +Command + +``` +pgagroal-cli conf +``` + +Examples + +``` +pgagroal-cli conf reload + +pgagroal-cli conf get max_connections + +pgagroal-cli conf set max_connections 25 + +``` + +The details about how to get and set values at run-time are explained in the following. + +#### conf get +Given a configuration setting name, provides the current value for such setting. + +The configuration setting name must be the same as the one used in the configuration files. +It is possible to specify the setting name with words separated by dots, so that it can assume +the form `section.context.key` where: +- `section` can be either + - [**pgagroal**](https://github.com/agroal/pgagroal) (optional) the search will be performed into the main configuration settings, that is + those under the `[pgagroal]` settings in the `pgagroal.conf` file; + - `limit` the search will match against the dataabse/limit configuration, i.e., the file `pgagroal_databases.conf`; + - `hba` the search will match against the Host Based Access configuration, i.e., the `pgagroal_hbs.conf`; + - `server` the search will match against a single server defined in the main `pgagroal.conf` file. +- `context` is the match criteria to find the information into a specific context, depending on the value of the `section`: + - if the `section` is set to `limit`, than the `context` is the database name to match into the `pgagroal_databases.conf`. + Please note that the same user could be listed more than once, in such case *only the first match* is reported back; + - if the `section` is set to `hba`, than the `context` is the username to match into the `pgagroal_hba.conf`. + Please note that the same user could be listed more than once, in such case *only the first match* is reported back; + - if the `section` is set to `server`, than the `context` is the name of the server in the `pgagroal.conf` main file; + - if the `section` is set to [**pgagroal**](https://github.com/agroal/pgagroal), the `context` must be empty; +- `key` is the configuration key to search for. + + +Examples +``` +pgagroal-cli conf get pipeline +performance + +pgagroal-cli conf get limit.pgbench.max_size +2 + +pgagroal-cli conf get server.venkman.primary +off + +``` + +In the above examples, `pipeline` is equivalent to `pgagroal.pipeline` and looks for a global configuration setting named `pipeline`. +The `limit.pgbench.max_size` looks for the `max_size` set into the *limit* file (`pgagroal_databases.conf`) for the database `pgbench`. +The `server.venkman.primary` searches for the configuration parameter `primary` into the *server* section named `venkman` in the main configuration file `pgagraol.conf`. + +If the `--verbose` option is specified, a descriptive string of the configuration parameter is printed as *name = value*: + +``` +pgagroal-cli conf get max_connections --verbose +max_connections = 4 +Success (0) +``` + +If the parameter name specified is not found or invalid, the program `pgagroal-cli` exit normally without printing any value. + + + +#### conf set +Allows the setting of a configuration parameter at run-time, if possible. + +Examples +``` +pgagroal-cli conf set log_level debug +pgagroal-cli conf set server.venkman.port 6432 +pgagroal conf set limit.pgbench.max_size 2 +``` + +The syntax for setting parameters is the same as for the command `conf get`, therefore parameters are organized into namespaces: +- `main` (optional) is the main pgagroal configuration namespace, for example `main.log_level` or simply `log_level`; +- `server` is the namespace referred to a specific server. It has to be followed by the name of the server and the name of the parameter to change, in a dotted notation, like `server.venkman.port`; +- `limit` is the namespace referred to a specific limit entry, followed by the name of the username used in the limit entry. + +When executed, the `conf set` command returns the run-time setting of the specified parameter: if such parameter is equal to the value supplied, the change has been applied, otherwise it means that the old setting has been kept. +The `--verbose` flag can be used to understand if the change has been applied: + +``` +$ pgagroal-cli conf set log_level debug +debug + +$ pgagroal-cli conf set log_level debug --verbose +log_level = debug +pgagroal-cli: Success (0) +``` + +When a setting modification cannot be applied, the system returns the "old" setting value and, if `--verbose` is specified, the error indication: + +``` +$ pgagroal-cli conf set max_connections 100 +40 + +$ pgagroal-cli conf set max_connections 100 --verbose +max_connections = 40 +pgagroal-cli: Error (2) +``` + +When a `conf set` cannot be applied, the system will report in the logs an indication about the problem. With regard to the previous example, the system reports in the logs something like the following (depending on your `log_level`): + +``` +DEBUG Trying to change main configuration setting to <100> +INFO Restart required for max_connections - Existing 40 New 100 +WARN 1 settings cannot be applied +DEBUG pgagroal_management_write_config_set: unable to apply changes to -> <100> +``` + +#### conf ls + +The command `conf ls` provides information about the location of the configuration files. +As an example: + +``` +Main Configuration file: /etc/pgagroal/pgagroal.conf +HBA file: /etc/pgagroal/pgagroal_hba.conf +Limit file: /etc/pgagroal/pgagroal_databases.conf +Frontend users file: /etc/pgagroal/pgagroal_frontend_users.conf +Admins file: /etc/pgagroal/pgagroal_admins.conf +Superuser file: +Users file: /etc/pgagroal/pgagroal_users.conf +``` + +### clear +Resets different parts of the pooler. It accepts an operational mode: +- `prometheus` resets the metrics provided without altering the pooler status; +- `server` resets the specified server status. + + +``` +pgagroal-cli clear [prometheus|server ] +``` + +Examples + +``` +pgagroal-cli clear spengler # pgagroal-cli clear server spengler +pgagroal-cli clear prometheus +``` + + +## Shell completions + +There is a minimal shell completion support for `pgagroal-cli`. +Please refer to the [Install pgagroal](https://github.com/agroal/pgagroal/blob/master/doc/tutorial/01_install.md) tutorial for detailed information about how to enable and use shell completions. + + +## JSON Output Format + +It is possible to obtain the output of a command in a JSON format by specyfing the `-F` (`--format`) option on the command line. +Supported output formats are: +- `text` (the default) +- `json` + +As an example, the following are invocations of commands with different output formats: + +``` +pgagroal-cli status # defaults to text output format + +pgagroal-cli status --format text # same as above +pgagroal-cli status -F text # same as above + +pgagroal-cli status --format json # outputs as JSON text +pgagroal-cli status -F json # same as above +``` + +Whenever a command produces output, the latter can be obtained in a JSON format. +Every command output consists of an object that contains two other objects: +- a `command` object, with all the details about the command and its output; +- an `application` object, with all the details about the executable that launched the command (e.g., `pgagroal-cli`). + +In the following, details about every object are provided: + +### The `application` object + +The `application` object is made by the following attributes: +- `name` a string representing the name of the executable that launched the command; +- `version` a string representing the version of the executable; +- `major`, `minor`, `patch` are integers representing every single part of the version of the application. + +As an example, when `pgagroal-cli` launches a command, the output includes an `application` object like the following: + +``` + "application": { + "name": "pgagroal-cli", + "major": 1, + "minor": 6, + "patch": 0, + "version": "1.6.0" + } +``` + + +### The `command` object + +The `command` object represents the launched command and contains also the answer from the [**pgagroal**](https://github.com/agroal/pgagroal). +The object is made by the following attributes: +- `name` a string representing the command launched (e.g., `status`); +- `status` a string that contains either "OK" or an error string if the command failed; +- `error` an interger value used as a flag to indicate if the command was in error or not, where `0` means success and `1` means error; +- `exit-status` an integer that contains zero if the command run succesfully, another value depending on the specific command in case of failure; +- `output` an object that contains the details of the executed command. + +The `output` object is *the variable part* in the JSON command output, that means its effective content depends on the launched command. + +Whenever the command output includes an array of stuff, for example a connection list, such array is wrapped into a `list` JSON array with a sibling named `count` that contains the integer size of the array (number of elements). + + +The following are a few examples of commands that provide output in JSON: + + +``` +pgagroal-cli ping --format json +{ + "command": { + "name": "ping", + "status": "OK", + "error": 0, + "exit-status": 0, + "output": { + "status": 1, + "message": "running" + } + }, + "application": { + "name": "pgagroal-cli", + "major": 1, + "minor": 6, + "patch": 0, + "version": "1.6.0" + } +} + + + +pgagroal-cli status --format json +{ + "command": { + "name": "status", + "status": "OK", + "error": 0, + "exit-status": 0, + "output": { + "status": { + "message": "Running", + "status": 1 + }, + "connections": { + "active": 0, + "total": 2, + "max": 15 + }, + "databases": { + "disabled": { + "count": 0, + "state": "disabled", + "list": [] + } + } + } + }, + "application": { + "name": "pgagroal-cli", + "major": 1, + "minor": 6, + "patch": 0, + "version": "1.6.0" + } +} +``` + +As an example, the following is the output of a faulty `conf set` command (note the `status`, `error` and `exist-status` values): + +``` +pgagroal-cli conf set max_connections 1000 --format json +{ + "command": { + "name": "conf set", + "status": "Current and expected values are different", + "error": true, + "exit-status": 2, + "output": { + "key": "max_connections", + "value": "15", + "expected": "1000" + } + }, + "application": { + "name": "pgagroal-cli", + "major": 1, + "minor": 6, + "patch": 0, + "version": "1.6.0" + } +} +``` + + +The `conf ls` command returns an array named `files` where each entry is made by a couple `description` and `path`, where the former +is the mnemonic name of the configuration file, and the latter is the value of the configuration file used: + +``` +$ pgagroal-cli conf ls --format json +{ + "command": { + "name": "conf ls", + "status": "OK", + "error": 0, + "exit-status": 0, + "output": { + "files": { + "list": [{ + "description": "Main Configuration file", + "path": "/etc/pgagroal/pgagroal.conf" + }, { + "description": "HBA File", + "path": "/etc/pgagroal/pgagroal_hba.conf" + }, { + "description": "Limit file", + "path": "/etc/pgagroal/pgagroal_databases.conf" + }, { + "description": "Frontend users file", + "path": "/etc/pgagroal/pgagroal_frontend_users.conf" + }, { + "description": "Admins file", + "path": "/etc/pgagroal/pgagroal_admins.conf" + }, { + "description": "Superuser file", + "path": "" + }, { + "description": "Users file", + "path": "/etc/pgagroal/pgagroal_users.conf" + }] + } + } + }, + "application": { + "name": "pgagroal-cli", + "major": 1, + "minor": 6, + "patch": 0, + "version": "1.6.0" + } +} +``` diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt new file mode 100644 index 00000000..ff07a48e --- /dev/null +++ b/doc/CMakeLists.txt @@ -0,0 +1,185 @@ +# man target +add_custom_target(man ALL) + +# man page definitions +set(PGAGROAL_SRC_FILE "${CMAKE_CURRENT_SOURCE_DIR}/man/pgagroal.1.rst") +set(PGAGROAL_DST_FILE "${CMAKE_CURRENT_BINARY_DIR}/pgagroal.1") +set(PGAGROAL_CLI_SRC_FILE "${CMAKE_CURRENT_SOURCE_DIR}/man/pgagroal-cli.1.rst") +set(PGAGROAL_CLI_DST_FILE "${CMAKE_CURRENT_BINARY_DIR}/pgagroal-cli.1") +set(PGAGROAL_ADMIN_SRC_FILE "${CMAKE_CURRENT_SOURCE_DIR}/man/pgagroal-admin.1.rst") +set(PGAGROAL_ADMIN_DST_FILE "${CMAKE_CURRENT_BINARY_DIR}/pgagroal-admin.1") +set(PGAGROAL_CONF_SRC_FILE "${CMAKE_CURRENT_SOURCE_DIR}/man/pgagroal.conf.5.rst") +set(PGAGROAL_CONF_DST_FILE "${CMAKE_CURRENT_BINARY_DIR}/pgagroal.conf.5") +set(PGAGROAL_HBA_CONF_SRC_FILE "${CMAKE_CURRENT_SOURCE_DIR}/man/pgagroal_hba.conf.5.rst") +set(PGAGROAL_HBA_CONF_DST_FILE "${CMAKE_CURRENT_BINARY_DIR}/pgagroal_hba.conf.5") +set(PGAGROAL_DATABASES_CONF_SRC_FILE "${CMAKE_CURRENT_SOURCE_DIR}/man/pgagroal_databases.conf.5.rst") +set(PGAGROAL_DATABASES_CONF_DST_FILE "${CMAKE_CURRENT_BINARY_DIR}/pgagroal_databases.conf.5") + +# pgagroal.1 +add_custom_command( + TARGET man + COMMAND ${RST2MAN_EXECUTABLE} ${PGAGROAL_SRC_FILE} ${PGAGROAL_DST_FILE} + OUTPUTS ${PGAGROAL_DST_FILE} +) + +# pgagroal-cli.1 +add_custom_command( + TARGET man + COMMAND ${RST2MAN_EXECUTABLE} ${PGAGROAL_CLI_SRC_FILE} ${PGAGROAL_CLI_DST_FILE} + OUTPUTS ${PGAGROAL_CLI_DST_FILE} +) + +# pgagroal-admin.1 +add_custom_command( + TARGET man + COMMAND ${RST2MAN_EXECUTABLE} ${PGAGROAL_ADMIN_SRC_FILE} ${PGAGROAL_ADMIN_DST_FILE} + OUTPUTS ${PGAGROAL_ADMIN_DST_FILE} +) + +# pgagroal.conf.5 +add_custom_command( + TARGET man + COMMAND ${RST2MAN_EXECUTABLE} ${PGAGROAL_CONF_SRC_FILE} ${PGAGROAL_CONF_DST_FILE} + OUTPUTS ${PGAGROAL_CONF_DST_FILE} +) + +# pgagroal_hba.conf.5 +add_custom_command( + TARGET man + COMMAND ${RST2MAN_EXECUTABLE} ${PGAGROAL_HBA_CONF_SRC_FILE} ${PGAGROAL_HBA_CONF_DST_FILE} + OUTPUTS ${PGAGROAL_HBA_CONF_DST_FILE} +) + +# pgagroal_databases.conf.5 +add_custom_command( + TARGET man + COMMAND ${RST2MAN_EXECUTABLE} ${PGAGROAL_DATABASES_CONF_SRC_FILE} ${PGAGROAL_DATABASES_CONF_DST_FILE} + OUTPUTS ${PGAGROAL_DATABASES_CONF_DST_FILE} +) + +# man pages +add_custom_command( + TARGET man + DEPENDS ${PGAGROAL_DST_FILE} ${PGAGROAL_CLI_DST_FILE} ${PGAGROAL_ADMIN_DST_FILE} ${PGAGROAL_CONF_DST_FILE} ${PGAGROAL_HBA_CONF_DST_FILE} ${PGAGROAL_DATABASES_CONF_DST_FILE} +) + +# +# Install configuration and documentation +# +install(FILES etc/pgagroal.conf DESTINATION share/doc/pgagroal/etc) +install(FILES etc/pgagroal_hba.conf DESTINATION share/doc/pgagroal/etc) + +install(DIRECTORY . DESTINATION share/doc/pgagroal FILES_MATCHING PATTERN "*.md" PATTERN "etc" EXCLUDE PATTERN "images" EXCLUDE PATTERN "man" EXCLUDE PATTERN "manual" EXCLUDE) +install(DIRECTORY images/ DESTINATION share/doc/pgagroal/images FILES_MATCHING PATTERN "*.png") + +install(FILES ${PGAGROAL_DST_FILE} DESTINATION share/man/man1) +install(FILES ${PGAGROAL_CLI_DST_FILE} DESTINATION share/man/man1) +install(FILES ${PGAGROAL_ADMIN_DST_FILE} DESTINATION share/man/man1) +install(FILES ${PGAGROAL_CONF_DST_FILE} DESTINATION share/man/man5) +install(FILES ${PGAGROAL_HBA_CONF_DST_FILE} DESTINATION share/man/man5) +install(FILES ${PGAGROAL_DATABASES_CONF_DST_FILE} DESTINATION share/man/man5) + +# +# Generate manual +# +if(generation) + # Files + FILE(GLOB PREDS "manual/0?-*.md") + FILE(GLOB POSTDS "manual/9?-*.md") + FILE(GLOB TDS "tutorial/??_*.md") + FILE(GLOB UMDS_PRE "manual/user-0?-*.md") + FILE(GLOB UMDS_POST "manual/user-1?-*.md") + FILE(GLOB DMDS "manual/dev-??-*.md") + FILE(GLOB AG "manual/advanced/??-*.md") + + # Manuals definitions + set(MANUAL_OUTPUT_DIR "${CMAKE_BINARY_DIR}/doc") + get_filename_component(PARENT_DIR ${CMAKE_BINARY_DIR} DIRECTORY) + set(IMAGE_DIR "${PARENT_DIR}/doc/images") + + add_custom_target( + copy_images + COMMAND ${CMAKE_COMMAND} -E copy_directory ${IMAGE_DIR} ${CMAKE_BINARY_DIR}/images + COMMENT "Copy images" + ) + + add_custom_command( + OUTPUT ${MANUAL_OUTPUT_DIR}/pgagroal-user-guide.pdf + COMMAND ${PANDOC_EXECUTABLE} -o ${MANUAL_OUTPUT_DIR}/pgagroal-user-guide.pdf --from markdown --template eisvogel --listings -N --toc ${PREDS} ${UMDS_PRE} ${TDS} ${UMDS_POST} ${POSTDS} + DEPENDS ${UMDS} + COMMENT "Generating User Guide PDF documentation" + ) + add_custom_command( + OUTPUT ${MANUAL_OUTPUT_DIR}/pgagroal-dev-guide.pdf + COMMAND ${PANDOC_EXECUTABLE} -o ${MANUAL_OUTPUT_DIR}/pgagroal-dev-guide.pdf --from markdown --template eisvogel --listings -N --toc ${PREDS} ${DMDS} ${POSTDS} + DEPENDS ${DMDS} + COMMENT "Generating Developer Guide PDF documentation" + ) + + add_custom_command( + OUTPUT ${MANUAL_OUTPUT_DIR}/pgagroal-user-guide.html + COMMAND ${PANDOC_EXECUTABLE} -o ${MANUAL_OUTPUT_DIR}/pgagroal-user-guide.html -s -f markdown-smart -N --toc -t html5 ${PREDS} ${UMDS_PRE} ${TDS} ${UMDS_POST} ${POSTDS} + DEPENDS ${UMDS} + COMMENT "Generating User Guide HTML documentation" + ) + add_custom_command( + OUTPUT ${MANUAL_OUTPUT_DIR}/pgagroal-dev-guide.html + COMMAND ${PANDOC_EXECUTABLE} -o ${MANUAL_OUTPUT_DIR}/pgagroal-dev-guide.html -s -f markdown-smart -N --toc -t html5 ${PREDS} ${DMDS} ${POSTDS} + DEPENDS ${DMDS} + COMMENT "Generating Developer Guide HTML documentation" + ) + + add_custom_command( + OUTPUT ${MANUAL_OUTPUT_DIR}/pgagroal-advanced.pdf + COMMAND ${PANDOC_EXECUTABLE} -o ${MANUAL_OUTPUT_DIR}/pgagroal-advanced.pdf --from markdown --template eisvogel --listings -N --toc ${AG} + DEPENDS ${UMDS} + COMMENT "Generating Advanced PDF documentation" + ) + add_custom_command( + OUTPUT ${MANUAL_OUTPUT_DIR}/pgagroal-advanced.html + COMMAND ${PANDOC_EXECUTABLE} -o ${MANUAL_OUTPUT_DIR}/pgagroal-advanced.html -s -f markdown-smart -N --toc -t html5 ${AG} + DEPENDS ${DMDS} + COMMENT "Generating Advanced HTML documentation" + ) + + add_custom_target( + all_docs ALL + DEPENDS copy_images ${MANUAL_OUTPUT_DIR}/pgagroal-user-guide.pdf ${MANUAL_OUTPUT_DIR}/pgagroal-user-guide.html ${MANUAL_OUTPUT_DIR}/pgagroal-dev-guide.pdf ${MANUAL_OUTPUT_DIR}/pgagroal-dev-guide.html ${MANUAL_OUTPUT_DIR}/pgagroal-advanced.pdf ${MANUAL_OUTPUT_DIR}/pgagroal-advanced.html + ) + + install(FILES ${MANUAL_OUTPUT_DIR}/pgagroal-user-guide.pdf DESTINATION share/doc/pgagroal/manual) + install(FILES ${MANUAL_OUTPUT_DIR}/pgagroal-user-guide.html DESTINATION share/doc/pgagroal/manual) + install(FILES ${MANUAL_OUTPUT_DIR}/pgagroal-dev-guide.pdf DESTINATION share/doc/pgagroal/manual) + install(FILES ${MANUAL_OUTPUT_DIR}/pgagroal-dev-guide.html DESTINATION share/doc/pgagroal/manual) + install(FILES ${MANUAL_OUTPUT_DIR}/pgagroal-advanced.pdf DESTINATION share/doc/pgagroal/manual) + install(FILES ${MANUAL_OUTPUT_DIR}/pgagroal-advanced-guide.html DESTINATION share/doc/pgagroal/manual) + +endif() + +# +# Generate API docs +# +if (DOXYGEN_FOUND) + add_custom_target(api ALL) + + set(DOXYGEN_GENERATE_HTML YES) + set(DOXYGEN_GENERATE_MAN NO) + set(DOXYGEN_WARN_AS_ERROR FAIL_ON_WARNINGS) + set(DOXYFILE_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile.doxygen) + + doxygen_add_docs(doxygen ${PROJECT_SOURCE_DIR}/src/include/) + + if (${DOXYGEN_VERSION} STREQUAL "1.10.0" OR ${DOXYGEN_VERSION} STREQUAL "1.11.0" OR ${DOXYGEN_VERSION} STREQUAL "1.12.0") + add_custom_command( + TARGET api + COMMAND ${DOXYGEN_EXECUTABLE} -q ${DOXYFILE_OUT} + COMMENT "Generating API documentation" + ) + else() + add_custom_command( + TARGET api + COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYFILE_OUT} + COMMENT "Generating API documentation" + ) + endif() +endif() diff --git a/doc/CONFIGURATION.md b/doc/CONFIGURATION.md new file mode 100644 index 00000000..d9c0f262 --- /dev/null +++ b/doc/CONFIGURATION.md @@ -0,0 +1,216 @@ +# pgagroal configuration + +The configuration is loaded from either the path specified by the `-c` flag or `/etc/pgagroal/pgagroal.conf`. + +The configuration of [**pgagroal**](https://github.com/agroal/pgagroal) is split into sections using the `[` and `]` characters. + +The main section, called `[pgagroal]`, is where you configure the overall properties +of the connection pool. + +The other sections configure each one server. There can be no more than `64` server sections, therefore +no more than `64` servers configured as backends. +Server sections don't have any requirements to their naming so you can give them +meaningful names like `[primary]` for the primary [PostgreSQL](https://www.postgresql.org) +instance. + +In any case, it is not possible to have duplicated sections, that means section names must be unique within +the configuration file. + +All properties within a section are in the format `key = value`. + +The characters `#` and `;` can be used for comments. A line is totally ignored if the +very first non-space character is a comment one, but it is possible to put a comment at the end of a line. +The `Bool` data type supports the following values: `on`, `yes`, `1`, `true`, `off`, `no`, `0` and `false`. + +Each value can be quoted by means of `"` or `'`. Quoted strings must begin and end with the very same quoting character. It is possible to nest quotes. +As an example of configuration snippet including quoting and comments: + +``` +# This line is ignored since it starts with '#' +# and so is this one +log_line_prefix = "PGAGROAL #%Y-%m-%d-%H:%M:%S" # quoted because it contains spaces +log_type= console#log to stdout +pipeline = 'performance' # no need to quote since it does not contain any spaces + +``` + +See a more complete [sample](./etc/pgagroal.conf) configuration for running [**pgagroal**](https://github.com/agroal/pgagroal) on `localhost`. + +## [pgagroal] + +This section is mandatory and the pooler will refuse to start if the configuration file does not specify one and only one. Usually this section is place on top of the configuration file, but its position within the file does not really matter. +The available keys and their accepted values are reported in the table below. + +| Property | Default | Unit | Required | Description | +|----------|---------|------|----------|-------------| +| host | | String | Yes | The bind address for pgagroal | +| port | | Int | Yes | The bind port for pgagroal | +| unix_socket_dir | | String | Yes | The Unix Domain Socket location | +| metrics | 0 | Int | No | The metrics port (disable = 0) | +| metrics_cache_max_age | 0 | String | No | The number of seconds to keep in cache a Prometheus (metrics) response. If set to zero, the caching will be disabled. Can be a string with a suffix, like `2m` to indicate 2 minutes | +| metrics_cache_max_size | 256k | String | No | The maximum amount of data to keep in cache when serving Prometheus responses. Changes require restart. This parameter determines the size of memory allocated for the cache even if `metrics_cache_max_age` or `metrics` are disabled. Its value, however, is taken into account only if `metrics_cache_max_age` is set to a non-zero value. Supports suffixes: 'B' (bytes), the default if omitted, 'K' or 'KB' (kilobytes), 'M' or 'MB' (megabytes), 'G' or 'GB' (gigabytes).| +| management | 0 | Int | No | The remote management port (disable = 0) | +| log_type | console | String | No | The logging type (console, file, syslog) | +| log_level | info | String | No | The logging level, any of the (case insensitive) strings `FATAL`, `ERROR`, `WARN`, `INFO`, `DEBUG` and `TRACE`. The `DEBUG` keyword can be more specific such as `DEBUG1` up to `DEBUG5`; higher numbers mean higher verbosity. Debug level greater than 5 will be set to `DEBUG5`, while levels lower than 1 will be set to `DEBUG1`, and the application will raise a warning about the ignored value. The word `TRACE` is a synonim for `DEBUG5`. Not recognized values will make the log_level be `INFO` | +| log_path | pgagroal.log | String | No | The log file location. Can be a strftime(3) compatible string. | +| log_rotation_age | 0 | String | No | The age that will trigger a log file rotation. If expressed as a positive number, is managed as seconds. Supports suffixes: 'S' (seconds, the default), 'M' (minutes), 'H' (hours), 'D' (days), 'W' (weeks). A value of `0` disables. | +| log_rotation_size | 0 | String | No | The size of the log file that will trigger a log rotation. Supports suffixes: 'B' (bytes), the default if omitted, 'K' or 'KB' (kilobytes), 'M' or 'MB' (megabytes), 'G' or 'GB' (gigabytes). A value of `0` (with or without suffix) disables. | +| log_line_prefix | %Y-%m-%d %H:%M:%S | String | No | A strftime(3) compatible string to use as prefix for every log line. Must be quoted if contains spaces. | +| log_mode | append | String | No | Append to or create the log file (append, create) | +| log_connections | `off` | Bool | No | Log connects | +| log_disconnections | `off` | Bool | No | Log disconnects | +| blocking_timeout | 30 | Int | No | The number of seconds the process will be blocking for a connection (disable = 0) | +| idle_timeout | 0 | Int | No | The number of seconds a connection is been kept alive (disable = 0) | +| rotate_frontend_password_timeout | 0 | Int | No | The number of seconds after which the passwords of frontend users are updated periodically (disable = 0) | +| rotate_frontend_password_length | 8 | Int | No | The length of the randomized frontend password | +| max_connection_age | 0 | Int | No | The maximum number of seconds that a connection will live (disable = 0) | +| validation | `off` | String | No | Should connection validation be performed. Valid options: `off`, `foreground` and `background` | +| background_interval | 300 | Int | No | The interval between background validation scans in seconds | +| max_retries | 5 | Int | No | The maximum number of iterations to obtain a connection | +| max_connections | 100 | Int | No | The maximum number of connections to PostgreSQL (max 10000) | +| allow_unknown_users | `true` | Bool | No | Allow unknown users to connect | +| authentication_timeout | 5 | Int | No | The number of seconds the process will wait for valid credentials | +| pipeline | `auto` | String | No | The pipeline type (`auto`, `performance`, `session`, `transaction`) | +| auth_query | `off` | Bool | No | Enable authentication query | +| failover | `off` | Bool | No | Enable failover support | +| failover_script | | String | No | The failover script to execute | +| tls | `off` | Bool | No | Enable Transport Layer Security (TLS) | +| tls_cert_file | | String | No | Certificate file for TLS. This file must be owned by either the user running pgagroal or root. | +| tls_key_file | | String | No | Private key file for TLS. This file must be owned by either the user running pgagroal or root. Additionally permissions must be at least `0640` when owned by root or `0600` otherwise. | +| tls_ca_file | | String | No | Certificate Authority (CA) file for TLS. This file must be owned by either the user running pgagroal or root. | +| libev | `auto` | String | No | Select the [libev](http://software.schmorp.de/pkg/libev.html) backend to use. Valid options: `auto`, `select`, `poll`, `epoll`, `iouring`, `devpoll` and `port` | +| keep_alive | on | Bool | No | Have `SO_KEEPALIVE` on sockets | +| nodelay | on | Bool | No | Have `TCP_NODELAY` on sockets | +| non_blocking | off | Bool | No | Have `O_NONBLOCK` on sockets | +| backlog | `max_connections` / 4 | Int | No | The backlog for `listen()`. Minimum `16` | +| hugepage | `try` | String | No | Huge page support (`off`, `try`, `on`) | +| tracker | off | Bool | No | Track connection lifecycle | +| track_prepared_statements | off | Bool | No | Track prepared statements (transaction pooling) | +| pidfile | | String | No | Path to the PID file. If omitted, automatically set to `unix_socket_dir`/pgagroal.`port`.pid | +| update_process_title | `verbose` | String | No | The behavior for updating the operating system process title, mainly related to connection processes. Allowed settings are: `never` (or `off`), does not update the process title; `strict` to set the process title without overriding the existing initial process title length; `minimal` to set the process title to `username/database`; `verbose` (or `full`) to set the process title to `user@host:port/database`. Please note that `strict` and `minimal` are honored only on those systems that do not provide a native way to set the process title (e.g., Linux). On other systems, there is no difference between `strict` and `minimal` and the assumed behaviour is `minimal` even if `strict` is used. `never` and `verbose` are always honored, on every system. On Linux systems the process title is always trimmed to 255 characters, while on system that provide a natve way to set the process title it can be longer. | + +__Danger zone__ + +| Property | Default | Unit | Required | Description | +|----------|---------|------|----------|-------------| +| disconnect_client | 0 | Int | No | Disconnect clients that have been idle for more than the specified seconds. This setting __DOES NOT__ take long running transactions into account | +| disconnect_client_force | off | Bool | No | Disconnect clients that have been active for more than the specified seconds. This setting __DOES NOT__ take long running transactions into account | + +## Server section + +Each section with a name different from [**pgagroal**](https://github.com/agroal/pgagroal) will be treated as an host section. +There can be up to `64` host sections, each with an unique name and different combination of `host` and `port` settings, otherwise the pooler will complain about duplicated server configuration. + +| Property | Default | Unit | Required | Description | +|----------|---------|------|----------|-------------| +| host | | String | Yes | The address of the PostgreSQL instance | +| port | | Int | Yes | The port of the PostgreSQL instance | +| primary | | Bool | No | Identify the instance as primary (hint) | +| tls | `off` | Bool | No | Enable Transport Layer Security (TLS) support (Experimental - no pooling). Changes require restart. | +| tls_cert_file | | String | No | Certificate file for TLS. This file must be owned by either the user running pgagroal or root. Changes require restart. | +| tls_key_file | | String | No | Private key file for TLS. This file must be owned by either the user running pgagroal or root. Additionally permissions must be at least `0640` when owned by root or `0600` otherwise.Changes require restart. | +| tls_ca_file | | String | No | Certificate Authority (CA) file for TLS. This file must be owned by either the user running pgagroal or root. Changes require restart. | + +Note, that if `host` starts with a `/` it represents a path and [**pgagroal**](https://github.com/agroal/pgagroal) will connect using a Unix Domain Socket. + +# pgagroal_hba configuration + +The `pgagroal_hba` configuration controls access to [**pgagroal**](https://github.com/agroal/pgagroal) through host-based authentication. + +The configuration is loaded from either the path specified by the `-a` flag or `/etc/pgagroal/pgagroal_hba.conf`. + +The format of the file follows the similar [PostgreSQL HBA configuration format](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html), and as such looks like + +``` +# +# TYPE DATABASE USER ADDRESS METHOD +# +host all all all all +``` + +| Column | Required | Description | +|--------|----------|-------------| +| TYPE | Yes | Specifies the access method for clients. `host` and `hostssl` are supported | +| DATABASE | Yes | Specifies the database for the rule. Either specific name or `all` for all databases | +| USER | Yes | Specifies the user for the rule. Either specific name or `all` for all users | +| ADDRESS | Yes | Specifies the network for the rule. `all` for all networks, or IPv4 address with a mask (`0.0.0.0/0`) or IPv6 address with a mask (`::0/0`) | +| METHOD | Yes | Specifies the authentication mode for the user. `all` for all methods, otherwise `trust`, `reject`, `password`, `md5` or `scram-sha-256` | + +Remote management users needs to have their database set to `admin` in order for the entry to be considered. + +There could be up to `64` HBA entries in the configuration file. + +# pgagroal_databases configuration + +The `pgagroal_databases` configuration defines limits for a database or a user or both. The limits are the number +of connections from [**pgagroal**](https://github.com/agroal/pgagroal) to PostgreSQL for each entry. + +The file also defines the initial and minimum pool size for a database and user pair. Note, that this feature requires +a user definition file, see below. + +The configuration is loaded from either the path specified by the `-l` flag or `/etc/pgagroal/pgagroal_databases.conf`. + +``` +# +# DATABASE USER MAX_SIZE INITIAL_SIZE MIN_SIZE +# +mydb myuser all +anotherdb userB 10 5 3 +``` + +| Column | Required | Description | +|--------|----------|-------------| +| DATABASE | Yes | Specifies the database for the rule. `all` for all databases | +| USER | Yes | Specifies the user for the rule. `all` for all users | +| MAX_SIZE | Yes | Specifies the maximum pool size for the entry. `all` for all remaining counts from `max_connections` | +| INITIAL_SIZE | No | Specifies the initial pool size for the entry. `all` for `MAX_SIZE` connections. Default is 0 | +| MIN_SIZE | No | Specifies the minimum pool size for the entry. `all` for `MAX_SIZE` connections. Default is 0 | + + +There can be up to `64` entries in the configuration file. + +In the case a limit entry has incoherent values, for example `INITIAL_SIZE` smaller than `MIN_SIZE`, the system will try to automatically adjust the settings on the fly, reporting messages in the logs. + +The system will find the best match limit entry for a given `DATABASE`-`USER` pair according to the following rules: +1. Use the first entry with an exact `DATABASE` and `USER` match. +2. If there is no exact match, use the entry with a `USER` match and `DATABASE` set to `all`. +3. If Rule 2 does not apply, use the entry with a `DATABASE` match and `USER` set to `all`. + +# pgagroal_users configuration + +The `pgagroal_users` configuration defines the users known to the system. This file is created and managed through +the `pgagroal-admin` tool. + +The configuration is loaded from either the path specified by the `-u` flag or `/etc/pgagroal/pgagroal_users.conf`. + +There can be up to `64` users known to [**pgagroal**](https://github.com/agroal/pgagroal). + +# pgagroal_frontend_users configuration + +The `pgagroal_frontend_users` configuration defines the passwords for the users connecting to pgagroal. +This allows the setup to use different passwords for the [**pgagroal**](https://github.com/agroal/pgagroal) to PostgreSQL authentication. +This file is created and managed through the `pgagroal-admin` tool. + +All users defined in the frontend authentication must be defined in the user vault (`-u`). + +Frontend users (`-F`) requires a user vault (`-u`) to be defined. + +The configuration is loaded from either the path specified by the `-F` flag or `/etc/pgagroal/pgagroal_frontend_users.conf`. + +# pgagroal_admins configuration + +The `pgagroal_admins` configuration defines the administrators known to the system. This file is created and managed through +the `pgagroal-admin` tool. + +The configuration is loaded from either the path specified by the `-A` flag or `/etc/pgagroal/pgagroal_admins.conf`. + +If pgagroal has both Transport Layer Security (TLS) and `management` enabled then `pgagroal-cli` can +connect with TLS using the files `~/.pgagroal/pgagroal.key` (must be 0600 permission), +`~/.pgagroal/pgagroal.crt` and `~/.pgagroal/root.crt`. + +# pgagroal_superuser configuration + +The `pgagroal_superuser` configuration defines the superuser known to the system. This file is created and managed through +the `pgagroal-admin` tool. It may only have one user defined. + +The configuration is loaded from either the path specified by the `-S` flag or `/etc/pgagroal/pgagroal_superuser.conf`. diff --git a/doc/DEVELOPERS.md b/doc/DEVELOPERS.md new file mode 100644 index 00000000..2a4e18a1 --- /dev/null +++ b/doc/DEVELOPERS.md @@ -0,0 +1,354 @@ +# Developer guide + +For Fedora 40 + +## Install PostgreSql + +``` sh +dnf install postgresql-server +``` + +, this will install PostgreSQL 15. + +## Install pgagroal + +### Pre-install + +#### Basic dependencies + +``` sh +dnf install git gcc cmake make libev libev-devel openssl openssl-devel systemd systemd-devel python3-docutils libatomic zlib zlib-devel libzstd libzstd-devel lz4 lz4-devel bzip2 bzip2-devel +``` + +#### Generate user and developer guide + +This process is optional. If you choose not to generate the PDF and HTML files, you can opt out of downloading these dependencies, and the process will automatically skip the generation. + +1. Download dependencies + + ``` sh + dnf install pandoc texlive-scheme-basic + ``` + +2. Download Eisvogel + + Use the command `pandoc --version` to locate the user data directory. On Fedora systems, this directory is typically located at `$HOME/.local/share/pandoc`. + + Download the `Eisvogel` template for `pandoc`, please visit the [pandoc-latex-template](https://github.com/Wandmalfarbe/pandoc-latex-template) repository. For a standard installation, you can follow the steps outlined below. + + ```sh + wget https://github.com/Wandmalfarbe/pandoc-latex-template/releases/download/2.4.2/Eisvogel-2.4.2.tar.gz + tar -xzf Eisvogel-2.4.2.tar.gz + mkdir -p $HOME/.local/share/pandoc/templates + mv eisvogel.latex $HOME/.local/share/pandoc/templates/ + ``` + +3. Add package for LaTeX + + Download the additional packages required for generating PDF and HTML files. + + ```sh + dnf install 'tex(footnote.sty)' 'tex(footnotebackref.sty)' 'tex(pagecolor.sty)' 'tex(hardwrap.sty)' 'tex(mdframed.sty)' 'tex(sourcesanspro.sty)' 'tex(ly1enc.def)' 'tex(sourcecodepro.sty)' 'tex(titling.sty)' 'tex(csquotes.sty)' 'tex(zref-abspage.sty)' 'tex(needspace.sty)' + ``` + +#### Generate API guide + +This process is optional. If you choose not to generate the API HTML files, you can opt out of downloading these dependencies, and the process will automatically skip the generation. + +Download dependencies + + ``` sh + dnf install graphviz doxygen + ``` + +### Build + +``` sh +cd /usr/local +git clone https://github.com/agroal/pgagroal.git +cd pgagroal +mkdir build +cd build +cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr/local .. +make +make install +``` + +This will install [**pgagroal**](https://github.com/agroal/pgagroal) in the `/usr/local` hierarchy with the debug profile. + +### Check version + +You can navigate to `build/src` and execute `./pgagroal -?` to make the call. Alternatively, you can install it into `/usr/local/` and call it directly using: + +``` sh +pgagroal -? +``` + +If you see an error saying `error while loading shared libraries: libpgagroal.so.1: cannot open shared object` running the above command. you may need to locate where your `libpgagroal.so.1` is. It could be in `/usr/local/lib` or `/usr/local/lib64` depending on your environment. Add the corresponding directory into `/etc/ld.so.conf`. + +To enable these directories, you would typically add the following lines in your `/etc/ld.so.conf` file: + +``` sh +/usr/local/lib +/usr/local/lib64 +``` + +Remember to run `ldconfig` to make the change effective. + +## Setup pgagroal + +Let's give it a try. The basic idea here is that we will use two users: one is `postgres`, which will run PostgreSQL, and one is [**pgagroal**](https://github.com/agroal/pgagroal), which will run [**pgagroal**](https://github.com/agroal/pgagroal) to do backup of PostgreSQL. + +In many installations, there is already an operating system user named `postgres` that is used to run the PostgreSQL server. You can use the command + +``` sh +getent passwd | grep postgres +``` + +to check if your OS has a user named postgres. If not use + +``` sh +useradd -ms /bin/bash postgres +passwd postgres +``` + +If the postgres user already exists, don't forget to set its password for convenience. + +### 1. postgres + +Open a new window, switch to the `postgres` user. This section will always operate within this user space. + +``` sh +sudo su - +su - postgres +``` + +#### Initialize cluster + +If you use dnf to install your postgresql, chances are the binary file is in `/usr/bin/` + +``` sh +export PATH=/usr/bin:$PATH +initdb /tmp/pgsql +``` + +#### Remove default acess + +Remove last lines from `/tmp/pgsql/pg_hba.conf` + +``` ini +host all all 127.0.0.1/32 trust +host all all ::1/128 trust +host replication all 127.0.0.1/32 trust +host replication all ::1/128 trust +``` + +#### Add access for users and a database + +Add new lines to `/tmp/pgsql/pg_hba.conf` + +``` ini +host mydb myuser 127.0.0.1/32 scram-sha-256 +host mydb myuser ::1/128 scram-sha-256 +host postgres repl 127.0.0.1/32 scram-sha-256 +host postgres repl ::1/128 scram-sha-256 +``` + +#### Set password_encryption + +Set `password_encryption` value in `/tmp/pgsql/postgresql.conf` to be `scram-sha-256` + +``` sh +password_encryption = scram-sha-256 +``` + +For version 12/13, the default is `md5`, while for version 14 and above, it is `scram-sha-256`. Therefore, you should ensure that the value in `/tmp/pgsql/postgresql.conf` matches the value in `/tmp/pgsql/pg_hba.conf`. + +#### Start PostgreSQL + +``` sh +pg_ctl -D /tmp/pgsql/ start +``` + +Here, you may encounter issues such as the port being occupied or permission being denied. If you experience a failure, you can go to `/tmp/pgsql/log` to check the reason. + +You can use + +``` sh +pg_isready +``` + +to test + +#### Add users and a database + +``` sh +createuser -P myuser +createdb -E UTF8 -O myuser mydb +``` + +#### Verify access + +For the user `myuser` (standard) use `mypass` + +``` sh +psql -h localhost -p 5432 -U myuser mydb +\q +``` + +#### Add pgagroal user + +``` sh +sudo su - +useradd -ms /bin/bash pgagroal +passwd pgagroal +exit +``` + +#### Create pgagroal configuration + +Create the `pgagroal_hba.conf` configuration file + +``` ini +cat > pgagroal_hba.conf +# +# TYPE DATABASE USER ADDRESS METHOD +# +host all all all all +``` + +Create the `pgagroal.conf` configuration file + +``` ini +cat > pgagroal.conf +[pgagroal] +host = * +port = 2345 + +log_type = file +log_level = info +log_path = /tmp/pgagroal.log + +max_connections = 100 +idle_timeout = 600 +validation = off +unix_socket_dir = /tmp/ + +[primary] +host = localhost +port = 5432 +``` + +In our main section called `[pgagroal]` we setup [**pgagroal**](https://github.com/agroal/pgagroal) to listen on all network addresses. +Logging will be performed at `info` level and put in a file called `/tmp/pgagroal.log`. We will use 100 connections at a maximum, and they will idle out after 10 minutes. No validation will be performed. +Last we specify the location of the `unix_socket_dir` used for management operations. + +Next we create a section called `[primary]` which has the information about our PostgreSQL instance. In this case it is running on localhost on port 5432. + +#### Start pgagroal + +``` sh +pgagroal -c pgagroal.conf -a pgagroal_hba.conf +``` + +#### Connect through pgagroal + +``` sh +psql -h localhost -p 2345 -U myuser mydb +``` + +#### Shutdown pgagroal + +``` sh +pgagroal-cli -c pgagroal.conf shutdown +``` + +## Basic git guide + +Here are some links that will help you + +* [How to Squash Commits in Git](https://www.git-tower.com/learn/git/faq/git-squash) +* [ProGit book](https://github.com/progit/progit2/releases) + +### Start by forking the repository + +This is done by the "Fork" button on GitHub. + +### Clone your repository locally + +This is done by + +```sh +git clone git@github.com:/pgagroal.git +``` + +### Add upstream + +Do + +```sh +cd pgagroal +git remote add upstream https://github.com/agroal/pgagroal.git +``` + +### Do a work branch + +```sh +git checkout -b mywork main +``` + +### Make the changes + +Remember to verify the compile and execution of the code + +### AUTHORS + +Remember to add your name to + +``` +AUTHORS +``` + +in your first pull request + +### Multiple commits + +If you have multiple commits on your branch then squash them + +``` sh +git rebase -i HEAD~2 +``` + +for example. It is `p` for the first one, then `s` for the rest + +### Rebase + +Always rebase + +``` sh +git fetch upstream +git rebase -i upstream/main +``` + +### Force push + +When you are done with your changes force push your branch + +``` sh +git push -f origin mywork +``` + +and then create a pull requests for it + +### Repeat + +Based on feedback keep making changes, squashing, rebasing and force pushing + +### Undo + +Normally you can reset to an earlier commit using `git reset --hard`. +But if you accidentally squashed two or more commits, and you want to undo that, +you need to know where to reset to, and the commit seems to have lost after you rebased. + +But they are not actually lost - using `git reflog`, you can find every commit the HEAD pointer +has ever pointed to. Find the commit you want to reset to, and do `git reset --hard`. diff --git a/doc/DISTRIBUTIONS.md b/doc/DISTRIBUTIONS.md new file mode 100644 index 00000000..eaea5460 --- /dev/null +++ b/doc/DISTRIBUTIONS.md @@ -0,0 +1,59 @@ +# Compiling `pgagroal` from sources + +[**pgagroal**](https://github.com/agroal/pgagroal) requires the following dependencies: + +* a C compiler like [gcc 8+](https://gcc.gnu.org) (C17) or [clang 8+](https://clang.llvm.org/) +* [cmake](https://cmake.org) +* [GNU make](https://www.gnu.org/software/make/) or BSD `make` +* [libev](http://software.schmorp.de/pkg/libev.html) +* [OpenSSL](http://www.openssl.org/) +* [rst2man](https://docutils.sourceforge.io/) +* [libatomic](https://gcc.gnu.org/wiki/Atomic) +* [Doxygen](https://doxygen.nl/index.html) +* [pdflatex](https://tug.org/texlive/) +* [zlib](https://zlib.net) +* [zstd](http://www.zstd.net) +* [lz4](https://lz4.github.io/lz4/) +* [bzip2](http://sourceware.org/bzip2/) +* on Linux platforms, there is also the need for + * [systemd](https://www.freedesktop.org/wiki/Software/systemd/) + + + +## Compiling on Rocky Linux + +All the dependencies can be installed via `dnf(8)` as follows: + +```sh +dnf install git gcc cmake make \ + libev libev-devel \ + openssl openssl-devel \ + systemd systemd-devel \ + python3-docutils \ + libatomic \ + zlib zlib-devel \ + libzstd libzstd-devel \ + lz4 lz4-devel \ + bzip2 bzip2-devel +``` + +Please note that, on Rocky Linux, in order to install the `python3-docutils` +package (that provides `rst2man` executable), you need to enable the `crb` repository: + +```sh +dnf config-manager --set-enabled crb +``` + +## Compiling on FreeBSD + +All the dependencies can be installed via `pkg(8)` as follows: + +```sh +pkg install cmake \ + libev libevent \ + py311-docutils \ + lzlib \ + liblz4 \ + lbizp2 \ + texlive-formats +``` diff --git a/doc/FAILOVER.md b/doc/FAILOVER.md new file mode 100644 index 00000000..f2f22726 --- /dev/null +++ b/doc/FAILOVER.md @@ -0,0 +1,42 @@ +# pgagroal failover + +pgagroal can failover a PostgreSQL instance if the clients can't write to it. + +In `pgagroal.conf` define + +``` +failover = on +failover_script = /path/to/myscript.sh +``` + +The script will be run as the same user as the pgagroal process so proper +permissions (access and execution) must be in place. + +The following information is passed to the script as parameters + +1. Old primary host +2. Old primary port +3. New primary host +4. New primary port + +so a script could look like + +```sh +#!/bin/bash + +OLD_PRIMARY_HOST=$1 +OLD_PRIMARY_PORT=$2 +NEW_PRIMARY_HOST=$3 +NEW_PRIMARY_PORT=$4 + +ssh -tt -o StrictHostKeyChecking=no postgres@${NEW_PRIMARY_HOST} pg_ctl promote -D /mnt/pgdata + +if [ $? -ne 0 ]; then + exit 1 +fi + +exit 0 +``` + +The script is assumed successful if it has an exit code of 0. Otherwise both servers will be +recorded as failed. diff --git a/doc/GETTING_STARTED.md b/doc/GETTING_STARTED.md new file mode 100644 index 00000000..f7454425 --- /dev/null +++ b/doc/GETTING_STARTED.md @@ -0,0 +1,298 @@ +# Getting started with pgagroal + +First of all, make sure that [**pgagroal**](https://github.com/agroal/pgagroal) is installed and in your path by +using `pgagroal -?`. You should see + +``` +pgagroal 2.0.0 + High-performance connection pool for PostgreSQL + +Usage: + pgagroal [ -c CONFIG_FILE ] [ -a HBA_FILE ] [ -d ] + +Options: + -c, --config CONFIG_FILE Set the path to the pgagroal.conf file + -a, --hba HBA_FILE Set the path to the pgagroal_hba.conf file + -l, --limit LIMIT_FILE Set the path to the pgagroal_databases.conf file + -u, --users USERS_FILE Set the path to the pgagroal_users.conf file + -F, --frontend FRONTEND_USERS_FILE Set the path to the pgagroal_frontend_users.conf file + -A, --admins ADMINS_FILE Set the path to the pgagroal_admins.conf file + -S, --superuser SUPERUSER_FILE Set the path to the pgagroal_superuser.conf file + -d, --daemon Run as a daemon + -V, --version Display version information + -?, --help Display help +``` + +If you don't have [**pgagroal**](https://github.com/agroal/pgagroal) in your path see [README](../README.md) on how to +compile and install [**pgagroal**](https://github.com/agroal/pgagroal) in your system. + +## Configuration + +Lets create a simple configuration file called `pgagroal.conf` with the content + +``` +[pgagroal] +host = * +port = 2345 + +log_type = file +log_level = info +log_path = /tmp/pgagroal.log + +max_connections = 100 +idle_timeout = 600 +validation = off +unix_socket_dir = /tmp/ + +[primary] +host = localhost +port = 5432 +``` + +In our main section called `[pgagroal]` we setup [**pgagroal**](https://github.com/agroal/pgagroal) to listen on all +network addresses on port 2345. Logging will be performed at `info` level and +put in a file called `/tmp/pgagroal.log`. We want a maximum of 100 connections +that are being closed if they have been idle for 10 minutes, and we also specify that +we don't want any connection validation to be performed. Last we specify the +location of the `unix_socket_dir` used for management operations. + +Next we create a section called `[primary]` which has the information about our +[PostgreSQL](https://www.postgresql.org) instance. In this case it is running +on `localhost` on port `5432`. + +Now we need a host based authentication (HBA) file. Create one called `pgagroal_hba.conf` +with the content + +``` +# +# TYPE DATABASE USER ADDRESS METHOD +# +host all all all all +``` + +This tells [**pgagroal**](https://github.com/agroal/pgagroal) that it can accept connections from all network addresses +for all databases and all user names. + +We are now ready to run [**pgagroal**](https://github.com/agroal/pgagroal). + +See [Configuration](./CONFIGURATION.md) for all configuration options. + +## Running + +We will run [**pgagroal**](https://github.com/agroal/pgagroal) using the command + +``` +pgagroal -c pgagroal.conf -a pgagroal_hba.conf +``` + +If this doesn't give an error, then we are ready to connect. + +We will assume that we have a user called `test` with the password `test` in our +[PostgreSQL](https://www.postgresql.org) instance. See their +[documentation](https://www.postgresql.org/docs/current/index.html) on how to setup +[PostgreSQL](https://www.postgresql.org), [add a user](https://www.postgresql.org/docs/current/app-createuser.html) +and [add a database](https://www.postgresql.org/docs/current/app-createdb.html). + +We will connect to [**pgagroal**](https://github.com/agroal/pgagroal) using the [psql](https://www.postgresql.org/docs/current/app-psql.html) +application. + +``` +psql -h localhost -p 2345 -U test test +``` + +That should give you a password prompt where `test` should be typed in. You are now connected +to [PostgreSQL](https://www.postgresql.org) through [**pgagroal**](https://github.com/agroal/pgagroal). + +Type `\q` to quit [psql](https://www.postgresql.org/docs/current/app-psql.html) and [**pgagroal**](https://github.com/agroal/pgagroal) +will now put the connection that you used into its pool. + +If you type the above `psql` command again [**pgagroal**](https://github.com/agroal/pgagroal) will reuse the existing connection and +thereby lower the overhead of getting a connection to [PostgreSQL](https://www.postgresql.org). + +Now you are ready to point your applications to use [**pgagroal**](https://github.com/agroal/pgagroal) instead of going directly to +[PostgreSQL](https://www.postgresql.org). [**pgagroal**](https://github.com/agroal/pgagroal) will work with any +[PostgreSQL](https://www.postgresql.org) compliant driver, for example [pgjdbc](https://jdbc.postgresql.org/), +[Npgsql](https://www.npgsql.org/) and [pq](https://github.com/lib/pq). + +[**pgagroal**](https://github.com/agroal/pgagroal) is stopped by pressing Ctrl-C (`^C`) in the console where you started it, or by sending +the `SIGTERM` signal to the process using `kill `. + +## Run-time administration + +[**pgagroal**](https://github.com/agroal/pgagroal) has a run-time administration tool called `pgagroal-cli`. + +You can see the commands it supports by using `pgagroal-cli -?` which will give + +``` +pgagroal-cli 2.0.0 + Command line utility for pgagroal + +Usage: + pgagroal-cli [ OPTIONS ] [ COMMAND ] + +Options: + -c, --config CONFIG_FILE Set the path to the pgagroal.conf file + Default: /etc/pgagroal/pgagroal.conf + -h, --host HOST Set the host name + -p, --port PORT Set the port number + -U, --user USERNAME Set the user name + -P, --password PASSWORD Set the password + -L, --logfile FILE Set the log file + -F, --format text|json Set the output format + -v, --verbose Output text string of result + -V, --version Display version information + -?, --help Display help + +Commands: + flush [mode] [database] Flush connections according to . + Allowed modes are: + - 'gracefully' (default) to flush all connections gracefully + - 'idle' to flush only idle connections + - 'all' to flush all connections. USE WITH CAUTION! + If no name is specified, applies to all databases. + ping Verifies if pgagroal is up and running + enable [database] Enables the specified databases (or all databases) + disable [database] Disables the specified databases (or all databases) + shutdown [mode] Stops pgagroal pooler. The can be: + - 'gracefully' (default) waits for active connections to quit + - 'immediate' forces connections to close and terminate + - 'cancel' avoid a previously issued 'shutdown gracefully' + status [details] Status of pgagroal, with optional details + switch-to Switches to the specified primary server + conf Manages the configuration (e.g., reloads the configuration + The subcommand can be: + - 'reload' to issue a configuration reload; + - 'get' to obtain information about a runtime configuration value; + conf get + - 'set' to modify a configuration value; + conf set ; + - 'ls' lists the configuration files used. + clear Resets either the Prometheus statistics or the specified server. + can be + - 'server' (default) followed by a server name + - a server name on its own + - 'prometheus' to reset the Prometheus metrics + +pgagroal: +Report bugs: +``` + +This tool can be used on the machine running [**pgagroal**](https://github.com/agroal/pgagroal) to flush connections. + +To flush all idle connections you would use + +``` +pgagroal-cli -c pgagroal.conf flush idle +``` + +To stop pgagroal you would use + +``` +pgagroal-cli -c pgagroal.conf stop +``` + +Check the outcome of the operations by verifying the exit code, like + +``` +echo $? +``` + +or by using the `-v` flag. + +If pgagroal has both Transport Layer Security (TLS) and `management` enabled then `pgagroal-cli` can +connect with TLS using the files `~/.pgagroal/pgagroal.key` (must be 0600 permission), +`~/.pgagroal/pgagroal.crt` and `~/.pgagroal/root.crt`. + +## Administration + +[**pgagroal**](https://github.com/agroal/pgagroal) has an administration tool called `pgagroal-admin`, which is used to control user +registration with [**pgagroal**](https://github.com/agroal/pgagroal). + +You can see the commands it supports by using `pgagroal-admin -?` which will give + +``` +pgagroal-admin 2.0.0 + Administration utility for pgagroal + +Usage: + pgagroal-admin [ -f FILE ] [ COMMAND ] + +Options: + -f, --file FILE Set the path to a user file + Defaults to /etc/pgagroal/pgagroal_users.conf + -U, --user USER Set the user name + -P, --password PASSWORD Set the password for the user + -g, --generate Generate a password + -l, --length Password length + -V, --version Display version information + -?, --help Display help + +Commands: + master-key Create or update the master key + user Manage a specific user, where can be + - add to add a new user + - del to remove an existing user + - edit to change the password for an existing user + - ls to list all available users + +pgagroal: https://agroal.github.io/pgagroal/ +Report bugs: https://github.com/agroal/pgagroal/issues +``` + +In order to set the master key for all users you can use + +``` +pgagroal-admin -g master-key +``` + +The master key must be at least 8 characters. + +Then use the other commands to add, update, remove or list the current user names, f.ex. + +``` +pgagroal-admin -f pgagroal_users.conf user add +``` + +## Next Steps + +Next steps in improving pgagroal's configuration could be + +* Update `pgagroal.conf` with the required settings for your system +* Set the access rights in `pgagroal_hba.conf` for each user and database +* Add a `pgagroal_users.conf` file using `pgagroal-admin` with a list of known users +* Disable access for unknown users by setting `allow_unknown_users` to `false` +* Define a `pgagroal_databases.conf` file with the limits and prefill settings for each database +* Enable Transport Layer Security v1.2+ (TLS) +* Deploy Grafana dashboard + +See [Configuration](./CONFIGURATION.md) for more information on these subjects. + +## Tutorials + +There are a few short tutorials available to help you better understand and configure [**pgagroal**](https://github.com/agroal/pgagroal): +- [Installing pgagroal](https://github.com/agroal/pgagroal/blob/master/doc/tutorial/01_install.md) +- [Enabling prefill](https://github.com/agroal/pgagroal/blob/master/doc/tutorial/02_prefill.md) +- [Enabling remote management](https://github.com/agroal/pgagroal/blob/master/doc/tutorial/03_remote_management.md) +- [Enabling Prometheus metrics](https://github.com/agroal/pgagroal/blob/master/doc/tutorial/04_prometheus.md) +- [Enabling split security](https://github.com/agroal/pgagroal/blob/master/doc/tutorial/05_split_security.md) + +## Closing + +The [pgagroal](https://github.com/agroal/pgagroal) community hopes that you find +the project interesting. + +Feel free to + +* [Ask a question](https://github.com/agroal/pgagroal/discussions) +* [Raise an issue](https://github.com/agroal/pgagroal/issues) +* [Submit a feature request](https://github.com/agroal/pgagroal/issues) +* [Write a code submission](https://github.com/agroal/pgagroal/pulls) + +All contributions are most welcome ! + +Please, consult our [Code of Conduct](../CODE_OF_CONDUCT.md) policies for interacting in our +community. + +Consider giving the project a [star](https://github.com/agroal/pgagroal/stargazers) on +[GitHub](https://github.com/agroal/pgagroal/) if you find it useful. And, feel free to follow +the project on [Twitter](https://twitter.com/pgagroal/) as well. diff --git a/doc/PERFORMANCE.md b/doc/PERFORMANCE.md new file mode 100644 index 00000000..ba8162f3 --- /dev/null +++ b/doc/PERFORMANCE.md @@ -0,0 +1,63 @@ +# pgagroal performance + +Performance is an important goal for [**pgagroal**](https://github.com/agroal/pgagroal) and effort have been made +to make [**pgagroal**](https://github.com/agroal/pgagroal) scale and use a limited number of resources. + +This report describe [**pgagroal**](https://github.com/agroal/pgagroal) in relationship to 3 other [PostgreSQL](https://www.postgresql.org) +connection pool implementations, which we will call `a`, `b` and `c`. + +The [pgbench](https://www.postgresql.org/docs/11/pgbench.html) program was used in the runs. All pool +configurations were made with performance in mind. + +All diagrams are using the same identifier for the connection pool in question, so `a` is `a` in all +diagrams and so on. + +The runs were performed on [RHEL](https://www.redhat.com/en/technologies/linux-platforms/enterprise-linux) 7.7 / +[EPEL](https://access.redhat.com/solutions/3358) / [DevTools](https://developers.redhat.com/products/developertoolset/overview) 8 +based machines on 10G network. All connection pools were the latest versions as of January 14, 2020. [**pgagroal**](https://github.com/agroal/pgagroal) was +using the `epoll` mode of [libev](http://software.schmorp.de/pkg/libev.html). + +## Simple + +This run uses + +``` +pgbench -M simple +``` + +![pgbench simple](https://github.com/agroal/pgagroal/raw/master/doc/images/perf-simple.png "pgbench simple") + +## Extended + +This run uses + +``` +pgbench -M extended +``` + +![pgbench extended](https://github.com/agroal/pgagroal/raw/master/doc/images/perf-extended.png "pgbench extended") + +## Prepared + +This run uses + +``` +pgbench -M prepared +``` + +![pgbench prepared](https://github.com/agroal/pgagroal/raw/master/doc/images/perf-prepared.png "pgbench prepared") + +## ReadOnly + +This run uses + +``` +pgbench -S -M prepared +``` + +![pgbench readonly](https://github.com/agroal/pgagroal/raw/master/doc/images/perf-readonly.png "pgbench readonly") + +## Closing + +**Please**, run your own benchmarks to see how [**pgagroal**](https://github.com/agroal/pgagroal) compare to your existing connection pool +deployment. diff --git a/doc/PIPELINES.md b/doc/PIPELINES.md new file mode 100644 index 00000000..52d9ee5f --- /dev/null +++ b/doc/PIPELINES.md @@ -0,0 +1,112 @@ +# pgagroal pipelines + +pgagroal supports 3 different pipelines + +* Performance +* Session +* Transaction + +The pipeline is defined in `pgagroal.conf` under the setting of + +``` +pipeline = auto +``` + +pgagroal will choose either the performance or the session pipeline +based on the configuration settings by default. + +# Performance + +The performance pipeline is fastest pipeline as it is a minimal implementation +of the pipeline architecture. + +However, it doesn't support Transport Layer Security (TLS), failover support and +the `disconnect_client` setting. + +A `DISCARD ALL` query is run after each client session. + +Select the performance pipeline by + +``` +pipeline = performance +``` + +# Session + +The session pipeline supports all features of pgagroal. + +A `DISCARD ALL` query is run after each client session. + +Select the session pipeline by + +``` +pipeline = session +``` + +# Transaction + +The transaction pipeline will release the connection back to the pool after each +transaction completes. This feature will support many more clients than there are +database connections. + +The transaction pipeline requires that there are users defined such that connections +can be kept in the pool in all security scenarios. + +However, there are some session based features of PostgreSQL that can't be supported in this +pipeline. + +* `SET` / `RESET` +* `LISTEN` / `NOTIFY` +* `WITH HOLD CURSOR` +* `PREPARE` / `DEALLOCATE` + +It is assumed that all clients using the same user name and database pair share the same +startup parameters towards PostgreSQL. + +__`SET` / `RESET`__ + +The `SET` functionality is a session based feature. + +__`LISTEN` / `NOTIFY`__ + +The `LISTEN` functionality is a session based feature. + +__`WITH HOLD CURSOR`__ + +The `WITH HOLD CURSOR` functionality is a session based feature. + +__`PREPARE` / `DEALLOCATE`__ + +While using `PREPARE` and `EXECUTE` can be used the prepared statements are tied to the +connection they were created in which means that clients can't be sure that they created +the prepared statement on the connection unless it is issued within the same transaction +where it is used. + +pgagroal can issue a `DEALLOCATE ALL` statement before a connection is returned back to +the pool if the `track_prepared_statements` setting is set to `on`. If `off` then no +statement is issued. + +Note, that pgagroal does not issue a `DISCARD ALL` statement when using the transaction +pipeline. + +__Performance considerations__ + +Clients may need to wait for a connection between transactions leading to a higher +latency. + +__Important__ + +Make sure that the `blocking_timeout` settings to set to 0. Otherwise active clients +may timeout during their workload. Likewise it is best to disable idle connection timeout +and max connection age by setting `idle_timeout` and `max_connection_age` to 0. + +It is highly recommended that you prefill all connections for each user. + +The transaction pipeline doesn't support the `disconnect_client` or +`allow_unknown_users` settings. + +Select the transaction pipeline by + +``` +pipeline = transaction +``` diff --git a/doc/RPM.md b/doc/RPM.md new file mode 100644 index 00000000..b81544a8 --- /dev/null +++ b/doc/RPM.md @@ -0,0 +1,35 @@ +# pgagroal rpm + +[**pgagroal**](https://github.com/agroal/pgagroal) can be built into a RPM for [Fedora](https://getfedora.org/) systems. + +## Requirements + +```sh +dnf install gcc rpm-build rpm-devel rpmlint make python bash coreutils diffutils patch rpmdevtools chrpath +``` + +## Setup RPM development + +```sh +rpmdev-setuptree +``` + +## Create source package + +```sh +git clone https://github.com/agroal/pgagroal.git +cd pgagroal +mkdir build +cd build +cmake -DCMAKE_BUILD_TYPE=Release .. +make package_source +``` + +## Create RPM package + +```sh +cp pgagroal-$VERSION.tar.gz ~/rpmbuild/SOURCES +QA_RPATHS=0x0001 rpmbuild -bb pgagroal.spec +``` + +The resulting RPM will be located in `~/rpmbuild/RPMS/x86_64/`, if your architecture is `x86_64`. diff --git a/doc/SECURITY.md b/doc/SECURITY.md new file mode 100644 index 00000000..2147248b --- /dev/null +++ b/doc/SECURITY.md @@ -0,0 +1,72 @@ +# pgagroal security + +## Pass-through security + +pgagroal use pass-through security by default. + +This means that pgagroal delegates to PostgreSQL to determine if the credentials used are valid. + +Once a connection is obtained pgagroal will replay the previous communication sequence to verify +the new client. This only works for connections using `trust`, `password` or `md5` authentication +methods, so `scram-sha-256` based connections are not cached. + +Note, that this can lead to replay attacks against the `md5` based connections since the hash +doesn't change. Make sure that pgagroal is deployed on a private trusted network, but consider +using either a user vault or authentication query instead. + +## User vault + +A user vault is a vault which defines the known users and their password. + +The vault is static, and is managed through the `pgagroal-admin` tool. + +The user vault is specified using the `-u` or `--users` command line parameter. + +### Frontend users + +The `-F` or `--frontend` command line parameter allows users to be defined for the client to +[**pgagroal**](https://github.com/agroal/pgagroal) authentication. This allows the setup to use different passwords for the [**pgagroal**](https://github.com/agroal/pgagroal) to +PostgreSQL authentication. + +All users defined in the frontend authentication must be defined in the user vault (`-u`). + +Frontend users (`-F`) requires a user vault (`-u`) to be defined. + +## Authentication query + +Authentication query will use the below defined function to query the database +for the user password + +``` +CREATE FUNCTION public.pgagroal_get_password( + IN p_user name, + OUT p_password text +) RETURNS text +LANGUAGE sql SECURITY DEFINER SET search_path = pg_catalog AS +$$SELECT passwd FROM pg_shadow WHERE usename = p_user$$; +``` + +This function needs to be installed in each database. + +The function requires a user that is able to execute it, like + +``` +-- Create a role used for the authentication query +CREATE ROLE pgagroal LOGIN; +-- Set the password +\password pgagroal + +-- Only allow access to "pgagroal" +REVOKE EXECUTE ON FUNCTION public.pgagroal_get_password(name) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION public.pgagroal_get_password(name) TO pgagroal; +``` + +Make sure that the user is different from the actual application users accessing +the database. The user accessing the function needs to have its credential present +in the vault passed to the `-S` or `--superuser` command line parameter. + +The user executing the authentication query must use either a MD5 or a SCRAM-SHA-256 +password protected based account. + +Note, that authentication query doesn't support user vaults - user vault (`-u`) and frontend users (`-F`) - +as well as limits (`-l`). diff --git a/doc/VAULT.md b/doc/VAULT.md new file mode 100644 index 00000000..1cd01d9a --- /dev/null +++ b/doc/VAULT.md @@ -0,0 +1,91 @@ +# pgagroal-vault configuration + +The configuration which is mandatory is loaded from either the path specified by the `-c` flag or `/etc/pgagroal/pgagroal_vault.conf`. + +The configuration of `pgagroal-vault` is split into sections using the `[` and `]` characters. + +The pgagroal-vault section, called `[pgagroal-vault]`, is where you configure the overall properties of the vault's server. + +The other section provide configuration for the management port of pgagroal. For now there can be only one pgagroal management port to connect. +This section don't have any requirements to their naming so you can give them +meaningful names but generally named as `[main]`. + +All properties within a section are in the format `key = value`. + +The characters `#` and `;` can be used for comments. A line is totally ignored if the +very first non-space character is a comment one, but it is possible to put a comment at the end of a line. +The `Bool` data type supports the following values: `on`, `yes`, `1`, `true`, `off`, `no`, `0` and `false`. + +See a more complete [sample](./etc/pgagroal_vault.conf) configuration for running `pgagroal-vault` on `localhost`. + +## [pgagroal-vault] + +This section is mandatory and the pooler will refuse to start if the configuration file does not specify one and only one. Usually this section is place on top of the configuration file, but its position within the file does not really matter. +The available keys and their accepted values are reported in the table below. + +| Property | Default | Unit | Required | Description | +|----------|---------|------|----------|-------------| +| host | | String | Yes | The bind address for pgagroal-vault | +| port | | Int | Yes | The bind port for pgagroal-vault | +| metrics | 0 | Int | No | The metrics port (disable = 0) | +| metrics_cache_max_age | 0 | String | No | The number of seconds to keep in cache a Prometheus (metrics) response. If set to zero, the caching will be disabled. Can be a string with a suffix, like `2m` to indicate 2 minutes | +| metrics_cache_max_size | 256k | String | No | The maximum amount of data to keep in cache when serving Prometheus responses. Changes require restart. This parameter determines the size of memory allocated for the cache even if `metrics_cache_max_age` or `metrics` are disabled. Its value, however, is taken into account only if `metrics_cache_max_age` is set to a non-zero value. Supports suffixes: 'B' (bytes), the default if omitted, 'K' or 'KB' (kilobytes), 'M' or 'MB' (megabytes), 'G' or 'GB' (gigabytes).| +| authentication_timeout | 5 | Int | No | The number of seconds the process will wait for valid credentials | +| log_type | console | String | No | The logging type (console, file, syslog) | +| log_level | info | String | No | The logging level, any of the (case insensitive) strings `FATAL`, `ERROR`, `WARN`, `INFO` and `DEBUG` (that can be more specific as `DEBUG1` thru `DEBUG5`). Debug level greater than 5 will be set to `DEBUG5`. Not recognized values will make the log_level be `INFO` | +| log_path | pgagroal.log | String | No | The log file location. Can be a strftime(3) compatible string. | +| log_rotation_age | 0 | String | No | The age that will trigger a log file rotation. If expressed as a positive number, is managed as seconds. Supports suffixes: 'S' (seconds, the default), 'M' (minutes), 'H' (hours), 'D' (days), 'W' (weeks). A value of `0` disables. | +| log_rotation_size | 0 | String | No | The size of the log file that will trigger a log rotation. Supports suffixes: 'B' (bytes), the default if omitted, 'K' or 'KB' (kilobytes), 'M' or 'MB' (megabytes), 'G' or 'GB' (gigabytes). A value of `0` (with or without suffix) disables. | +| log_line_prefix | %Y-%m-%d %H:%M:%S | String | No | A strftime(3) compatible string to use as prefix for every log line. Must be quoted if contains spaces. | +| log_mode | append | String | No | Append to or create the log file (append, create) | +| log_connections | `off` | Bool | No | Log connects | +| log_disconnections | `off` | Bool | No | Log disconnects | +| hugepage | `try` | String | No | Huge page support (`off`, `try`, `on`) | +| tls | `off` | Bool | No | Enable Transport Layer Security (TLS) | +| tls_cert_file | | String | No | Certificate file for TLS. This file must be owned by either the user running pgagroal or root. | +| tls_key_file | | String | No | Private key file for TLS. This file must be owned by either the user running pgagroal or root. Additionally permissions must be at least `0640` when owned by root or `0600` otherwise. | +| tls_ca_file | | String | No | Certificate Authority (CA) file for TLS. This file must be owned by either the user running pgagroal or root. | + +## [main] + +The section with a name different from `pgagroal-vault` will be treated as a main section. + +| Property | Default | Unit | Required | Description | +|----------|---------|------|----------|-------------| +| host | | String | Yes | The address of the pgagroal running the management server | +| port | | Int | Yes | The management port of pgagroal | +| user | | String | Yes | The admin user of the pgagroal remote management service | + +Note: For `pgagroal-vault` to function and connect properly to pgagroal, the remote server for management of the [**pgagroal**](https://github.com/agroal/pgagroal) should be enabled i.e. `management` should be greater than 0. + +# Enable SSL connection with management port + +The SSL handshake has to be initiated between `vault` and `remote` of `pgagroal` to enable secured SSL connection between the both. + +The `vault` serves as the client, while the `remote` functions as the server. The `vault` initiates the SSL/TLS handshake with the `remote`, and concurrently, the `remote` accepts the SSL/TLS request from the `vault`. + +## SSL configuration at the server side + +Update the `[pgagroal]` section in main configuration file by including the following:- + +- Enable management port +- Enable `tls` to `on` +- Add `tls_cert_file`, `tls_key_file` and `tls_ca_file` fields + +``` +management = 2347 +tls = on +tls_cert_file = /path/to/server_cert_file +tls_key_file = /path/to/server_key_file +tls_ca_file = /path/to/CA_root_cert_file +``` + +## SSL configuration at the client side + +For client side authentication, add the required certificates like the certificate of `vault` in `pgagroal.crt`, private key of `vault` in `pgagroal.key` and the root certificate of CA in `root.crt` in the `.pgagroal` directory. + +Some permissions requirements:- + +- The certificate file `pgagroal.crt` must be owned by either the user running pgagroal or root. +- The key file `pgagroal.key` must be owned by either the user running pgagroal or root. Additionally permissions must be at least `0640` when owned by root or `0600` otherwise. +- The Certificate Authority (CA) file `root.crt` must be owned by either the user running pgagroal or root. diff --git a/doc/etc/pgagroal.conf b/doc/etc/pgagroal.conf new file mode 100644 index 00000000..caa84237 --- /dev/null +++ b/doc/etc/pgagroal.conf @@ -0,0 +1,16 @@ +[pgagroal] +host = localhost +port = 2345 + +log_type = console +log_level = info +log_path = + +max_connections = 100 +idle_timeout = 600 +validation = off +unix_socket_dir = /tmp/ + +[primary] +host = localhost +port = 5432 diff --git a/doc/etc/pgagroal.service b/doc/etc/pgagroal.service new file mode 100644 index 00000000..38086a47 --- /dev/null +++ b/doc/etc/pgagroal.service @@ -0,0 +1,21 @@ +# systemd service unit for pgagroal +# +# - Adjust the user running the service in User +# - Adjust the path in ExecStart +# +[Unit] +Description=High-performance connection pool for PostgreSQL +Documentation=man:pgagroal(1) +Documentation=https://agroal.github.io/pgagroal/ +After=network.target + +[Service] +Type=exec +User=pgagroal +ExecStart=/usr/bin/pgagroal +ExecReload=/bin/kill -HUP $MAINPID +KillSignal=SIGINT +#LimitNOFILE=1024 + +[Install] +WantedBy=multi-user.target diff --git a/doc/etc/pgagroal.socket b/doc/etc/pgagroal.socket new file mode 100644 index 00000000..4160c19d --- /dev/null +++ b/doc/etc/pgagroal.socket @@ -0,0 +1,13 @@ +# systemd socket unit for pgagroal +# +# See pgagroal.service for more information. +# +[Unit] +Description=Sockets for pgagroal + +[Socket] +ListenStream=/tmp/.s.PGSQL.2345 +#ListenStream=2345 + +[Install] +WantedBy=sockets.target diff --git a/doc/etc/pgagroal_hba.conf b/doc/etc/pgagroal_hba.conf new file mode 100644 index 00000000..6dee6d19 --- /dev/null +++ b/doc/etc/pgagroal_hba.conf @@ -0,0 +1,4 @@ +# +# TYPE DATABASE USER ADDRESS METHOD +# +host all all all all diff --git a/doc/etc/pgagroal_vault.conf b/doc/etc/pgagroal_vault.conf new file mode 100644 index 00000000..75ed1219 --- /dev/null +++ b/doc/etc/pgagroal_vault.conf @@ -0,0 +1,12 @@ +[pgagroal-vault] +host = localhost +port = 2500 + +log_type = console +log_level = info +log_path = + +[main] +host = localhost +port = 2347 +user = admin diff --git a/doc/images/perf-extended.png b/doc/images/perf-extended.png new file mode 100644 index 0000000000000000000000000000000000000000..ffa246ba03fd31bc87f6cf269810c800441c7748 GIT binary patch literal 26392 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 V1#uMvuew2bsh}cXB5V5Le*uBW^k)D7 literal 0 HcmV?d00001 diff --git a/doc/images/perf-prepared.png b/doc/images/perf-prepared.png new file mode 100644 index 0000000000000000000000000000000000000000..c50ae079b91dff5c5b12e1043bacc9ce84bbbc2d GIT binary patch literal 27976 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%0DaR6uUs7yjSQdA5|9` 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 X9D6J<%?G?1?TM_El4Pm4q3{0yt0&4p literal 0 HcmV?d00001 diff --git a/doc/images/perf-readonly.png b/doc/images/perf-readonly.png new file mode 100644 index 0000000000000000000000000000000000000000..8c51e2ab0ec2645f7c7c9cde8aad8ea0815bcdc1 GIT binary patch literal 26643 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%0A4)5 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 Z8$}}&Uvrztfq(fyQIJ)YDU&k!@L!Fm#=!sp literal 0 HcmV?d00001 diff --git a/doc/images/perf-simple.png b/doc/images/perf-simple.png new file mode 100644 index 0000000000000000000000000000000000000000..793ce48b37e3ae4da244a936583d0dd0b9813208 GIT binary patch literal 28110 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:/users/``, the vault extracts ```` from the URL. It then connects to the pgagroal main process to fetch the current ```` corresponding to the ````. + +If the vault successfully fetches the ````, it responds with an HTTP status code 200 and includes ```` in the response body. Otherwise, the server responds with an HTTP 404 error indicating that the password for the specified user could not be found. + +**Note:** For pgagroal-vault to operate correctly, the management port of the pgagroal server must be open and functional. + +OPTIONS +======= + +-c, --config CONFIG_FILE + Set the path to the pgagroal_vault.conf file + +-u, --users USERS_FILE + Set the path to the pgagroal_vault_users.conf file + +-?, --help + Display help + +REPORTING BUGS +============== + +pgagroal is maintained on GitHub at https://github.com/agroal/pgagroal + +COPYRIGHT +========= + +pgagroal is licensed under the 3-clause BSD License. + +SEE ALSO +======== + +pgagroal.conf(5), pgagroal_hba.conf(5), pgagroal_databases.conf(5), pgagroal_vault.conf(5), pgagroal-cli(1), pgagroal-admin(1), pgagroal(1) diff --git a/doc/man/pgagroal.1.rst b/doc/man/pgagroal.1.rst new file mode 100644 index 00000000..9a5f7ac1 --- /dev/null +++ b/doc/man/pgagroal.1.rst @@ -0,0 +1,64 @@ +======== +pgagroal +======== + +----------------------------------------------- +High-performance connection pool for PostgreSQL +----------------------------------------------- + +:Manual section: 1 + +SYNOPSIS +======== + +pgagroal [ -c CONFIG_FILE ] [ -a HBA_FILE ] [ -d ] + +DESCRIPTION +=========== + +pgagroal is a high-performance protocol-native connection pool for PostgreSQL. + +OPTIONS +======= + +-c, --config CONFIG_FILE + Set the path to the pgagroal.conf file + +-a, --hba HBA_FILE + Set the path to the pgagroal_hba.conf file + +-l, --limit LIMIT_FILE + Set the path to the pgagroal_databases.conf file + +-u, --users USERS_FILE + Set the path to the pgagroal_users.conf file + +-A, --admins ADMINS_FILE + Set the path to the pgagroal_admins.conf file + +-S, --superuser SUPERUSER_FILE + Set the path to the pgagroal_superuser.conf file + +-d, --daemon + Run as a daemon + +-V, --version + Display version information + +-?, --help + Display help + +REPORTING BUGS +============== + +pgagroal is maintained on GitHub at https://github.com/agroal/pgagroal + +COPYRIGHT +========= + +pgagroal is licensed under the 3-clause BSD License. + +SEE ALSO +======== + +pgagroal.conf(5), pgagroal_hba.conf(5), pgagroal_databases.conf(5), pgagroal_vault.conf(5), pgagroal-cli(1), pgagroal-admin(1), pgagroal-vault(1) diff --git a/doc/man/pgagroal.conf.5.rst b/doc/man/pgagroal.conf.5.rst new file mode 100644 index 00000000..2a24bbda --- /dev/null +++ b/doc/man/pgagroal.conf.5.rst @@ -0,0 +1,222 @@ +============= +pgagroal.conf +============= + +------------------------------------ +Main configuration file for pgagroal +------------------------------------ + +:Manual section: 5 + +DESCRIPTION +=========== + +pgagroal.conf is the main configuration file for pgagroal. + +The file is split into different sections specified by the ``[`` and ``]`` characters. The main section is called ``[pgagroal]``. + +Other sections specifies the PostgreSQL server configuration. + +All properties are in the format ``key = value``. + +The characters ``#`` and ``;`` can be used for comments; must be the first character on the line. +The ``Bool`` data type supports the following values: ``on``, ``1``, ``true``, ``off``, ``0`` and ``false``. + +OPTIONS +======= + +The options for the main section are + +host + The bind address for pgagroal. Mandatory + +port + The bind port for pgagroal. Mandatory + +unix_socket_dir + The Unix Domain Socket location. Mandatory + +metrics + The metrics port. Default is 0 (disabled) + +metrics_cache_max_age + The number of seconds to keep in cache a Prometheus (metrics) response. + If set to zero, the caching will be disabled. Can be a string with a suffix, like ``2m`` to indicate 2 minutes. + Default is 0 (disabled) + +metrics_cache_max_size + The maximum amount of data to keep in cache when serving Prometheus responses. Changes require restart. + This parameter determines the size of memory allocated for the cache even if ``metrics_cache_max_age`` or + ``metrics`` are disabled. Its value, however, is taken into account only if ``metrics_cache_max_age`` is set + to a non-zero value. Supports suffixes: ``B`` (bytes), the default if omitted, ``K`` or ``KB`` (kilobytes), + ``M`` or ``MB`` (megabytes), ``G`` or ``GB`` (gigabytes). + Default is 256k + +management + The remote management port. Default is 0 (disabled) + +log_type + The logging type (console, file, syslog). Default is console + +log_level + The logging level, any of the (case insensitive) strings ``FATAL``, ``ERROR``, ``WARN``, ``INFO`` and ``DEBUG`` + (that can be more specific as ``DEBUG1`` thru ``DEBUG5``). Debug level greater than 5 will be set to ``DEBUG5``. + Not recognized values will make the ``log_level`` be ``INFO``. Default is info + +log_path + The log file location. Default is pgagroal.log. Can be a strftime(3) compatible string + +log_rotation_age + The age that will trigger a log file rotation. If expressed as a positive number, is managed as seconds. + Supports suffixes: ``S`` (seconds, the default), ``M`` (minutes), ``H`` (hours), ``D`` (days), ``W`` (weeks). + A value of ``0`` disables. Default is 0 (disabled) + +log_rotation_size + The size of the log file that will trigger a log rotation. Supports suffixes: ``B`` (bytes), the default if omitted, + ``K`` or ``KB`` (kilobytes), ``M`` or ``MB`` (megabytes), ``G`` or ``GB`` (gigabytes). A value of ``0`` (with or without suffix) disables. + Default is 0 + +log_line_prefix + A strftime(3) compatible string to use as prefix for every log line. Must be quoted if contains spaces. + Default is ``%Y-%m-%d %H:%M:%S`` + +log_mode + Append to or create the log file (append, create). Default is append + +log_connections + Log connects. Default is off + +log_disconnections + Log disconnects. Default is off + +blocking_timeout + The number of seconds the process will be blocking for a connection (disable = 0). Default is 30 + +idle_timeout + The number of seconds a connection is been kept alive (disable = 0). Default is 0 + +rotate_frontend_password_timeout + The number of seconds after which the passwords of frontend users are updated periodically (disable = 0). Default is 0 + +rotate_frontend_password_length + The length of randomized frontend passwords. Default is 8 + +max_connection_age + The maximum number of seconds that a connection will live (disable = 0). Default is 0 + +validation + Should connection validation be performed. Valid options: off, foreground and background. Default is off + +background_interval + The interval between background validation scans in seconds. Default is 300 + +max_retries + The maximum number of iterations to obtain a connection. Default is 5 + +max_connections + The maximum number of connections (max 1000). Default is 1000 + +allow_unknown_users + Allow unknown users to connect. Default is true + +authentication_timeout + The number of seconds the process will wait for valid credentials. Default is 5 + +pipeline + The pipeline type. Valid options are auto, performance, session and transaction. Default is auto + +auth_query + Enable authentication query. Default is false + +failover + Enable failover support. Default is false + +failover_script + The failover script + +tls + Enable Transport Layer Security (TLS). Default is false. Changes require restart in the server section. + +tls_cert_file + Certificate file for TLS. Changes require restart in the server section. + +tls_key_file + Private key file for TLS. Changes require restart in the server section. + +tls_ca_file + Certificate Authority (CA) file for TLS. Changes require restart in the server section. + +libev + The libev backend to use. Valid options: auto, select, poll, epoll, iouring, devpoll and port. Default is auto + +keep_alive + Have SO_KEEPALIVE on sockets. Default is on + +nodelay + Have TCP_NODELAY on sockets. Default is on + +non_blocking + Have O_NONBLOCK on sockets. Default is off + +backlog + The backlog for listen(). Minimum 16. Default is max_connections / 4 + +hugepage + Huge page support. Default is try + +tracker + Track connection lifecycle. Default is off + +track_prepared_statements + Track prepared statements (transaction pooling). Default is off + +pidfile + Path to the PID file. If omitted, automatically set to ``unix_socket_dir/pgagroal.port.pid`` + +update_process_title + The behavior for updating the operating system process title, mainly related to connection processes. + Allowed settings are: ``never`` (or ``off``), does not update the process title; ``strict`` to set the + process title without overriding the existing initial process title length; ``minimal`` to set the process + title to ``username/database``; ``verbose`` (or ``full``) to set the process title to ``user@host:port/database``. + Please note that ``strict`` and ``minimal`` are honored only on those systems that do not provide a native way + to set the process title (e.g., Linux). On other systems, there is no difference between ``strict`` and ``minimal`` + and the assumed behaviour is ``minimal`` even if ``strict`` is used. ``never`` and ``verbose`` are always honored, + on every system. On Linux systems the process title is always trimmed to 255 characters, while on system that + provide a natve way to set the process title it can be longer + +Danger zone + +disconnect_client + Disconnect clients that have been idle for more than the specified seconds. This setting DOES NOT take long running transactions into account. Default is 0 + +disconnect_client_force + Disconnect clients that have been active for more than the specified seconds. This setting DOES NOT take long running transactions into account. Default is off + +The options for the PostgreSQL section are + +host + The address of the PostgreSQL instance. Mandatory + +port + The port of the PostgreSQL instance. Mandatory + +primary + Identify the instance as the primary instance (hint) + +tls + Enable Transport Layer Security (TLS) support (Experimental - no pooling). Default is off + +REPORTING BUGS +============== + +pgagroal is maintained on GitHub at https://github.com/agroal/pgagroal + +COPYRIGHT +========= + +pgagroal is licensed under the 3-clause BSD License. + +SEE ALSO +======== + +pgagroal_hba.conf(5), pgagroal_databases.conf(5), pgagroal_vault.conf(5), pgagroal(1), pgagroal-cli(1), pgagroal-admin(1), pgagroal-vault(1) diff --git a/doc/man/pgagroal_databases.conf.5.rst b/doc/man/pgagroal_databases.conf.5.rst new file mode 100644 index 00000000..91f13b71 --- /dev/null +++ b/doc/man/pgagroal_databases.conf.5.rst @@ -0,0 +1,58 @@ +======================= +pgagroal_databases.conf +======================= + +------------------------------------------------ +Limits for databases, users or both for pgagroal +------------------------------------------------ + +:Manual section: 5 + +DESCRIPTION +=========== + +The pgagroal_databases.conf configuration file defines limits for a database or a user or both. + +FORMAT +====== + +DATABASE + Specifies the database for the rule. Either specific name or all for all databases + +USER + Specifies the user for the rule. Either specific name or all for all users + +MAX_SIZE + Specifies the maximum pool size for the entry. all for all connections + +INITIAL_SIZE + Specifies the initial pool size for the entry. Default is 0. Requires a pgagroal_users.conf configuration + +MIN_SIZE + Specifies the minimum pool size for the entry. Default is 0. Requires a pgagroal_users.conf configuration + +EXAMPLE +======= + +:: + + # + # DATABASE USER MAX_SIZE INITIAL_SIZE MIN_SIZE + # + all all all + + +REPORTING BUGS +============== + +pgagroal is maintained on GitHub at https://github.com/agroal/pgagroal + +COPYRIGHT +========= + +pgagroal is licensed under the 3-clause BSD License. + +SEE ALSO +======== + +pgagroal.conf(5), pgagroal_hba.conf(5), pgagroal_vault.conf(5), pgagroal(1), pgagroal-cli(1), pgagroal-admin(1), pgagroal-vault(1) diff --git a/doc/man/pgagroal_hba.conf.5.rst b/doc/man/pgagroal_hba.conf.5.rst new file mode 100644 index 00000000..3ba6165e --- /dev/null +++ b/doc/man/pgagroal_hba.conf.5.rst @@ -0,0 +1,58 @@ +================= +pgagroal_hba.conf +================= + +------------------------------------------------- +Host based access configuration file for pgagroal +------------------------------------------------- + +:Manual section: 5 + +DESCRIPTION +=========== + +pgagroal_hba.conf specifies the host based access pattern for pgagroal. + +FORMAT +====== + +TYPE + Specifies the access method for clients. Only host supported + +DATABASE + Specifies the database for the rule. Either specific name or all for all databases + +USER + Specifies the user for the rule. Either specific name or all for all users + +ADDRESS + Specifies the network for the rule. all for all networks, or IPv4 address with a mask (0.0.0.0/0) or IPv6 address with a mask (::0/0) + +METHOD + Specifies the authentication mode for the user. all for all methods, otherwise trust, reject, password, md5 or scram-sha-256 + +EXAMPLE +======= + +:: + + # + # TYPE DATABASE USER ADDRESS METHOD + # + host all all all all + + +REPORTING BUGS +============== + +pgagroal is maintained on GitHub at https://github.com/agroal/pgagroal + +COPYRIGHT +========= + +pgagroal is licensed under the 3-clause BSD License. + +SEE ALSO +======== + +pgagroal.conf(5), pgagroal_databases.conf(5), pgagroal_vault.conf(5), pgagroal(1), pgagroal-cli(1), pgagroal-admin(1), pgagroal-vault(1) diff --git a/doc/man/pgagroal_vault.conf.5.rst b/doc/man/pgagroal_vault.conf.5.rst new file mode 100644 index 00000000..c0fa67c9 --- /dev/null +++ b/doc/man/pgagroal_vault.conf.5.rst @@ -0,0 +1,128 @@ +=================== +pgagroal_vault.conf +=================== + +------------------------------------------ +Main configuration file for pgagroal-vault +------------------------------------------ + +:Manual section: 5 + +DESCRIPTION +=========== + +pgagroal_vault.conf is the main configuration file for pgagroal-vault. + +The file is split into different sections specified by the ``[`` and ``]`` characters. The main section is called ``[pgagroal-vault]``. + +Other sections (generally called the ``main`` section) specifies the ``pgagroal`` remote management configuration. + +All properties are in the format ``key = value``. + +The characters ``#`` and ``;`` can be used for comments; must be the first character on the line. +The ``Bool`` data type supports the following values: ``on``, ``1``, ``true``, ``off``, ``0`` and ``false``. + +OPTIONS +======= + +The options for the pgagroal-vault section are + +host + The bind address for pgagroal-vault. Mandatory + +port + The bind port for pgagroal-vault. Mandatory + +metrics + The metrics port. Default is 0 (disabled) + +metrics_cache_max_age + The number of seconds to keep in cache a Prometheus (metrics) response. + If set to zero, the caching will be disabled. Can be a string with a suffix, like ``2m`` to indicate 2 minutes. + Default is 0 (disabled) + +metrics_cache_max_size + The maximum amount of data to keep in cache when serving Prometheus responses. Changes require restart. + This parameter determines the size of memory allocated for the cache even if ``metrics_cache_max_age`` or + ``metrics`` are disabled. Its value, however, is taken into account only if ``metrics_cache_max_age`` is set + to a non-zero value. Supports suffixes: ``B`` (bytes), the default if omitted, ``K`` or ``KB`` (kilobytes), + ``M`` or ``MB`` (megabytes), ``G`` or ``GB`` (gigabytes). + Default is 256k + +log_type + The logging type (console, file, syslog). Default is console + +log_level + The logging level, any of the (case insensitive) strings ``FATAL``, ``ERROR``, ``WARN``, ``INFO`` and ``DEBUG`` + (that can be more specific as ``DEBUG1`` thru ``DEBUG5``). Debug level greater than 5 will be set to ``DEBUG5``. + Not recognized values will make the ``log_level`` be ``INFO``. Default is info + +log_path + The log file location. Default is pgagroal-vault.log. Can be a strftime(3) compatible string + +log_rotation_age + The age that will trigger a log file rotation. If expressed as a positive number, is managed as seconds. + Supports suffixes: ``S`` (seconds, the default), ``M`` (minutes), ``H`` (hours), ``D`` (days), ``W`` (weeks). + A value of ``0`` disables. Default is 0 (disabled) + +log_rotation_size + The size of the log file that will trigger a log rotation. Supports suffixes: ``B`` (bytes), the default if omitted, + ``K`` or ``KB`` (kilobytes), ``M`` or ``MB`` (megabytes), ``G`` or ``GB`` (gigabytes). A value of ``0`` (with or without suffix) disables. + Default is 0 + +log_line_prefix + A strftime(3) compatible string to use as prefix for every log line. Must be quoted if contains spaces. + Default is ``%Y-%m-%d %H:%M:%S`` + +log_mode + Append to or create the log file (append, create). Default is append + +log_connections + Log connects. Default is off + +log_disconnections + Log disconnects. Default is off + +authentication_timeout + The number of seconds the process will wait for valid credentials. Default is 5 + +hugepage + Huge page support. Default is try + +tls + Enable Transport Layer Security (TLS). Default is false. Changes require restart in the server section. + +tls_cert_file + Certificate file for TLS. Changes require restart in the server section. + +tls_key_file + Private key file for TLS. Changes require restart in the server section. + +tls_ca_file + Certificate Authority (CA) file for TLS. Changes require restart in the server section. + +The options for the main section are + +host + The address of the pgagroal instance running the management server. Mandatory + +port + The management port of pgagroal. Mandatory + +user + The admin user of the pgagroal remote management service. Mandatory + +REPORTING BUGS +============== + +pgagroal is maintained on GitHub at https://github.com/agroal/pgagroal + +COPYRIGHT +========= + +pgagroal is licensed under the 3-clause BSD License. + +SEE ALSO +======== + +pgagroal.conf(5), pgagroal_hba.conf(5), pgagroal_databases.conf(5), pgagroal(1), pgagroal-cli(1), pgagroal-admin(1), pgagroal-vault(1) diff --git a/doc/manual/01-introduction.md b/doc/manual/01-introduction.md new file mode 100644 index 00000000..a1223a9d --- /dev/null +++ b/doc/manual/01-introduction.md @@ -0,0 +1,34 @@ +\newpage + +# Introduction + +[**pgagroal**][pgagroal] is a high-performance protocol-native connection pool for [PostgreSQL][postgresql]. + +## Features + +* High performance +* Connection pool +* Limit connections for users and databases +* Prefill support +* Remove idle connections +* Perform connection validation +* Enable / disable database access +* Graceful / fast shutdown +* Prometheus support +* Grafana 8 dashboard +* Remote management +* Authentication query support +* Failover support +* Transport Layer Security (TLS) v1.2+ support +* Daemon mode +* User vault + +## Platforms + +The supported platforms are + +* [Fedora][fedora] 32+ +* [RHEL][rhel] 8 / RockyLinux 8 +* [RHEL][rhel] 9 / RockyLinux 9 +* [FreeBSD][freebsd] +* [OpenBSD][openbsd] diff --git a/doc/manual/02-installation.md b/doc/manual/02-installation.md new file mode 100644 index 00000000..99c15c2d --- /dev/null +++ b/doc/manual/02-installation.md @@ -0,0 +1,185 @@ +\newpage + +# Installation + +## Fedora + +You need to add the [PostgreSQL YUM repository](https://yum.postgresql.org/), for example for Fedora 39 + +``` +dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/F-39-x86_64/pgdg-fedora-repo-latest.noarch.rpm +``` + +and do the install via + +``` +dnf install -y pgagroal +``` + +* [PostgreSQL YUM](https://yum.postgresql.org/howto/) +* [Linux downloads](https://www.postgresql.org/download/linux/redhat/) + +## RHEL 9 / Rocky Linux 9 + +``` +dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm +dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm +``` + +and do the install via + +``` +dnf install -y pgagroal +``` + +## Compiling the source + +We recommend using Fedora to test and run [**pgagroal**][pgagroal], but other Linux systems, FreeBSD and MacOS are also supported. + +[**pgagroal**][pgagroal] requires + +* [gcc 8+](https://gcc.gnu.org) (C17) +* [cmake](https://cmake.org) +* [make](https://www.gnu.org/software/make/) +* [libev](http://software.schmorp.de/pkg/libev.html) +* [OpenSSL](http://www.openssl.org/) +* [systemd](https://www.freedesktop.org/wiki/Software/systemd/) +* [rst2man](https://docutils.sourceforge.io/) +* [libatomic](https://gcc.gnu.org/wiki/Atomic) +* [zlib](https://zlib.net) +* [zstd](http://www.zstd.net) +* [lz4](https://lz4.github.io/lz4/) +* [bzip2](http://sourceware.org/bzip2/) + +```sh +dnf install git gcc cmake make libev libev-devel \ + openssl openssl-devel \ + systemd systemd-devel \ + python3-docutils libatomic \ + zlib zlib-devel \ + libzstd libzstd-devel \ + lz4 lz4-devel \ + bzip2 bzip2-devel +``` + +Alternative [clang 8+](https://clang.llvm.org/) can be used. + + +### RHEL / Rocky Linux + +On RHEL / Rocky, before you install the required packages some additional repositoriesneed to be enabled or installed first. + +First you need to install the subscription-manager + +``` sh +dnf install subscription-manager +``` + +It is ok to disregard the registration and subscription warning. + +Otherwise, if you have a Red Hat corporate account (you need to specify the company/organization name in your account), you can register using + +``` sh +subscription-manager register --username --password --auto-attach +``` + +Then install the EPEL repository, + +``` sh +dnf install epel-release +``` + +Then to enable CodeReady Builder + +``` sh +dnf config-manager --set-enabled codeready-builder-for-rhel-9-rhui-rpms +dnf config-manager --set-enabled crb +dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm +``` + +Then use the `dnf` command for [**pgagroal**][pgagroal] to install the required packages. + + +### FreeBSD + +On FreeBSD, `pkg` is used instead of `dnf` or `yum`. + +Use `pkg install ` to install the following packages + +``` sh +git gcc cmake libev openssl libssh py39-docutils +``` + +### Build + +**Release build** + +The following commands will install [**pgagroal**][pgagroal] in the `/usr/local` hierarchy. + +```sh +git clone https://github.com/agroal/pgagroal.git +cd pgagroal +mkdir build +cd build +cmake -DCMAKE_INSTALL_PREFIX=/usr/local .. +make +sudo make install +``` + +See [RPM](https://github.com/agroal/pgagroal/blob/main/doc/RPM.md) for how to build a RPM of [**pgagroal**][pgagroal]. + +**Debug build** + +The following commands will create a `DEBUG` version of [**pgagroal**][pgagroal]. + +```sh +git clone https://github.com/agroal/pgagroal.git +cd pgagroal +mkdir build +cd build +cmake -DCMAKE_BUILD_TYPE=Debug .. +make +``` + +## Compiling the documentation + +[**pgagroal**][pgagroal]'s documentation requires + +* [pandoc](https://pandoc.org/) +* [texlive](https://www.tug.org/texlive/) + +```sh +dnf install pandoc texlive-scheme-basic \ + 'tex(footnote.sty)' 'tex(footnotebackref.sty)' \ + 'tex(pagecolor.sty)' 'tex(hardwrap.sty)' \ + 'tex(mdframed.sty)' 'tex(sourcesanspro.sty)' \ + 'tex(ly1enc.def)' 'tex(sourcecodepro.sty)' \ + 'tex(titling.sty)' 'tex(csquotes.sty)' \ + 'tex(zref-abspage.sty)' 'tex(needspace.sty)' + +``` + +You will need the `Eisvogel` template as well which you can install through + +``` +wget https://github.com/Wandmalfarbe/pandoc-latex-template/releases/download/2.4.2/Eisvogel-2.4.2.tar.gz +tar -xzf Eisvogel-2.4.2.tar.gz +mkdir -p $HOME/.local/share/pandoc/templates +mv eisvogel.latex $HOME/.local/share/pandoc/templates +``` + +where `$HOME` is your home directory. + +### Generate API guide + +This process is optional. If you choose not to generate the API HTML files, you can opt out of downloading these dependencies, and the process will automatically skip the generation. + +Download dependencies + +``` sh +dnf install graphviz doxygen +``` + +### Build + +These packages will be detected during `cmake` and built as part of the main build. diff --git a/doc/manual/97-acknowledgement.md b/doc/manual/97-acknowledgement.md new file mode 100644 index 00000000..2f744178 --- /dev/null +++ b/doc/manual/97-acknowledgement.md @@ -0,0 +1,50 @@ +\newpage + +# Acknowledgement + +## Authors + +[**pgagroal**][pgagroal] was created by the following authors: + +``` +Jesper Pedersen +David Fetter +Will Leinweber +Junduo Dong +Luca Ferrari +Nikita Bugrovsky +Lawrence Wu +Yongting You <2010youy01@gmail.com> +Ashutosh Sharma +Henrique de Carvalho +Yihe Lu +Eugenio Gigante +Mohanad Khaled +Haoran Zhang +Christian Englert +``` + +## Committers + +``` +Jesper Pedersen +Luca Ferrari +``` + +## Contributing + +Contributions to [**pgagroal**][pgagroal] are managed on [GitHub][pgagroal] + +* [Ask a question][ask] +* [Raise an issue][issue] +* [Feature request][request] +* [Code submission][submission] + +Contributions are most welcome! + +Please, consult our [Code of Conduct][conduct] policies for interacting in our +community. + +Consider giving the project a [star][star] on +[GitHub][pgagroal] if you find it useful. And, feel free to follow +the project on [Twitter][twitter] as well. diff --git a/doc/manual/98-licenses.md b/doc/manual/98-licenses.md new file mode 100644 index 00000000..ba7d265f --- /dev/null +++ b/doc/manual/98-licenses.md @@ -0,0 +1,78 @@ +\newpage + +# License + +``` +Copyright (C) 2025 The pgagroal community + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following +disclaimer in the documentation and/or other materials provided +with the distribution. + +3. Neither the name of the copyright holder nor the names of +its contributors may be used to endorse or promote products +derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY +OF SUCH DAMAGE. +``` + +[BSD-3-Clause][license] + + +## libart +Our adaptive radix tree (ART) implementation is based on + [The Adaptive Radix Tree: ARTful Indexing for Main-Memory Databases][ART_paper] + and [libart][libart] which has a [3-BSD license][license] as + +``` +Copyright (c) 2012, Armon Dadgar +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name of the organization nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARMON DADGAR +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` diff --git a/doc/manual/99-references.md b/doc/manual/99-references.md new file mode 100644 index 00000000..69154256 --- /dev/null +++ b/doc/manual/99-references.md @@ -0,0 +1,72 @@ + + + [pgagroal]: https://github.com/agroal/pgagroal + [postgresql]: https://www.postgresql.org + [fedora]: https://getfedora.org/ + [rhel]: https://www.redhat.com/en/technologies/linux-platforms/enterprise-linux + [appstram]: https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/installing_managing_and_removing_user-space_components/using-appstream_using-appstream + [freebsd]: https://www.freebsd.org/ + [openbsd]: http://www.openbsd.org/ + [gcc]: https://gcc.gnu.org + [cmake]: https://cmake.org + [make]: https://www.gnu.org/software/make/ + [libev]: http://software.schmorp.de/pkg/libev.html + [openssl]: http://www.openssl.org/ + [systemd]: https://www.freedesktop.org/wiki/Software/systemd/ + [rst2man]: https://docutils.sourceforge.io/ + [pandoc]: https://pandoc.org/ + [pandoc_latex_template]: https://github.com/Wandmalfarbe/pandoc-latex-template + [texlive]: https://www.tug.org/texlive/ + [clang]: https://clang.llvm.org/ + [git_squash]: https://www.git-tower.com/learn/git/faq/git-squash + [progit]: https://github.com/progit/progit2/releases + [prometheus]: https://prometheus.io/ + [wireshark]: https://www.wireshark.org/ + [pgprtdbg]: https://github.com/jesperpedersen/pgprtdbg + [ART_paper]: http://www-db.in.tum.de/~leis/papers/ART.pdf + [libart]: https://github.com/armon/libart + + + [rpm]: https://github.com/agroal/pgagroal/blob/main/doc/RPM.md + [configuration]: https://github.com/agroal/pgagroal/blob/main/doc/CONFIGURATION.md + + [t_install]: https://github.com/agroal/pgagroal/blob/master/doc/tutorial/01_install.md + [t_prefill]: https://github.com/agroal/pgagroal/blob/master/doc/tutorial/02_prefill.md + [t_remote_management]: https://github.com/agroal/pgagroal/blob/master/doc/tutorial/03_remote_management.md + [t_prometheus]: https://github.com/agroal/pgagroal/blob/master/doc/tutorial/04_prometheus.md + [t_split_security]: https://github.com/agroal/pgagroal/blob/master/doc/tutorial/05_split_security.md + + [sample]: https://github.com/agroal/pgagroal/blob/main/doc/etc/pgagroal.conf + + + [main_c]: https://github.com/agroal/pgagroal/blob/main/src/main.c + [cli_c]: https://github.com/agroal/pgagroal/blob/main/src/cli.c + [admin_c]: https://github.com/agroal/pgagroal/blob/main/src/admin.c + [vault_c]: https://github.com/agroal/pgagroal/blob/main/src/vault.c + + [shmem_h]: https://github.com/agroal/pgagroal/blob/main/src/include/shmem.h + [pgagroal_h]: https://github.com/agroal/pgagroal/blob/main/src/include/pgagroal.h + [messge_h]: https://github.com/agroal/pgagroal/blob/main/src/include/message.h + [network_h]: https://github.com/agroal/pgagroal/blob/main/src/include/network.h + [memory_h]: https://github.com/agroal/pgagroal/blob/main/src/include/memory.h + [management_h]: https://github.com/agroal/pgagroal/blob/main/src/include/management.h + [remote_h]: https://github.com/agroal/pgagroal/blob/main/src/include/remote.h + [prometheus_h]: https://github.com/agroal/pgagroal/blob/main/src/include/prometheus.h + [logging_h]: https://github.com/agroal/pgagroal/blob/main/src/include/logging.h + + [message_c]: https://github.com/agroal/pgagroal/blob/main/src/libpgagroal/message.c + [network_c]: https://github.com/agroal/pgagroal/blob/main/src/libpgagroal/network.c + [memory_c]: https://github.com/agroal/pgagroal/blob/main/src/libpgagroal/memory.c + [remote_c]: https://github.com/agroal/pgagroal/blob/main/src/libpgagroal/remote.c + [prometheus_c]: https://github.com/agroal/pgagroal/blob/main/src/libpgagroal/prometheus.c + [logging_c]: https://github.com/agroal/pgagroal/blob/main/src/libpgagroal/logging.c + + + [ask]: https://github.com/agroal/pgagroal/discussions + [issue]: https://github.com/agroal/pgagroal/issues + [request]: https://github.com/agroal/pgagroal/issues + [submission]: https://github.com/agroal/pgagroal/pulls + [conduct]: https://github.com/agroal/pgagroal/blob/main/CODE_OF_CONDUCT.md + [star]: https://github.com/agroal/pgagroal/stargazers + [twitter]: https://twitter.com/pgagroal/ + [license]: https://opensource.org/licenses/BSD-3-Clause diff --git a/doc/manual/advanced/00-frontpage.md b/doc/manual/advanced/00-frontpage.md new file mode 100644 index 00000000..4d7d86bc --- /dev/null +++ b/doc/manual/advanced/00-frontpage.md @@ -0,0 +1,13 @@ +--- +title: "Advanced connection management with pgagroal" +keywords: [pgagroal, PostgreSQL] +lang: "en" +titlepage: true, +titlepage-color: "0064A5" +titlepage-text-color: "FFFFFF" +titlepage-rule-color: "360049" +titlepage-rule-height: 0 +toc-own-page: true +listings-disable-line-numbers: true +table-use-row-colors: true +... diff --git a/doc/manual/advanced/01-preface.md b/doc/manual/advanced/01-preface.md new file mode 100644 index 00000000..d63c7a7a --- /dev/null +++ b/doc/manual/advanced/01-preface.md @@ -0,0 +1,12 @@ +# Preface + +Acme Boot is a startup company that have decided to use [**PostgreSQL**][postgresql] as +its database technology. + +The following technologies will be used for the database cluster + +* [**Rocky Linux**][rocky] **9.x** +* [**PostgreSQL**][postgresql] **17.x** +* [**pgagroal**][pgagroal] + +Note, that this guide will focus on the [**pgagroal**][pgagroal] aspect of the platform. diff --git a/doc/manual/advanced/02-introduction.md b/doc/manual/advanced/02-introduction.md new file mode 100644 index 00000000..a1223a9d --- /dev/null +++ b/doc/manual/advanced/02-introduction.md @@ -0,0 +1,34 @@ +\newpage + +# Introduction + +[**pgagroal**][pgagroal] is a high-performance protocol-native connection pool for [PostgreSQL][postgresql]. + +## Features + +* High performance +* Connection pool +* Limit connections for users and databases +* Prefill support +* Remove idle connections +* Perform connection validation +* Enable / disable database access +* Graceful / fast shutdown +* Prometheus support +* Grafana 8 dashboard +* Remote management +* Authentication query support +* Failover support +* Transport Layer Security (TLS) v1.2+ support +* Daemon mode +* User vault + +## Platforms + +The supported platforms are + +* [Fedora][fedora] 32+ +* [RHEL][rhel] 8 / RockyLinux 8 +* [RHEL][rhel] 9 / RockyLinux 9 +* [FreeBSD][freebsd] +* [OpenBSD][openbsd] diff --git a/doc/manual/advanced/03-installation.md b/doc/manual/advanced/03-installation.md new file mode 100644 index 00000000..b32a6c23 --- /dev/null +++ b/doc/manual/advanced/03-installation.md @@ -0,0 +1,172 @@ +\newpage + +# Installation + +## Rocky Linux 9.x + +We can download the [Rocky Linux](https://www.rockylinux.org/) distruction from their web site + +``` +https://rockylinux.org/download +``` + +The installation and setup is beyond the scope of this guide. + +Ideally, you would use dedicated user accounts to run [**PostgreSQL**][postgresql] and [**pgagroal**][pgagroal] + +``` +useradd postgres +usermod -a -G wheel postgres +useradd pgagroal +usermod -a -G wheel pgagroal +``` + +Add a configuration directory for [**pgagroal**][pgagroal] + +``` +mkdir /etc/pgagroal +chown -R pgagroal:pgagroal /etc/pgagroal +``` + +and lets open the ports in the firewall that we will need + +``` +firewall-cmd --permanent --zone=public --add-port=2345/tcp +firewall-cmd --permanent --zone=public --add-port=2346/tcp +``` + +## PostgreSQL 17 + +We will install PostgreSQL 17 from the official [YUM repository][yum] with the community binaries, + +**x86_64** + +``` +dnf -qy module disable postgresql +dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm +``` + +**aarch64** + +``` +dnf -qy module disable postgresql +dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-aarch64/pgdg-redhat-repo-latest.noarch.rpm +``` + +and do the install via + +``` +dnf install -y postgresql17 postgresql17-server postgresql17-contrib +``` + +First, we will update `~/.bashrc` with + +``` +cat >> ~/.bashrc +export PGHOST=/tmp +export PATH=/usr/pgsql-17/bin/:$PATH +``` + +then Ctrl-d to save, and + +``` +source ~/.bashrc +``` + +to reload the Bash environment. + +Then we can do the PostgreSQL initialization + +``` +mkdir DB +initdb -k DB +``` + +and update configuration - for a 8 GB memory machine. + +**postgresql.conf** +``` +listen_addresses = '*' +port = 5432 +max_connections = 100 +unix_socket_directories = '/tmp' +password_encryption = scram-sha-256 +shared_buffers = 2GB +huge_pages = try +max_prepared_transactions = 100 +work_mem = 16MB +dynamic_shared_memory_type = posix +wal_level = replica +wal_log_hints = on +max_wal_size = 16GB +min_wal_size = 2GB +log_destination = 'stderr' +logging_collector = on +log_directory = 'log' +log_filename = 'postgresql.log' +log_rotation_age = 0 +log_rotation_size = 0 +log_truncate_on_rotation = on +log_line_prefix = '%p [%m] [%x] ' +log_timezone = UTC +datestyle = 'iso, mdy' +timezone = UTC +lc_messages = 'en_US.UTF-8' +lc_monetary = 'en_US.UTF-8' +lc_numeric = 'en_US.UTF-8' +lc_time = 'en_US.UTF-8' +``` + +Please, check with other sources in order to create a setup for your local setup. + +Now, we are ready to start PostgreSQL + +``` +pg_ctl -D DB -l /tmp/ start +``` + +## pgagroal + +We will install [**pgagroal**][pgagroal] from the official [YUM repository][yum] as well, + +``` +dnf install -y pgagroal +``` + +First, we will need to create a master security key for the [**pgagroal**][pgagroal] installation, by + +``` +pgagroal-admin -g master-key +``` + +Then we will create the configuration for [**pgagroal**][pgagroal], + +``` +cat > /etc/pgagroal/pgagroal.conf +[pgagroal] +host = * +port = 2345 + +metrics = 2346 + +log_type = file +log_level = info +log_path = /tmp/pgagroal.log + +max_connections = 100 +idle_timeout = 600 +validation = off +unix_socket_dir = /tmp/ + +[primary] +host = localhost +port = 5432 +``` + +and end with a Ctrl-d to save the file. + +Start [**pgagroal**][pgagroal] now, by + +``` +pgagroal -d +``` diff --git a/doc/manual/advanced/04-configuration.md b/doc/manual/advanced/04-configuration.md new file mode 100644 index 00000000..ae2675dd --- /dev/null +++ b/doc/manual/advanced/04-configuration.md @@ -0,0 +1,193 @@ +\newpage + +# Configuration + +The configuration is loaded from either the path specified by the `-c` flag or `/etc/pgagroal/pgagroal.conf`. + +The configuration of [**pgagroal**][pgagroal] is split into sections using the `[` and `]` characters. + +The main section, called `[pgagroal]`, is where you configure the overall properties of [**pgagroal**][pgagroal]. + +Other sections doesn't have any requirements to their naming so you can give them meaningful names like `[primary]` for the primary [PostgreSQL][postgresql] instance. + +All properties are in the format `key = value`. + +The characters `#` and `;` can be used for comments; must be the first character on the line. + +The `Bool` data type supports the following values: `on`, `yes`, `1`, `true`, `off`, `no`, `0` and `false`. + +See a [sample][sample] configuration for running [**pgagroal**][pgagroal] on `localhost`. + +## [pgagroal] + +This section is mandatory and the pooler will refuse to start if the configuration file does not specify one and only one. Usually this section is place on top of the configuration file, but its position within the file does not really matter. +The available keys and their accepted values are reported in the table below. + +| Property | Default | Unit | Required | Description | +|----------|---------|------|----------|-------------| +| host | | String | Yes | The bind address for pgagroal | +| port | | Int | Yes | The bind port for pgagroal | +| unix_socket_dir | | String | Yes | The Unix Domain Socket location | +| metrics | 0 | Int | No | The metrics port (disable = 0) | +| metrics_cache_max_age | 0 | String | No | The number of seconds to keep in cache a Prometheus (metrics) response. If set to zero, the caching will be disabled. Can be a string with a suffix, like `2m` to indicate 2 minutes | +| metrics_cache_max_size | 256k | String | No | The maximum amount of data to keep in cache when serving Prometheus responses. Changes require restart. This parameter determines the size of memory allocated for the cache even if `metrics_cache_max_age` or `metrics` are disabled. Its value, however, is taken into account only if `metrics_cache_max_age` is set to a non-zero value. Supports suffixes: 'B' (bytes), the default if omitted, 'K' or 'KB' (kilobytes), 'M' or 'MB' (megabytes), 'G' or 'GB' (gigabytes).| +| management | 0 | Int | No | The remote management port (disable = 0) | +| log_type | console | String | No | The logging type (console, file, syslog) | +| log_level | info | String | No | The logging level, any of the (case insensitive) strings `FATAL`, `ERROR`, `WARN`, `INFO` and `DEBUG` (that can be more specific as `DEBUG1` thru `DEBUG5`). Debug level greater than 5 will be set to `DEBUG5`. Not recognized values will make the log_level be `INFO` | +| log_path | pgagroal.log | String | No | The log file location. Can be a strftime(3) compatible string. | +| log_rotation_age | 0 | String | No | The age that will trigger a log file rotation. If expressed as a positive number, is managed as seconds. Supports suffixes: 'S' (seconds, the default), 'M' (minutes), 'H' (hours), 'D' (days), 'W' (weeks). A value of `0` disables. | +| log_rotation_size | 0 | String | No | The size of the log file that will trigger a log rotation. Supports suffixes: 'B' (bytes), the default if omitted, 'K' or 'KB' (kilobytes), 'M' or 'MB' (megabytes), 'G' or 'GB' (gigabytes). A value of `0` (with or without suffix) disables. | +| log_line_prefix | %Y-%m-%d %H:%M:%S | String | No | A strftime(3) compatible string to use as prefix for every log line. Must be quoted if contains spaces. | +| log_mode | append | String | No | Append to or create the log file (append, create) | +| log_connections | `off` | Bool | No | Log connects | +| log_disconnections | `off` | Bool | No | Log disconnects | +| blocking_timeout | 30 | Int | No | The number of seconds the process will be blocking for a connection (disable = 0) | +| idle_timeout | 0 | Int | No | The number of seconds a connection is been kept alive (disable = 0) | +| rotate_frontend_password_timeout | 0 | Int | No | The number of seconds after which the passwords of frontend users are updated periodically (disable = 0) | +| rotate_frontend_password_length | 8 | Int | No | The length of the randomized frontend password | +| max_connection_age | 0 | Int | No | The maximum number of seconds that a connection will live (disable = 0) | +| validation | `off` | String | No | Should connection validation be performed. Valid options: `off`, `foreground` and `background` | +| background_interval | 300 | Int | No | The interval between background validation scans in seconds | +| max_retries | 5 | Int | No | The maximum number of iterations to obtain a connection | +| max_connections | 100 | Int | No | The maximum number of connections to PostgreSQL (max 10000) | +| allow_unknown_users | `true` | Bool | No | Allow unknown users to connect | +| authentication_timeout | 5 | Int | No | The number of seconds the process will wait for valid credentials | +| pipeline | `auto` | String | No | The pipeline type (`auto`, `performance`, `session`, `transaction`) | +| auth_query | `off` | Bool | No | Enable authentication query | +| failover | `off` | Bool | No | Enable failover support | +| failover_script | | String | No | The failover script to execute | +| tls | `off` | Bool | No | Enable Transport Layer Security (TLS) | +| tls_cert_file | | String | No | Certificate file for TLS. This file must be owned by either the user running pgagroal or root. | +| tls_key_file | | String | No | Private key file for TLS. This file must be owned by either the user running pgagroal or root. Additionally permissions must be at least `0640` when owned by root or `0600` otherwise. | +| tls_ca_file | | String | No | Certificate Authority (CA) file for TLS. This file must be owned by either the user running pgagroal or root. | +| libev | `auto` | String | No | Select the [libev](http://software.schmorp.de/pkg/libev.html) backend to use. Valid options: `auto`, `select`, `poll`, `epoll`, `iouring`, `devpoll` and `port` | +| keep_alive | on | Bool | No | Have `SO_KEEPALIVE` on sockets | +| nodelay | on | Bool | No | Have `TCP_NODELAY` on sockets | +| non_blocking | off | Bool | No | Have `O_NONBLOCK` on sockets | +| backlog | `max_connections` / 4 | Int | No | The backlog for `listen()`. Minimum `16` | +| hugepage | `try` | String | No | Huge page support (`off`, `try`, `on`) | +| tracker | off | Bool | No | Track connection lifecycle | +| track_prepared_statements | off | Bool | No | Track prepared statements (transaction pooling) | +| pidfile | | String | No | Path to the PID file. If omitted, automatically set to `unix_socket_dir`/pgagroal.`port`.pid | +| update_process_title | `verbose` | String | No | The behavior for updating the operating system process title, mainly related to connection processes. Allowed settings are: `never` (or `off`), does not update the process title; `strict` to set the process title without overriding the existing initial process title length; `minimal` to set the process title to `username/database`; `verbose` (or `full`) to set the process title to `user@host:port/database`. Please note that `strict` and `minimal` are honored only on those systems that do not provide a native way to set the process title (e.g., Linux). On other systems, there is no difference between `strict` and `minimal` and the assumed behaviour is `minimal` even if `strict` is used. `never` and `verbose` are always honored, on every system. On Linux systems the process title is always trimmed to 255 characters, while on system that provide a natve way to set the process title it can be longer. | + +__Danger zone__ + +| Property | Default | Unit | Required | Description | +|----------|---------|------|----------|-------------| +| disconnect_client | 0 | Int | No | Disconnect clients that have been idle for more than the specified seconds. This setting __DOES NOT__ take long running transactions into account | +| disconnect_client_force | off | Bool | No | Disconnect clients that have been active for more than the specified seconds. This setting __DOES NOT__ take long running transactions into account | + + +### Server section + +Each section with a name different from [**pgagroal**](https://github.com/agroal/pgagroal) will be treated as an host section. +There can be up to `64` host sections, each with an unique name and different combination of `host` and `port` settings, otherwise the pooler will complain about duplicated server configuration. + +| Property | Default | Unit | Required | Description | +|----------|---------|------|----------|-------------| +| host | | String | Yes | The address of the PostgreSQL instance | +| port | | Int | Yes | The port of the PostgreSQL instance | +| primary | | Bool | No | Identify the instance as primary (hint) | +| tls | `off` | Bool | No | Enable Transport Layer Security (TLS) support (Experimental - no pooling). Changes require restart. | +| tls_cert_file | | String | No | Certificate file for TLS. This file must be owned by either the user running pgagroal or root. Changes require restart. | +| tls_key_file | | String | No | Private key file for TLS. This file must be owned by either the user running pgagroal or root. Additionally permissions must be at least `0640` when owned by root or `0600` otherwise.Changes require restart. | +| tls_ca_file | | String | No | Certificate Authority (CA) file for TLS. This file must be owned by either the user running pgagroal or root. Changes require restart. | + +Note, that if `host` starts with a `/` it represents a path and [**pgagroal**](https://github.com/agroal/pgagroal) will connect using a Unix Domain Socket. + +## pgagroal_hba configuration + +The `pgagroal_hba` configuration controls access to [**pgagroal**](https://github.com/agroal/pgagroal) through host-based authentication. + +The configuration is loaded from either the path specified by the `-a` flag or `/etc/pgagroal/pgagroal_hba.conf`. + +The format of the file follows the similar [PostgreSQL HBA configuration format](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html), and as such looks like + +``` +# +# TYPE DATABASE USER ADDRESS METHOD +# +host all all all all +``` + +| Column | Required | Description | +|--------|----------|-------------| +| TYPE | Yes | Specifies the access method for clients. `host` and `hostssl` are supported | +| DATABASE | Yes | Specifies the database for the rule. Either specific name or `all` for all databases | +| USER | Yes | Specifies the user for the rule. Either specific name or `all` for all users | +| ADDRESS | Yes | Specifies the network for the rule. `all` for all networks, or IPv4 address with a mask (`0.0.0.0/0`) or IPv6 address with a mask (`::0/0`) | +| METHOD | Yes | Specifies the authentication mode for the user. `all` for all methods, otherwise `trust`, `reject`, `password`, `md5` or `scram-sha-256` | + +Remote management users needs to have their database set to `admin` in order for the entry to be considered. + +There can be up to `64` HBA entries in the configuration file. + +## pgagroal_databases configuration + +The `pgagroal_databases` configuration defines limits for a database or a user or both. The limits are the number +of connections from [**pgagroal**](https://github.com/agroal/pgagroal) to PostgreSQL for each entry. + +The file also defines the initial and minimum pool size for a database and user pair. Note, that this feature requires +a user definition file, see below. + +The configuration is loaded from either the path specified by the `-l` flag or `/etc/pgagroal/pgagroal_databases.conf`. + +``` +# +# DATABASE USER MAX_SIZE INITIAL_SIZE MIN_SIZE +# +mydb myuser all +anotherdb userB 10 5 3 +``` + +| Column | Required | Description | +|--------|----------|-------------| +| DATABASE | Yes | Specifies the database for the rule. `all` for all databases | +| USER | Yes | Specifies the user for the rule. `all` for all users | +| MAX_SIZE | Yes | Specifies the maximum pool size for the entry. `all` for all remaining counts from `max_connections` | +| INITIAL_SIZE | No | Specifies the initial pool size for the entry. `all` for `MAX_SIZE` connections. Default is 0 | +| MIN_SIZE | No | Specifies the minimum pool size for the entry. `all` for `MAX_SIZE` connections. Default is 0 | + + +There can be up to `64` entries in the configuration file. + +In the case a limit entry has incoherent values, for example `INITIAL_SIZE` smaller than `MIN_SIZE`, the system will try to automatically adjust the settings on the fly, reporting messages in the logs. + +The system will find the best match limit entry for a given `DATABASE`-`USER` pair according to the following rules: +1. Use the first entry with an exact `DATABASE` and `USER` match. +2. If there is no exact match, use the entry with a `USER` match and `DATABASE` set to `all`. +3. If Rule 2 does not apply, use the entry with a `DATABASE` match and `USER` set to `all`. + +## pgagroal_users configuration + +The `pgagroal_users` configuration defines the users known to the system. This file is created and managed through the `pgagroal-admin` tool. + +The configuration is loaded from either the path specified by the `-u` flag or `/etc/pgagroal/pgagroal_users.conf`. + +## pgagroal_frontend_users configuration + +The `pgagroal_frontend_users` configuration defines the passwords for the users connecting to pgagroal. +This allows the setup to use different passwords for the [**pgagroal**](https://github.com/agroal/pgagroal) to PostgreSQL authentication. +This file is created and managed through the `pgagroal-admin` tool. + +All users defined in the frontend authentication must be defined in the user vault (`-u`). + +Frontend users (`-F`) requires a user vault (`-u`) to be defined. + +The configuration is loaded from either the path specified by the `-F` flag or `/etc/pgagroal/pgagroal_frontend_users.conf`. + +## pgagroal_admins configuration + +The `pgagroal_admins` configuration defines the administrators known to the system. This file is created and managed through the `pgagroal-admin` tool. + +The configuration is loaded from either the path specified by the `-A` flag or `/etc/pgagroal/pgagroal_admins.conf`. + +If pgagroal has both Transport Layer Security (TLS) and `management` enabled then `pgagroal-cli` can connect with TLS using the files `~/.pgagroal/pgagroal.key` (must be 0600 permission), `~/.pgagroal/pgagroal.crt` and `~/.pgagroal/root.crt`. + +## pgagroal_superuser configuration + +The `pgagroal_superuser` configuration defines the superuser known to the system. This file is created and managed through +the `pgagroal-admin` tool. It may only have one user defined. + +The configuration is loaded from either the path specified by the `-S` flag or `/etc/pgagroal/pgagroal_superuser.conf`. diff --git a/doc/manual/advanced/05-prefill.md b/doc/manual/advanced/05-prefill.md new file mode 100644 index 00000000..bd74b85c --- /dev/null +++ b/doc/manual/advanced/05-prefill.md @@ -0,0 +1,59 @@ +\newpage + +# Prefill + +## Create prefill configuration + +Prefill is instrumented by the `pgagroal_databases.conf` configuration file, where you need +to list databases, usernames, and limits. Every username/database pair has to be specified on a separated line. + +The limits are assumed as: + +* *max number of allowed connections* for that username/database +* *initial number of connections*, that is the effective prefill; +* *minimum number of connections* to always keep open for the pair username/database. + +Assuming you want to configure the prefill for the `mydb` database with the `myuser` username, +you have to edit the file `/etc/pgagroal/pgagroal_databases.conf` with your editor of choice +or using `cat` from the command line, as follows: + +``` +cd /etc/pgagroal +cat > pgagroal_databases.conf +mydb myuser 2 1 0 +``` + +and press `Ctrl-d` to save the file. + +This will create a configuration where `mydb` will have a maximum connection size of 2, +an initial connection size of 1 and a minimum connection size of 0 for the `myuser` user. + +The file must be owned by the operating system user [**pgagroal**](https://github.com/agroal/pgagroal). + +The `max_size` value is mandatory, while the `initial_size` and `min_size` are optional and if not explicitly set are assumed to be `0`. +See [the `pgagroal_databases.conf` file documentation](https://github.com/agroal/pgagroal/blob/master/doc/CONFIGURATION.md#pgagroal_databases-configuration) for more details. + +## Restart pgagroal + +In order to apply changes to the prefill configuration, you need to restart [**pgagroal**](https://github.com/agroal/pgagroal). +You can do so by stopping it and then re-launch the daemon, as [**pgagroal**](https://github.com/agroal/pgagroal) operating system user: + +``` +pgagroal-cli shutdown +pgagroal -d +``` + +## Check the prefill + +You can check the prefill by running, as the [**pgagroal**](https://github.com/agroal/pgagroal) operating system user, the `status` command: + +``` +pgagroal-cli status +Status: Running +Active connections: 0 +Total connections: 1 +Max connections: 100 + +``` + +where the `Total connections` is set by the *initial* connection specified in the limit file. diff --git a/doc/manual/advanced/06-remote_management.md b/doc/manual/advanced/06-remote_management.md new file mode 100644 index 00000000..cd246535 --- /dev/null +++ b/doc/manual/advanced/06-remote_management.md @@ -0,0 +1,66 @@ +\newpage + +# Remote administration + +## Enable remote management + +On the pooler machine, you need to enable the remote management. In order to do so, +add the `management` setting to the main `pgagroal.conf` configuration file. +The value of setting is the number of a free TCP/IP port to which the remote +management will connect to. + +With your editor of choice, edit the `/etc/pgagroal/pgagroal.conf` file and add the +`management` option likely the following: + +``` +management = 2347 +``` + +under the `[pgagroal]` section, so that the configuration file looks like: + +``` +[pgagroal] +... +management = 2347 +``` + +See [the pgagroal configuration settings](https://github.com/agroal/pgagroal/blob/master/doc/CONFIGURATION.md#pgagroal) for more details. + +## Add remote admin user + +Remote management is done via a specific admin user, that has to be created within the pooler vault. +As the [**pgagroal**][pgagroal] operating system user, run the following command: + +``` +cd /etc/pgagroal +pgagroal-admin -f /etc/pgagroal/pgagroal_admins.conf -U admin -P admin1234 add-user +``` + +The above will create the `admin` username with the `admin1234` password. + +**We strongly encourage you to choose non trivial usernames and passwords!** + + +## Restart pgagroal + +In order to make the changes available, and therefore activate the remote management, you have to restart [**pgagroal**][pgagroal], for example by issuing the following commands from the [**pgagroal**][pgagroal] operatng system user: + +``` +pgagroal-cli shutdown +pgagroal -d +``` + +## Connect via remote administration interface + +In order to connect remotely, you need to specify at least the `-h` and `-p` flags on the `pgagroal-cli` command line. Such flags will tell `pgagroal-cli` to connect to a remote host. You can also specify the username you want to connect with by specifying the `-U` flag. +So, to get the status of the pool remotely, you can issue: + +``` +pgagroal-cli -h localhost -p 2347 -U admin status +``` + +and type the password `admin1234` when asked for it. + +If you don't specify the `-U` flag on the command line, you will be asked for a username too. + +Please note that the above example uses `localhost` as the remote host, but clearly you can specify any *real* remote host you want to manage. diff --git a/doc/manual/advanced/07-split_security.md b/doc/manual/advanced/07-split_security.md new file mode 100644 index 00000000..3722957f --- /dev/null +++ b/doc/manual/advanced/07-split_security.md @@ -0,0 +1,45 @@ +\newpage + +# Split security model + +## Create frontend users + +Frontend users are stored into the `pgagroal_frontend_users.conf` file, that can be managed via the `pgagroal-admin` command line tool. +See [the documentation on frontend users](https://github.com/agroal/pgagroal/blob/master/doc/CONFIGURATION.md#pgagroal_frontend_users-configuration) for more details. + +As an example, consider the user `myuser` that has the `mypassword` password defined on the PostgreSQL side. It is possible to *remap* the user password on the [**pgagroal**][pgagroal] side, +so that an application can connect to the [**pgagroal**][pgagroal] using a different password, like `application_password`. In turn, [**pgagroal**][pgagroal] will connect to PostgreSQL +using the `mypassword` password. Therefore, the application doesn't not know the *real* password used to connect to PostgreSQL. + +To achieve this, as [**pgagroal**][pgagroal] operating system run the following command: + +``` +pgagroal-admin -f /etc/pgagroal/pgagroal_frontend_users.conf -U myuser -P application_password user add +``` + +You will need a password mapping for each user defined in the `pgagroal_users.conf` configuration file. + +## Restart pgagroal + +In order to apply changes, you need to restart [**pgagroal**][pgagroal] so do: + +``` +pgagroal-cli shutdown +pgagroal -d +``` + +## Connect to PostgreSQL + +You can now use the "application password" to access the PostgreSQL instance. As an example, +run the following as any operatng system user: + +``` +psql -h localhost -p 2345 -U myuser mydb +``` + +using `application_password` as the password. +As already explained, [**pgagroal**][pgagroal] will then use the `mypassword` password against PostgreSQL. + +This **split security model** allows you to avoid sharing password between applications and PostgreSQL, +letting the [**pgagroal**][pgagroal] to be the secret-keeper. This not only improves security, but also allows you +to change the PostgreSQL password without having the application change its configuration. diff --git a/doc/manual/advanced/08-tls.md b/doc/manual/advanced/08-tls.md new file mode 100644 index 00000000..5d81158d --- /dev/null +++ b/doc/manual/advanced/08-tls.md @@ -0,0 +1,66 @@ +\newpage + +# Transport Level Security (TLS) + +## Creating Certificates + +This tutorial will show you how to create self-signed certificate for the server, valid for 365 days, use the following OpenSSL command, replacing `dbhost.yourdomain.com` with the server's host name, here `localhost`: + +``` +openssl req -new -x509 -days 365 -nodes -text -out server.crt \ + -keyout server.key -subj "/CN=dbhost.yourdomain.com" +``` + +then do - + +``` +chmod og-rwx server.key +``` + +because the server will reject the file if its permissions are more liberal than this. For more details on how to create your server private key and certificate, refer to the OpenSSL documentation. + +For the purpose of this tutorial we will assume the client certificate and key same as the server certificate and server key and therefore, these equations always holds - + +* `` = `` +* `` = `` +* `` = `` +* `` = `` + +## TLS in `pgagroal` + +### Modify the `pgagroal` configuration + +It is now time to modify the [pgagroal] section of configuration file `/etc/pgagroal/pgagroal_vault.conf`, with your editor of choice by adding the following lines in the [pgagroal] section. + +``` +tls = on +tls_cert_file = +tls_key_file = +``` + +### Only Server Authentication + +If you wish to do only server authentication the aforementioned configuration suffice. + +**Client Request** + +``` +PGSSLMODE=verify-full PGSSLROOTCERT= psql -h localhost -p 2345 -U +``` + +### Full Client and Server Authentication + +To enable the server to request the client certificates add the following configuration lines + +``` +tls = on +tls_cert_file = +tls_key_file = +tls_ca_file = +``` + +**Client Request** + +``` +PGSSLMODE=verify-full PGSSLCERT= PGSSLKEY= PGSSLROOTCERT= psql -h localhost -p 2345 -U +``` diff --git a/doc/manual/advanced/09-vault.md b/doc/manual/advanced/09-vault.md new file mode 100644 index 00000000..cf20b850 --- /dev/null +++ b/doc/manual/advanced/09-vault.md @@ -0,0 +1,167 @@ +\newpage + +# pgagroal-vault + +## Enable rotation of frontend passwords + +In the main configuration file of [**pgagroal**][pgagroal] add the following configuration for rotating frontend passwords and make sure management port is enabled in the [**pgagroal**][pgagroal] section of the configuration file. + +``` +management = 2347 +rotate_frontend_password_timeout = 60 +rotate_frontend_password_length = 12 +``` + +## Configuration + +In order to run `pgagroal-vault`, you need to configure the vault `pgagroal_vault.conf` configuration file, that will tell the vault how to work, which address to listen, address of management service in [**pgagroal**][pgagroal] so on, +and then `pgagroal_vault_users.conf` that will instrument the vault about the admin username and password of the remote management. + +### pgagroal-vault.conf + +It is now time to create the main `/etc/pgagroal/pgagroal_vault.conf` configration file, with your editor of choice or using `cat` from the command line, create the following content: + +``` +cd /etc/pgagroal +cat > pgagroal_vault.conf +[pgagroal-vault] +host = localhost +port = 2500 + +metrics = 2501 + +log_type = console +log_level = info +log_path = /tmp/pgagroal-vault.log + +[main] +host = localhost +port = 2347 +user = admin +``` + +and press `Ctrl-d` (if running `cat`) to save the file. + +### Add users file + +As the [**pgagroal**][pgagroal] operating system user, run the following command: + +``` +pgagroal-admin -f /etc/pgagroal/pgagroal_vault_users.conf -U admin -P admin1234 user add +``` + +The above will create the `admin` username with the `admin1234` password. Alternately, `/etc/pgagroal/pgagroal_admins.conf` can be provided for vault users information. + +See [the documentation about `pgagroal_vault.conf` for more details](https://github.com/agroal/pgagroal/blob/master/doc/VAULT.md). + +## Start the vault + +It is now time to start `pgagroal-vault`, so as the [**pgagroal**][pgagroal] operating system user run: + +``` +pgagroal-vault -d +``` + +If both `pgagroal` and `pgagroal-vault` are on the same operating system they can use the same `pgagroal_admins.conf` file. + +This command initializes an HTTP server on localhost port 2500, which is primed to exclusively handle GET requests from clients. + +## Connect to the vault + +Since we have deployed an HTTP server we can simply use `curl` to send GET requests + +### Correct requests + +If the requested URL is of form `http://:/users/` such that `` exists, the server will return a header response with a 200 status code and the frontend password corresponding to the `` in the response body. + +**Example** + +` +curl -i http://localhost:2500/users/myuser +` + +Output + +``` +HTTP/1.1 200 OK +Content-Type: text/plain + + +password +``` + +### Incorrect requests + +All the POST requests will be ignored and the server will send a `HTTP 404 ERROR` as a response. + +Any URL other than the format: `http://:/users/*` will result in `HTTP 404 ERROR`. + +**Example** + +` +curl -i http://localhost:2500/user +` + +Output + +``` +HTTP/1.1 404 Not Found + +``` + +A URL of form `http://:/users/` such that `` does not exist will also give `HTTP 404 ERROR`. + +**Example** + +` +curl -i http://localhost:2500/users/randomuser +` + +Output + +``` +HTTP/1.1 404 Not Found + +``` + + +## Transport Level Security (TLS) + +### Enable TLS + +It is now time to modify the `[pgagroal-vault]` section of configuration file `/etc/pgagroal/pgagroal_vault.conf` with your editor of choice by adding the following lines in the [pgagroal-vault] section. + +``` +tls = on +tls_cert_file = +tls_key_file = +``` + +This will add TLS support to the server alongside the standard `http` endpoint, allowing clients to make requests to either the `https` or `http` endpoint. + +### Only Server Authentication + +If you wish to do only server authentication the aforementioned configuration suffice. + +**Client Request** + +``` +curl --cacert -i https://localhost:2500/users/ +``` + +### Full Client and Server Authentication + +To enable the server to request the client certificates add the following configuration lines + +``` +tls = on +tls_cert_file = +tls_key_file = +tls_ca_file = +``` + +**Client Request** + +``` +curl --cert --key --cacert -i https://localhost:2500/users/ +``` diff --git a/doc/manual/advanced/10-prometheus.md b/doc/manual/advanced/10-prometheus.md new file mode 100644 index 00000000..e0facd50 --- /dev/null +++ b/doc/manual/advanced/10-prometheus.md @@ -0,0 +1,31 @@ +\newpage + +# Prometheus metrics + +## pgagroal + +Once [**pgagroal**][pgagroal] is running you can access the metrics with a browser at the pooler address, specifying the `metrics` port number and routing to the `/metrics` page. For example, point your web browser at: + +``` +http://localhost:2346/metrics +``` + +It is also possible to get an explaination of what is the meaning of each metric by pointing your web browser at: + +``` +http://localhost:2346/ +``` + +## pgagroal-vault + +Once [**pgagroal-vault**][pgagroal] is running you can access the metrics with a browser at the pooler address, specifying the `metrics` port number and routing to the `/metrics` page. For example, point your web browser at: + +``` +http://localhost:2501/metrics +``` + +It is also possible to get an explaination of what is the meaning of each metric by pointing your web browser at: + +``` +http://localhost:2501/ +``` diff --git a/doc/manual/advanced/97-acknowledgement.md b/doc/manual/advanced/97-acknowledgement.md new file mode 100644 index 00000000..2f744178 --- /dev/null +++ b/doc/manual/advanced/97-acknowledgement.md @@ -0,0 +1,50 @@ +\newpage + +# Acknowledgement + +## Authors + +[**pgagroal**][pgagroal] was created by the following authors: + +``` +Jesper Pedersen +David Fetter +Will Leinweber +Junduo Dong +Luca Ferrari +Nikita Bugrovsky +Lawrence Wu +Yongting You <2010youy01@gmail.com> +Ashutosh Sharma +Henrique de Carvalho +Yihe Lu +Eugenio Gigante +Mohanad Khaled +Haoran Zhang +Christian Englert +``` + +## Committers + +``` +Jesper Pedersen +Luca Ferrari +``` + +## Contributing + +Contributions to [**pgagroal**][pgagroal] are managed on [GitHub][pgagroal] + +* [Ask a question][ask] +* [Raise an issue][issue] +* [Feature request][request] +* [Code submission][submission] + +Contributions are most welcome! + +Please, consult our [Code of Conduct][conduct] policies for interacting in our +community. + +Consider giving the project a [star][star] on +[GitHub][pgagroal] if you find it useful. And, feel free to follow +the project on [Twitter][twitter] as well. diff --git a/doc/manual/advanced/98-licenses.md b/doc/manual/advanced/98-licenses.md new file mode 100644 index 00000000..ba7d265f --- /dev/null +++ b/doc/manual/advanced/98-licenses.md @@ -0,0 +1,78 @@ +\newpage + +# License + +``` +Copyright (C) 2025 The pgagroal community + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following +disclaimer in the documentation and/or other materials provided +with the distribution. + +3. Neither the name of the copyright holder nor the names of +its contributors may be used to endorse or promote products +derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY +OF SUCH DAMAGE. +``` + +[BSD-3-Clause][license] + + +## libart +Our adaptive radix tree (ART) implementation is based on + [The Adaptive Radix Tree: ARTful Indexing for Main-Memory Databases][ART_paper] + and [libart][libart] which has a [3-BSD license][license] as + +``` +Copyright (c) 2012, Armon Dadgar +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name of the organization nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARMON DADGAR +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` diff --git a/doc/manual/advanced/99-references.md b/doc/manual/advanced/99-references.md new file mode 100644 index 00000000..0d6bebf1 --- /dev/null +++ b/doc/manual/advanced/99-references.md @@ -0,0 +1,73 @@ + + + [pgagroal]: https://github.com/agroal/pgagroal + [postgresql]: https://www.postgresql.org + [rocky]: https://www.rockylinux.org + [fedora]: https://getfedora.org/ + [rhel]: https://www.redhat.com/en/technologies/linux-platforms/enterprise-linux + [appstram]: https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/installing_managing_and_removing_user-space_components/using-appstream_using-appstream + [freebsd]: https://www.freebsd.org/ + [openbsd]: http://www.openbsd.org/ + [gcc]: https://gcc.gnu.org + [cmake]: https://cmake.org + [make]: https://www.gnu.org/software/make/ + [libev]: http://software.schmorp.de/pkg/libev.html + [openssl]: http://www.openssl.org/ + [systemd]: https://www.freedesktop.org/wiki/Software/systemd/ + [rst2man]: https://docutils.sourceforge.io/ + [pandoc]: https://pandoc.org/ + [pandoc_latex_template]: https://github.com/Wandmalfarbe/pandoc-latex-template + [texlive]: https://www.tug.org/texlive/ + [clang]: https://clang.llvm.org/ + [git_squash]: https://www.git-tower.com/learn/git/faq/git-squash + [progit]: https://github.com/progit/progit2/releases + [prometheus]: https://prometheus.io/ + [wireshark]: https://www.wireshark.org/ + [pgprtdbg]: https://github.com/jesperpedersen/pgprtdbg + [ART_paper]: http://www-db.in.tum.de/~leis/papers/ART.pdf + [libart]: https://github.com/armon/libart + + + [rpm]: https://github.com/agroal/pgagroal/blob/main/doc/RPM.md + [configuration]: https://github.com/agroal/pgagroal/blob/main/doc/CONFIGURATION.md + + [t_install]: https://github.com/agroal/pgagroal/blob/master/doc/tutorial/01_install.md + [t_prefill]: https://github.com/agroal/pgagroal/blob/master/doc/tutorial/02_prefill.md + [t_remote_management]: https://github.com/agroal/pgagroal/blob/master/doc/tutorial/03_remote_management.md + [t_prometheus]: https://github.com/agroal/pgagroal/blob/master/doc/tutorial/04_prometheus.md + [t_split_security]: https://github.com/agroal/pgagroal/blob/master/doc/tutorial/05_split_security.md + + [sample]: https://github.com/agroal/pgagroal/blob/main/doc/etc/pgagroal.conf + + + [main_c]: https://github.com/agroal/pgagroal/blob/main/src/main.c + [cli_c]: https://github.com/agroal/pgagroal/blob/main/src/cli.c + [admin_c]: https://github.com/agroal/pgagroal/blob/main/src/admin.c + [vault_c]: https://github.com/agroal/pgagroal/blob/main/src/vault.c + + [shmem_h]: https://github.com/agroal/pgagroal/blob/main/src/include/shmem.h + [pgagroal_h]: https://github.com/agroal/pgagroal/blob/main/src/include/pgagroal.h + [messge_h]: https://github.com/agroal/pgagroal/blob/main/src/include/message.h + [network_h]: https://github.com/agroal/pgagroal/blob/main/src/include/network.h + [memory_h]: https://github.com/agroal/pgagroal/blob/main/src/include/memory.h + [management_h]: https://github.com/agroal/pgagroal/blob/main/src/include/management.h + [remote_h]: https://github.com/agroal/pgagroal/blob/main/src/include/remote.h + [prometheus_h]: https://github.com/agroal/pgagroal/blob/main/src/include/prometheus.h + [logging_h]: https://github.com/agroal/pgagroal/blob/main/src/include/logging.h + + [message_c]: https://github.com/agroal/pgagroal/blob/main/src/libpgagroal/message.c + [network_c]: https://github.com/agroal/pgagroal/blob/main/src/libpgagroal/network.c + [memory_c]: https://github.com/agroal/pgagroal/blob/main/src/libpgagroal/memory.c + [remote_c]: https://github.com/agroal/pgagroal/blob/main/src/libpgagroal/remote.c + [prometheus_c]: https://github.com/agroal/pgagroal/blob/main/src/libpgagroal/prometheus.c + [logging_c]: https://github.com/agroal/pgagroal/blob/main/src/libpgagroal/logging.c + + + [ask]: https://github.com/agroal/pgagroal/discussions + [issue]: https://github.com/agroal/pgagroal/issues + [request]: https://github.com/agroal/pgagroal/issues + [submission]: https://github.com/agroal/pgagroal/pulls + [conduct]: https://github.com/agroal/pgagroal/blob/main/CODE_OF_CONDUCT.md + [star]: https://github.com/agroal/pgagroal/stargazers + [twitter]: https://twitter.com/pgagroal/ + [license]: https://opensource.org/licenses/BSD-3-Clause diff --git a/doc/manual/dev-00-head.md b/doc/manual/dev-00-head.md new file mode 100644 index 00000000..d999a404 --- /dev/null +++ b/doc/manual/dev-00-head.md @@ -0,0 +1,14 @@ +--- +title: "pgagroal" +keywords: [pgagroal, PostgreSQL] +subtitle: "Developer Guide" +lang: "en" +titlepage: true, +titlepage-color: "0064A5" +titlepage-text-color: "FFFFFF" +titlepage-rule-color: "360049" +titlepage-rule-height: 0 +toc-own-page: true +listings-disable-line-numbers: true +table-use-row-colors: true +... diff --git a/doc/manual/dev-01-git.md b/doc/manual/dev-01-git.md new file mode 100644 index 00000000..bb303ec6 --- /dev/null +++ b/doc/manual/dev-01-git.md @@ -0,0 +1,101 @@ +\newpage + +# Git guide + +Here are some links that will help you + +* [How to Squash Commits in Git][git_squash] +* [ProGit book][progit] + +## Basic steps + +### Start by forking the repository + +This is done by the "Fork" button on GitHub. + +## Clone your repository locally + +This is done by + +```sh +git clone git@github.com:/pgagroal.git +``` + +### Add upstream + +Do + +```sh +cd pgagroal +git remote add upstream https://github.com/agroal/pgagroal.git +``` + +### Do a work branch + +```sh +git checkout -b mywork main +``` + +### Make the changes + +Remember to verify the compile and execution of the code. + +Use + +``` +[#xyz] Description +``` + +as the commit message where `[#xyz]` is the issue number for the work, and +`Description` is a short description of the issue in the first line + +### Multiple commits + +If you have multiple commits on your branch then squash them + +``` sh +git rebase -i HEAD~2 +``` + +for example. It is `p` for the first one, then `s` for the rest + +### Rebase + +Always rebase + +``` sh +git fetch upstream +git rebase -i upstream/main +``` + +### Force push + +When you are done with your changes force push your branch + +``` sh +git push -f origin mywork +``` + +and then create a pull request for it + +### Format source code + +Use + +``` sh +./uncrustify.sh +``` + +to format the source code + +### Repeat + +Based on feedback keep making changes, squashing, rebasing and force pushing + +### Undo + +Normally you can reset to an earlier commit using `git reset --hard`. + +But if you accidentally squashed two or more commits, and you want to undo that, you need to know where to reset to, and the commit seems to have lost after you rebased. + +But they are not actually lost - using `git reflog`, you can find every commit the HEAD pointer has ever pointed to. Find the commit you want to reset to, and do `git reset --hard`. diff --git a/doc/manual/dev-02-architecture.md b/doc/manual/dev-02-architecture.md new file mode 100644 index 00000000..5c672111 --- /dev/null +++ b/doc/manual/dev-02-architecture.md @@ -0,0 +1,315 @@ +\newpage + +# Architecture + +## Overview + +[**pgagroal**](https://github.com/agroal/pgagroal) use a process model (`fork()`), where each process handles one connection to [PostgreSQL](https://www.postgresql.org). +This was done such a potential crash on one connection won't take the entire pool down. + +The main process is defined in [main.c](../src/main.c). When a client connects it is processed in its own process, which +is handle in [worker.h](../src/include/worker.h) ([worker.c](../src/libpgagroal/worker.c)). + +Once the client disconnects the connection is put back in the pool, and the child process is terminated. + +## Shared memory + +A memory segment ([shmem.h](../src/include/shmem.h)) is shared among all processes which contains the [**pgagroal**](https://github.com/agroal/pgagroal) +state containing the configuration of the pool, the list of servers and the state of each connection. + +The configuration of [**pgagroal**](https://github.com/agroal/pgagroal) (`struct configuration`), the configuration of the servers (`struct server`) and +the state of each connection (`struct connection`) is initialized in this shared memory segment. +These structs are all defined in [pgagroal.h](../src/include/pgagroal.h). + +The shared memory segment is created using the `mmap()` call. + +## Atomic operations + +The [atomic operation library](https://en.cppreference.com/w/c/atomic) is used to define the state of each of the +connection, and move them around in the connection state diagram. The state diagram has the follow states + +| State name | Description | +|------------|-------------| +| `STATE_NOTINIT` | The connection has not been initialized | +| `STATE_INIT` | The connection is being initialized | +| `STATE_FREE` | The connection is free | +| `STATE_IN_USE` | The connection is in use | +| `STATE_GRACEFULLY` | The connection will be killed upon return to the pool | +| `STATE_FLUSH` | The connection is being flushed | +| `STATE_IDLE_CHECK` | The connection is being idle timeout checked | +| `STATE_MAX_CONNECTION_AGE` | The connection is being max connection age checked | +| `STATE_VALIDATION` | The connection is being validated | +| `STATE_REMOVE` | The connection is being removed | + +These state are defined in [pgagroal.h](../src/include/pgagroal.h). + +## Pool + +The [**pgagroal**](https://github.com/agroal/pgagroal) pool API is defined in [pool.h](../src/include/pool.h) ([pool.c](../src/libpgagroal/pool.c)). + +This API defines the functionality of the pool such as getting a connection from the pool, and returning it. +There is no ordering among processes, so a newly created process can obtain a connection before an older process. + +The pool operates on the `struct connection` data type defined in [pgagroal.h](../src/include/pgagroal.h). + +## Network and messages + +All communication is abstracted using the `struct message` data type defined in [message.h](../src/include/message.h). + +Reading and writing messages are handled in the [message.h](../src/include/message.h) ([message.c](../src/libpgagroal/message.c)) +files. + +Network operations are defined in [network.h](../src/include/network.h) ([network.c](../src/libpgagroal/network.c)). + +## Memory + +Each process uses a fixed memory block for its network communication, which is allocated upon startup of the worker. + +That way we don't have to allocate memory for each network message, and more importantly free it after end of use. + +The memory interface is defined in [memory.h](../src/include/memory.h) ([memory.c](../src/libpgagroal/memory.c)). + +## Management + +[**pgagroal**][pgagroal] has a management interface which defines the administrator abilities that can be performed when it is running. +This include for example taking a backup. The `pgagroal-cli` program is used for these operations ([cli.c][cli_c]). + +The management interface is defined in [management.h][management_h]. The management interface +uses its own protocol which uses JSON as its foundation. + +### Write + +The client sends a single JSON string to the server, + +| Field | Type | Description | +| :------------ | :----- | :------------------------------ | +| `compression` | uint8 | The compression type | +| `encryption` | uint8 | The encryption type | +| `length` | uint32 | The length of the JSON document | +| `json` | String | The JSON document | + +The server sends a single JSON string to the client, + +| Field | Type | Description | +| :------------ | :----- | :------------------------------ | +| `compression` | uint8 | The compression type | +| `encryption` | uint8 | The encryption type | +| `length` | uint32 | The length of the JSON document | +| `json` | String | The JSON document | + +### Read + +The server sends a single JSON string to the client, + +| Field | Type | Description | +| :------------ | :----- | :------------------------------ | +| `compression` | uint8 | The compression type | +| `encryption` | uint8 | The encryption type | +| `length` | uint32 | The length of the JSON document | +| `json` | String | The JSON document | + +The client sends to the server a single JSON documents, + +| Field | Type | Description | +| :------------ | :----- | :------------------------------ | +| `compression` | uint8 | The compression type | +| `encryption` | uint8 | The encryption type | +| `length` | uint32 | The length of the JSON document | +| `json` | String | The JSON document | + +### Remote management + +The remote management functionality uses the same protocol as the standard management method. + +However, before the management packet is sent the client has to authenticate using SCRAM-SHA-256 using the +same message format that PostgreSQL uses, e.g. StartupMessage, AuthenticationSASL, AuthenticationSASLContinue, +AuthenticationSASLFinal and AuthenticationOk. The SSLRequest message is supported. + +The remote management interface is defined in [remote.h](../src/include/remote.h) ([remote.c](../src/libpgagroal/remote.c)). + +## libev usage + +[libev](http://software.schmorp.de/pkg/libev.html) is used to handle network interactions, which is "activated" +upon an `EV_READ` event. + +Each process has its own event loop, such that the process only gets notified when data related only to that process +is ready. The main loop handles the system wide "services" such as idle timeout checks and so on. + +## Pipeline + +[**pgagroal**](https://github.com/agroal/pgagroal) has the concept of a pipeline that defines how communication is routed from the client through [**pgagroal**](https://github.com/agroal/pgagroal) to +[PostgreSQL](https://www.postgresql.org). Likewise in the other direction. + +A pipeline is defined by + +```C +struct pipeline +{ + initialize initialize; + start start; + callback client; + callback server; + stop stop; + destroy destroy; + periodic periodic; +}; +``` + +in [pipeline.h](../src/include/pipeline.h). + +The functions in the pipeline are defined as + +| Function | Description | +|----------|-------------| +| `initialize` | Global initialization of the pipeline, may return a pointer to a shared memory segment | +| `start` | Called when the pipeline instance is started | +| `client` | Client to [**pgagroal**](https://github.com/agroal/pgagroal) communication | +| `server` | [PostgreSQL](https://www.postgresql.org) to [**pgagroal**](https://github.com/agroal/pgagroal) communication | +| `stop` | Called when the pipeline instance is stopped | +| `destroy` | Global destruction of the pipeline | +| `periodic` | Called periodic | + +The functions `start`, `client`, `server` and `stop` has access to the following information + +```C +struct worker_io +{ + struct ev_io io; /* The libev base type */ + int client_fd; /* The client descriptor */ + int server_fd; /* The server descriptor */ + int slot; /* The slot */ + SSL* client_ssl; /* The client SSL context */ + SSL* server_ssl; /* The server SSL context */ +}; +``` +defined in [worker.h](../src/include/worker.h). + +### Performance pipeline + +One of the goals for [**pgagroal**](https://github.com/agroal/pgagroal) is performance, so the performance pipeline will only look for the +[`Terminate`](https://www.postgresql.org/docs/11/protocol-message-formats.html) message from the client and act on that. +Likewise the performance pipeline will only look for `FATAL` errors from the server. This makes the pipeline very fast, since there +is a minimum overhead in the interaction. + +The pipeline is defined in [pipeline_perf.c](../src/libpgagroal/pipeline_perf.c) in the functions + +| Function | Description | +|----------|-------------| +| `performance_initialize` | Nothing | +| `performance_start` | Nothing | +| `performance_client` | Client to [**pgagroal**](https://github.com/agroal/pgagroal) communication | +| `performance_server` | [PostgreSQL](https://www.postgresql.org) to [**pgagroal**](https://github.com/agroal/pgagroal) communication | +| `performance_stop` | Nothing | +| `performance_destroy` | Nothing | +| `performance_periodic` | Nothing | + +### Session pipeline + +The session pipeline works like the performance pipeline with the exception that it checks if +a Transport Layer Security (TLS) transport should be used. + +The pipeline is defined in [pipeline_session.c](../src/libpgagroal/pipeline_session.c) in the functions + +| Function | Description | +|----------|-------------| +| `session_initialize` | Initialize memory segment if disconnect_client is active | +| `session_start` | Prepares the client segment if disconnect_client is active | +| `session_client` | Client to [**pgagroal**](https://github.com/agroal/pgagroal) communication | +| `session_server` | [PostgreSQL](https://www.postgresql.org) to [**pgagroal**](https://github.com/agroal/pgagroal) communication | +| `session_stop` | Updates the client segment if disconnect_client is active | +| `session_destroy` | Destroys memory segment if initialized | +| `session_periodic` | Checks if clients should be disconnected | + +### Transaction pipeline + +The transaction pipeline will return the connection to the server after each transaction. The pipeline supports +Transport Layer Security (TLS). + +The pipeline uses the [ReadyForQuery](https://www.postgresql.org/docs/current/protocol-message-formats.html) message +to check the status of the transaction, and therefore needs to maintain track of the message headers. + +The pipeline has a management interface in order to receive the socket descriptors from the parent process when a new +connection is added to the pool. The pool will retry if the client in question doesn't consider the socket descriptor valid. + +The pipeline is defined in [pipeline_transaction.c](../src/libpgagroal/pipeline_transaction.c) in the functions + +| Function | Description | +|----------|-------------| +| `transaction_initialize` | Nothing | +| `transaction_start` | Setup process variables and returns the connection to the pool | +| `transaction_client` | Client to [**pgagroal**](https://github.com/agroal/pgagroal) communication. Obtain connection if needed | +| `transaction_server` | [PostgreSQL](https://www.postgresql.org) to [**pgagroal**](https://github.com/agroal/pgagroal) communication. Keep track of message headers | +| `transaction_stop` | Return connection to the pool if needed. Possible rollback of active transaction | +| `transaction_destroy` | Nothing | +| `transaction_periodic` | Nothing | + +## Signals + +The main process of [**pgagroal**](https://github.com/agroal/pgagroal) supports the following signals `SIGTERM`, `SIGINT` and `SIGALRM` +as a mechanism for shutting down. The `SIGTRAP` signal will put [**pgagroal**](https://github.com/agroal/pgagroal) into graceful shutdown, meaning that +exisiting connections are allowed to finish their session. The `SIGABRT` is used to request a core dump (`abort()`). +The `SIGHUP` signal will trigger a reload of the configuration. + +The child processes support `SIGQUIT` as a mechanism to shutdown. This will not shutdown the pool itself. + +It should not be needed to use `SIGKILL` for [**pgagroal**](https://github.com/agroal/pgagroal). Please, consider using `SIGABRT` instead, and share the +core dump and debug logs with the [**pgagroal**](https://github.com/agroal/pgagroal) community. + +## Reload + +The `SIGHUP` signal will trigger a reload of the configuration. + +However, some configuration settings requires a full restart of [**pgagroal**](https://github.com/agroal/pgagroal) in order to take effect. These are + +* `hugepage` +* `libev` +* `log_path` +* `log_type` +* `max_connections` +* `pipeline` +* `unix_socket_dir` +* `pidfile` +* Limit rules defined by `pgagroal_databases.conf` +* TLS rules defined by server section + +The configuration can also be reloaded using `pgagroal-cli -c pgagroal.conf conf reload`. The command is only supported +over the local interface, and hence doesn't work remotely. + +## Prometheus + +pgagroal has support for [Prometheus](https://prometheus.io/) when the `metrics` port is specified. + +**Note:** It is crucial to carefully initialize Prometheus memory in any program files for example functions like `pgagroal_init_prometheus()` and `pgagroal_init_prometheus_cache()` should only be invoked if `metrics` is greater than 0. + +The module serves two endpoints + +* `/` - Overview of the functionality (`text/html`) +* `/metrics` - The metrics (`text/plain`) + +All other URLs will result in a 403 response. + +The metrics endpoint supports `Transfer-Encoding: chunked` to account for a large amount of data. + +The implementation is done in [prometheus.h](../src/include/prometheus.h) and +[prometheus.c](../src/libpgagroal/prometheus.c). + +## Failover support + +pgagroal can failover a PostgreSQL instance if clients can't write to it. + +This is done using an external script provided by the user. + +The implementation is done in [server.h](../src/include/server.h) and +[server.c](../src/libpgagroal/server.c). + +## Logging + +Simple logging implementation based on a `atomic_schar` lock. + +The implementation is done in [logging.h](../src/include/logging.h) and +[logging.c](../src/libpgagroal/logging.c). + +## Protocol + +The protocol interactions can be debugged using [Wireshark](https://www.wireshark.org/) or +[pgprtdbg](https://github.com/jesperpedersen/pgprtdbg). diff --git a/doc/manual/dev-03-rpm.md b/doc/manual/dev-03-rpm.md new file mode 100644 index 00000000..fd24ef44 --- /dev/null +++ b/doc/manual/dev-03-rpm.md @@ -0,0 +1,37 @@ +\newpage + +# RPM + +[**pgagroal**][pgagroal] can be built into a RPM for [Fedora][fedora] systems. + +## Requirements + +```sh +dnf install gcc rpm-build rpm-devel rpmlint make python bash coreutils diffutils patch rpmdevtools chrpath +``` + +## Setup RPM development + +```sh +rpmdev-setuptree +``` + +## Create source package + +```sh +git clone https://github.com/agroal/pgagroal.git +cd pgagroal +mkdir build +cd build +cmake -DCMAKE_BUILD_TYPE=Release .. +make package_source +``` + +## Create RPM package + +```sh +cp pgagroal-$VERSION.tar.gz ~/rpmbuild/SOURCES +QA_RPATHS=0x0001 rpmbuild -bb pgagroal.spec +``` + +The resulting RPM will be located in `~/rpmbuild/RPMS/x86_64/`, if your architecture is `x86_64`. diff --git a/doc/manual/user-00-head.md b/doc/manual/user-00-head.md new file mode 100644 index 00000000..b51d1e70 --- /dev/null +++ b/doc/manual/user-00-head.md @@ -0,0 +1,14 @@ +--- +title: "pgagroal" +keywords: [pgagroal, PostgreSQL] +subtitle: "User Guide" +lang: "en" +titlepage: true, +titlepage-color: "0064A5" +titlepage-text-color: "FFFFFF" +titlepage-rule-color: "360049" +titlepage-rule-height: 0 +toc-own-page: true +listings-disable-line-numbers: true +table-use-row-colors: true +... diff --git a/doc/manual/user-01-quickstart.md b/doc/manual/user-01-quickstart.md new file mode 100644 index 00000000..93d3db89 --- /dev/null +++ b/doc/manual/user-01-quickstart.md @@ -0,0 +1,233 @@ +\newpage + +# Quick start + +Make sure that [**pgagroal**][pgagroal] is installed and in your path by using `pgagroal -?`. You should see + +``` console +pgagroal 1.7.0 + High-performance connection pool for PostgreSQL + +Usage: + pgagroal [ -c CONFIG_FILE ] [ -a HBA_FILE ] [ -d ] + +Options: + -c, --config CONFIG_FILE Set the path to the pgagroal.conf file + -a, --hba HBA_FILE Set the path to the pgagroal_hba.conf file + -l, --limit LIMIT_FILE Set the path to the pgagroal_databases.conf file + -u, --users USERS_FILE Set the path to the pgagroal_users.conf file + -F, --frontend FRONTEND_USERS_FILE Set the path to the pgagroal_frontend_users.conf file + -A, --admins ADMINS_FILE Set the path to the pgagroal_admins.conf file + -S, --superuser SUPERUSER_FILE Set the path to the pgagroal_superuser.conf file + -d, --daemon Run as a daemon + -V, --version Display version information + -?, --help Display help +``` + +If you encounter any issues following the above steps, you can refer to the **Installation** chapter to see how to install or compile pgagroal on your system. + +## Configuration + +Lets create a simple configuration file called `pgagroal.conf` with the content + +``` ini +[pgagroal] +host = * +port = 2345 + +log_type = file +log_level = info +log_path = /tmp/pgagroal.log + +max_connections = 100 +idle_timeout = 600 +validation = off +unix_socket_dir = /tmp/ + +[primary] +host = localhost +port = 5432 +``` + +In our main section called `[pgagroal]` we setup [**pgagroal**][pgagroal] to listen on all network addresses. Logging will be performed at `info` level and put in a file called `/tmp/pgagroal.log`. +We will use 100 connections in the pool with a idle timeout of 10 minutes. Validation is off. Last we specify the location of the `unix_socket_dir` used for management operations and the path for the PostgreSQL command line tools. + +Next we create a section called `[primary]` which has the information about our [PostgreSQL][postgresql] instance. In this case it is running on `localhost` on port `5432`. + +Now we need a host based authentication (HBA) file. Create one called `pgagroal_hba.conf` +with the content + +``` +# +# TYPE DATABASE USER ADDRESS METHOD +# +host all all all all +``` + +This tells [**pgagroal**](https://github.com/agroal/pgagroal) that it can accept connections from all network addresses +for all databases and all user names. + +We are now ready to run [**pgagroal**](https://github.com/agroal/pgagroal). + +See the **Configuration** charpter for all configuration options. + +## Running + +We will run [**pgagroal**][pgagroal] using the command + +``` sh +pgagroal -c pgagroal.conf -a pgagroal_hba.conf +``` + +If this doesn't give an error, then we are ready to do backups. + +[**pgagroal**][pgagroal] is stopped by pressing Ctrl-C (`^C`) in the console where you started it, or by sending the `SIGTERM` signal to the process using `kill `. + +## Run-time administration + +[**pgagroal**][pgagroal] has a run-time administration tool called `pgagroal-cli`. + +You can see the commands it supports by using `pgagroal-cli -?` which will give + +``` console +pgagroal-cli 1.7.0 + Command line utility for pgagroal + +Usage: + pgagroal-cli [ OPTIONS ] [ COMMAND ] + +Options: + -c, --config CONFIG_FILE Set the path to the pgagroal.conf file + Default: /etc/pgagroal/pgagroal.conf + -h, --host HOST Set the host name + -p, --port PORT Set the port number + -U, --user USERNAME Set the user name + -P, --password PASSWORD Set the password + -L, --logfile FILE Set the log file + -F, --format text|json Set the output format + -v, --verbose Output text string of result + -V, --version Display version information + -?, --help Display help + +Commands: + flush [mode] [database] Flush connections according to . + Allowed modes are: + - 'gracefully' (default) to flush all connections gracefully + - 'idle' to flush only idle connections + - 'all' to flush all connections. USE WITH CAUTION! + If no name is specified, applies to all databases. + ping Verifies if pgagroal is up and running + enable [database] Enables the specified databases (or all databases) + disable [database] Disables the specified databases (or all databases) + shutdown [mode] Stops pgagroal pooler. The can be: + - 'gracefully' (default) waits for active connections to quit + - 'immediate' forces connections to close and terminate + - 'cancel' avoid a previously issued 'shutdown gracefully' + status [details] Status of pgagroal, with optional details + switch-to Switches to the specified primary server + conf Manages the configuration (e.g., reloads the configuration + The subcommand can be: + - 'reload' to issue a configuration reload; + - 'get' to obtain information about a runtime configuration value; + conf get + - 'set' to modify a configuration value; + conf set ; + - 'ls' lists the configuration files used. + clear Resets either the Prometheus statistics or the specified server. + can be + - 'server' (default) followed by a server name + - a server name on its own + - 'prometheus' to reset the Prometheus metrics + +pgagroal: +Report bugs: +``` + +This tool can be used on the machine running [**pgagroal**](https://github.com/agroal/pgagroal) to flush connections. + +To flush all idle connections you would use + +``` +pgagroal-cli -c pgagroal.conf flush idle +``` + +To shutdown pgagroal you would use + +``` +pgagroal-cli -c pgagroal.conf shutdown +``` + +Check the outcome of the operations by verifying the exit code, like + +``` +echo $? +``` + +or by using the `-v` flag. + +If pgagroal has both Transport Layer Security (TLS) and `management` enabled then `pgagroal-cli` can +connect with TLS using the files `~/.pgagroal/pgagroal.key` (must be 0600 permission), +`~/.pgagroal/pgagroal.crt` and `~/.pgagroal/root.crt`. + +## Administration + +[**pgagroal**][pgagroal] has an administration tool called `pgagroal-admin`, which is used to control user registration with [**pgagroal**][pgagroal]. + +You can see the commands it supports by using `pgagroal-admin -?` which will give + +``` console +pgagroal-admin 1.7.0 + Administration utility for pgagroal + +Usage: + pgagroal-admin [ -f FILE ] [ COMMAND ] + +Options: + -f, --file FILE Set the path to a user file + Defaults to /etc/pgagroal/pgagroal_users.conf + -U, --user USER Set the user name + -P, --password PASSWORD Set the password for the user + -g, --generate Generate a password + -l, --length Password length + -V, --version Display version information + -?, --help Display help + +Commands: + master-key Create or update the master key + user Manage a specific user, where can be + - add to add a new user + - del to remove an existing user + - edit to change the password for an existing user + - ls to list all available users + +pgagroal: https://agroal.github.io/pgagroal/ +Report bugs: https://github.com/agroal/pgagroal/issues +``` + +In order to set the master key for all users you can use + +``` sh +pgagroal-admin -g master-key +``` + +The master key must be at least 8 characters. + +Then use the other commands to add, update, remove or list the current user names, f.ex. + +``` sh +pgagroal-admin -f pgagroal_users.conf user add +``` + +## Next Steps + +Next steps in improving pgagroal's configuration could be + +* Update `pgagroal.conf` with the required settings for your system +* Set the access rights in `pgagroal_hba.conf` for each user and database +* Add a `pgagroal_users.conf` file using `pgagroal-admin` with a list of known users +* Disable access for unknown users by setting `allow_unknown_users` to `false` +* Define a `pgagroal_databases.conf` file with the limits and prefill settings for each database +* Enable Transport Layer Security v1.2+ (TLS) +* Deploy Grafana dashboard + +See [Configuration][configuration] for more information on these subjects. diff --git a/doc/manual/user-02-configuration.md b/doc/manual/user-02-configuration.md new file mode 100644 index 00000000..ae2675dd --- /dev/null +++ b/doc/manual/user-02-configuration.md @@ -0,0 +1,193 @@ +\newpage + +# Configuration + +The configuration is loaded from either the path specified by the `-c` flag or `/etc/pgagroal/pgagroal.conf`. + +The configuration of [**pgagroal**][pgagroal] is split into sections using the `[` and `]` characters. + +The main section, called `[pgagroal]`, is where you configure the overall properties of [**pgagroal**][pgagroal]. + +Other sections doesn't have any requirements to their naming so you can give them meaningful names like `[primary]` for the primary [PostgreSQL][postgresql] instance. + +All properties are in the format `key = value`. + +The characters `#` and `;` can be used for comments; must be the first character on the line. + +The `Bool` data type supports the following values: `on`, `yes`, `1`, `true`, `off`, `no`, `0` and `false`. + +See a [sample][sample] configuration for running [**pgagroal**][pgagroal] on `localhost`. + +## [pgagroal] + +This section is mandatory and the pooler will refuse to start if the configuration file does not specify one and only one. Usually this section is place on top of the configuration file, but its position within the file does not really matter. +The available keys and their accepted values are reported in the table below. + +| Property | Default | Unit | Required | Description | +|----------|---------|------|----------|-------------| +| host | | String | Yes | The bind address for pgagroal | +| port | | Int | Yes | The bind port for pgagroal | +| unix_socket_dir | | String | Yes | The Unix Domain Socket location | +| metrics | 0 | Int | No | The metrics port (disable = 0) | +| metrics_cache_max_age | 0 | String | No | The number of seconds to keep in cache a Prometheus (metrics) response. If set to zero, the caching will be disabled. Can be a string with a suffix, like `2m` to indicate 2 minutes | +| metrics_cache_max_size | 256k | String | No | The maximum amount of data to keep in cache when serving Prometheus responses. Changes require restart. This parameter determines the size of memory allocated for the cache even if `metrics_cache_max_age` or `metrics` are disabled. Its value, however, is taken into account only if `metrics_cache_max_age` is set to a non-zero value. Supports suffixes: 'B' (bytes), the default if omitted, 'K' or 'KB' (kilobytes), 'M' or 'MB' (megabytes), 'G' or 'GB' (gigabytes).| +| management | 0 | Int | No | The remote management port (disable = 0) | +| log_type | console | String | No | The logging type (console, file, syslog) | +| log_level | info | String | No | The logging level, any of the (case insensitive) strings `FATAL`, `ERROR`, `WARN`, `INFO` and `DEBUG` (that can be more specific as `DEBUG1` thru `DEBUG5`). Debug level greater than 5 will be set to `DEBUG5`. Not recognized values will make the log_level be `INFO` | +| log_path | pgagroal.log | String | No | The log file location. Can be a strftime(3) compatible string. | +| log_rotation_age | 0 | String | No | The age that will trigger a log file rotation. If expressed as a positive number, is managed as seconds. Supports suffixes: 'S' (seconds, the default), 'M' (minutes), 'H' (hours), 'D' (days), 'W' (weeks). A value of `0` disables. | +| log_rotation_size | 0 | String | No | The size of the log file that will trigger a log rotation. Supports suffixes: 'B' (bytes), the default if omitted, 'K' or 'KB' (kilobytes), 'M' or 'MB' (megabytes), 'G' or 'GB' (gigabytes). A value of `0` (with or without suffix) disables. | +| log_line_prefix | %Y-%m-%d %H:%M:%S | String | No | A strftime(3) compatible string to use as prefix for every log line. Must be quoted if contains spaces. | +| log_mode | append | String | No | Append to or create the log file (append, create) | +| log_connections | `off` | Bool | No | Log connects | +| log_disconnections | `off` | Bool | No | Log disconnects | +| blocking_timeout | 30 | Int | No | The number of seconds the process will be blocking for a connection (disable = 0) | +| idle_timeout | 0 | Int | No | The number of seconds a connection is been kept alive (disable = 0) | +| rotate_frontend_password_timeout | 0 | Int | No | The number of seconds after which the passwords of frontend users are updated periodically (disable = 0) | +| rotate_frontend_password_length | 8 | Int | No | The length of the randomized frontend password | +| max_connection_age | 0 | Int | No | The maximum number of seconds that a connection will live (disable = 0) | +| validation | `off` | String | No | Should connection validation be performed. Valid options: `off`, `foreground` and `background` | +| background_interval | 300 | Int | No | The interval between background validation scans in seconds | +| max_retries | 5 | Int | No | The maximum number of iterations to obtain a connection | +| max_connections | 100 | Int | No | The maximum number of connections to PostgreSQL (max 10000) | +| allow_unknown_users | `true` | Bool | No | Allow unknown users to connect | +| authentication_timeout | 5 | Int | No | The number of seconds the process will wait for valid credentials | +| pipeline | `auto` | String | No | The pipeline type (`auto`, `performance`, `session`, `transaction`) | +| auth_query | `off` | Bool | No | Enable authentication query | +| failover | `off` | Bool | No | Enable failover support | +| failover_script | | String | No | The failover script to execute | +| tls | `off` | Bool | No | Enable Transport Layer Security (TLS) | +| tls_cert_file | | String | No | Certificate file for TLS. This file must be owned by either the user running pgagroal or root. | +| tls_key_file | | String | No | Private key file for TLS. This file must be owned by either the user running pgagroal or root. Additionally permissions must be at least `0640` when owned by root or `0600` otherwise. | +| tls_ca_file | | String | No | Certificate Authority (CA) file for TLS. This file must be owned by either the user running pgagroal or root. | +| libev | `auto` | String | No | Select the [libev](http://software.schmorp.de/pkg/libev.html) backend to use. Valid options: `auto`, `select`, `poll`, `epoll`, `iouring`, `devpoll` and `port` | +| keep_alive | on | Bool | No | Have `SO_KEEPALIVE` on sockets | +| nodelay | on | Bool | No | Have `TCP_NODELAY` on sockets | +| non_blocking | off | Bool | No | Have `O_NONBLOCK` on sockets | +| backlog | `max_connections` / 4 | Int | No | The backlog for `listen()`. Minimum `16` | +| hugepage | `try` | String | No | Huge page support (`off`, `try`, `on`) | +| tracker | off | Bool | No | Track connection lifecycle | +| track_prepared_statements | off | Bool | No | Track prepared statements (transaction pooling) | +| pidfile | | String | No | Path to the PID file. If omitted, automatically set to `unix_socket_dir`/pgagroal.`port`.pid | +| update_process_title | `verbose` | String | No | The behavior for updating the operating system process title, mainly related to connection processes. Allowed settings are: `never` (or `off`), does not update the process title; `strict` to set the process title without overriding the existing initial process title length; `minimal` to set the process title to `username/database`; `verbose` (or `full`) to set the process title to `user@host:port/database`. Please note that `strict` and `minimal` are honored only on those systems that do not provide a native way to set the process title (e.g., Linux). On other systems, there is no difference between `strict` and `minimal` and the assumed behaviour is `minimal` even if `strict` is used. `never` and `verbose` are always honored, on every system. On Linux systems the process title is always trimmed to 255 characters, while on system that provide a natve way to set the process title it can be longer. | + +__Danger zone__ + +| Property | Default | Unit | Required | Description | +|----------|---------|------|----------|-------------| +| disconnect_client | 0 | Int | No | Disconnect clients that have been idle for more than the specified seconds. This setting __DOES NOT__ take long running transactions into account | +| disconnect_client_force | off | Bool | No | Disconnect clients that have been active for more than the specified seconds. This setting __DOES NOT__ take long running transactions into account | + + +### Server section + +Each section with a name different from [**pgagroal**](https://github.com/agroal/pgagroal) will be treated as an host section. +There can be up to `64` host sections, each with an unique name and different combination of `host` and `port` settings, otherwise the pooler will complain about duplicated server configuration. + +| Property | Default | Unit | Required | Description | +|----------|---------|------|----------|-------------| +| host | | String | Yes | The address of the PostgreSQL instance | +| port | | Int | Yes | The port of the PostgreSQL instance | +| primary | | Bool | No | Identify the instance as primary (hint) | +| tls | `off` | Bool | No | Enable Transport Layer Security (TLS) support (Experimental - no pooling). Changes require restart. | +| tls_cert_file | | String | No | Certificate file for TLS. This file must be owned by either the user running pgagroal or root. Changes require restart. | +| tls_key_file | | String | No | Private key file for TLS. This file must be owned by either the user running pgagroal or root. Additionally permissions must be at least `0640` when owned by root or `0600` otherwise.Changes require restart. | +| tls_ca_file | | String | No | Certificate Authority (CA) file for TLS. This file must be owned by either the user running pgagroal or root. Changes require restart. | + +Note, that if `host` starts with a `/` it represents a path and [**pgagroal**](https://github.com/agroal/pgagroal) will connect using a Unix Domain Socket. + +## pgagroal_hba configuration + +The `pgagroal_hba` configuration controls access to [**pgagroal**](https://github.com/agroal/pgagroal) through host-based authentication. + +The configuration is loaded from either the path specified by the `-a` flag or `/etc/pgagroal/pgagroal_hba.conf`. + +The format of the file follows the similar [PostgreSQL HBA configuration format](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html), and as such looks like + +``` +# +# TYPE DATABASE USER ADDRESS METHOD +# +host all all all all +``` + +| Column | Required | Description | +|--------|----------|-------------| +| TYPE | Yes | Specifies the access method for clients. `host` and `hostssl` are supported | +| DATABASE | Yes | Specifies the database for the rule. Either specific name or `all` for all databases | +| USER | Yes | Specifies the user for the rule. Either specific name or `all` for all users | +| ADDRESS | Yes | Specifies the network for the rule. `all` for all networks, or IPv4 address with a mask (`0.0.0.0/0`) or IPv6 address with a mask (`::0/0`) | +| METHOD | Yes | Specifies the authentication mode for the user. `all` for all methods, otherwise `trust`, `reject`, `password`, `md5` or `scram-sha-256` | + +Remote management users needs to have their database set to `admin` in order for the entry to be considered. + +There can be up to `64` HBA entries in the configuration file. + +## pgagroal_databases configuration + +The `pgagroal_databases` configuration defines limits for a database or a user or both. The limits are the number +of connections from [**pgagroal**](https://github.com/agroal/pgagroal) to PostgreSQL for each entry. + +The file also defines the initial and minimum pool size for a database and user pair. Note, that this feature requires +a user definition file, see below. + +The configuration is loaded from either the path specified by the `-l` flag or `/etc/pgagroal/pgagroal_databases.conf`. + +``` +# +# DATABASE USER MAX_SIZE INITIAL_SIZE MIN_SIZE +# +mydb myuser all +anotherdb userB 10 5 3 +``` + +| Column | Required | Description | +|--------|----------|-------------| +| DATABASE | Yes | Specifies the database for the rule. `all` for all databases | +| USER | Yes | Specifies the user for the rule. `all` for all users | +| MAX_SIZE | Yes | Specifies the maximum pool size for the entry. `all` for all remaining counts from `max_connections` | +| INITIAL_SIZE | No | Specifies the initial pool size for the entry. `all` for `MAX_SIZE` connections. Default is 0 | +| MIN_SIZE | No | Specifies the minimum pool size for the entry. `all` for `MAX_SIZE` connections. Default is 0 | + + +There can be up to `64` entries in the configuration file. + +In the case a limit entry has incoherent values, for example `INITIAL_SIZE` smaller than `MIN_SIZE`, the system will try to automatically adjust the settings on the fly, reporting messages in the logs. + +The system will find the best match limit entry for a given `DATABASE`-`USER` pair according to the following rules: +1. Use the first entry with an exact `DATABASE` and `USER` match. +2. If there is no exact match, use the entry with a `USER` match and `DATABASE` set to `all`. +3. If Rule 2 does not apply, use the entry with a `DATABASE` match and `USER` set to `all`. + +## pgagroal_users configuration + +The `pgagroal_users` configuration defines the users known to the system. This file is created and managed through the `pgagroal-admin` tool. + +The configuration is loaded from either the path specified by the `-u` flag or `/etc/pgagroal/pgagroal_users.conf`. + +## pgagroal_frontend_users configuration + +The `pgagroal_frontend_users` configuration defines the passwords for the users connecting to pgagroal. +This allows the setup to use different passwords for the [**pgagroal**](https://github.com/agroal/pgagroal) to PostgreSQL authentication. +This file is created and managed through the `pgagroal-admin` tool. + +All users defined in the frontend authentication must be defined in the user vault (`-u`). + +Frontend users (`-F`) requires a user vault (`-u`) to be defined. + +The configuration is loaded from either the path specified by the `-F` flag or `/etc/pgagroal/pgagroal_frontend_users.conf`. + +## pgagroal_admins configuration + +The `pgagroal_admins` configuration defines the administrators known to the system. This file is created and managed through the `pgagroal-admin` tool. + +The configuration is loaded from either the path specified by the `-A` flag or `/etc/pgagroal/pgagroal_admins.conf`. + +If pgagroal has both Transport Layer Security (TLS) and `management` enabled then `pgagroal-cli` can connect with TLS using the files `~/.pgagroal/pgagroal.key` (must be 0600 permission), `~/.pgagroal/pgagroal.crt` and `~/.pgagroal/root.crt`. + +## pgagroal_superuser configuration + +The `pgagroal_superuser` configuration defines the superuser known to the system. This file is created and managed through +the `pgagroal-admin` tool. It may only have one user defined. + +The configuration is loaded from either the path specified by the `-S` flag or `/etc/pgagroal/pgagroal_superuser.conf`. diff --git a/doc/manual/user-04-tutorials.md b/doc/manual/user-04-tutorials.md new file mode 100644 index 00000000..10dd7ccb --- /dev/null +++ b/doc/manual/user-04-tutorials.md @@ -0,0 +1,3 @@ +\newpage + +# Tutorials diff --git a/doc/manual/user-10-cli.md b/doc/manual/user-10-cli.md new file mode 100644 index 00000000..93a15ff1 --- /dev/null +++ b/doc/manual/user-10-cli.md @@ -0,0 +1,582 @@ +\newpage + +# Command line interface + +`pgagroal-cli` is a command line interface to interact with [**pgagroal**](https://github.com/agroal/pgagroal). +The executable accepts a set of options, as well as a command to execute. +If no command is provided, the program will show the help screen. + +The `pgagroal-cli` utility has the following synopsis: + +``` sh +pgagroal-cli 1.7.0 + Command line utility for pgagroal + +Usage: + pgagroal-cli [ OPTIONS ] [ COMMAND ] + +Options: + -c, --config CONFIG_FILE Set the path to the pgagroal.conf file + Default: /etc/pgagroal/pgagroal.conf + -h, --host HOST Set the host name + -p, --port PORT Set the port number + -U, --user USERNAME Set the user name + -P, --password PASSWORD Set the password + -L, --logfile FILE Set the log file + -F, --format text|json Set the output format + -v, --verbose Output text string of result + -V, --version Display version information + -?, --help Display help + +Commands: + flush [mode] [database] Flush connections according to . + Allowed modes are: + - 'gracefully' (default) to flush all connections gracefully + - 'idle' to flush only idle connections + - 'all' to flush all connections. USE WITH CAUTION! + If no name is specified, applies to all databases. + ping Verifies if pgagroal is up and running + enable [database] Enables the specified databases (or all databases) + disable [database] Disables the specified databases (or all databases) + shutdown [mode] Stops pgagroal pooler. The can be: + - 'gracefully' (default) waits for active connections to quit + - 'immediate' forces connections to close and terminate + - 'cancel' avoid a previously issued 'shutdown gracefully' + status [details] Status of pgagroal, with optional details + switch-to Switches to the specified primary server + conf Manages the configuration (e.g., reloads the configuration + The subcommand can be: + - 'reload' to issue a configuration reload; + - 'get' to obtain information about a runtime configuration value; + conf get + - 'set' to modify a configuration value; + conf set ; + - 'ls' lists the configuration files used. + clear Resets either the Prometheus statistics or the specified server. + can be + - 'server' (default) followed by a server name + - a server name on its own + - 'prometheus' to reset the Prometheus metrics + +pgagroal: +Report bugs: +``` + +Options can be specified either in short or long form, in any position of the command line. + +By default the command output, if any, is reported as text. It is possible to specify JSON as the output format, +and this is the suggested format if there is the need to automtically parse the command output, since the text format +could be subject to changes in future releases. For more information about the JSON output format, +please see the [JSON Output Format](#json-output-format) section. + +## Commands + +### flush +The `flush` command performs a connection flushing. +It accepts a *mode* to operate the actual flushing: +- `gracefully` (the default if not specified), flush connections when possible; +- `idle` to flush only connections in state *idle*; +- `all` to flush all the connections (**use with caution!**). + +The command accepts a database name, that if provided, restricts the scope of +`flush` only to connections related to such database. +If no database is provided, the `flush` command is operated against all databases. + + +Command + +``` +pgagroal-cli flush [gracefully|idle|all] [*|] +``` + +Examples + +``` +pgagroal-cli flush # pgagroal-cli flush gracefully '*' +pgagroal-cli flush idle # pgagroal-cli flush idle '*' +pgagroal-cli flush all # pgagroal-cli flush all '*' +pgagroal-cli flush pgbench # pgagroal-cli flush gracefully pgbench +``` + +### ping +The `ping` command checks if [**pgagroal**](https://github.com/agroal/pgagroal) is running. +In case of success, the command does not print anything on the standard output unless the `--verbose` flag is used. + +Command + +``` +pgagroal-cli ping +``` + +Example + +``` +pgagroal-cli ping --verbose # pgagroal-cli: Success (0) +pgagroal-cli ping # $? = 0 +``` + +In the case [**pgagroal**](https://github.com/agroal/pgagroal) is not running, a message is printed on the standard error and the exit status is set to a non-zero value: + +``` +pgagroal-cli ping # $? = 1 +Connection error on /tmp +``` + +### enable +Enables a database (or all databases). + +Command + +``` +pgagroal-cli enable [|*] +``` + +Example + +``` +pgagroal-cli enable +``` + +### disable +Disables a database (or all databases). + +Command + +``` +pgagroal-cli disable [|*] +``` + +Example + +``` +pgagroal-cli disable +``` + +### shutdown +The `shutdown` command is used to stop the connection pooler. +It supports the following operating modes: +- `gracefully` (the default) closes the pooler as soon as no active connections are running; +- `immediate` force an immediate stop. + +If the `gracefully` mode is requested, chances are the system will take some time to +perform the effective shutdown, and therefore it is possible to abort the request +issuing another `shutdown` command with the mode `cancel`. + + +Command + +``` +pgagroal-cli shutdown [gracefully|immediate|cancel] +``` + +Examples + +``` +pgagroal-cli shutdown # pgagroal-cli shutdown gracefully +... +pgagroal-cli shutdown cancel # stops the above command +``` + + +### status +The `status` command reports the current status of the [**pgagroal**](https://github.com/agroal/pgagroal) pooler. +Without any subcommand, `status` reports back a short set of information about the pooler. + +Command + +``` +pgagroal-cli status +``` + +Example + +``` +pgagroal-cli status + +``` + +With the `details` subcommand, a more verbose output is printed with a detail about every connection. + +Example + +``` +pgagroal-cli status details +``` + +### switch-to +Switch to another primary server. + +Command + +``` +pgagroal-cli switch-to +``` + +Example + +``` +pgagroal-cli switch-to replica +``` + +### conf +Manages the configuration of the running instance. +This command requires one subcommand, that can be: +- `reload` issue a reload of the configuration, applying at runtime any changes from the configuration files; +- `get` provides a configuration parameter value; +- `set` modifies a configuration parameter at runtime; +- `ls` prints where the configuration files are located. + +Command + +``` +pgagroal-cli conf +``` + +Examples + +``` +pgagroal-cli conf reload + +pgagroal-cli conf get max_connections + +pgagroal-cli conf set max_connections 25 + +``` + +The details about how to get and set values at run-time are explained in the following. + +#### conf get +Given a configuration setting name, provides the current value for such setting. + +The configuration setting name must be the same as the one used in the configuration files. +It is possible to specify the setting name with words separated by dots, so that it can assume +the form `section.context.key` where: +- `section` can be either + - [**pgagroal**](https://github.com/agroal/pgagroal) (optional) the search will be performed into the main configuration settings, that is + those under the `[pgagroal]` settings in the `pgagroal.conf` file; + - `limit` the search will match against the dataabse/limit configuration, i.e., the file `pgagroal_databases.conf`; + - `hba` the search will match against the Host Based Access configuration, i.e., the `pgagroal_hbs.conf`; + - `server` the search will match against a single server defined in the main `pgagroal.conf` file. +- `context` is the match criteria to find the information into a specific context, depending on the value of the `section`: + - if the `section` is set to `limit`, than the `context` is the database name to match into the `pgagroal_databases.conf`. + Please note that the same user could be listed more than once, in such case *only the first match* is reported back; + - if the `section` is set to `hba`, than the `context` is the username to match into the `pgagroal_hba.conf`. + Please note that the same user could be listed more than once, in such case *only the first match* is reported back; + - if the `section` is set to `server`, than the `context` is the name of the server in the `pgagroal.conf` main file; + - if the `section` is set to [**pgagroal**](https://github.com/agroal/pgagroal), the `context` must be empty; +- `key` is the configuration key to search for. + + +Examples +``` +pgagroal-cli conf get pipeline +performance + +pgagroal-cli conf get limit.pgbench.max_size +2 + +pgagroal-cli conf get server.venkman.primary +off + +``` + +In the above examples, `pipeline` is equivalent to `pgagroal.pipeline` and looks for a global configuration setting named `pipeline`. +The `limit.pgbench.max_size` looks for the `max_size` set into the *limit* file (`pgagroal_databases.conf`) for the database `pgbench`. +The `server.venkman.primary` searches for the configuration parameter `primary` into the *server* section named `venkman` in the main configuration file `pgagraol.conf`. + +If the `--verbose` option is specified, a descriptive string of the configuration parameter is printed as *name = value*: + +``` +pgagroal-cli conf get max_connections --verbose +max_connections = 4 +Success (0) +``` + +If the parameter name specified is not found or invalid, the program `pgagroal-cli` exit normally without printing any value. + + + +#### conf set +Allows the setting of a configuration parameter at run-time, if possible. + +Examples +``` +pgagroal-cli conf set log_level debug +pgagroal-cli conf set server.venkman.port 6432 +pgagroal conf set limit.pgbench.max_size 2 +``` + +The syntax for setting parameters is the same as for the command `conf get`, therefore parameters are organized into namespaces: +- `main` (optional) is the main pgagroal configuration namespace, for example `main.log_level` or simply `log_level`; +- `server` is the namespace referred to a specific server. It has to be followed by the name of the server and the name of the parameter to change, in a dotted notation, like `server.venkman.port`; +- `limit` is the namespace referred to a specific limit entry, followed by the name of the username used in the limit entry. + +When executed, the `conf set` command returns the run-time setting of the specified parameter: if such parameter is equal to the value supplied, the change has been applied, otherwise it means that the old setting has been kept. +The `--verbose` flag can be used to understand if the change has been applied: + +``` +$ pgagroal-cli conf set log_level debug +debug + +$ pgagroal-cli conf set log_level debug --verbose +log_level = debug +pgagroal-cli: Success (0) +``` + +When a setting modification cannot be applied, the system returns the "old" setting value and, if `--verbose` is specified, the error indication: + +``` +$ pgagroal-cli conf set max_connections 100 +40 + +$ pgagroal-cli conf set max_connections 100 --verbose +max_connections = 40 +pgagroal-cli: Error (2) +``` + +When a `conf set` cannot be applied, the system will report in the logs an indication about the problem. With regard to the previous example, the system reports in the logs something like the following (depending on your `log_level`): + +``` +DEBUG Trying to change main configuration setting to <100> +INFO Restart required for max_connections - Existing 40 New 100 +WARN 1 settings cannot be applied +DEBUG pgagroal_management_write_config_set: unable to apply changes to -> <100> +``` + +#### conf ls + +The command `conf ls` provides information about the location of the configuration files. +As an example: + +``` +Main Configuration file: /etc/pgagroal/pgagroal.conf +HBA file: /etc/pgagroal/pgagroal_hba.conf +Limit file: /etc/pgagroal/pgagroal_databases.conf +Frontend users file: /etc/pgagroal/pgagroal_frontend_users.conf +Admins file: /etc/pgagroal/pgagroal_admins.conf +Superuser file: +Users file: /etc/pgagroal/pgagroal_users.conf +``` + +### clear +Resets different parts of the pooler. It accepts an operational mode: +- `prometheus` resets the metrics provided without altering the pooler status; +- `server` resets the specified server status. + + +``` +pgagroal-cli clear [prometheus|server ] +``` + +Examples + +``` +pgagroal-cli clear spengler # pgagroal-cli clear server spengler +pgagroal-cli clear prometheus +``` + + +## Shell completions + +There is a minimal shell completion support for `pgagroal-cli`. +Please refer to the [Install pgagroal](https://github.com/agroal/pgagroal/blob/master/doc/tutorial/01_install.md) tutorial for detailed information about how to enable and use shell completions. + + +## JSON Output Format + +It is possible to obtain the output of a command in a JSON format by specyfing the `-F` (`--format`) option on the command line. +Supported output formats are: +- `text` (the default) +- `json` + +As an example, the following are invocations of commands with different output formats: + +``` +pgagroal-cli status # defaults to text output format + +pgagroal-cli status --format text # same as above +pgagroal-cli status -F text # same as above + +pgagroal-cli status --format json # outputs as JSON text +pgagroal-cli status -F json # same as above +``` + +Whenever a command produces output, the latter can be obtained in a JSON format. +Every command output consists of an object that contains two other objects: +- a `command` object, with all the details about the command and its output; +- an `application` object, with all the details about the executable that launched the command (e.g., `pgagroal-cli`). + +In the following, details about every object are provided: + +### The `application` object + +The `application` object is made by the following attributes: +- `name` a string representing the name of the executable that launched the command; +- `version` a string representing the version of the executable; +- `major`, `minor`, `patch` are integers representing every single part of the version of the application. + +As an example, when `pgagroal-cli` launches a command, the output includes an `application` object like the following: + +``` + "application": { + "name": "pgagroal-cli", + "major": 1, + "minor": 6, + "patch": 0, + "version": "1.7.0" + } +``` + + +### The `command` object + +The `command` object represents the launched command and contains also the answer from the [**pgagroal**](https://github.com/agroal/pgagroal). +The object is made by the following attributes: +- `name` a string representing the command launched (e.g., `status`); +- `status` a string that contains either "OK" or an error string if the command failed; +- `error` an interger value used as a flag to indicate if the command was in error or not, where `0` means success and `1` means error; +- `exit-status` an integer that contains zero if the command run succesfully, another value depending on the specific command in case of failure; +- `output` an object that contains the details of the executed command. + +The `output` object is *the variable part* in the JSON command output, that means its effective content depends on the launched command. + +Whenever the command output includes an array of stuff, for example a connection list, such array is wrapped into a `list` JSON array with a sibling named `count` that contains the integer size of the array (number of elements). + + +The following are a few examples of commands that provide output in JSON: + + +``` +pgagroal-cli ping --format json +{ + "command": { + "name": "ping", + "status": "OK", + "error": 0, + "exit-status": 0, + "output": { + "status": 1, + "message": "running" + } + }, + "application": { + "name": "pgagroal-cli", + "major": 1, + "minor": 6, + "patch": 0, + "version": "1.7.0" + } +} + + + +pgagroal-cli status --format json +{ + "command": { + "name": "status", + "status": "OK", + "error": 0, + "exit-status": 0, + "output": { + "status": { + "message": "Running", + "status": 1 + }, + "connections": { + "active": 0, + "total": 2, + "max": 15 + }, + "databases": { + "disabled": { + "count": 0, + "state": "disabled", + "list": [] + } + } + } + }, + "application": { + "name": "pgagroal-cli", + "major": 1, + "minor": 6, + "patch": 0, + "version": "1.7.0" + } +} +``` + +As an example, the following is the output of a faulty `conf set` command (note the `status`, `error` and `exist-status` values): + +``` +pgagroal-cli conf set max_connections 1000 --format json +{ + "command": { + "name": "conf set", + "status": "Current and expected values are different", + "error": true, + "exit-status": 2, + "output": { + "key": "max_connections", + "value": "15", + "expected": "1000" + } + }, + "application": { + "name": "pgagroal-cli", + "major": 1, + "minor": 6, + "patch": 0, + "version": "1.7.0" + } +} +``` + + +The `conf ls` command returns an array named `files` where each entry is made by a couple `description` and `path`, where the former +is the mnemonic name of the configuration file, and the latter is the value of the configuration file used: + +``` +$ pgagroal-cli conf ls --format json +{ + "command": { + "name": "conf ls", + "status": "OK", + "error": 0, + "exit-status": 0, + "output": { + "files": { + "list": [{ + "description": "Main Configuration file", + "path": "/etc/pgagroal/pgagroal.conf" + }, { + "description": "HBA File", + "path": "/etc/pgagroal/pgagroal_hba.conf" + }, { + "description": "Limit file", + "path": "/etc/pgagroal/pgagroal_databases.conf" + }, { + "description": "Frontend users file", + "path": "/etc/pgagroal/pgagroal_frontend_users.conf" + }, { + "description": "Admins file", + "path": "/etc/pgagroal/pgagroal_admins.conf" + }, { + "description": "Superuser file", + "path": "" + }, { + "description": "Users file", + "path": "/etc/pgagroal/pgagroal_users.conf" + }] + } + } + }, + "application": { + "name": "pgagroal-cli", + "major": 1, + "minor": 6, + "patch": 0, + "version": "1.7.0" + } +} +``` diff --git a/doc/manual/user-11-prometheus.md b/doc/manual/user-11-prometheus.md new file mode 100644 index 00000000..da64d633 --- /dev/null +++ b/doc/manual/user-11-prometheus.md @@ -0,0 +1,261 @@ +\newpage + +# Prometheus metrics + +[**pgagroal**][pgagroal] has the following [Prometheus][prometheus] metrics. + +## pgagroal_state + +The state of pgagroal + +| Attribute | Description | +|-----------|------------------------------------| +|value | State | +| | * Running | +| | * Graceful shutdown | + +## pgagroal_pipeline_mode + +The mode of pipeline + +| Attribute | Description | +|-----------|------------------------------------| +|value | Mode | +| | * Performance | +| | * Session | +| | * Transaction | + +## pgagroal_logging_info + +The number of INFO statements + +## pgagroal_logging_warn + +The number of WARN statements + +## pgagroal_logging_error + +The number of ERROR statements + +## pgagroal_logging_fatal + +The number of FATAL statements + +## pgagroal_server_error + +Errors for servers + +| Attribute | Description | +|-----------|------------------------------------| +|name | The name of the server | +|state | The server state | +| | * not_init | +| | * primary | +| | * replica | +| | * failover | +| | * failed | + +## pgagroal_failed_servers + +The number of failed servers. + +Only set if failover is enabled + +## pgagroal_wait_time + +The waiting time of clients + +## pgagroal_query_count + +The number of queries. + +Only session and transaction modes are supported + +## pgagroal_connection_query_count + +The number of queries per connection. + +Only session and transaction modes are supported + +| Attribute | Description | +|-----------|------------------------------------| +|id | The connection identifier | +|user | The user name | +|database | The database | +|application_name | The application name | + +## pgagroal_tx_count + +The number of transactions. Only session and transaction modes are supported + +## pgagroal_active_connections + +The number of active connections + +## pgagroal_total_connections + +The number of total connections + +## pgagroal_max_connections + +The maximum number of connections + +## pgagroal_connection + +Connection information + +| Attribute | Description | +|-----------|------------------------------------| +|id | The connection identifier | +|user | The user name | +|database | The database | +|application_name | The application name | +|state | The connection state | +| | * not_init | +| | * init | +| | * free | +| | * in_use | +| | * gracefully | +| | * flush | +| | * idle_check | +| | * max_connection_age | +| | * validation | +| | * remove | + +## pgagroal_limit + +Limit information + +| Attribute | Description | +|-----------|------------------------------------| +|user | The user name | +|database | The database | +|type | The information type | +| | * not_init | +| | * min | +| | * initial | +| | * max | +| | * active | + +## pgagroal_limit_awaiting + +Connections awaiting on hold reported by limit entries + +| Attribute | Description | +|-----------|------------------------------------| +|user | The user name | +|database | The database | + +## pgagroal_session_time + +Histogram of session times + +## pgagroal_connection_error + +Number of connection errors + +## pgagroal_connection_kill + +Number of connection kills + +## pgagroal_connection_remove + +Number of connection removes + +## pgagroal_connection_timeout + +Number of connection time outs + +## pgagroal_connection_return + +Number of connection returns + +## pgagroal_connection_invalid + +Number of connection invalids + +## pgagroal_connection_get + +Number of connection gets + +## pgagroal_connection_idletimeout + +Number of connection idle timeouts + +## pgagroal_connection_max_connection_age + +Number of connection max age timeouts + +## pgagroal_connection_flush + +Number of connection flushes + +## pgagroal_connection_success + +Number of connection successes + +## pgagroal_connection_awaiting + +Number of connection suspended due to blocking_timeout + +## pgagroal_auth_user_success + +Number of successful user authentications + +## pgagroal_auth_user_bad_password + +Number of bad passwords during user authentication + +## pgagroal_auth_user_error + +Number of errors during user authentication + +## pgagroal_client_wait + +Number of waiting clients + +## pgagroal_client_active + +Number of active clients + +## pgagroal_network_sent + +Bytes sent by clients. Only session and transaction modes are supported + +## pgagroal_network_received + +Bytes received from servers. Only session and transaction modes are supported + +## pgagroal_client_sockets + +Number of sockets the client used + +## pgagroal_self_sockets + +Number of sockets used by pgagroal itself + +[**pgagroal-vault**][pgagroal-vault] has the following [Prometheus][prometheus] metrics. + +## pgagroal_vault_logging_info + +The number of INFO statements + +## pgagroal_vault_logging_warn + +The number of WARN statements + +## pgagroal_vault_logging_error + +The number of ERROR statements + +## pgagroal_vault_logging_fatal + +The number of FATAL statements + +## pgagroal_vault_client_sockets + +Number of sockets the client used + +## pgagroal_vault_self_sockets + +Number of sockets used by pgagroal-vault itself \ No newline at end of file diff --git a/doc/manual/user-12-vault.md b/doc/manual/user-12-vault.md new file mode 100644 index 00000000..c2490a16 --- /dev/null +++ b/doc/manual/user-12-vault.md @@ -0,0 +1,93 @@ +\newpage + +# pgagroal-vault configuration + +The configuration which is mandatory is loaded from either the path specified by the `-c` flag or `/etc/pgagroal/pgagroal_vault.conf`. + +The configuration of `pgagroal-vault` is split into sections using the `[` and `]` characters. + +The pgagroal-vault section, called `[pgagroal-vault]`, is where you configure the overall properties of the vault's server. + +The other section provide configuration for the management port of pgagroal. For now there can be only one pgagroal management port to connect. +This section don't have any requirements to their naming so you can give them +meaningful names but generally named as `[main]`. + +All properties within a section are in the format `key = value`. + +The characters `#` and `;` can be used for comments. A line is totally ignored if the +very first non-space character is a comment one, but it is possible to put a comment at the end of a line. +The `Bool` data type supports the following values: `on`, `yes`, `1`, `true`, `off`, `no`, `0` and `false`. + +See a more complete [sample](./etc/pgagroal_vault.conf) configuration for running `pgagroal-vault` on `localhost`. + +## [pgagroal-vault] + +This section is mandatory and the pooler will refuse to start if the configuration file does not specify one and only one. Usually this section is place on top of the configuration file, but its position within the file does not really matter. +The available keys and their accepted values are reported in the table below. + +| Property | Default | Unit | Required | Description | +|----------|---------|------|----------|-------------| +| host | | String | Yes | The bind address for pgagroal-vault | +| port | | Int | Yes | The bind port for pgagroal-vault | +| metrics | 0 | Int | No | The metrics port (disable = 0) | +| metrics_cache_max_age | 0 | String | No | The number of seconds to keep in cache a Prometheus (metrics) response. If set to zero, the caching will be disabled. Can be a string with a suffix, like `2m` to indicate 2 minutes | +| metrics_cache_max_size | 256k | String | No | The maximum amount of data to keep in cache when serving Prometheus responses. Changes require restart. This parameter determines the size of memory allocated for the cache even if `metrics_cache_max_age` or `metrics` are disabled. Its value, however, is taken into account only if `metrics_cache_max_age` is set to a non-zero value. Supports suffixes: 'B' (bytes), the default if omitted, 'K' or 'KB' (kilobytes), 'M' or 'MB' (megabytes), 'G' or 'GB' (gigabytes).| +| log_type | console | String | No | The logging type (console, file, syslog) | +| log_level | info | String | No | The logging level, any of the (case insensitive) strings `FATAL`, `ERROR`, `WARN`, `INFO` and `DEBUG` (that can be more specific as `DEBUG1` thru `DEBUG5`). Debug level greater than 5 will be set to `DEBUG5`. Not recognized values will make the log_level be `INFO` | +| log_path | pgagroal.log | String | No | The log file location. Can be a strftime(3) compatible string. | +| log_rotation_age | 0 | String | No | The age that will trigger a log file rotation. If expressed as a positive number, is managed as seconds. Supports suffixes: 'S' (seconds, the default), 'M' (minutes), 'H' (hours), 'D' (days), 'W' (weeks). A value of `0` disables. | +| log_rotation_size | 0 | String | No | The size of the log file that will trigger a log rotation. Supports suffixes: 'B' (bytes), the default if omitted, 'K' or 'KB' (kilobytes), 'M' or 'MB' (megabytes), 'G' or 'GB' (gigabytes). A value of `0` (with or without suffix) disables. | +| log_line_prefix | %Y-%m-%d %H:%M:%S | String | No | A strftime(3) compatible string to use as prefix for every log line. Must be quoted if contains spaces. | +| log_mode | append | String | No | Append to or create the log file (append, create) | +| log_connections | `off` | Bool | No | Log connects | +| log_disconnections | `off` | Bool | No | Log disconnects | +| authentication_timeout | 5 | Int | No | The number of seconds the process will wait for valid credentials | +| hugepage | `try` | String | No | Huge page support (`off`, `try`, `on`) | +| tls | `off` | Bool | No | Enable Transport Layer Security (TLS) | +| tls_cert_file | | String | No | Certificate file for TLS. This file must be owned by either the user running pgagroal or root. | +| tls_key_file | | String | No | Private key file for TLS. This file must be owned by either the user running pgagroal or root. Additionally permissions must be at least `0640` when owned by root or `0600` otherwise. | +| tls_ca_file | | String | No | Certificate Authority (CA) file for TLS. This file must be owned by either the user running pgagroal or root. | + +## [main] + +The section with a name different from `pgagroal-vault` will be treated as a main section. + +| Property | Default | Unit | Required | Description | +|----------|---------|------|----------|-------------| +| host | | String | Yes | The address of the pgagroal running the management server | +| port | | Int | Yes | The management port of pgagroal | +| user | | String | Yes | The admin user of the pgagroal remote management service | + +Note: For `pgagroal-vault` to function and connect properly to pgagroal, the remote server for management of the [**pgagroal**](https://github.com/agroal/pgagroal) should be enabled i.e. `management` should be greater than 0. + +# Enable SSL connection with management port + +The SSL handshake has to be initiated between `vault` and `remote` of `pgagroal` to enable secured SSL connection between the both. + +The `vault` serves as the client, while the `remote` functions as the server. The `vault` initiates the SSL/TLS handshake with the `remote`, and concurrently, the `remote` accepts the SSL/TLS request from the `vault`. + +## SSL configuration at the server side + +Update the `[pgagroal]` section in main configuration file by including the following:- + +- Enable management port +- Enable `tls` to `on` +- Add `tls_cert_file`, `tls_key_file` and `tls_ca_file` fields + +``` +management = 2347 +tls = on +tls_cert_file = /path/to/server_cert_file +tls_key_file = /path/to/server_key_file +tls_ca_file = /path/to/CA_root_cert_file +``` + +## SSL configuration at the client side + +For client side authentication, add the required certificates like the certificate of `vault` in `pgagroal.crt`, private key of `vault` in `pgagroal.key` and the root certificate of CA in `root.crt` in the `.pgagroal` directory. + +Some permissions requirements:- + +- The certificate file `pgagroal.crt` must be owned by either the user running pgagroal or root. +- The key file `pgagroal.key` must be owned by either the user running pgagroal or root. Additionally permissions must be at least `0640` when owned by root or `0600` otherwise. +- The Certificate Authority (CA) file `root.crt` must be owned by either the user running pgagroal or root. diff --git a/doc/tutorial/01_install.md b/doc/tutorial/01_install.md new file mode 100644 index 00000000..5132cb38 --- /dev/null +++ b/doc/tutorial/01_install.md @@ -0,0 +1,235 @@ +## Install pgagroal + +This tutorial will show you how to do a simple installation of [**pgagroal**](https://github.com/agroal/pgagroal), +in order to get a running connection pool. + +## Preface + +This tutorial assumes that you already have an installation of [PostgreSQL](https://www.postgresql.org) 13 (or higher). + +For RPM based distributions such as Fedora and RHEL you can add the +[PostgreSQL YUM repository](https://yum.postgresql.org/) and do the install via the distribution package manager `dnf`: + +``` +dnf install -y pgagroal +``` + +If you don't have [PostgreSQL](https://www.postgresql.org) already installed, you can install both [PostgreSQL](https://www.postgresql.org) and [**pgagroal**](https://github.com/agroal/pgagroal) +in a single pass: + +``` +dnf install -y postgresql14 postgresql14-server pgagroal +``` + +Assuming you want to install version 14 of [PostgreSQL](https://www.postgresql.org). + +### PostgreSQL setup + +In the case you don't have yet a [PostgreSQL](https://www.postgresql.org) running instance, you need to initialize the cluster the connection pooler will connect to. The followings are simple and quick steps to get a cluster up and running as soon as possible. + +It is assumed that you run an RPM based distribution, like Fedora or RHEL. Some commands could be in different paths depending on the operating system distribution you are using. + +**Initialize cluster** + +You need to define a `PGDATA` data directory where [PostgreSQL](https://www.postgresql.org) will store the data in. +In the following, it is assumed that the [PostgreSQL](https://www.postgresql.org) directory is `/postgres/14/data`, then +you can do the following commands in a shell as the operating system user `postgres`: + +``` +export PATH=/usr/pgsql-14/bin:$PATH +mkdir -p /postgres/14/data +export PGDATA=/postgres/14/data +initdb -k $PGDATA +``` + +(`postgres` user) + +**Remove default accesses** + +By default, [PostgreSQL](https://www.postgresql.org) allows trusted accesses from the local machine to any database within the cluster. +It is better to harden your cluster, thus providing accesses only to who and when it is needed. +In order to do this, with your text editor of choice, edit the file `$PGDATA/pg_hba.conf` and remote the following lines: + + +``` +host all all 127.0.0.1/32 trust +host all all ::1/128 trust +host replication all 127.0.0.1/32 trust +host replication all ::1/128 trust +``` + +from `/postgres/14/data/pg_hab.conf` + +(`postgres` user) + +**Add access for a user and a database** + +Assume you will have a database named `mydb` and a user named `myuser` that will be granted access to such database. In order to do so, edit again the `$PGDATA/pg_hba.conf` file and add a couple of lines like the followings: + +``` +host mydb myuser 127.0.0.1/32 scram-sha-256 +host mydb myuser ::1/128 scram-sha-256 +``` + +The first line grants access to the user `myuser` against the database `mydb` on IPv4 `localhost`; the second line does the same but on IPv6 `localhost` connection. + +Please check the value of the setting `password_encryption` in the configuration file `$PGDATA/postgresql.conf` in order to ensure it matches `scram-sha-256` as the last column in the previous two lines. + +**Start PostgreSQL** + +It is now time to run the [PostgreSQL](https://www.postgresql.org) instance, so as the `postgres` operating system user, run: + +``` +pg_ctl -D $PGDATA -l /tmp/logfile start +``` + +**Create the database and the user** + +It is now time to create the database and the user of the previous step. As operating system user `postgres`, execute: + +``` +psql -c "CREATE ROLE myuser WITH LOGIN PASSWORD 'mypassword';" +psql -c "CREATE DATABASE mydb WITH OWNER myuser;" +``` + +It is strongly suggested to choose a strong password to protect the database access ! + +**Verify access** + +You can check the connectivity of the database user executing, from a shell, as any operating system user, the following command: + +``` +psql -h localhost -p 5432 -U myuser -c 'SELECT current_timestamp:' mydb +``` + +Type the `mypassword` password when asked, and if you get back the current date and time, everything is working fine! + +### pgagroal setup + +In order to run [**pgagroal**](https://github.com/agroal/pgagroal), you need at list to configure the main `pgagroal.conf` configuration file, that will tell the pooler how to work, and then `pgagroal_hba.conf` that will instrument the pooler about which users are allowed to connect thru the pooler. + +[**pgagroal**](https://github.com/agroal/pgagroal) as a daemon cannot be run by `root` operating system user, it is a good idea to create an unprivileged operating system user to run the pooler. + +**Add a user to run pgagroal** + +As a privileged operating system user (either `root` or via `sudo` or `doas`), run the followings: + +``` +useradd -ms /bin/bash pgagroal +passwd pgagroal +``` + +The above will create an operating system [**pgagroal**](https://github.com/agroal/pgagroal) that is the one that is going to run the pooler. + + +**Create basic configuration** + +As the [**pgagroal**](https://github.com/agroal/pgagroal) operating system user, add a master key to protect the [**pgagroal**](https://github.com/agroal/pgagroal) vault and then add the `myuser` to the pooler: + +``` +pgagroal-admin master-key +pgagroal-admin -f /etc/pgagroal/pgagroal_users.conf -U myuser -P mypassword user add +``` + +**You have to choose a password for the master key - remember it !** + +It is now time to create the main `/etc/pgagroal/pgagroal.conf` configuration file with your editor of choice or using `cat` from the command line, create the following content: + +``` +cd /etc/pgagroal +cat > pgagroal.conf +[pgagroal] +host = * +port = 2345 + +log_type = file +log_level = info +log_path = /tmp/pgagroal.log + +max_connections = 100 +idle_timeout = 600 +validation = off +unix_socket_dir = /tmp/ + +[primary] +host = localhost +port = 5432 +``` + +and press `Ctrl-d` (if running `cat`) to save the file. + +Similarly, create the `/etc/pgagroal/pgagroal_hba.conf` file; + +``` +cd /etc/pgagroal +cat > pgagroal_hba.conf +host mydb myuser all all +``` + +and press `Ctrl-d` (if using `cat`) to save the file. The above line tells `pgagral` to allow the user `myuser` to *try* +to connect to `mydb` using a TCP-IP connection. + +See [the documentation about `pgagroal_hba.conf` for more details](https://github.com/agroal/pgagroal/blob/master/doc/CONFIGURATION.md#pgagroal_hba-configuration). + +**Start pgagroal** + +It is now time to start [**pgagroal**](https://github.com/agroal/pgagroal), so as the [**pgagroal**](https://github.com/agroal/pgagroal) operating system user run: + +``` +pgagroal -d +``` + +If the system is running, you will see some output on the log file `/tmp/pgagroal.log`. + +Since the default configuration files are usually searched into the `/etc/pgagroal/` directory, and have well defined names, you can omit the files from the command line if you named them `pgagroal.conf`, `pgagroal_hba.conf` and `pgagroal_users.conf`. + +You will not need to specify any command line flag for files that have the standard name like: + +* `/etc/pgagroal/pgagroal.conf` (main configuration file) +* `/etc/pgagroal/pgagroal_hba.conf` (host based access configuration file) +* `/etc/pgagroal/pgagroal_databases.conf` (limits file) +* `/etc/pgagroal/pgagroal_admins.conf` (remote management file) +* `/etc/pgagroal/pgagroal_frontend_users.conf` (split security user remapping) + +**In the case you named the configuration files differently or in a different folder, you need to specify them on the command line!** + +### Shell completion + +There is a minimal shell completion support for `pgagroal-cli` and `pgagroal-admin`. If you are running such commands from a Bash or Zsh, you can take some advantage of command completion. + +**Installing command completions in Bash** + +There is a completion script into `contrib/shell_comp/pgagroal_comp.bash` that can be used +to help you complete the command line while you are typing. + +It is required to source the script into your current shell, for instance +by doing: + +``` shell +source contrib/shell_comp/pgagroal_comp.bash +``` + +At this point, the completions should be active, so you can type the name of one the commands between `pgagroal-cli` and `pgagroal-admin` and hit `` to help the command line completion. + +**Installing the command completions on Zsh** + +In order to enable completion into `zsh` you first need to have `compinit` loaded; +ensure your `.zshrc` file contains the following lines: + +``` shell +autoload -U compinit +compinit +``` + +and add the sourcing of the `contrib/shell_comp/pgagroal_comp.zsh` file into your `~/.zshrc` +also associating the `_pgagroal_cli` and `_pgagroal_admin` functions +to completion by means of `compdef`: + +``` shell +source contrib/shell_comp/pgagroal_comp.zsh +compdef _pgagroal_cli pgagroal-cli +compdef _pgagroal_admin pgagroal-admin +``` + +If you want completions only for one command, e.g., `pgagroal-admin`, remove the `compdef` line that references the command you don't want to have automatic completion. +At this point, digit the name of a `pgagroal-cli` or `pgagroal-admin` command and hit `` to trigger the completion system. diff --git a/doc/tutorial/02_prefill.md b/doc/tutorial/02_prefill.md new file mode 100644 index 00000000..7586524b --- /dev/null +++ b/doc/tutorial/02_prefill.md @@ -0,0 +1,78 @@ +## Enable prefill for pgagroal + +This tutorial will show you how to do enable *prefill* for [**pgagroal**](https://github.com/agroal/pgagroal). + +The prefill is the capability to activate connections against a specific database +even if no one has been actively requested by a user or an application. This allows the pooler +to serve a connection faster once it is effectively requested, since the connection is already +established. + +Prefill is done by making [**pgagroal**](https://github.com/agroal/pgagroal) to open the specified amount of connections to a specific database with a specific username, +therefore you need to know credentials used on the [PostgreSQL](https://www.postgresql.org) side. + +### Preface + +This tutorial assumes that you have already an installation of [PostgreSQL](https://www.postgresql.org) 13 (or higher) and [**pgagroal**](https://github.com/agroal/pgagroal). + +In particular, this tutorial refers to the configuration done in [Install pgagroal](https://github.com/pgagroal/pgagroal/blob/master/doc/tutorial/01_install.md). + +### Create prefill configuration + +Prefill is instrumented by the `pgagroal_databases.conf` configuration file, where you need +to list databases, usernames, and limits. + +Every username/database pair has to be specified on a separated line. + +The limits are assumed as: + +* *max number of allowed connections* for that username/database +* *initial number of connections* that is the effective prefill; +* *minimum number of connections* to always keep open for the pair username/database. + +Assuming you want to configure the prefill for the `mydb` database with the `myuser` username, +you have to edit the file `/etc/pgagroal/pgagroal_databases.conf` with your editor of choice +or using `cat` from the command line, as follows: + +``` +cd /etc/pgagroal +cat > pgagroal_databases.conf +mydb myuser 2 1 0 +``` + +and press `Ctrl-d` to save the file. + +This will create a configuration where `mydb` will have a maximum connection size of 2, +an initial connection size of 1 and a minimum connection size of 0 for the `myuser` user using the `mydb` database. + +The file must be owned by the operating system user [**pgagroal**](https://github.com/agroal/pgagroal). + +The `max_size` value is mandatory, while the `initial_size` and `min_size` are optional and if not explicitly set are assumed to be `0`. + +See [the `pgagroal_databases.conf` file documentation](https://github.com/agroal/pgagroal/blob/master/doc/CONFIGURATION.md#pgagroal_databases-configuration) for more details. + +### Restart pgagroal + +In order to apply changes to the prefill configuration, you need to restart [**pgagroal**](https://github.com/agroal/pgagroal). +You can do so by stopping it and then re-launch the daemon, as [**pgagroal**](https://github.com/agroal/pgagroal) operating system user: + +``` +pgagroal-cli shutdown +pgagroal -d +``` + +Since the default configuration files are usually searched into the `/etc/pgagroal/` directory, and have well defined names, you can omit the files +from the command line if you named them `pgagroal.conf`, `pgagroal_hba.conf`, `pgagroal_users.conf` and `pgagroal_databases.conf`. + +### Check the prefill + +You can check the prefill by running, as the [**pgagroal**](https://github.com/agroal/pgagroal) operating system user, the `status` command: + +``` +pgagroal-cli status +Status: Running +Active connections: 0 +Total connections: 1 +Max connections: 100 +``` + +where the `Total connections` is set by the *initial* connection specified in the limit file. diff --git a/doc/tutorial/03_remote_management.md b/doc/tutorial/03_remote_management.md new file mode 100644 index 00000000..52f65c33 --- /dev/null +++ b/doc/tutorial/03_remote_management.md @@ -0,0 +1,80 @@ +## Remote administration for pgagroal + +This tutorial will show you how to do setup remote management for [**pgagroal**](https://github.com/agroal/pgagroal). + +[**pgagroal**](https://github.com/agroal/pgagroal) is managed via a command line tool named `pgagroal-cli`. Such tool +connects via a local Unix socket if running on the same machine the pooler is +running on, but it is possible to use `pgagroal-cli` from a different machine +and make it to connect to the pooler machine via *remote management*. + +## Preface + +This tutorial assumes that you have already an installation of [PostgreSQL](https://www.postgresql.org) 13 (or higher) and [**pgagroal**](https://github.com/agroal/pgagroal). + +In particular, this tutorial refers to the configuration done in [Install pgagroal](https://github.com/pgagroal/pgagroal/blob/master/doc/tutorial/01_install.md). + +### Enable remote management + +On the pooler machine, you need to enable the remote management. In order to do so, +add the `management` setting to the main `pgagroal.conf` configuration file. +The value of setting is the number of a free TCP/IP port to which the remote +management will connect to. + +With your editor of choice, edit the `/etc/pgagroal/pgagroal.conf` file and add the +`management` option likely the following: + +``` +management = 2347 +``` + +under the `[pgagroal]` section, so that the configuration file looks like: + +``` +[pgagroal] +... +management = 2347 +``` + +See [the pgagroal configuration settings](https://github.com/agroal/pgagroal/blob/master/doc/CONFIGURATION.md#pgagroal) for more details. + +### Add remote admin user + +Remote management is done via a specific admin user, that has to be created within the pooler vault. + +As the [**pgagroal**](https://github.com/agroal/pgagroal) operating system user, run the following command: + +``` +cd /etc/pgagroal +pgagroal-admin -f pgagroal_admins.conf -U admin -P admin1234 add-user +``` + +The above will create the `admin` username with the `admin1234` password. + +**We strongly encourage you to choose non trivial usernames and passwords!** + +### Restart pgagroal + +In order to make the changes available, and therefore activate the remote management, you have to restart [**pgagroal**](https://github.com/agroal/pgagroal), for example by issuing the following commands from the [**pgagroal**](https://github.com/agroal/pgagroal) operatng system user: + +``` +pgagroal-cli shutdown +pgagroal -d +``` + +Since the default configuration files are usually searched into the `/etc/pgagroal/` directory, and have well defined names, you can omit the files from the command line +if you named them `pgagroal.conf`, `pgagroal_hba.conf`, `pgagroal_users.conf` and `pgagroal_admins.conf`. + +### Connect via remote administration interface + +In order to connect remotely, you need to specify at least the `-h` and `-p` flags on the `pgagroal-cli` command line. Such flags will tell `pgagroal-cli` to connect to a remote host. You can also specify the username you want to connect with by specifying the `-U` flag. +So, to get the status of the pool remotely, you can issue: + +``` +pgagroal-cli -h localhost -p 2347 -U admin status +``` + +and type the password `admin1234` when asked for it. + +If you don't specify the `-U` flag on the command line, you will be asked for a username too. + +Please note that the above example uses `localhost` as the remote host, but clearly you can specify any *real* remote host you want to manage. diff --git a/doc/tutorial/04_prometheus.md b/doc/tutorial/04_prometheus.md new file mode 100644 index 00000000..f7255633 --- /dev/null +++ b/doc/tutorial/04_prometheus.md @@ -0,0 +1,103 @@ +## Prometheus metrics for pgagroal + +This tutorial will show you how to do basic [Prometheus](https://prometheus.io/) setup for [**pgagroal**](https://github.com/agroal/pgagroal). + +[**pgagroal**](https://github.com/agroal/pgagroal) is able to provide a set of metrics about what it is happening within the pooler, +so that a Prometheus instance can collect them and help you monitor the pooler. + +### Preface + +This tutorial assumes that you have already an installation of [PostgreSQL](https://www.postgresql.org) 13 (or higher) and [**pgagroal**](https://github.com/agroal/pgagroal). + +In particular, this tutorial refers to the configuration done in [Install pgagroal](https://github.com/pgagroal/pgagroal/blob/master/doc/tutorial/01_install.md). + +### Change the pgagroal configuration + +In order to enable to export of the metrics, you need to add the `metrics` option in the main `pgagroal.conf` configuration. The value of this setting is the TCP/IP port number that Prometheus will use to grab the exported metrics. + +Add a line like the following to `/etc/pgagroal/pgagroal.conf` by editing such file with your editor of choice: + +``` +metrics = 2346 +``` + +Place it withingr the `[pgagroal]` section, like + +``` +[pgagroal] +... +metrics = 2346 +``` + +This will bind the TCP/IP port number `2346` to the metrics export. + +See [the pgagroal configuration settings](https://github.com/agroal/pgagroal/blob/master/doc/CONFIGURATION.md#pgagroal) with particular regard to `metrics`, `metrics_cache_max_age` and `metrics_cache_max_size` for more details. + +### Restart pgagroal + +In order to apply changes, you need to restart [**pgagroal**](https://github.com/agroal/pgagroal), therefore run the following commands +as the [**pgagroal**](https://github.com/agroal/pgagroal) operating system user: + +``` +pgagroal-cli shutdown +pgagroal -d +``` + +If you need to specify other configuration files, for example for remote management (see [the related tutorial](https://github.com/pgagroal/pgagroal/blob/master/doc/tutorial/03_remote_management.md)), add them on the [**pgagroal**](https://github.com/agroal/pgagroal) command line. +If the cofiguration files have standard names, you can omit them. + +### Get Prometheus metrics + +Once [**pgagroal**](https://github.com/agroal/pgagroal) is running you can access the metrics with a browser at the pooler address, specifying the `metrics` port number and routing to the `/metrics` page. For example, point your web browser at: + +``` +http://localhost:2346/metrics +``` + +It is also possible to get an explaination of what is the meaning of each metric by pointing your web browser at: + +``` +http://localhost:2346/ +``` + +## Prometheus metrics for pgagroal-vault + +This tutorial will show you how to do basic [Prometheus](https://prometheus.io/) setup for [**pgagroal-vault**](https://github.com/agroal/pgagroal). + +**pgagroal-vault** is able to provide a set of metrics about what it is happening within the vault, so that a Prometheus instance can collect them and help you monitor the vault activities. + +### Change the pgagroal-vault configuration + +In order to enable to export of the metrics, you need to add the `metrics` option in the main `pgagroal_vault.conf` configuration. The value of this setting is the TCP/IP port number that Prometheus will use to grab the exported metrics. + +Add a line like the following to `/etc/pgagroal/pgagroal_vault.conf` by editing such file with your editor of choice: + +``` +metrics = 2501 +``` + +Place it within the `[pgagroal-vault]` section, like + +``` +[pgagroal-vault] +... +metrics = 2501 +``` + +This will bind the TCP/IP port number `2501` to the metrics export. + +See [the pgagroal-vault configuration settings](https://github.com/agroal/pgagroal/blob/master/doc/VAULT.md#pgagroal-vault) with particular regard to `metrics`, `metrics_cache_max_age` and `metrics_cache_max_size` for more details. + +### Get Prometheus metrics + +Once **pgagroal-vault** is running you can access the metrics with a browser at the pgagroal-vault address, specifying the `metrics` port number and routing to the `/metrics` page. For example, point your web browser at: + +``` +http://localhost:2501/metrics +``` + +It is also possible to get an explaination of what is the meaning of each metric by pointing your web browser at: + +``` +http://localhost:2501/ +``` diff --git a/doc/tutorial/05_split_security.md b/doc/tutorial/05_split_security.md new file mode 100644 index 00000000..19083571 --- /dev/null +++ b/doc/tutorial/05_split_security.md @@ -0,0 +1,60 @@ +## Split security model in pgagroal + +This tutorial will show you how to split the security model of [**pgagroal**](https://github.com/agroal/pgagroal). + +The idea is that the pooler can act as a *user proxy* between your application and +the [PostgreSQL](https://www.postgresql.org) instance, so that your application does not need to know the exact password +to use to connect to [PostgreSQL](https://www.postgresql.org). +[**pgagroal**](https://github.com/agroal/pgagroal) will authenticate the connection request with its credentials, and then will +authenticate against [PostgreSQL](https://www.postgresql.org) with the correct password. + +This *user mapping* is named *frontend users*. + +### Preface + +This tutorial assumes that you have already an installation of [PostgreSQL](https://www.postgresql.org) 13 (or higher) and [**pgagroal**](https://github.com/agroal/pgagroal). + +In particular, this tutorial refers to the configuration done in [Install pgagroal](https://github.com/pgagroal/pgagroal/blob/master/doc/tutorial/01_install.md). + + +### Create frontend users + +Frontend users are stored into the `pgagroal_frontend_users.conf` file, that can be managed via the `pgagroal-admin` command line tool. +See [the documentation on frontend users](https://github.com/agroal/pgagroal/blob/master/doc/CONFIGURATION.md#pgagroal_frontend_users-configuration) for more details. + +As an example, consider the user `myuser` created in the [Installing pgagroal tutorial](https://github.com/pgagroal/pgagroal/blob/master/doc/tutorial/01_install.md)): such user has the `mypassword` password defined on the [PostgreSQL](https://www.postgresql.org) side. It is possible to *remap* the user password on the [**pgagroal**](https://github.com/agroal/pgagroal) side, so that an application can connect to the [**pgagroal**](https://github.com/agroal/pgagroal) using a different password, like `application_password`. In turn, [**pgagroal**](https://github.com/agroal/pgagroal) will connect to [PostgreSQL](https://www.postgresql.org) using the `mypassword` password. Therefore, the application could not know the *real* password used to connect to [PostgreSQL](https://www.postgresql.org). + +To achieve this, as [**pgagroal**](https://github.com/agroal/pgagroal) operating system run the following command: + +``` +pgagroal-admin -f /etc/pgagroal/pgagroal_frontend_users.conf -U myuser -P application_password user add +``` + +([**pgagroal**](https://github.com/agroal/pgagroal) user) + +You will need a password mapping for each user defined in the `pgagroal_users.conf` configuration file. + +### Restart pgagroal + +In order to apply changes, you need to restart [**pgagroal**](https://github.com/agroal/pgagroal), so as the [**pgagroal**](https://github.com/agroal/pgagroal) operating system user do: + +``` +pgagroal-cli shutdown +pgagroal -d +``` + +### Connect to PostgreSQL + +You can now use the "application password" to access the [PostgreSQL](https://www.postgresql.org) instance. As an example, +run the following as any operatng system user: + +``` +psql -h localhost -p 2345 -U myuser mydb +``` + +using `application_password` as the password. +As already explained, [**pgagroal**](https://github.com/agroal/pgagroal) will then use the `mypassword` password against [PostgreSQL](https://www.postgresql.org). + +This **split security model** allows you to avoid sharing password between applications and [PostgreSQL](https://www.postgresql.org), +letting the [**pgagroal**](https://github.com/agroal/pgagroal) to be the secret-keeper. This not only improves security, but also allows you +to change the [PostgreSQL](https://www.postgresql.org) password without having the application to note it. diff --git a/doc/tutorial/06_tls.md b/doc/tutorial/06_tls.md new file mode 100644 index 00000000..f05faeb7 --- /dev/null +++ b/doc/tutorial/06_tls.md @@ -0,0 +1,122 @@ +## Creating Certificates + +This tutorial will show you how to create self-signed certificate for the server, valid for 365 days, use the following OpenSSL command, replacing `dbhost.yourdomain.com` with the server's host name, here `localhost`: + +``` +openssl req -new -x509 -days 365 -nodes -text -out server.crt \ + -keyout server.key -subj "/CN=dbhost.yourdomain.com" +``` + +then do - + +``` +chmod og-rwx server.key +``` + +because the server will reject the file if its permissions are more liberal than this. For more details on how to create your server private key and certificate, refer to the OpenSSL documentation. + +For the purpose of this tutorial we will assume the client certificate and key same as the server certificate and server key and therefore, these equations always holds - + +* `` = `` +* `` = `` +* `` = `` +* `` = `` + +## TLS in `pgagroal` + +This tutorial will show you how to enable TLS between `client` and [**pgagroal**](https://github.com/agroal/pgagroal). + +### Preface + +This tutorial assumes that you have already an installation of [PostgreSQL](https://www.postgresql.org) 13 (or higher) and [**pgagroal**](https://github.com/agroal/pgagroal). + +In particular, this tutorial refers to the configuration done in [Install pgagroal](https://github.com/pgagroal/pgagroal/blob/master/doc/tutorial/01_install.md). + +### Modify the `pgagroal` configuration + +It is now time to modify the [pgagroal] section of configuration file `/etc/pgagroal/pgagroal.conf`, with your editor of choice by adding the following lines in the [pgagroal] section. + +``` +tls = on +tls_cert_file = +tls_key_file = +``` + +**Only Server Authentication** + +If you wish to do only server authentication the aforementioned configuration suffice. + +**Client Request** + +``` +PGSSLMODE=verify-full PGSSLROOTCERT= psql -h localhost -p 2345 -U +``` + +**Full Client and Server Authentication** + +To enable the server to request the client certificates add the following configuration lines + +``` +tls = on +tls_cert_file = +tls_key_file = +tls_ca_file = +``` + +**Client Request** + +``` +PGSSLMODE=verify-full PGSSLCERT= PGSSLKEY= PGSSLROOTCERT= psql -h localhost -p 2345 -U +``` + +## TLS in `pgagroal-vault` + +This tutorial will show you how to enable tls between [**pgagroal-vault**](https://github.com/agroal/pgagroal) and the client (`curl`). + +### Preface + +This tutorial assumes that you have already an installation of [PostgreSQL](https://www.postgresql.org) 13 (or higher) and [**pgagroal**](https://github.com/agroal/pgagroal). + +This tutorial also assumes that you have a functional [**pgagroal-vault**](https://github.com/agroal/pgagroal). + +In particular, this tutorial refers to the configuration done in [Install pgagroal](https://github.com/pgagroal/pgagroal/blob/master/doc/tutorial/01_install.md) and +the configuration done in [Setup pgagroal-vault](https://github.com/pgagroal/pgagroal/blob/master/doc/tutorial/07_vault.md). + +### Modify the `pgagroal-vault` configuration + +It is now time to modify the [pgagroal-vault] section of configuration file `/etc/pgagroal/pgagroal_vault.conf`, with your editor of choice by adding the following lines in the [pgagroal-vault] section. + +``` +tls = on +tls_cert_file = +tls_key_file = +``` + +This will add TLS support to the server alongside the standard `http` endpoint, allowing clients to make requests to either the `https` or `http` endpoint. + +**Only Server Authentication** + +If you wish to do only server authentication the aforementioned configuration suffice. + +**Client Request** + +``` +curl --cacert -i https://localhost:2500/users/ +``` + +**Full Client and Server Authentication** + +To enable the server to request the client certificates add the following configuration lines + +``` +tls = on +tls_cert_file = +tls_key_file = +tls_ca_file = +``` + +**Client Request** + +``` +curl --cert --key --cacert -i https://localhost:2500/users/ +``` diff --git a/doc/tutorial/07_vault.md b/doc/tutorial/07_vault.md new file mode 100644 index 00000000..a0f16f3b --- /dev/null +++ b/doc/tutorial/07_vault.md @@ -0,0 +1,152 @@ +## Setup pgagroal-vault + +This tutorial will show you how to do setup `pgagroal-vault`. + +### Preface + +This tutorial assumes that you have already an installation of [PostgreSQL](https://www.postgresql.org) 13 (or higher) and [**pgagroal**](https://github.com/agroal/pgagroal) and also assumes that the remote management is enabled in [**pgagroal**](https://github.com/agroal/pgagroal). + +In particular, this tutorial refers to the configuration done in [Install pgagroal](https://github.com/pgagroal/pgagroal/blob/master/doc/tutorial/01_install.md). + +Moreover refer to [Enable Remote Management](https://github.com/pgagroal/pgagroal/blob/master/doc/tutorial/03_remote_management.md) to enable remote management and add remote admin user. + +### `pgagroal` users setup + +Assuming that the master key is already generated and an admin is already present for remote management with `admin` username and `admin1234` password. + +**Create a user** + +As the [**pgagroal**](https://github.com/agroal/pgagroal) operating system user, add the `myuser` to the pooler: + +``` +pgagroal-admin -f /etc/pgagroal/pgagroal_users.conf -U myuser -P mypassword user add +``` + +The `myuser` and `mypassword` should be the original PostgresSQL's user and its corresponding password. + +**Create a frontend user** + +As the [**pgagroal**](https://github.com/agroal/pgagroal) operating system user, add the `myuser` to the pooler: + +``` +pgagroal-admin -f /etc/pgagroal/pgagroal_frontend_users.conf -U myuser -P password user add +``` + +**Remember the frontend password should be between [8-1024] characters long.** + +### Enable Rotation of frontend passwords + +In the main configuration file of [**pgagroal**](https://github.com/agroal/pgagroal) add the following configurations for rotating frontend passwords and make sure management port is enabled in the **[pgagroal]** section of the configuration file. + +``` +management = 2347 +rotate_frontend_password_timeout = 60 +rotate_frontend_password_length = 12 +``` + +### `pgagroal-vault` setup + +In order to run `pgagroal-vault`, you need to configure the vault `pgagroal_vault.conf` configuration file, that will tell the vault how to work, which address to listen, address of management service in [**pgagroal**](https://github.com/agroal/pgagroal) so on, and then `pgagroal_vault_users.conf` that will instrument the vault about the admin username and password of the remote management. + +**Create basic configuration** + +It is now time to create the main `/etc/pgagroal/pgagroal_vault.conf` configuration file, with your editor of choice or using `cat` from the command line, create the following content: + +``` +cd /etc/pgagroal +cat > pgagroal_vault.conf +[pgagroal-vault] +host = localhost +port = 2500 + +log_type = file +log_level = info +log_path = /tmp/pgagroal_vault.log + +[main] +host = localhost +port = 2347 +user = admin +``` + +and press `Ctrl-d` (if running `cat`) to save the file. + +**Add users file** + +As the [**pgagroal**](https://github.com/agroal/pgagroal) operating system user, run the following command: + +``` +pgagroal-admin -f /etc/pgagroal/pgagroal_vault_users.conf -U admin -P admin1234 add-user +``` + +The above will create the `admin` username with the `admin1234` password.Alternately, `/etc/pgagroal/pgagroal_admins.conf` can be provided for vault users information. + +See [the documentation about `pgagroal_vault.conf` for more details](https://github.com/agroal/pgagroal/blob/master/doc/VAULT.md). + +### Start pgagroal-vault + +It is now time to start `pgagroal-vault`, so as the [**pgagroal**](https://github.com/agroal/pgagroal) operating system user run: + +``` +pgagroal-vault -d +``` + +**Remember if both pgagroal and vault are operating on same system `pgagroal_vault_users.conf` can be same as `pgagroal_admins.conf`** + +This command initializes an HTTP server on localhost port 2500, which is primed to exclusively handle GET requests from clients. + +### Connect to vault + +Since we have deployed an HTTP server we can simply use `curl` to send GET requests + +**Correct requests** + +If the requested URL is of form `http://:/users/` such that `` exists, the server will return a header response with `200 status code` and the frontend password corresponding to the `` in the response body. + +**Example** + +` +curl -i http://localhost:2500/users/myuser +` + +Output + +``` +HTTP/1.1 200 OK +Content-Type: text/plain + + +password +``` + +**Incorrect requests** + +All the POST requests will be ignored and the server will send a `HTTP 404 ERROR` as a response. + +Any URL other than the format: `http://:/users/*` will result in `HTTP 404 ERROR`. + +**Example** + +` +curl -i http://localhost:2500/user +` + +Output + +``` +HTTP/1.1 404 Not Found +``` + +A URL of form `http://:/users/` such that `` does not exist will also give `HTTP 404 ERROR`. + +**Example** + +` +curl -i http://localhost:2500/users/randomuser +` + +Output + +``` +HTTP/1.1 404 Not Found +``` diff --git a/pgagroal.spec b/pgagroal.spec new file mode 100644 index 00000000..b393a36f --- /dev/null +++ b/pgagroal.spec @@ -0,0 +1,147 @@ +Name: pgagroal +Version: 2.0.0 +Release: 1%{dist} +Summary: High-performance connection pool for PostgreSQL +License: BSD +URL: https://github.com/agroal/pgagroal +Source0: https://github.com/agroal/pgagroal/archive/%{version}.tar.gz + +BuildRequires: gcc cmake make python3-docutils +BuildRequires: libev libev-devel openssl openssl-devel systemd systemd-devel libatomic zlib zlib-devel libzstd libzstd-devel lz4 lz4-devel bzip2 bzip2-devel +Requires: libev openssl systemd libatomic zlib libzstd lz4 bzip2 + +%description +pgagroal is a high-performance connection pool for PostgreSQL. + +%prep +%setup -q + +%build + +%{__mkdir} build +cd build +cmake -DCMAKE_BUILD_TYPE=Release .. +%{__make} + +%install + +%{__mkdir} -p %{buildroot}%{_sysconfdir} +%{__mkdir} -p %{buildroot}%{_bindir} +%{__mkdir} -p %{buildroot}%{_libdir} +%{__mkdir} -p %{buildroot}%{_docdir}/%{name}/grafana +%{__mkdir} -p %{buildroot}%{_docdir}/%{name}/etc +%{__mkdir} -p %{buildroot}%{_docdir}/%{name}/images +%{__mkdir} -p %{buildroot}%{_docdir}/%{name}/shell_comp +%{__mkdir} -p %{buildroot}%{_docdir}/%{name}/tutorial +%{__mkdir} -p %{buildroot}%{_mandir}/man1 +%{__mkdir} -p %{buildroot}%{_mandir}/man5 +%{__mkdir} -p %{buildroot}%{_sysconfdir}/pgagroal + +%{__install} -m 644 %{_builddir}/%{name}-%{version}/LICENSE %{buildroot}%{_docdir}/%{name}/LICENSE +%{__install} -m 644 %{_builddir}/%{name}-%{version}/CODE_OF_CONDUCT.md %{buildroot}%{_docdir}/%{name}/CODE_OF_CONDUCT.md +%{__install} -m 644 %{_builddir}/%{name}-%{version}/README.md %{buildroot}%{_docdir}/%{name}/README.md +%{__install} -m 644 %{_builddir}/%{name}-%{version}/contrib/grafana/dashboard.json %{buildroot}%{_docdir}/%{name}/grafana/dashboard.json +%{__install} -m 644 %{_builddir}/%{name}-%{version}/contrib/grafana/README.md %{buildroot}%{_docdir}/%{name}/grafana/README.md +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/ARCHITECTURE.md %{buildroot}%{_docdir}/%{name}/ARCHITECTURE.md +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/CLI.md %{buildroot}%{_docdir}/%{name}/CLI.md +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/CONFIGURATION.md %{buildroot}%{_docdir}/%{name}/CONFIGURATION.md +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/VAULT.md %{buildroot}%{_docdir}/%{name}/VAULT.md +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/FAILOVER.md %{buildroot}%{_docdir}/%{name}/FAILOVER.md +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/GETTING_STARTED.md %{buildroot}%{_docdir}/%{name}/GETTING_STARTED.md +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/PERFORMANCE.md %{buildroot}%{_docdir}/%{name}/PERFORMANCE.md +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/PIPELINES.md %{buildroot}%{_docdir}/%{name}/PIPELINES.md +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/RPM.md %{buildroot}%{_docdir}/%{name}/RPM.md +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/SECURITY.md %{buildroot}%{_docdir}/%{name}/SECURITY.md +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/DISTRIBUTIONS.md %{buildroot}%{_docdir}/%{name}/DISTRIBUTIONS.md +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/etc/pgagroal.service %{buildroot}%{_docdir}/%{name}/etc/pgagroal.service +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/etc/pgagroal.socket %{buildroot}%{_docdir}/%{name}/etc/pgagroal.socket +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/images/perf-extended.png %{buildroot}%{_docdir}/%{name}/images/perf-extended.png +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/images/perf-prepared.png %{buildroot}%{_docdir}/%{name}/images/perf-prepared.png +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/images/perf-readonly.png %{buildroot}%{_docdir}/%{name}/images/perf-readonly.png +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/images/perf-simple.png %{buildroot}%{_docdir}/%{name}/images/perf-simple.png +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/shell_comp/pgagroal_comp.bash %{buildroot}%{_docdir}/%{name}/shell_comp/pgagroal_comp.bash +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/shell_comp/pgagroal_comp.zsh %{buildroot}%{_docdir}/%{name}/shell_comp/pgagroal_comp.zsh +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/tutorial/01_install.md %{buildroot}%{_docdir}/%{name}/tutorial/01_install.md +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/tutorial/02_prefill.md %{buildroot}%{_docdir}/%{name}/tutorial/02_prefill.md +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/tutorial/03_remote_management.md %{buildroot}%{_docdir}/%{name}/tutorial/03_remote_management.md +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/tutorial/04_prometheus.md %{buildroot}%{_docdir}/%{name}/tutorial/04_prometheus.md +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/tutorial/05_split_security.md %{buildroot}%{_docdir}/%{name}/tutorial/05_split_security.md + +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/etc/pgagroal.conf %{buildroot}%{_sysconfdir}/pgagroal/pgagroal.conf +%{__install} -m 644 %{_builddir}/%{name}-%{version}/doc/etc/pgagroal_hba.conf %{buildroot}%{_sysconfdir}/pgagroal/pgagroal_hba.conf + +%{__install} -m 644 %{_builddir}/%{name}-%{version}/build/doc/pgagroal.1 %{buildroot}%{_mandir}/man1/pgagroal.1 +%{__install} -m 644 %{_builddir}/%{name}-%{version}/build/doc/pgagroal-admin.1 %{buildroot}%{_mandir}/man1/pgagroal-admin.1 +%{__install} -m 644 %{_builddir}/%{name}-%{version}/build/doc/pgagroal-cli.1 %{buildroot}%{_mandir}/man1/pgagroal-cli.1 +%{__install} -m 644 %{_builddir}/%{name}-%{version}/build/doc/pgagroal-vault.1 %{buildroot}%{_mandir}/man1/pgagroal-vault.1 +%{__install} -m 644 %{_builddir}/%{name}-%{version}/build/doc/pgagroal.conf.5 %{buildroot}%{_mandir}/man5/pgagroal.conf.5 +%{__install} -m 644 %{_builddir}/%{name}-%{version}/build/doc/pgagroal_databases.conf.5 %{buildroot}%{_mandir}/man5/pgagroal_databases.conf.5 +%{__install} -m 644 %{_builddir}/%{name}-%{version}/build/doc/pgagroal_hba.conf.5 %{buildroot}%{_mandir}/man5/pgagroal_hba.conf.5 +%{__install} -m 644 %{_builddir}/%{name}-%{version}/build/doc/pgagroal_vault.conf.5 %{buildroot}%{_mandir}/man5/pgagroal_vault.conf.5 + +%{__install} -m 755 %{_builddir}/%{name}-%{version}/build/src/pgagroal %{buildroot}%{_bindir}/pgagroal +%{__install} -m 755 %{_builddir}/%{name}-%{version}/build/src/pgagroal-cli %{buildroot}%{_bindir}/pgagroal-cli +%{__install} -m 755 %{_builddir}/%{name}-%{version}/build/src/pgagroal-admin %{buildroot}%{_bindir}/pgagroal-admin +%{__install} -m 755 %{_builddir}/%{name}-%{version}/build/src/pgagroal-vault %{buildroot}%{_bindir}/pgagroal-vault + +%{__install} -m 755 %{_builddir}/%{name}-%{version}/build/src/libpgagroal.so.%{version} %{buildroot}%{_libdir}/libpgagroal.so.%{version} + +chrpath -r %{_libdir} %{buildroot}%{_bindir}/pgagroal +chrpath -r %{_libdir} %{buildroot}%{_bindir}/pgagroal-cli +chrpath -r %{_libdir} %{buildroot}%{_bindir}/pgagroal-admin +chrpath -r %{_libdir} %{buildroot}%{_bindir}/pgagroal-vault + +cd %{buildroot}%{_libdir}/ +%{__ln_s} -f libpgagroal.so.%{version} libpgagroal.so.2 +%{__ln_s} -f libpgagroal.so.2 libpgagroal.so + +%files +%license %{_docdir}/%{name}/LICENSE +%{_docdir}/%{name}/ARCHITECTURE.md +%{_docdir}/%{name}/CLI.md +%{_docdir}/%{name}/CODE_OF_CONDUCT.md +%{_docdir}/%{name}/CONFIGURATION.md +%{_docdir}/%{name}/VAULT.md +%{_docdir}/%{name}/FAILOVER.md +%{_docdir}/%{name}/GETTING_STARTED.md +%{_docdir}/%{name}/PERFORMANCE.md +%{_docdir}/%{name}/PIPELINES.md +%{_docdir}/%{name}/README.md +%{_docdir}/%{name}/RPM.md +%{_docdir}/%{name}/SECURITY.md +%{_docdir}/%{name}/DISTRIBUTIONS.md +%{_docdir}/%{name}/grafana/dashboard.json +%{_docdir}/%{name}/grafana/README.md +%{_docdir}/%{name}/etc/pgagroal.service +%{_docdir}/%{name}/etc/pgagroal.socket +%{_docdir}/%{name}/images/perf-extended.png +%{_docdir}/%{name}/images/perf-prepared.png +%{_docdir}/%{name}/images/perf-readonly.png +%{_docdir}/%{name}/images/perf-simple.png +%{_docdir}/%{name}/images/perf-simple.png +%{_docdir}/%{name}/shell_comp/pgagroal_comp.bash +%{_docdir}/%{name}/shell_comp/pgagroal_comp.zsh +%{_docdir}/%{name}/tutorial/01_install.md +%{_docdir}/%{name}/tutorial/02_prefill.md +%{_docdir}/%{name}/tutorial/03_remote_management.md +%{_docdir}/%{name}/tutorial/04_prometheus.md +%{_docdir}/%{name}/tutorial/05_split_security.md +%{_mandir}/man1/pgagroal.1* +%{_mandir}/man1/pgagroal-admin.1* +%{_mandir}/man1/pgagroal-cli.1* +%{_mandir}/man1/pgagroal-vault.1* +%{_mandir}/man5/pgagroal.conf.5* +%{_mandir}/man5/pgagroal_databases.conf.5* +%{_mandir}/man5/pgagroal_hba.conf.5* +%{_mandir}/man5/pgagroal_vault.conf.5* +%config %{_sysconfdir}/pgagroal/pgagroal.conf +%config %{_sysconfdir}/pgagroal/pgagroal_hba.conf +%{_bindir}/pgagroal +%{_bindir}/pgagroal-cli +%{_bindir}/pgagroal-admin +%{_bindir}/pgagroal-vault +%{_libdir}/libpgagroal.so +%{_libdir}/libpgagroal.so.2 +%{_libdir}/libpgagroal.so.%{version} + +%changelog diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 00000000..476a91b1 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,363 @@ +# +# Add files for libpgagroal +# +FILE(GLOB SOURCE_FILES "libpgagroal/*.c") +FILE(GLOB HEADER_FILES "include/*.h") + +set(SOURCES ${SOURCE_FILES} ${HEADER_FILES}) + +# +# OS +# +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + + add_compile_options(-DHAVE_LINUX) + add_compile_options(-D_POSIX_C_SOURCE=200809L) + + # + # Include directories + # + include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${LIBEV_INCLUDE_DIRS} + ${OPENSSL_INCLUDE_DIR} + ${SYSTEMD_INCLUDE_DIRS} + ${THREAD_INCLUDE_DIRS} + ${ZLIB_INCLUDE_DIRS} + ${BZIP2_INCLUDE_DIRS} + ${ZSTD_INCLUDE_DIRS} + ${LZ4_INCLUDE_DIRS} + ) + + # + # Library directories + # + link_libraries( + ${LIBEV_LIBRARIES} + ${OPENSSL_CRYPTO_LIBRARY} + ${OPENSSL_SSL_LIBRARY} + ${SYSTEMD_LIBRARIES} + ${LIBATOMIC_LIBRARY} + ${THREAD_LIBRARY} + ${ZLIB_LIBRARIES} + ${BZIP2_LIBRARIES} + ${ZSTD_LIBRARIES} + ${LZ4_LIBRARIES} + ) + + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined") +elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + find_program(HOMEBREW_EXECUTABLE brew) + if(EXISTS ${HOMEBREW_EXECUTABLE}) + execute_process( + COMMAND ${HOMEBREW_EXECUTABLE} --prefix + OUTPUT_VARIABLE HOMEBREW_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE) + message(STATUS "Homebrew found at ${HOMEBREW_PREFIX}") + endif() + + # + # Detecting OpenSSL + # + execute_process( + COMMAND ${HOMEBREW_EXECUTABLE} --prefix openssl + OUTPUT_VARIABLE HOMEBREW_OPENSSL + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(DEFINED HOMEBREW_OPENSSL) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I${HOMEBREW_OPENSSL}/include") + else() + message(FATAL_ERROR "Homebrew's OpenSSL needed") + endif() + + add_compile_options(-D_DARWIN_C_SOURCE) + add_compile_options(-DHAVE_OSX) + + # + # Include directories + # + include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${LIBEV_INCLUDE_DIRS} + ${OPENSSL_INCLUDE_DIRS} + ${THREAD_INCLUDE_DIRS} + ${ZLIB_INCLUDE_DIRS} + ${BZIP2_INCLUDE_DIRS} + ${ZSTD_INCLUDE_DIRS} + ${LZ4_INCLUDE_DIRS} + ) + + # + # Library directories + # + link_libraries( + ${LIBEV_LIBRARIES} + ${OPENSSL_LIBRARIES} + ${THREAD_LIBRARY} + ${ZLIB_LIBRARIES} + ${BZIP2_LIBRARIES} + ${ZSTD_LIBRARIES} + ${LZ4_LIBRARIES} + ) +else() + + add_compile_options(-D_XOPEN_SOURCE=700) + add_compile_options(-D_BSD_SOURCE) + add_compile_options(-D_DEFAULT_SOURCE) + add_compile_options(-D__BSD_VISIBLE) + + if (${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + add_compile_options(-DHAVE_OPENBSD) + elseif (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") + add_compile_options(-DHAVE_FREEBSD) + endif() + + # + # Include directories + # + include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${LIBEV_INCLUDE_DIRS} + ${OPENSSL_INCLUDE_DIRS} + ${THREAD_INCLUDE_DIRS} + ${ZLIB_INCLUDE_DIRS} + ${BZIP2_INCLUDE_DIRS} + ${ZSTD_INCLUDE_DIRS} + ${LZ4_INCLUDE_DIRS} + ) + + # + # Library directories + # + link_libraries( + ${LIBEV_LIBRARIES} + ${OPENSSL_LIBRARIES} + ${THREAD_LIBRARY} + ${ZLIB_LIBRARIES} + ${BZIP2_LIBRARIES} + ${ZSTD_LIBRARIES} + ${LZ4_LIBRARIES} + ) +endif() + +# +# Compile options +# +add_compile_options(-g) +add_compile_options(-Wall) +add_compile_options(-Werror) +add_compile_options(-std=c17) +add_compile_options(-D__USE_ISOC11) +add_compile_options(-D_GNU_SOURCE) +add_compile_options(-Wno-deprecated) +add_compile_options(-Wno-deprecated-declarations) + +# +# version number and string management +# +add_compile_options(-DPGAGROAL_MAJOR_VERSION=${VERSION_MAJOR}) +add_compile_options(-DPGAGROAL_MINOR_VERSION=${VERSION_MINOR}) +add_compile_options(-DPGAGROAL_PATCH_VERSION=${VERSION_PATCH}) +add_compile_options(-DPGAGROAL_VERSION="${VERSION_STRING}") + +if(CMAKE_C_COMPILER_ID STREQUAL "GNU") + add_compile_options(-Wstrict-prototypes) +endif() + +check_c_compiler_flag(-fPIC HAS_PIC) +if (HAS_PIC) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIC") +endif() + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + include(CheckPIESupported) + check_pie_supported() +endif() + +if (CMAKE_BUILD_TYPE MATCHES Debug) + add_compile_options(-O0) + add_compile_options(-DDEBUG) + + check_c_compiler_flag(-Wformat HAS_FORMAT) + if (HAS_FORMAT) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wformat") + endif() + + check_c_compiler_flag(-Wformat-security HAS_FORMAT_SECURITY) + if (HAS_FORMAT_SECURITY) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wformat-security") + endif() + + check_c_compiler_flag(-fstack-protector-strong HAS_STACK_PROTECTOR_STRONG) + if (HAS_STACK_PROTECTOR_STRONG) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fstack-protector-strong") + endif() + + check_c_compiler_flag(-rdynamic HAS_DYNAMIC) + if (HAS_DYNAMIC) + set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -rdynamic") + set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -rdynamic") + endif() + + check_c_compiler_flag(-fstack-protector-strong HAS_STACKPROTECTOR_STRONG) + if (HAS_STACKPROTECTOR_STRONG) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fstack-protector-strong") + else() + check_c_compiler_flag(-fstack-protector HAS_STACKPROTECTOR) + if (HAS_STACKPROTECTOR) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fstack-protector") + endif() + endif() + + check_c_compiler_flag(-Wl,arg HAS_LINKER) + check_linker_flag(C "-z relro" LINKER_HAS_RELRO) + if (HAS_LINKER AND LINKER_HAS_RELRO) + set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,relro") + set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,relro") + endif() + + check_linker_flag(C "-z now" LINKER_HAS_NOW) + if (HAS_LINKER AND LINKER_HAS_NOW) + set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,now") + set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,now") + endif() + + if(CMAKE_C_COMPILER_ID STREQUAL "Clang") + if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + add_compile_options(-fsanitize=address) + + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_LINKER_FLAGS_DEBUG} -fsanitize=address") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS_DEBUG} -fsanitize=address") + endif() + endif() + + check_c_compiler_flag(-fno-omit-frame-pointer HAS_NO_OMIT_FRAME_POINTER) + if (HAS_NO_OMIT_FRAME_POINTER) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fno-omit-frame-pointer") + endif() +endif() + +if (CMAKE_BUILD_TYPE MATCHES Release OR CMAKE_BUILD_TYPE MATCHES RelWithDebInfo) + add_compile_options(-O2) + add_compile_options(-DNDEBUG) + + check_c_compiler_flag(-Wformat HAS_FORMAT) + if (HAS_FORMAT) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wformat") + endif() + + check_c_compiler_flag(-Wformat-security HAS_FORMAT_SECURITY) + if (HAS_FORMAT_SECURITY) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wformat-security") + endif() + + check_c_compiler_flag(-fstack-protector-strong HAS_STACK_PROTECTOR_STRONG) + if (HAS_STACK_PROTECTOR_STRONG) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fstack-protector-strong") + endif() + + check_c_compiler_flag(-rdynamic HAS_DYNAMIC) + if (HAS_DYNAMIC) + set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -rdynamic") + set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -rdynamic") + endif() + + check_c_compiler_flag(-fstack-protector-strong HAS_STACKPROTECTOR_STRONG) + if (HAS_STACKPROTECTOR_STRONG) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fstack-protector-strong") + else() + check_c_compiler_flag(-fstack-protector HAS_STACKPROTECTOR) + if (HAS_STACKPROTECTOR) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fstack-protector") + endif() + endif() + + check_c_compiler_flag(-Wl,arg HAS_LINKER) + check_linker_flag(C "-z relro" LINKER_HAS_RELRO) + if (HAS_LINKER AND HAS_RELRO) + set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,relro") + set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,relro") + endif() + + check_linker_flag(C "-z now" LINKER_HAS_NOW) + if (HAS_LINKER AND LINKER_HAS_NOW) + set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,now") + set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,now") + endif() +endif (CMAKE_BUILD_TYPE MATCHES Release OR CMAKE_BUILD_TYPE MATCHES RelWithDebInfo) + +if (CMAKE_BUILD_TYPE MATCHES Performance) + add_compile_options(-O2) + add_compile_options(-DNDEBUG) + + if(CMAKE_C_COMPILER_ID STREQUAL "GNU") + add_compile_options(-march=native) + add_compile_options(-mtune=native) + add_compile_options(-flto) + endif() +endif (CMAKE_BUILD_TYPE MATCHES Performance) + +# +# Build libpgagroal +# +add_library(pgagroal SHARED ${SOURCES}) +set_target_properties(pgagroal PROPERTIES LINKER_LANGUAGE C VERSION ${VERSION_STRING} + SOVERSION ${VERSION_MAJOR}) +target_link_libraries(pgagroal PUBLIC) + +install(TARGETS pgagroal DESTINATION ${CMAKE_INSTALL_LIBDIR}/) + +# +# Build pgagroal +# +add_executable(pgagroal-bin main.c ${RESOURCE_OBJECT}) +if (CMAKE_C_LINK_PIE_SUPPORTED) + set_target_properties(pgagroal-bin PROPERTIES LINKER_LANGUAGE C POSITION_INDEPENDENT_CODE TRUE OUTPUT_NAME pgagroal) +else() + set_target_properties(pgagroal-bin PROPERTIES LINKER_LANGUAGE C POSITION_INDEPENDENT_CODE FALSE OUTPUT_NAME pgagroal) +endif() +target_link_libraries(pgagroal-bin pgagroal) + +install(TARGETS pgagroal-bin DESTINATION ${CMAKE_INSTALL_BINDIR}) + +# +# Build pgagroal-cli +# +add_executable(pgagroal-cli-bin cli.c ${RESOURCE_OBJECT}) +if (CMAKE_C_LINK_PIE_SUPPORTED) + set_target_properties(pgagroal-cli-bin PROPERTIES LINKER_LANGUAGE C POSITION_INDEPENDENT_CODE TRUE OUTPUT_NAME pgagroal-cli) +else() + set_target_properties(pgagroal-cli-bin PROPERTIES LINKER_LANGUAGE C POSITION_INDEPENDENT_CODE FALSE OUTPUT_NAME pgagroal-cli) +endif() +target_link_libraries(pgagroal-cli-bin pgagroal) + +install(TARGETS pgagroal-cli-bin DESTINATION ${CMAKE_INSTALL_BINDIR}) + +# +# Build pgagroal-admin +# +add_executable(pgagroal-admin-bin admin.c ${RESOURCE_OBJECT}) +if (CMAKE_C_LINK_PIE_SUPPORTED) + set_target_properties(pgagroal-admin-bin PROPERTIES LINKER_LANGUAGE C POSITION_INDEPENDENT_CODE TRUE OUTPUT_NAME pgagroal-admin) +else() + set_target_properties(pgagroal-admin-bin PROPERTIES LINKER_LANGUAGE C POSITION_INDEPENDENT_CODE FALSE OUTPUT_NAME pgagroal-admin) +endif() +target_link_libraries(pgagroal-admin-bin pgagroal) + +install(TARGETS pgagroal-admin-bin DESTINATION ${CMAKE_INSTALL_BINDIR}) + + + +# +# Build pgagroal-vault +# +add_executable(pgagroal-vault-bin vault.c ${RESOURCE_OBJECT}) +if (CMAKE_C_LINK_PIE_SUPPORTED) + set_target_properties(pgagroal-vault-bin PROPERTIES LINKER_LANGUAGE C POSITION_INDEPENDENT_CODE TRUE OUTPUT_NAME pgagroal-vault) +else() + set_target_properties(pgagroal-vault-bin PROPERTIES LINKER_LANGUAGE C POSITION_INDEPENDENT_CODE FALSE OUTPUT_NAME pgagroal-vault) +endif() +target_link_libraries(pgagroal-vault-bin pgagroal) + +install(TARGETS pgagroal-vault-bin DESTINATION ${CMAKE_INSTALL_BINDIR}) + + diff --git a/src/admin.c b/src/admin.c new file mode 100644 index 00000000..41d2396d --- /dev/null +++ b/src/admin.c @@ -0,0 +1,937 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ACTION_UNKNOWN 0 +#define ACTION_MASTER_KEY 1 +#define ACTION_ADD_USER 2 +#define ACTION_UPDATE_USER 3 +#define ACTION_REMOVE_USER 4 +#define ACTION_LIST_USERS 5 + +static int master_key(char* password, bool generate_pwd, int pwd_length); +static int add_user(char* users_path, char* username, char* password, bool generate_pwd, int pwd_length); +static int update_user(char* users_path, char* username, char* password, bool generate_pwd, int pwd_length); +static int remove_user(char* users_path, char* username); +static int list_users(char* users_path); + +const struct pgagroal_command command_table[] = +{ + { + .command = "master-key", + .subcommand = "", + .accepted_argument_count = {0}, + .deprecated = false, + .action = ACTION_MASTER_KEY, + .log_message = "", + }, + { + .command = "user", + .subcommand = "add", + .accepted_argument_count = {0}, + .deprecated = false, + .action = ACTION_ADD_USER, + .log_message = " [%s]", + }, + { + .command = "user", + .subcommand = "edit", + .accepted_argument_count = {0}, + .deprecated = false, + .action = ACTION_UPDATE_USER, + .log_message = " [%s]", + }, + { + .command = "user", + .subcommand = "del", + .accepted_argument_count = {0}, + .deprecated = false, + .action = ACTION_REMOVE_USER, + .log_message = " [%s]", + }, + { + .command = "user", + .subcommand = "ls", + .accepted_argument_count = {0}, + .deprecated = false, + .action = ACTION_LIST_USERS, + .log_message = "", + }, +}; + +static void +version(void) +{ + printf("pgagroal-admin %s\n", PGAGROAL_VERSION); + exit(1); +} + +static void +usage(void) +{ + printf("pgagroal-admin %s\n", PGAGROAL_VERSION); + printf(" Administration utility for pgagroal\n"); + printf("\n"); + + printf("Usage:\n"); + printf(" pgagroal-admin [ -f FILE ] [ COMMAND ] \n"); + printf("\n"); + printf("Options:\n"); + printf(" -f, --file FILE Set the path to a user file\n"); + printf(" Defaults to %s\n", PGAGROAL_DEFAULT_USERS_FILE); + printf(" -U, --user USER Set the user name\n"); + printf(" -P, --password PASSWORD Set the password for the user\n"); + printf(" -g, --generate Generate a password\n"); + printf(" -l, --length Password length\n"); + printf(" -V, --version Display version information\n"); + printf(" -?, --help Display help\n"); + printf("\n"); + printf("Commands:\n"); + printf(" master-key Create or update the master key\n"); + printf(" user Manage a specific user, where can be\n"); + printf(" - add to add a new user\n"); + printf(" - del to remove an existing user\n"); + printf(" - edit to change the password for an existing user\n"); + printf(" - ls to list all available users\n"); + printf("\n"); + printf("pgagroal: %s\n", PGAGROAL_HOMEPAGE); + printf("Report bugs: %s\n", PGAGROAL_ISSUES); +} + +int +main(int argc, char** argv) +{ + int c; + char* username = NULL; + char* password = NULL; + char* file_path = NULL; + bool generate_pwd = false; + int pwd_length = DEFAULT_PASSWORD_LENGTH; + int option_index = 0; + size_t command_count = sizeof(command_table) / sizeof(struct pgagroal_command); + struct pgagroal_parsed_command parsed = {.cmd = NULL, .args = {0}}; + + while (1) + { + static struct option long_options[] = + { + {"user", required_argument, 0, 'U'}, + {"password", required_argument, 0, 'P'}, + {"file", required_argument, 0, 'f'}, + {"generate", no_argument, 0, 'g'}, + {"length", required_argument, 0, 'l'}, + {"version", no_argument, 0, 'V'}, + {"help", no_argument, 0, '?'} + }; + + c = getopt_long(argc, argv, "gV?f:U:P:l:", + long_options, &option_index); + + if (c == -1) + { + break; + } + + switch (c) + { + case 'U': + username = optarg; + break; + case 'P': + password = optarg; + break; + case 'f': + file_path = optarg; + break; + case 'g': + generate_pwd = true; + break; + case 'l': + pwd_length = atoi(optarg); + break; + case 'V': + version(); + break; + case '?': + usage(); + exit(1); + break; + default: + break; + } + } + + if (getuid() == 0) + { + errx(1, "Using the root account is not allowed"); + } + + if (!parse_command(argc, argv, optind, &parsed, command_table, command_count)) + { + usage(); + goto error; + } + + // if here, the action is understood, but we need + // the file to operate onto! + // Therefore, if the user did not specify any config file + // the default one is used. Note that in the case of ACTION_MASTER_KEY + // there is no need for the file_path to be set, so setting to a default + // value does nothing. + // Setting the file also means we don't have to check against the file_path value. + if (file_path == NULL) + { + file_path = PGAGROAL_DEFAULT_USERS_FILE; + } + + if (parsed.cmd->action == ACTION_MASTER_KEY) + { + if (master_key(password, generate_pwd, pwd_length)) + { + errx(1, "Cannot generate master key"); + } + } + else if (parsed.cmd->action == ACTION_ADD_USER) + { + if (add_user(file_path, username, password, generate_pwd, pwd_length)) + { + errx(1, "Error for "); + } + } + else if (parsed.cmd->action == ACTION_UPDATE_USER) + { + if (update_user(file_path, username, password, generate_pwd, pwd_length)) + { + errx(1, "Error for "); + } + } + else if (parsed.cmd->action == ACTION_REMOVE_USER) + { + + if (remove_user(file_path, username)) + { + errx(1, "Error for "); + } + } + else if (parsed.cmd->action == ACTION_LIST_USERS) + { + + if (list_users(file_path)) + { + errx(1, "Error for "); + } + + } + + exit(0); + +error: + + exit(1); +} + +static int +master_key(char* password, bool generate_pwd, int pwd_length) +{ + FILE* file = NULL; + char buf[MISC_LENGTH]; + char* encoded = NULL; + size_t encoded_length; + struct stat st = {0}; + bool do_free = true; + + if (password != NULL) + { + do_free = false; + } + + if (pgagroal_get_home_directory() == NULL) + { + char* username = pgagroal_get_user_name(); + + if (username != NULL) + { + warnx("No home directory for user \'%s\'", username); + } + else + { + warnx("No home directory for user running pgagroal"); + } + + goto error; + } + + memset(&buf, 0, sizeof(buf)); + snprintf(&buf[0], sizeof(buf), "%s/.pgagroal", pgagroal_get_home_directory()); + + if (stat(&buf[0], &st) == -1) + { + mkdir(&buf[0], S_IRWXU); + } + else + { + if (S_ISDIR(st.st_mode) && st.st_mode & S_IRWXU && !(st.st_mode & S_IRWXG) && !(st.st_mode & S_IRWXO)) + { + /* Ok */ + } + else + { + warnx("Wrong permissions for directory <%s> (must be 0700)", &buf[0]); + goto error; + } + } + + memset(&buf, 0, sizeof(buf)); + snprintf(&buf[0], sizeof(buf), "%s/.pgagroal/master.key", pgagroal_get_home_directory()); + + if (pgagroal_exists(&buf[0])) + { + warnx("The file %s already exists, cannot continue", &buf[0]); + goto error; + } + + if (stat(&buf[0], &st) == -1) + { + /* Ok */ + } + else + { + if (S_ISREG(st.st_mode) && st.st_mode & (S_IRUSR | S_IWUSR) && !(st.st_mode & S_IRWXG) && !(st.st_mode & S_IRWXO)) + { + /* Ok */ + } + else + { + warnx("Wrong permissions for file <%s> (must be 0600)", &buf[0]); + goto error; + } + } + + file = fopen(&buf[0], "w+"); + if (file == NULL) + { + warnx("Could not write to master key file <%s>", &buf[0]); + goto error; + } + + if (password == NULL) + { + if (!generate_pwd) + { + while (password == NULL) + { + printf("Master key (will not echo): "); + password = pgagroal_get_password(); + printf("\n"); + + if (password != NULL && strlen(password) < MIN_PASSWORD_LENGTH) + { + printf("Invalid key length, must be at least %d chars.\n", MIN_PASSWORD_LENGTH); + free(password); + password = NULL; + } + } + } + else + { + if (pgagroal_generate_password(pwd_length, &password)) + { + do_free = false; + goto error; + } + } + } + else + { + do_free = false; + } + + pgagroal_base64_encode(password, strlen(password), &encoded, &encoded_length); + fputs(encoded, file); + free(encoded); + + if (do_free) + { + free(password); + } + + fclose(file); + + chmod(&buf[0], S_IRUSR | S_IWUSR); + printf("Master Key stored into %s\n", &buf[0]); + return 0; + +error: + + free(encoded); + + if (do_free) + { + free(password); + } + + if (file) + { + fclose(file); + } + + return 1; +} + +static int +add_user(char* users_path, char* username, char* password, bool generate_pwd, int pwd_length) +{ + FILE* users_file = NULL; + char line[MISC_LENGTH]; + char* entry = NULL; + char* master_key = NULL; + char* ptr = NULL; + char* encrypted = NULL; + int encrypted_length = 0; + char* encoded = NULL; + size_t encoded_length; + char un[MAX_USERNAME_LENGTH]; + int number_of_users = 0; + bool do_verify = true; + char* verify = NULL; + bool do_free = true; + + if (pgagroal_get_master_key(&master_key)) + { + warnx("Invalid master key"); + goto error; + } + + if (password != NULL) + { + do_verify = false; + do_free = false; + } + + users_file = fopen(users_path, "a+"); + if (users_file == NULL) + { + warnx("Could not append to users file <%s>", users_path); + goto error; + } + + /* User */ + if (username == NULL) + { +username: + printf("User name: "); + + memset(&un, 0, sizeof(un)); + if (fgets(&un[0], sizeof(un), stdin) == NULL) + { + goto error; + } + un[strlen(un) - 1] = 0; + username = &un[0]; + } + + if (username == NULL || strlen(username) == 0) + { + goto username; + } + + /* Verify */ + while (fgets(line, sizeof(line), users_file)) + { + ptr = strtok(line, ":"); + if (!strcmp(username, ptr)) + { + warnx("Existing user: %s", username); + goto error; + } + + number_of_users++; + } + + if (number_of_users > NUMBER_OF_USERS) + { + warnx("Too many users"); + goto error; + } + + /* Password */ + if (password == NULL) + { +password: + if (generate_pwd) + { + if (pgagroal_generate_password(pwd_length, &password)) + { + do_free = false; + goto error; + } + do_verify = false; + printf("Password : %s", password); + } + else + { + printf("Password : "); + + if (password != NULL) + { + free(password); + password = NULL; + } + + password = pgagroal_get_password(); + } + printf("\n"); + } + + for (int i = 0; i < strlen(password); i++) + { + if ((unsigned char)(*(password + i)) & 0x80) + { + goto password; + } + } + + if (do_verify) + { + printf("Verify : "); + + if (verify != NULL) + { + free(verify); + verify = NULL; + } + + verify = pgagroal_get_password(); + printf("\n"); + + if (strlen(password) != strlen(verify) || memcmp(password, verify, strlen(password)) != 0) + { + goto password; + } + } + + pgagroal_encrypt(password, master_key, &encrypted, &encrypted_length, ENCRYPTION_AES_256_CBC); + pgagroal_base64_encode(encrypted, encrypted_length, &encoded, &encoded_length); + + entry = pgagroal_append(entry, username); + entry = pgagroal_append(entry, ":"); + entry = pgagroal_append(entry, encoded); + entry = pgagroal_append(entry, "\n"); + + fputs(entry, users_file); + + free(entry); + free(master_key); + free(encrypted); + free(encoded); + if (do_free) + { + free(password); + } + free(verify); + + fclose(users_file); + + return 0; + +error: + + free(entry); + free(master_key); + free(encrypted); + free(encoded); + if (do_free) + { + free(password); + } + free(verify); + + if (users_file) + { + fclose(users_file); + } + + return 1; +} + +static int +update_user(char* users_path, char* username, char* password, bool generate_pwd, int pwd_length) +{ + FILE* users_file = NULL; + FILE* users_file_tmp = NULL; + char tmpfilename[MISC_LENGTH]; + char line[MISC_LENGTH]; + char line_copy[MISC_LENGTH]; + char* master_key = NULL; + char* ptr = NULL; + char* entry = NULL; + char* encrypted = NULL; + int encrypted_length = 0; + char* encoded = NULL; + size_t encoded_length; + char un[MAX_USERNAME_LENGTH]; + bool found = false; + bool do_verify = true; + char* verify = NULL; + bool do_free = true; + + memset(&tmpfilename, 0, sizeof(tmpfilename)); + + if (pgagroal_get_master_key(&master_key)) + { + warnx("Invalid master key"); + goto error; + } + + if (password != NULL) + { + do_verify = false; + do_free = false; + } + + users_file = fopen(users_path, "r"); + if (!users_file) + { + warnx("File <%s> not found", users_path); + goto error; + } + + snprintf(tmpfilename, sizeof(tmpfilename), "%s.tmp", users_path); + users_file_tmp = fopen(tmpfilename, "w+"); + if (users_file_tmp == NULL) + { + warnx("Could not write to temporary user file <%s>", tmpfilename); + goto error; + } + + /* User */ + if (username == NULL) + { +username: + printf("User name: "); + + memset(&un, 0, sizeof(un)); + if (fgets(&un[0], sizeof(un), stdin) == NULL) + { + goto error; + } + un[strlen(un) - 1] = 0; + username = &un[0]; + } + + if (username == NULL || strlen(username) == 0) + { + goto username; + } + + /* Update */ + while (fgets(line, sizeof(line), users_file)) + { + memset(&line_copy, 0, sizeof(line_copy)); + memcpy(&line_copy, &line, strlen(line)); + + ptr = strtok(line, ":"); + if (!strcmp(username, ptr)) + { + /* Password */ + if (password == NULL) + { +password: + if (generate_pwd) + { + if (pgagroal_generate_password(pwd_length, &password)) + { + do_free = false; + goto error; + } + do_verify = false; + printf("Password : %s", password); + } + else + { + printf("Password : "); + + if (password != NULL) + { + free(password); + password = NULL; + } + + password = pgagroal_get_password(); + } + printf("\n"); + } + + for (int i = 0; i < strlen(password); i++) + { + if ((unsigned char)(*(password + i)) & 0x80) + { + goto password; + } + } + + if (do_verify) + { + printf("Verify : "); + + if (verify != NULL) + { + free(verify); + verify = NULL; + } + + verify = pgagroal_get_password(); + printf("\n"); + + if (strlen(password) != strlen(verify) || memcmp(password, verify, strlen(password)) != 0) + { + goto password; + } + } + + pgagroal_encrypt(password, master_key, &encrypted, &encrypted_length, ENCRYPTION_AES_256_CBC); + pgagroal_base64_encode(encrypted, encrypted_length, &encoded, &encoded_length); + + entry = NULL; + entry = pgagroal_append(entry, username); + entry = pgagroal_append(entry, ":"); + entry = pgagroal_append(entry, encoded); + entry = pgagroal_append(entry, "\n"); + + fputs(entry, users_file_tmp); + free(entry); + + found = true; + } + else + { + fputs(line_copy, users_file_tmp); + } + } + + if (!found) + { + warnx("User '%s' not found", username); + goto error; + } + + free(master_key); + free(encrypted); + free(encoded); + if (do_free) + { + free(password); + } + free(verify); + + fclose(users_file); + fclose(users_file_tmp); + + rename(tmpfilename, users_path); + + return 0; + +error: + + free(master_key); + free(encrypted); + free(encoded); + if (do_free) + { + free(password); + } + free(verify); + + if (users_file) + { + fclose(users_file); + } + + if (users_file_tmp) + { + fclose(users_file_tmp); + } + + if (strlen(tmpfilename) > 0) + { + remove(tmpfilename); + } + + return 1; +} + +static int +remove_user(char* users_path, char* username) +{ + FILE* users_file = NULL; + FILE* users_file_tmp = NULL; + char tmpfilename[MISC_LENGTH]; + char line[MISC_LENGTH]; + char line_copy[MISC_LENGTH]; + char* ptr = NULL; + char un[MAX_USERNAME_LENGTH]; + bool found = false; + + users_file = fopen(users_path, "r"); + if (!users_file) + { + warnx("File <%s> not found", users_path); + goto error; + } + + memset(&tmpfilename, 0, sizeof(tmpfilename)); + snprintf(tmpfilename, sizeof(tmpfilename), "%s.tmp", users_path); + users_file_tmp = fopen(tmpfilename, "w+"); + if (users_file_tmp == NULL) + { + warnx("Could not write to temporary user file <%s>", tmpfilename); + goto error; + } + + /* User */ + if (username == NULL) + { +username: + printf("User name: "); + + memset(&un, 0, sizeof(un)); + if (fgets(&un[0], sizeof(un), stdin) == NULL) + { + goto error; + } + un[strlen(un) - 1] = 0; + username = &un[0]; + } + + if (username == NULL || strlen(username) == 0) + { + goto username; + } + + /* Remove */ + while (fgets(line, sizeof(line), users_file)) + { + memset(&line_copy, 0, sizeof(line_copy)); + memcpy(&line_copy, &line, strlen(line)); + + ptr = strtok(line, ":"); + if (!strcmp(username, ptr)) + { + found = true; + } + else + { + fputs(line_copy, users_file_tmp); + } + } + + if (!found) + { + warnx("User '%s' not found", username); + goto error; + } + + fclose(users_file); + fclose(users_file_tmp); + + rename(tmpfilename, users_path); + + return 0; + +error: + + if (users_file) + { + fclose(users_file); + } + + if (users_file_tmp) + { + fclose(users_file_tmp); + } + + if (strlen(tmpfilename) > 0) + { + remove(tmpfilename); + } + + return 1; +} + +static int +list_users(char* users_path) +{ + FILE* users_file = NULL; + char line[MISC_LENGTH]; + char* ptr = NULL; + + users_file = fopen(users_path, "r"); + if (!users_file) + { + goto error; + } + + /* List */ + while (fgets(line, sizeof(line), users_file)) + { + ptr = strtok(line, ":"); + printf("%s\n", ptr); + } + + fclose(users_file); + + return 0; + +error: + + if (users_file) + { + fclose(users_file); + } + + return 1; +} diff --git a/src/cli.c b/src/cli.c new file mode 100644 index 00000000..35f2c5a8 --- /dev/null +++ b/src/cli.c @@ -0,0 +1,1912 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define HELP 99 + +#define COMMAND_CANCELSHUTDOWN "cancel-shutdown" +#define COMMAND_CLEAR "clear" +#define COMMAND_CLEAR_SERVER "clear-server" +#define COMMAND_DISABLEDB "disable-db" +#define COMMAND_ENABLEDB "enable-db" +#define COMMAND_FLUSH "flush" +#define COMMAND_GRACEFULLY "shutdown-gracefully" +#define COMMAND_PING "ping" +#define COMMAND_RELOAD "reload" +#define COMMAND_SHUTDOWN "shutdown" +#define COMMAND_STATUS "status" +#define COMMAND_STATUS_DETAILS "status-details" +#define COMMAND_SWITCH_TO "switch-to" +#define COMMAND_CONFIG_LS "conf-ls" +#define COMMAND_CONFIG_GET "conf-get" +#define COMMAND_CONFIG_SET "conf-set" + +#define OUTPUT_FORMAT_JSON "json" +#define OUTPUT_FORMAT_TEXT "text" + +#define UNSPECIFIED "Unspecified" + +static void display_helper(char* command); +static void help_cancel_shutdown(void); +/* static void help_config(void); */ +static void help_clear(void); +static void help_conf(void); +static void help_disabledb(void); +static void help_enabledb(void); +static void help_flush(void); +static void help_ping(void); +static void help_shutdown(void); +static void help_status_details(void); +static void help_switch_to(void); + +static int cancel_shutdown(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); +static int conf_get(SSL* ssl, int socket, char* config_key, uint8_t compression, uint8_t encryption, int32_t output_format); +static int conf_ls(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); +static int conf_set(SSL* ssl, int socket, char* config_key, char* config_value, uint8_t compression, uint8_t encryption, int32_t output_format); +static int details(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); +static int disabledb(SSL* ssl, int socket, char* database, uint8_t compression, uint8_t encryption, int32_t output_format); +static int enabledb(SSL* ssl, int socket, char* database, uint8_t compression, uint8_t encryption, int32_t output_format); +static int flush(SSL* ssl, int socket, int32_t mode, char* database, uint8_t compression, uint8_t encryption, int32_t output_format); +static int gracefully(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); +static int pgagroal_shutdown(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); +static int ping(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); +static int reload(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); +static int clear(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); +static int clear_server(SSL* ssl, int socket, char* server, uint8_t compression, uint8_t encryption, int32_t output_format); +static int status(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); +static int switch_to(SSL* ssl, int socket, char* server, uint8_t compression, uint8_t encryption, int32_t output_format); + +static int process_result(SSL* ssl, int socket, int32_t output_format); +static int process_ls_result(SSL* ssl, int socket, int32_t output_format); +static int process_get_result(SSL* ssl, int socket, char* config_key, int32_t output_format); +static int process_set_result(SSL* ssl, int socket, char* config_key, int32_t output_format); + +static int get_conf_path_result(struct json* j, uintptr_t* r); +static int get_config_key_result(char* config_key, struct json* j, uintptr_t* r, int32_t output_format); + +static char* translate_command(int32_t cmd_code); +static char* translate_output_format(int32_t out_code); +static char* translate_compression(int32_t compression_code); +static char* translate_encryption(int32_t encryption_code); +static void translate_json_object(struct json* j); + +const struct pgagroal_command command_table[] = { + { + .command = "flush", + .subcommand = "", + .accepted_argument_count = {0, 1}, + .action = MANAGEMENT_FLUSH, + .mode = FLUSH_GRACEFULLY, + .default_argument = "*", + .deprecated = false, + .log_message = " [%s]", + }, + { + .command = "ping", + .subcommand = "", + .accepted_argument_count = {0}, + .action = MANAGEMENT_PING, + .deprecated = false, + .log_message = "" + }, + { + .command = "enable", + .subcommand = "", + .accepted_argument_count = {0, 1}, + .action = MANAGEMENT_ENABLEDB, + .default_argument = "*", + .deprecated = false, + .log_message = " [%s]", + }, + { + .command = "disable", + .subcommand = "", + .accepted_argument_count = {0, 1}, + .action = MANAGEMENT_DISABLEDB, + .default_argument = "*", + .deprecated = false, + .log_message = " [%s]", + }, + { + .command = "shutdown", + .subcommand = "", + .accepted_argument_count = {0}, + .action = MANAGEMENT_GRACEFULLY, + .deprecated = false, + .log_message = "" + }, + { + .command = "status", + .subcommand = "", + .accepted_argument_count = {0}, + .action = MANAGEMENT_STATUS, + .deprecated = false, + .log_message = "" + }, + { + .command = "switch-to", + .subcommand = "", + .accepted_argument_count = {1}, + .action = MANAGEMENT_SWITCH_TO, + .deprecated = false, + .log_message = " [%s]" + }, + { + .command = "clear", + .subcommand = "", + .accepted_argument_count = {1}, + .action = MANAGEMENT_CLEAR_SERVER, + .deprecated = false, + .log_message = "", + }, + { + .command = "shutdown", + .subcommand = "gracefully", + .accepted_argument_count = {0}, + .action = MANAGEMENT_GRACEFULLY, + .deprecated = false, + .log_message = "" + }, + { + .command = "shutdown", + .subcommand = "immediate", + .accepted_argument_count = {0}, + .action = MANAGEMENT_SHUTDOWN, + .deprecated = false, + .log_message = "" + }, + { + .command = "shutdown", + .subcommand = "cancel", + .accepted_argument_count = {0}, + .action = MANAGEMENT_CANCEL_SHUTDOWN, + .deprecated = false, + .log_message = "" + }, + { + .command = "conf", + .subcommand = "reload", + .accepted_argument_count = {0}, + .action = MANAGEMENT_RELOAD, + .deprecated = false, + .log_message = "" + }, + { + .command = "conf", + .subcommand = "ls", + .accepted_argument_count = {0}, + .action = MANAGEMENT_CONFIG_LS, + .deprecated = false, + .log_message = "" + }, + { + .command = "conf", + .subcommand = "get", + .accepted_argument_count = {0, 1}, + .action = MANAGEMENT_CONFIG_GET, + .deprecated = false, + .log_message = " [%s]" + }, + { + .command = "conf", + .subcommand = "set", + .accepted_argument_count = {2}, + .action = MANAGEMENT_CONFIG_SET, + .deprecated = false, + .log_message = " [%s] = [%s]" + }, + { + .command = "clear", + .subcommand = "server", + .accepted_argument_count = {0, 1}, + .action = MANAGEMENT_CLEAR_SERVER, + .default_argument = "server", + .deprecated = false, + .log_message = " [%s]", + }, + { + .command = "flush", + .subcommand = "idle", + .accepted_argument_count = {0, 1}, + .action = MANAGEMENT_FLUSH, + .mode = FLUSH_IDLE, + .default_argument = "*", + .deprecated = false, + .log_message = " [%s]", + }, + { + .command = "flush", + .subcommand = "gracefully", + .accepted_argument_count = {0, 1}, + .action = MANAGEMENT_FLUSH, + .mode = FLUSH_GRACEFULLY, + .default_argument = "*", + .deprecated = false, + .log_message = " [%s]", + }, + { + .command = "flush", + .subcommand = "all", + .accepted_argument_count = {0, 1}, + .action = MANAGEMENT_FLUSH, + .mode = FLUSH_ALL, + .default_argument = "*", + .deprecated = false, + .log_message = " [%s]", + }, + { + .command = "clear", + .subcommand = "prometheus", + .accepted_argument_count = {0}, + .action = MANAGEMENT_CLEAR, + .deprecated = false, + .log_message = "" + }, + { + .command = "status", + .subcommand = "details", + .accepted_argument_count = {0}, + .action = MANAGEMENT_DETAILS, + .deprecated = false, + .log_message = "" + }, +}; + +static void +version(void) +{ + printf("pgagroal-cli %s\n", PGAGROAL_VERSION); + exit(1); +} + +static void +usage(void) +{ + printf("pgagroal-cli %s\n", PGAGROAL_VERSION); + printf(" Command line utility for pgagroal\n"); + printf("\n"); + + printf("Usage:\n"); + printf(" pgagroal-cli [ OPTIONS ] [ COMMAND ] \n"); + printf("\n"); + printf("Options:\n"); + printf(" -c, --config CONFIG_FILE Set the path to the pgagroal.conf file\n"); + printf(" Default: %s\n", PGAGROAL_DEFAULT_CONF_FILE); + printf(" -h, --host HOST Set the host name\n"); + printf(" -p, --port PORT Set the port number\n"); + printf(" -U, --user USERNAME Set the user name\n"); + printf(" -P, --password PASSWORD Set the password\n"); + printf(" -L, --logfile FILE Set the log file\n"); + printf(" -F, --format text|json|raw Set the output format\n"); + printf(" -C, --compress none|gz|zstd|lz4|bz2 Compress the wire protocol\n"); + printf(" -E, --encrypt none|aes|aes256|aes192|aes128 Encrypt the wire protocol\n"); + printf(" -v, --verbose Output text string of result\n"); + printf(" -V, --version Display version information\n"); + printf(" -?, --help Display help\n"); + printf("\n"); + printf("Commands:\n"); + printf(" flush [mode] [database] Flush connections according to [mode].\n"); + printf(" Allowed modes are:\n"); + printf(" - 'gracefully' (default) to flush all connections gracefully\n"); + printf(" - 'idle' to flush only idle connections\n"); + printf(" - 'all' to flush all connections. USE WITH CAUTION!\n"); + printf(" If no [database] name is specified, applies to all databases.\n"); + printf(" ping Verifies if pgagroal is up and running\n"); + printf(" enable [database] Enables the specified databases (or all databases)\n"); + printf(" disable [database] Disables the specified databases (or all databases)\n"); + printf(" shutdown [mode] Stops pgagroal pooler. The [mode] can be:\n"); + printf(" - 'gracefully' (default) waits for active connections to quit\n"); + printf(" - 'immediate' forces connections to close and terminate\n"); + printf(" - 'cancel' avoid a previously issued 'shutdown gracefully'\n"); + printf(" status [details] Status of pgagroal, with optional details\n"); + printf(" switch-to Switches to the specified primary server\n"); + printf(" conf Manages the configuration (e.g., reloads the configuration\n"); + printf(" The subcommand can be:\n"); + printf(" - 'reload' to issue a configuration reload;\n"); + printf(" - 'ls' lists the configuration files used.\n"); + printf(" - 'get' to obtain information about a runtime configuration value;\n"); + printf(" conf get \n"); + printf(" - 'set' to modify a configuration value;\n"); + printf(" conf set ;\n"); + printf(" clear Resets either the Prometheus statistics or the specified server.\n"); + printf(" can be\n"); + printf(" - 'server' (default) followed by a server name\n"); + printf(" - a server name on its own\n"); + printf(" - 'prometheus' to reset the Prometheus metrics\n"); + printf("\n"); + printf("pgagroal: <%s>\n", PGAGROAL_HOMEPAGE); + printf("Report bugs: <%s>\n", PGAGROAL_ISSUES); +} + +int +main(int argc, char** argv) +{ + int socket = -1; + SSL* s_ssl = NULL; + int ret; + int exit_code = 0; + char* configuration_path = NULL; + char* host = NULL; + char* port = NULL; + char* username = NULL; + char* password = NULL; + bool verbose = false; + char* logfile = NULL; + int c; + int option_index = 0; + size_t size; + char un[MAX_USERNAME_LENGTH]; + struct main_configuration* config = NULL; + bool remote_connection = false; + long l_port; + int32_t output_format = MANAGEMENT_OUTPUT_FORMAT_TEXT; + int32_t compression = MANAGEMENT_COMPRESSION_NONE; + int32_t encryption = MANAGEMENT_ENCRYPTION_NONE; + size_t command_count = sizeof(command_table) / sizeof(struct pgagroal_command); + struct pgagroal_parsed_command parsed = {.cmd = NULL, .args = {0}}; + + while (1) + { + static struct option long_options[] = + { + {"config", required_argument, 0, 'c'}, + {"host", required_argument, 0, 'h'}, + {"port", required_argument, 0, 'p'}, + {"user", required_argument, 0, 'U'}, + {"password", required_argument, 0, 'P'}, + {"logfile", required_argument, 0, 'L'}, + {"format", required_argument, 0, 'F' }, + {"compress", required_argument, 0, 'C'}, + {"encrypt", required_argument, 0, 'E'}, + {"verbose", no_argument, 0, 'v'}, + {"version", no_argument, 0, 'V'}, + {"help", no_argument, 0, '?'} + }; + + c = getopt_long(argc, argv, "vV?c:h:p:U:P:L:F:C:E:", + long_options, &option_index); + + if (c == -1) + { + break; + } + + switch (c) + { + case 'c': + configuration_path = optarg; + break; + case 'h': + host = optarg; + break; + case 'p': + port = optarg; + break; + case 'U': + username = optarg; + break; + case 'P': + password = strdup(optarg); + if (password == NULL) + { + errx(1, "Error allocating memory for password"); + } + break; + case 'L': + logfile = optarg; + break; + case 'F': + if (!strncmp(optarg, "json", MISC_LENGTH)) + { + output_format = MANAGEMENT_OUTPUT_FORMAT_JSON; + } + else if (!strncmp(optarg, "raw", MISC_LENGTH)) + { + output_format = MANAGEMENT_OUTPUT_FORMAT_RAW; + } + else if (!strncmp(optarg, "text", MISC_LENGTH)) + { + output_format = MANAGEMENT_OUTPUT_FORMAT_TEXT; + } + else + { + warnx("pgagroal-cli: Format type is not correct"); + exit(1); + } + break; + case 'C': + if (!strncmp(optarg, "gz", MISC_LENGTH)) + { + compression = MANAGEMENT_COMPRESSION_GZIP; + } + else if (!strncmp(optarg, "zstd", MISC_LENGTH)) + { + compression = MANAGEMENT_COMPRESSION_ZSTD; + } + else if (!strncmp(optarg, "lz4", MISC_LENGTH)) + { + compression = MANAGEMENT_COMPRESSION_LZ4; + } + else if (!strncmp(optarg, "bz2", MISC_LENGTH)) + { + compression = MANAGEMENT_COMPRESSION_BZIP2; + } + else if (!strncmp(optarg, "none", MISC_LENGTH)) + { + break; + } + else + { + warnx("pgagroal-cli: Compress method is not correct"); + exit(1); + } + break; + case 'E': + if (!strncmp(optarg, "aes", MISC_LENGTH)) + { + encryption = MANAGEMENT_ENCRYPTION_AES256; + } + else if (!strncmp(optarg, "aes256", MISC_LENGTH)) + { + encryption = MANAGEMENT_ENCRYPTION_AES256; + } + else if (!strncmp(optarg, "aes192", MISC_LENGTH)) + { + encryption = MANAGEMENT_ENCRYPTION_AES192; + } + else if (!strncmp(optarg, "aes128", MISC_LENGTH)) + { + encryption = MANAGEMENT_ENCRYPTION_AES128; + } + else if (!strncmp(optarg, "none", MISC_LENGTH)) + { + break; + } + else + { + warnx("pgagroal-cli: Encrypt method is not correct"); + exit(1); + } + break; + case 'v': + verbose = true; + break; + case 'V': + version(); + break; + case '?': + usage(); + exit(1); + break; + default: + break; + } + } + + if (getuid() == 0) + { + errx(1, "Using the root account is not allowed"); + } + + // if the user has specified the host and port + // options, she wants a remote connection + // but both remote connection parameters have to be set + if (host != NULL || port != NULL) + { + remote_connection = host != NULL && port != NULL; + if (!remote_connection) + { + printf("pgagroal-cli: you need both -h and -p options to perform a remote connection\n"); + exit(1); + } + } + + // if the user has specified either a username or a password + // there must be all the other pieces for a remote connection + if ((username != NULL || password != NULL) && !remote_connection) + { + errx(1, "you need also -h and -p options to perform a remote connection"); + } + + // and she cannot use "local" and "remote" connections at the same time + if (configuration_path != NULL && remote_connection) + { + errx(1, "Use either -c or -h/-p to define endpoint"); + } + + if (argc <= 1) + { + usage(); + exit(1); + } + + size = sizeof(struct main_configuration); + if (pgagroal_create_shared_memory(size, HUGEPAGE_OFF, &shmem)) + { + errx(1, "Error creating shared memory"); + } + pgagroal_init_configuration(shmem); + + if (configuration_path != NULL) + { + ret = pgagroal_read_configuration(shmem, configuration_path, false); + if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND) + { + errx(1, "Configuration not found: <%s>", configuration_path); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG) + { + errx(1, "Too many sections in the configuration file <%s>", configuration_path); + } + + if (logfile) + { + config = (struct main_configuration*)shmem; + + config->common.log_type = PGAGROAL_LOGGING_TYPE_FILE; + memset(&config->common.log_path[0], 0, MISC_LENGTH); + memcpy(&config->common.log_path[0], logfile, MIN(MISC_LENGTH - 1, strlen(logfile))); + } + + if (pgagroal_start_logging()) + { + errx(1, "Cannot start the logging subsystem"); + } + + config = (struct main_configuration*)shmem; + } + else + { + ret = pgagroal_read_configuration(shmem, PGAGROAL_DEFAULT_CONF_FILE, false); + if (ret != PGAGROAL_CONFIGURATION_STATUS_OK) + { + if (!remote_connection) + { + errx(1, "Host (-h) and port (-p) must be specified to connect to the remote host"); + } + } + else + { + configuration_path = PGAGROAL_DEFAULT_CONF_FILE; + + if (logfile) + { + config = (struct main_configuration*)shmem; + + config->common.log_type = PGAGROAL_LOGGING_TYPE_FILE; + memset(&config->common.log_path[0], 0, MISC_LENGTH); + memcpy(&config->common.log_path[0], logfile, MIN(MISC_LENGTH - 1, strlen(logfile))); + } + + if (pgagroal_start_logging()) + { + errx(1, "Cannot start the logging subsystem"); + } + + } + } + + if (!parse_command(argc, argv, optind, &parsed, command_table, command_count)) + { + if (argc > optind) + { + char* command = argv[optind]; + display_helper(command); + } + else + { + usage(); + } + exit_code = 1; + goto done; + } + + config = (struct main_configuration*)shmem; + + if (!remote_connection) + { + /* Local connection */ + if (pgagroal_connect_unix_socket(config->unix_socket_dir, MAIN_UDS, &socket)) + { + exit_code = 1; + goto done; + } + } + else + { + /* Remote connection */ + if (pgagroal_connect(host, atoi(port), &socket, config->keep_alive, config->non_blocking, config->nodelay)) + { + /* Remote connection */ + l_port = strtol(port, NULL, 10); + if ((errno == ERANGE && (l_port == LONG_MAX || l_port == LONG_MIN)) || (errno != 0 && l_port == 0)) + { + warnx("Specified port %s out of range", port); + goto done; + } + + // cannot connect to port less than 1024 because pgagroal + // cannot be run as root! + if (l_port <= 1024) + { + warnx("Not allowed port %ld", l_port); + goto done; + } + + if (pgagroal_connect(host, (int)l_port, &socket, config->keep_alive, config->non_blocking, config->nodelay)) + { + warnx("No route to host: %s:%ld\n", host, l_port); + goto done; + } + + } + + /* User name */ + if (username == NULL) + { +username: + printf("User name: "); + + memset(&un, 0, sizeof(un)); + if (fgets(&un[0], sizeof(un), stdin) == NULL) + { + exit_code = 1; + goto done; + } + un[strlen(un) - 1] = 0; + username = &un[0]; + } + + if (username == NULL || strlen(username) == 0) + { + goto username; + } + + /* Password */ + if (password == NULL) + { + printf("Password : "); + password = pgagroal_get_password(); + printf("\n"); + } + + for (int i = 0; i < strlen(password); i++) + { + if ((unsigned char)(*(password + i)) & 0x80) + { + warnx("pgagroal-cli: Bad credentials for %s\n", username); + goto done; + } + } + + /* Authenticate */ + if (pgagroal_remote_management_scram_sha256(username, password, socket, &s_ssl) != AUTH_SUCCESS) + { + printf("pgagroal-cli: Bad credentials for %s\n", username); + goto done; + } + } + + if (parsed.cmd->action == MANAGEMENT_FLUSH) + { + exit_code = flush(s_ssl, socket, parsed.cmd->mode, parsed.args[0], compression, encryption, output_format); + } + else if (parsed.cmd->action == MANAGEMENT_ENABLEDB) + { + exit_code = enabledb(s_ssl, socket, parsed.args[0], compression, encryption, output_format); + } + else if (parsed.cmd->action == MANAGEMENT_DISABLEDB) + { + exit_code = disabledb(s_ssl, socket, parsed.args[0], compression, encryption, output_format); + } + else if (parsed.cmd->action == MANAGEMENT_GRACEFULLY) + { + exit_code = gracefully(s_ssl, socket, compression, encryption, output_format); + } + else if (parsed.cmd->action == MANAGEMENT_SHUTDOWN) + { + exit_code = pgagroal_shutdown(s_ssl, socket, compression, encryption, output_format); + } + else if (parsed.cmd->action == MANAGEMENT_CANCEL_SHUTDOWN) + { + exit_code = cancel_shutdown(s_ssl, socket, compression, encryption, output_format); + } + else if (parsed.cmd->action == MANAGEMENT_STATUS) + { + exit_code = status(s_ssl, socket, compression, encryption, output_format); + } + else if (parsed.cmd->action == MANAGEMENT_DETAILS) + { + exit_code = details(s_ssl, socket, compression, encryption, output_format); + } + else if (parsed.cmd->action == MANAGEMENT_PING) + { + exit_code = ping(s_ssl, socket, compression, encryption, output_format); + } + else if (parsed.cmd->action == MANAGEMENT_CLEAR) + { + exit_code = clear(s_ssl, socket, compression, encryption, output_format); + } + else if (parsed.cmd->action == MANAGEMENT_CLEAR_SERVER) + { + exit_code = clear_server(s_ssl, socket, parsed.args[0], compression, encryption, output_format); + } + else if (parsed.cmd->action == MANAGEMENT_SWITCH_TO) + { + exit_code = switch_to(s_ssl, socket, parsed.args[0], compression, encryption, output_format); + } + else if (parsed.cmd->action == MANAGEMENT_RELOAD) + { + if (configuration_path == NULL) + { + warnx("Configuration path has to specified to use "); + parsed.cmd = NULL; + goto done; + } + else + { + exit_code = reload(s_ssl, socket, compression, encryption, output_format); + } + } + else if (parsed.cmd->action == MANAGEMENT_CONFIG_LS) + { + exit_code = conf_ls(s_ssl, socket, compression, encryption, output_format); + } + else if (parsed.cmd->action == MANAGEMENT_CONFIG_GET) + { + if (parsed.args[0]) + { + exit_code = conf_get(s_ssl, socket, parsed.args[0], compression, encryption, output_format); + } + else + { + exit_code = conf_get(s_ssl, socket, NULL, compression, encryption, output_format); + } + } + else if (parsed.cmd->action == MANAGEMENT_CONFIG_SET) + { + exit_code = conf_set(s_ssl, socket, parsed.args[0], parsed.args[1], compression, encryption, output_format); + } + +done: + + if (s_ssl != NULL) + { + int res; + SSL_CTX* ctx = SSL_get_SSL_CTX(s_ssl); + res = SSL_shutdown(s_ssl); + if (res == 0) + { + SSL_shutdown(s_ssl); + } + SSL_free(s_ssl); + SSL_CTX_free(ctx); + } + + pgagroal_disconnect(socket); + pgagroal_stop_logging(); + pgagroal_destroy_shared_memory(shmem, size); + + free(password); + + if (verbose) + { + warnx("%s (%d)", exit_code == 0 ? "Success" : "Error", exit_code); + } + + return exit_code; +} + +static void +help_cancel_shutdown(void) +{ + printf("Cancel shutdown of pgagroal\n"); + printf(" pgagroal-cli cancel-shutdown\n"); +} + +static void +help_shutdown(void) +{ + printf("Shutdown pgagroal\n"); + printf(" pgagroal-cli shutdown\n"); +} + +static void +help_ping(void) +{ + printf("Check if pgagroal is alive\n"); + printf(" pgagroal-cli ping\n"); +} + +static void +help_status_details(void) +{ + printf("Status of pgagroal\n"); + printf(" pgagroal-cli status [details]\n"); +} + +static void +help_disabledb(void) +{ + printf("Disable a database\n"); + printf(" pgagroal-cli disabledb |*\n"); +} + +static void +help_enabledb(void) +{ + printf("Enable a database\n"); + printf(" pgagroal-cli enabledb |*\n"); +} + +static void +help_conf(void) +{ + printf("Manage the configuration\n"); + printf(" pgagroal-cli conf [reload]\n"); + printf(" pgagroal-cli conf [ls]\n"); + printf(" pgagroal-cli conf [get] \n"); + printf(" pgagroal-cli conf [set] \n"); +} + +static void +help_clear(void) +{ + printf("Reset data\n"); + printf(" pgagroal-cli clear [prometheus]\n"); +} + +static void +help_flush(void) +{ + printf("Flush connections\n"); + printf(" pgagroal-cli flush [gracefully|idle|all] [*|]\n"); +} + +static void +help_switch_to(void) +{ + printf("Switch to another primary server\n"); + printf(" pgagroal-cli switch-to \n"); +} + +static void +display_helper(char* command) +{ + if (!strcmp(command, COMMAND_CANCELSHUTDOWN)) + { + help_cancel_shutdown(); + } + else if (!strcmp(command, COMMAND_CONFIG_GET) || + !strcmp(command, COMMAND_CONFIG_LS) || + !strcmp(command, COMMAND_CONFIG_SET) || + !strcmp(command, COMMAND_RELOAD)) + { + help_conf(); + } + else if (!strcmp(command, COMMAND_DISABLEDB)) + { + help_disabledb(); + } + else if (!strcmp(command, COMMAND_ENABLEDB)) + { + help_enabledb(); + } + else if (!strcmp(command, COMMAND_FLUSH)) + { + help_flush(); + } + else if (!strcmp(command, COMMAND_PING)) + { + help_ping(); + } + else if (!strcmp(command, COMMAND_CLEAR) || + !strcmp(command, COMMAND_CLEAR_SERVER)) + { + help_clear(); + } + else if (!strcmp(command, COMMAND_SHUTDOWN)) + { + help_shutdown(); + } + else if (!strcmp(command, COMMAND_STATUS)) + { + help_status_details(); + } + else if (!strcmp(command, COMMAND_SWITCH_TO)) + { + help_switch_to(); + } + else + { + usage(); + } +} + +static int +flush(SSL* ssl, int socket, int32_t mode, char* database, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + if (pgagroal_management_request_flush(ssl, socket, mode, database, compression, encryption, output_format)) + { + goto error; + } + + if (process_result(ssl, socket, output_format)) + { + goto error; + } + + return 0; + +error: + + return 1; +} + +static int +enabledb(SSL* ssl, int socket, char* database, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + if (pgagroal_management_request_enabledb(ssl, socket, database, compression, encryption, output_format)) + { + goto error; + } + + if (process_result(ssl, socket, output_format)) + { + goto error; + } + + return 0; + +error: + + return 1; +} + +static int +disabledb(SSL* ssl, int socket, char* database, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + if (pgagroal_management_request_disabledb(ssl, socket, database, compression, encryption, output_format)) + { + goto error; + } + + if (process_result(ssl, socket, output_format)) + { + goto error; + } + + return 0; + +error: + + return 1; +} + +static int +gracefully(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + if (pgagroal_management_request_gracefully(ssl, socket, compression, encryption, output_format)) + { + goto error; + } + + if (process_result(ssl, socket, output_format)) + { + goto error; + } + + return 0; + +error: + + return 1; +} + +static int +pgagroal_shutdown(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + if (pgagroal_management_request_shutdown(ssl, socket, compression, encryption, output_format)) + { + goto error; + } + + if (process_result(ssl, socket, output_format)) + { + goto error; + } + + return 0; + +error: + + return 1; +} + +static int +cancel_shutdown(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + if (pgagroal_management_request_cancel_shutdown(ssl, socket, compression, encryption, output_format)) + { + goto error; + } + + if (process_result(ssl, socket, output_format)) + { + goto error; + } + + return 0; + +error: + + return 1; +} + +static int +status(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + if (pgagroal_management_request_status(ssl, socket, compression, encryption, output_format)) + { + goto error; + } + + if (process_result(ssl, socket, output_format)) + { + goto error; + } + + return 0; + +error: + + return 1; +} + +static int +details(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + if (pgagroal_management_request_details(ssl, socket, compression, encryption, output_format)) + { + goto error; + } + + if (process_result(ssl, socket, output_format)) + { + goto error; + } + + return 0; + +error: + + return 1; +} + +static int +ping(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + if (pgagroal_management_request_ping(ssl, socket, compression, encryption, output_format)) + { + goto error; + } + + if (process_result(ssl, socket, output_format)) + { + goto error; + } + + return 0; + +error: + + return 1; +} + +static int +clear(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + if (pgagroal_management_request_clear(ssl, socket, compression, encryption, output_format)) + { + goto error; + } + + if (process_result(ssl, socket, output_format)) + { + goto error; + } + + return 0; + +error: + + return 1; +} + +static int +clear_server(SSL* ssl, int socket, char* server, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + if (pgagroal_management_request_clear_server(ssl, socket, server, compression, encryption, output_format)) + { + goto error; + } + + if (process_result(ssl, socket, output_format)) + { + goto error; + } + + return 0; + +error: + + return 1; +} + +static int +switch_to(SSL* ssl, int socket, char* server, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + if (pgagroal_management_request_switch_to(ssl, socket, server, compression, encryption, output_format)) + { + goto error; + } + + if (process_result(ssl, socket, output_format)) + { + goto error; + } + + return 0; + +error: + + return 1; +} + +static int +reload(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + if (pgagroal_management_request_reload(ssl, socket, compression, encryption, output_format)) + { + goto error; + } + + if (process_result(ssl, socket, output_format)) + { + goto error; + } + + return 0; + +error: + + return 1; +} + +static int +conf_ls(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + if (pgagroal_management_request_conf_ls(ssl, socket, compression, encryption, output_format)) + { + goto error; + } + + if (process_ls_result(ssl, socket, output_format)) + { + goto error; + } + + return 0; + +error: + + return 1; +} + +static int +conf_get(SSL* ssl, int socket, char* config_key, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + if (pgagroal_management_request_conf_get(ssl, socket, compression, encryption, output_format)) + { + goto error; + } + + if (process_get_result(ssl, socket, config_key, output_format)) + { + goto error; + } + + return 0; + +error: + + return 1; +} + +static int +conf_set(SSL* ssl, int socket, char* config_key, char* config_value, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + if (pgagroal_management_request_conf_set(ssl, socket, config_key, config_value, compression, encryption, output_format)) + { + goto error; + } + + if (process_set_result(ssl, socket, config_key, output_format)) + { + goto error; + } + + return 0; + +error: + + return 1; +} + +static int +process_result(SSL* ssl, int socket, int32_t output_format) +{ + struct json* read = NULL; + + if (pgagroal_management_read_json(ssl, socket, NULL, NULL, &read)) + { + goto error; + } + + if (MANAGEMENT_OUTPUT_FORMAT_RAW != output_format) + { + translate_json_object(read); + } + + if (MANAGEMENT_OUTPUT_FORMAT_TEXT == output_format) + { + pgagroal_json_print(read, FORMAT_TEXT); + } + else + { + pgagroal_json_print(read, FORMAT_JSON); + } + + pgagroal_json_destroy(read); + + return 0; + +error: + + pgagroal_json_destroy(read); + + return 1; +} + +static int +process_ls_result(SSL* ssl, int socket, int32_t output_format) +{ + struct json* read = NULL; + struct json* json_res = NULL; + uintptr_t res; + + if (pgagroal_management_read_json(ssl, socket, NULL, NULL, &read)) + { + goto error; + } + + if (get_conf_path_result(read, &res)) + { + goto error; + } + + json_res = (struct json*)res; + + if (MANAGEMENT_OUTPUT_FORMAT_JSON == output_format) + { + pgagroal_json_print(json_res, FORMAT_JSON_COMPACT); + } + else + { + struct json_iterator* iter = NULL; + pgagroal_json_iterator_create(json_res, &iter); + while (pgagroal_json_iterator_next(iter)) + { + char* value = pgagroal_value_to_string(iter->value, FORMAT_TEXT, NULL, 0); + printf("%s\n", value); + free(value); + } + pgagroal_json_iterator_destroy(iter); + } + + pgagroal_json_destroy(read); + pgagroal_json_destroy(json_res); + return 0; + +error: + + pgagroal_json_destroy(read); + pgagroal_json_destroy(json_res); + return 1; +} + +static int +process_get_result(SSL* ssl, int socket, char* config_key, int32_t output_format) +{ + struct json* read = NULL; + bool is_char = false; + char* char_res = NULL; + struct json* json_res = NULL; + uintptr_t res; + + if (pgagroal_management_read_json(ssl, socket, NULL, NULL, &read)) + { + goto error; + } + + if (get_config_key_result(config_key, read, &res, output_format)) + { + if (MANAGEMENT_OUTPUT_FORMAT_JSON == output_format) + { + json_res = (struct json*)res; + pgagroal_json_print(json_res, FORMAT_JSON_COMPACT); + } + else + { + is_char = true; + char_res = (char*)res; + printf("%s\n", char_res); + } + goto error; + } + + if (!config_key) // error response | complete configuration + { + json_res = (struct json*)res; + + if (MANAGEMENT_OUTPUT_FORMAT_TEXT == output_format) + { + pgagroal_json_print(json_res, FORMAT_TEXT); + } + else + { + pgagroal_json_print(json_res, FORMAT_JSON); + } + } + else + { + if (MANAGEMENT_OUTPUT_FORMAT_JSON == output_format) + { + json_res = (struct json*)res; + pgagroal_json_print(json_res, FORMAT_JSON_COMPACT); + } + else + { + is_char = true; + char_res = (char*)res; + printf("%s\n", char_res); + } + } + + pgagroal_json_destroy(read); + if (config_key) + { + if (is_char) + { + free(char_res); + } + else + { + pgagroal_json_destroy(json_res); + } + } + + return 0; + +error: + + pgagroal_json_destroy(read); + if (config_key) + { + if (is_char) + { + free(char_res); + } + else + { + pgagroal_json_destroy(json_res); + } + } + + return 1; +} + +static int +process_set_result(SSL* ssl, int socket, char* config_key, int32_t output_format) +{ + struct json* read = NULL; + bool is_char = false; + char* char_res = NULL; + int status = 0; + struct json* json_res = NULL; + uintptr_t res; + + if (pgagroal_management_read_json(ssl, socket, NULL, NULL, &read)) + { + goto error; + } + + status = get_config_key_result(config_key, read, &res, output_format); + if (MANAGEMENT_OUTPUT_FORMAT_JSON == output_format) + { + json_res = (struct json*)res; + pgagroal_json_print(json_res, FORMAT_JSON_COMPACT); + } + else + { + is_char = true; + char_res = (char*)res; + printf("%s\n", char_res); + } + + if (status == 1) + { + goto error; + } + + pgagroal_json_destroy(read); + if (config_key) + { + if (is_char) + { + free(char_res); + } + else + { + pgagroal_json_destroy(json_res); + } + } + + return 0; + +error: + + pgagroal_json_destroy(read); + if (config_key) + { + if (is_char) + { + free(char_res); + } + else + { + pgagroal_json_destroy(json_res); + } + } + + return 1; +} + +static int +get_config_key_result(char* config_key, struct json* j, uintptr_t* r, int32_t output_format) +{ + char server[MISC_LENGTH]; + char key[MISC_LENGTH]; + + struct json* configuration_js = NULL; + struct json* filtered_response = NULL; + struct json* response = NULL; + struct json* outcome = NULL; + struct json_iterator* iter; + char* config_value = NULL; + int begin = -1, end = -1; + + if (!config_key) + { + *r = (uintptr_t)j; + return 0; + } + + if (pgagroal_json_create(&filtered_response)) + { + goto error; + } + + memset(server, 0, MISC_LENGTH); + memset(key, 0, MISC_LENGTH); + + for (int i = 0; i < strlen(config_key); i++) + { + if (config_key[i] == '.') + { + if (!strlen(server)) + { + memcpy(server, &config_key[begin], end - begin + 1); + server[end - begin + 1] = '\0'; + begin = end = -1; + continue; + } + } + + if (begin < 0) + { + begin = i; + } + + end = i; + + } + + // if the key has not been found, since there is no ending dot, + // try to extract it from the string + if (!strlen(key)) + { + memcpy(key, &config_key[begin], end - begin + 1); + key[end - begin + 1] = '\0'; + } + + response = (struct json*)pgagroal_json_get(j, MANAGEMENT_CATEGORY_RESPONSE); + outcome = (struct json*)pgagroal_json_get(j, MANAGEMENT_CATEGORY_OUTCOME); + if (!response || !outcome) + { + goto error; + } + + // Check if error response + if (pgagroal_json_contains_key(outcome, MANAGEMENT_ARGUMENT_ERROR)) + { + goto error; + } + + if (strlen(server) > 0) + { + configuration_js = (struct json*)pgagroal_json_get(response, server); + if (!configuration_js) + { + goto error; + } + } + else + { + configuration_js = response; + } + + pgagroal_json_iterator_create(configuration_js, &iter); + while (pgagroal_json_iterator_next(iter)) + { + if (!strcmp(key, iter->key)) + { + config_value = pgagroal_value_to_string(iter->value, FORMAT_TEXT, NULL, 0); + if (iter->value->type == ValueJSON) + { + struct json* server_data = NULL; + pgagroal_json_clone((struct json*)iter->value->data, &server_data); + pgagroal_json_put(filtered_response, key, (uintptr_t)server_data, iter->value->type); + } + else + { + pgagroal_json_put(filtered_response, key, (uintptr_t)iter->value->data, iter->value->type); + } + } + } + pgagroal_json_iterator_destroy(iter); + + if (!config_value) // if key doesn't match with any field in configuration + { + goto error; + } + + if (output_format == MANAGEMENT_OUTPUT_FORMAT_JSON || !config_key) + { + *r = (uintptr_t)filtered_response; + free(config_value); + } + else + { + *r = (uintptr_t)config_value; + pgagroal_json_destroy(filtered_response); + } + + return 0; + +error: + + if (output_format == MANAGEMENT_OUTPUT_FORMAT_JSON) + { + pgagroal_json_put(filtered_response, "Outcome", (uintptr_t)false, ValueBool); + *r = (uintptr_t)filtered_response; + free(config_value); + } + else + { + config_value = (char*)malloc(6); + memcpy(config_value, "Error\0", 6); + *r = (uintptr_t)config_value; + pgagroal_json_destroy(filtered_response); + } + + return 1; +} + +static int +get_conf_path_result(struct json* j, uintptr_t* r) +{ + struct json* conf_path_response = NULL; + struct json* response = NULL; + + response = (struct json*)pgagroal_json_get(j, MANAGEMENT_CATEGORY_RESPONSE); + + if (!response) + { + goto error; + } + + if (pgagroal_json_create(&conf_path_response)) + { + goto error; + } + + if (pgagroal_json_contains_key(response, CONFIGURATION_ARGUMENT_ADMIN_CONF_PATH)) + { + pgagroal_json_put(conf_path_response, CONFIGURATION_ARGUMENT_ADMIN_CONF_PATH, (uintptr_t)pgagroal_json_get(response, CONFIGURATION_ARGUMENT_ADMIN_CONF_PATH), ValueString); + } + if (pgagroal_json_contains_key(response, CONFIGURATION_ARGUMENT_MAIN_CONF_PATH)) + { + pgagroal_json_put(conf_path_response, CONFIGURATION_ARGUMENT_MAIN_CONF_PATH, (uintptr_t)pgagroal_json_get(response, CONFIGURATION_ARGUMENT_MAIN_CONF_PATH), ValueString); + } + if (pgagroal_json_contains_key(response, CONFIGURATION_ARGUMENT_USER_CONF_PATH)) + { + pgagroal_json_put(conf_path_response, CONFIGURATION_ARGUMENT_USER_CONF_PATH, (uintptr_t)pgagroal_json_get(response, CONFIGURATION_ARGUMENT_USER_CONF_PATH), ValueString); + } + if (pgagroal_json_contains_key(response, CONFIGURATION_ARGUMENT_HBA_CONF_PATH)) + { + pgagroal_json_put(conf_path_response, CONFIGURATION_ARGUMENT_HBA_CONF_PATH, (uintptr_t)pgagroal_json_get(response, CONFIGURATION_ARGUMENT_HBA_CONF_PATH), ValueString); + } + if (pgagroal_json_contains_key(response, CONFIGURATION_ARGUMENT_FRONTEND_USERS_CONF_PATH)) + { + pgagroal_json_put(conf_path_response, CONFIGURATION_ARGUMENT_FRONTEND_USERS_CONF_PATH, (uintptr_t)pgagroal_json_get(response, CONFIGURATION_ARGUMENT_FRONTEND_USERS_CONF_PATH), ValueString); + } + if (pgagroal_json_contains_key(response, CONFIGURATION_ARGUMENT_LIMIT_CONF_PATH)) + { + pgagroal_json_put(conf_path_response, CONFIGURATION_ARGUMENT_LIMIT_CONF_PATH, (uintptr_t)pgagroal_json_get(response, CONFIGURATION_ARGUMENT_LIMIT_CONF_PATH), ValueString); + } + if (pgagroal_json_contains_key(response, CONFIGURATION_ARGUMENT_SUPERUSER_CONF_PATH)) + { + pgagroal_json_put(conf_path_response, CONFIGURATION_ARGUMENT_SUPERUSER_CONF_PATH, (uintptr_t)pgagroal_json_get(response, CONFIGURATION_ARGUMENT_SUPERUSER_CONF_PATH), ValueString); + } + + *r = (uintptr_t)conf_path_response; + + return 0; +error: + + return 1; + +} + +static char* +translate_command(int32_t cmd_code) +{ + char* command_output = NULL; + switch (cmd_code) + { + case MANAGEMENT_CANCEL_SHUTDOWN: + command_output = pgagroal_append(command_output, COMMAND_CANCELSHUTDOWN); + break; + case MANAGEMENT_DETAILS: + command_output = pgagroal_append(command_output, COMMAND_STATUS_DETAILS); + break; + case MANAGEMENT_DISABLEDB: + command_output = pgagroal_append(command_output, COMMAND_DISABLEDB); + break; + case MANAGEMENT_ENABLEDB: + command_output = pgagroal_append(command_output, COMMAND_ENABLEDB); + break; + case MANAGEMENT_FLUSH: + command_output = pgagroal_append(command_output, COMMAND_FLUSH); + break; + case MANAGEMENT_GRACEFULLY: + command_output = pgagroal_append(command_output, COMMAND_GRACEFULLY); + break; + case MANAGEMENT_PING: + command_output = pgagroal_append(command_output, COMMAND_PING); + break; + case MANAGEMENT_RELOAD: + command_output = pgagroal_append(command_output, COMMAND_RELOAD); + break; + case MANAGEMENT_CLEAR: + command_output = pgagroal_append(command_output, COMMAND_CLEAR); + break; + case MANAGEMENT_CLEAR_SERVER: + command_output = pgagroal_append(command_output, COMMAND_CLEAR_SERVER); + break; + case MANAGEMENT_SHUTDOWN: + command_output = pgagroal_append(command_output, COMMAND_SHUTDOWN); + break; + case MANAGEMENT_STATUS: + command_output = pgagroal_append(command_output, COMMAND_STATUS); + break; + case MANAGEMENT_SWITCH_TO: + command_output = pgagroal_append(command_output, COMMAND_SWITCH_TO); + break; + default: + break; + } + return command_output; +} + +static char* +translate_output_format(int32_t out_code) +{ + char* output_format_output = NULL; + switch (out_code) + { + case MANAGEMENT_OUTPUT_FORMAT_JSON: + output_format_output = pgagroal_append(output_format_output, OUTPUT_FORMAT_JSON); + break; + case MANAGEMENT_OUTPUT_FORMAT_TEXT: + output_format_output = pgagroal_append(output_format_output, OUTPUT_FORMAT_TEXT); + break; + default: + break; + } + return output_format_output; +} + +static char* +translate_compression(int32_t compression_code) +{ + char* compression_output = NULL; + switch (compression_code) + { + case COMPRESSION_CLIENT_GZIP: + case COMPRESSION_SERVER_GZIP: + compression_output = pgagroal_append(compression_output, "gzip"); + break; + case COMPRESSION_CLIENT_ZSTD: + case COMPRESSION_SERVER_ZSTD: + compression_output = pgagroal_append(compression_output, "zstd"); + break; + case COMPRESSION_CLIENT_LZ4: + case COMPRESSION_SERVER_LZ4: + compression_output = pgagroal_append(compression_output, "lz4"); + break; + case COMPRESSION_CLIENT_BZIP2: + compression_output = pgagroal_append(compression_output, "bzip2"); + break; + default: + compression_output = pgagroal_append(compression_output, "none"); + break; + } + return compression_output; +} + +static char* +translate_encryption(int32_t encryption_code) +{ + char* encryption_output = NULL; + switch (encryption_code) + { + case ENCRYPTION_AES_256_CBC: + encryption_output = pgagroal_append(encryption_output, "aes-256-cbc"); + break; + case ENCRYPTION_AES_192_CBC: + encryption_output = pgagroal_append(encryption_output, "aes-192-cbc"); + break; + case ENCRYPTION_AES_128_CBC: + encryption_output = pgagroal_append(encryption_output, "aes-128-cbc"); + break; + case ENCRYPTION_AES_256_CTR: + encryption_output = pgagroal_append(encryption_output, "aes-256-ctr"); + break; + case ENCRYPTION_AES_192_CTR: + encryption_output = pgagroal_append(encryption_output, "aes-192-ctr"); + break; + case ENCRYPTION_AES_128_CTR: + encryption_output = pgagroal_append(encryption_output, "aes-128-ctr"); + break; + default: + encryption_output = pgagroal_append(encryption_output, "none"); + break; + } + return encryption_output; +} + +static void +translate_json_object(struct json* j) +{ + struct json* header = NULL; + int32_t command = 0; + char* translated_command = NULL; + int32_t out_format = -1; + char* translated_out_format = NULL; + int32_t out_compression = -1; + char* translated_compression = NULL; + int32_t out_encryption = -1; + char* translated_encryption = NULL; + + // Translate arguments of header + header = (struct json*)pgagroal_json_get(j, MANAGEMENT_CATEGORY_HEADER); + + if (header) + { + command = (int32_t)pgagroal_json_get(header, MANAGEMENT_ARGUMENT_COMMAND); + translated_command = translate_command(command); + if (translated_command) + { + pgagroal_json_put(header, MANAGEMENT_ARGUMENT_COMMAND, (uintptr_t)translated_command, ValueString); + } + + out_format = (int32_t)pgagroal_json_get(header, MANAGEMENT_ARGUMENT_OUTPUT); + translated_out_format = translate_output_format(out_format); + if (translated_out_format) + { + pgagroal_json_put(header, MANAGEMENT_ARGUMENT_OUTPUT, (uintptr_t)translated_out_format, ValueString); + } + + out_compression = (int32_t)pgagroal_json_get(header, MANAGEMENT_ARGUMENT_COMPRESSION); + translated_compression = translate_compression(out_compression); + if (translated_compression) + { + pgagroal_json_put(header, MANAGEMENT_ARGUMENT_COMPRESSION, (uintptr_t)translated_compression, ValueString); + } + + out_encryption = (int32_t)pgagroal_json_get(header, MANAGEMENT_ARGUMENT_ENCRYPTION); + translated_encryption = translate_encryption(out_encryption); + if (translated_encryption) + { + pgagroal_json_put(header, MANAGEMENT_ARGUMENT_ENCRYPTION, (uintptr_t)translated_encryption, ValueString); + } + + free(translated_command); + free(translated_out_format); + free(translated_compression); + free(translated_encryption); + } +} diff --git a/src/include/aes.h b/src/include/aes.h new file mode 100644 index 00000000..096b8784 --- /dev/null +++ b/src/include/aes.h @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_AES_H +#define PGAGROAL_AES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + +/** + * Encrypt a string + * @param plaintext The string + * @param password The master password + * @param ciphertext The ciphertext output + * @param ciphertext_length The length of the ciphertext + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_encrypt(char* plaintext, char* password, char** ciphertext, int* ciphertext_length, int mode); + +/** + * Decrypt a string + * @param ciphertext The string + * @param ciphertext_length The length of the ciphertext + * @param password The master password + * @param plaintext The plaintext output + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_decrypt(char* ciphertext, int ciphertext_length, char* password, char** plaintext, int mode); + +/** + * + * Encrypt a buffer + * @param origin_buffer The original buffer + * @param origin_size The size of the buffer + * @param enc_buffer The result buffer + * @param enc_size The result buffer size + * @param mode The aes mode + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_encrypt_buffer(unsigned char* origin_buffer, size_t origin_size, unsigned char** enc_buffer, size_t* enc_size, int mode); + +/** + * + * Decrypt a buffer + * @param origin_buffer The original buffer + * @param origin_size The size of the buffer + * @param dec_buffer The result buffer + * @param dec_size The result buffer size + * @param mode The aes mode + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_decrypt_buffer(unsigned char* origin_buffer, size_t origin_size, unsigned char** dec_buffer, size_t* dec_size, int mode); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/art.h b/src/include/art.h new file mode 100644 index 00000000..f857336b --- /dev/null +++ b/src/include/art.h @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_ART_H +#define PGAGROAL_ART_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + +#define MAX_PREFIX_LEN 10 + +typedef int (*art_callback)(void* data, const unsigned char* key, uint32_t key_len, struct value* value); + +typedef void (*value_destroy_callback)(void* value); + +/** @struct art + * The ART tree + */ +struct art +{ + struct art_node* root; /**< The root node of ART */ + uint64_t size; /**< The size of the ART */ +}; + +/** @struct art_iterator + * Defines an art_iterator + */ +struct art_iterator +{ + struct deque* que; /**< The deque */ + struct art* tree; /**< The ART */ + uint32_t count; /**< The count of the iterator */ + unsigned char* key; /**< The key */ + struct value* value; /**< The value */ +}; + +/** + * Initializes an adaptive radix tree + * @param tree [out] The tree + * @return 0 on success, 1 if otherwise + */ +int +pgagroal_art_create(struct art** tree); + +/** + * inserts a new value into the art tree,note that the key is copied while the value is sometimes not(depending on value type) + * @param t The tree + * @param key The key + * @param key_len The length of the key + * @param value The value data + * @param type The value type + * @return 0 if the item was successfully inserted, otherwise 1 + */ +int +pgagroal_art_insert(struct art* t, unsigned char* key, uint32_t key_len, uintptr_t value, enum value_type type); + +/** + * inserts a new ValueRef value into the art tree with a custom to_string and destroy data callback config + * @param t The tree + * @param key The key + * @param key_len The length of the key + * @param value The value data + * @param config The config + * @return 0 if the item was successfully inserted, otherwise 1 + */ +int +pgagroal_art_insert_with_config(struct art* t, unsigned char* key, uint32_t key_len, uintptr_t value, struct value_config* config); + +/** + * Check if a key exists in the ART tree + * @param t The tree + * @param key The key + * @param key_len The length of the key + * @return true if the key exists, false if otherwise + */ +bool +pgagroal_art_contains_key(struct art* t, unsigned char* key, uint32_t key_len); + +/** + * Searches for a value in the ART tree + * @param t The tree + * @param key The key + * @param key_len The length of the key + * @return NULL if the item was not found, otherwise the value pointer is returned + */ +uintptr_t +pgagroal_art_search(struct art* t, unsigned char* key, uint32_t key_len); + +/** + * Deletes a value from the ART tree + * @param t The tree + * @param key The key + * @param key_len The length of the key + * @return 0 if success or value not found, 1 if otherwise + */ +int +pgagroal_art_delete(struct art* t, unsigned char* key, uint32_t key_len); + +/** + * Get the next key value pair into iterator + * @param iter The iterator + * @return true if iterator has next, otherwise false + */ +bool +pgagroal_art_iterator_next(struct art_iterator* iter); + +/** + * Create an art iterator + * @param t The tree + * @param iter [out] The iterator + * @return 0 if success, otherwise 1 + */ +int +pgagroal_art_iterator_create(struct art* t, struct art_iterator** iter); + +/** + * Destroy the iterator + * @param iter The iterator + */ +void +pgagroal_art_iterator_destroy(struct art_iterator* iter); + +/** + * Convert the ART tree to string + * @param t The ART tree + * @param format The format + * @param tag The optional tag + * @param indent The indent + * @return The string + */ +char* +pgagroal_art_to_string(struct art* t, int32_t format, char* tag, int indent); + +/** + * Destroys an ART tree + * @return 0 on success, 1 if otherwise + */ +int +pgagroal_art_destroy(struct art* tree); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/bzip2_compression.h b/src/include/bzip2_compression.h new file mode 100644 index 00000000..eee2e8e8 --- /dev/null +++ b/src/include/bzip2_compression.h @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_BZIP_H +#define PGAGROAL_BZIP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * BZip compress a string + * @param s The original string + * @param buffer The point to the compressed data buffer + * @param buffer_size The size of the compressed buffer will be stored. + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_bzip2_string(char* s, unsigned char** buffer, size_t* buffer_size); + +/** + * BUNZip decompress a buffer to string + * @param compressed_buffer The buffer containing the GZIP compressed data + * @param compressed_size The size of the compressed buffer + * @param output_string The pointer to a string where the decompressed data will be stored + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_bunzip2_string(unsigned char* compressed_buffer, size_t compressed_size, char** output_string); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/configuration.h b/src/include/configuration.h new file mode 100644 index 00000000..9717eb48 --- /dev/null +++ b/src/include/configuration.h @@ -0,0 +1,500 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_CONFIGURATION_H +#define PGAGROAL_CONFIGURATION_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/* + * The main section that must be present in the `pgagroal.conf` + * configuration file. + */ +#define PGAGROAL_MAIN_INI_SECTION "pgagroal" +/* + * The main section that must be present in the `pgagroal_vault.conf` + * configuration file. + */ +#define PGAGROAL_VAULT_INI_SECTION "pgagroal-vault" + +/* + * The following constants are used to clearly identify + * a section the user wants to get configuration + * or change. They are used in the config-get + * and config-set operations. + */ +#define PGAGROAL_CONF_SERVER_PREFIX "server" +#define PGAGROAL_CONF_HBA_PREFIX "hba" +#define PGAGROAL_CONF_LIMIT_PREFIX "limit" + +/** + * Status that pgagroal_read_configuration() could provide. + * Use only negative values for errors, since a positive return + * value will indicate the number of problems within sections. + */ +#define PGAGROAL_CONFIGURATION_STATUS_OK 0 +#define PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND -1 +#define PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG -2 +#define PGAGROAL_CONFIGURATION_STATUS_KO -3 +#define PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT -4 + +#define CONFIGURATION_ARGUMENT_MAIN_CONF_PATH "main_configuration_path" +#define CONFIGURATION_ARGUMENT_LIMIT_CONF_PATH "limit_configuration_path" +#define CONFIGURATION_ARGUMENT_HBA_CONF_PATH "hba_configuration_path" +#define CONFIGURATION_ARGUMENT_USER_CONF_PATH "users_configuration_path" +#define CONFIGURATION_ARGUMENT_FRONTEND_USERS_CONF_PATH "frontend_users_configuration_path" +#define CONFIGURATION_ARGUMENT_ADMIN_CONF_PATH "admin_configuration_path" +#define CONFIGURATION_ARGUMENT_SUPERUSER_CONF_PATH "superuser_configuration_path" + +#define CONFIGURATION_ARGUMENT_HOST "host" +#define CONFIGURATION_ARGUMENT_PORT "port" +#define CONFIGURATION_ARGUMENT_UNIX_SOCKET_DIR "unix_socket_dir" +#define CONFIGURATION_ARGUMENT_METRICS "metrics" +#define CONFIGURATION_ARGUMENT_METRICS_CACHE_MAX_AGE "metrics_cache_max_age" +#define CONFIGURATION_ARGUMENT_METRICS_CACHE_MAX_SIZE "metrics_cache_max_size" +#define CONFIGURATION_ARGUMENT_MANAGEMENT "management" +#define CONFIGURATION_ARGUMENT_LOG_TYPE "log_type" +#define CONFIGURATION_ARGUMENT_LOG_LEVEL "log_level" +#define CONFIGURATION_ARGUMENT_LOG_PATH "log_path" +#define CONFIGURATION_ARGUMENT_LOG_ROTATION_AGE "log_rotation_age" +#define CONFIGURATION_ARGUMENT_LOG_ROTATION_SIZE "log_rotation_size" +#define CONFIGURATION_ARGUMENT_LOG_LINE_PREFIX "log_line_prefix" +#define CONFIGURATION_ARGUMENT_LOG_MODE "log_mode" +#define CONFIGURATION_ARGUMENT_LOG_CONNECTIONS "log_connections" +#define CONFIGURATION_ARGUMENT_LOG_DISCONNECTIONS "log_disconnections" +#define CONFIGURATION_ARGUMENT_BLOCKING_TIMEOUT "blocking_timeout" +#define CONFIGURATION_ARGUMENT_IDLE_TIMEOUT "idle_timeout" +#define CONFIGURATION_ARGUMENT_ROTATE_FRONTEND_PASSWORD_TIMEOUT "rotate_frontend_password_timeout" +#define CONFIGURATION_ARGUMENT_ROTATE_FRONTEND_PASSWORD_LENGTH "rotate_frontend_password_length" +#define CONFIGURATION_ARGUMENT_MAX_CONNECTION_AGE "max_connection_age" +#define CONFIGURATION_ARGUMENT_VALIDATION "validation" +#define CONFIGURATION_ARGUMENT_BACKGROUND_INTERVAL "background_interval" +#define CONFIGURATION_ARGUMENT_MAX_RETRIES "max_retries" +#define CONFIGURATION_ARGUMENT_MAX_CONNECTIONS "max_connections" +#define CONFIGURATION_ARGUMENT_ALLOW_UNKNOWN_USERS "allow_unknown_users" +#define CONFIGURATION_ARGUMENT_AUTHENTICATION_TIMEOUT "authentication_timeout" +#define CONFIGURATION_ARGUMENT_PIPELINE "pipeline" +#define CONFIGURATION_ARGUMENT_AUTH_QUERY "auth_query" +#define CONFIGURATION_ARGUMENT_FAILOVER "failover" +#define CONFIGURATION_ARGUMENT_FAILOVER_SCRIPT "failover_script" +#define CONFIGURATION_ARGUMENT_TLS "tls" +#define CONFIGURATION_ARGUMENT_TLS_CERT_FILE "tls_cert_file" +#define CONFIGURATION_ARGUMENT_TLS_KEY_FILE "tls_key_file" +#define CONFIGURATION_ARGUMENT_TLS_CA_FILE "tls_ca_file" +#define CONFIGURATION_ARGUMENT_LIBEV "libev" +#define CONFIGURATION_ARGUMENT_KEEP_ALIVE "keep_alive" +#define CONFIGURATION_ARGUMENT_NODELAY "nodelay" +#define CONFIGURATION_ARGUMENT_NON_BLOCKING "non_blocking" +#define CONFIGURATION_ARGUMENT_BACKLOG "backlog" +#define CONFIGURATION_ARGUMENT_HUGEPAGE "hugepage" +#define CONFIGURATION_ARGUMENT_TRACKER "tracker" +#define CONFIGURATION_ARGUMENT_TRACK_PREPARED_STATEMENTS "track_prepared_statements" +#define CONFIGURATION_ARGUMENT_PIDFILE "pidfile" +#define CONFIGURATION_ARGUMENT_UPDATE_PROCESS_TITLE "update_process_title" +#define CONFIGURATION_ARGUMENT_PRIMARY "primary" + +/** + * Initialize the configuration structure + * @param shmem The shared memory segment + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_init_configuration(void* shmem); + +/** + * Initialize the vault configuration structure + * @param shmem The shared memory segment + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_vault_init_configuration(void* shmem); + +/** + * Read the configuration from a file + * @param shmem The shared memory segment + * @param filename The file name + * @param emit_warnings true if unknown parameters have to + * reported on stderr + * @return 0 (i.e, PGAGROAL_CONFIGURATION_STATUS_OK) upon success, otherwise + * - PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND if the file does not exists + * - PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG if the file contains too many sections + * - a positive value to indicate how many errors (with regard to sections) have been found + * - PGAGROAL_CONFIGURATION_STATUS_KO if the file has generic errors, most notably it lacks + * a [pgagroal] section + */ +int +pgagroal_read_configuration(void* shmem, char* filename, bool emit_warnings); + +/** + * Validate the configuration + * @param shmem The shared memory segment + * @param has_unix_socket Has Unix socket + * @param has_main_sockets Has main sockets + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_validate_configuration(void* shmem, bool has_unix_socket, bool has_main_sockets); + +/** + * Read the configuration of vault from a file + * @param shmem The shared memory segment + * @param filename The file name + * @param emit_warnings true if unknown parameters have to + * reported on stderr + * @return 0 (i.e, PGAGROAL_CONFIGURATION_STATUS_OK) upon success, otherwise + * - PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND if the file does not exists + * - PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG if the file contains too many sections + * - a positive value to indicate how many errors (with regard to sections) have been found + * - PGAGROAL_CONFIGURATION_STATUS_KO if the file has generic errors, most notably it lacks + * a [pgagroal-vault] section + */ +int +pgagroal_vault_read_configuration(void* shmem, char* filename, bool emit_warnings); + +/** + * Validate the configuration of vault + * @param shmem The shared memory segment + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_vault_validate_configuration(void* shmem); + +/** + * Read the HBA configuration from a file + * @param shmem The shared memory segment + * @param filename The file name + * @return 0 (i.e, PGAGROAL_CONFIGURATION_STATUS_OK) upon success, otherwise + * - PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND if the file does not exists + * - PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG if the file contains too many entries + */ +int +pgagroal_read_hba_configuration(void* shmem, char* filename); + +/** + * Validate the HBA configuration from a file + * @param shmem The shared memory segment + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_validate_hba_configuration(void* shmem); + +/** + * Read the LIMIT configuration from a file + * @param shmem The shared memory segment + * @param filename The file name + * @return 0 (i.e, PGAGROAL_CONFIGURATION_STATUS_OK) upon success, otherwise + * - PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND if the file does not exists + * - PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG if the file contains too many limits + */ +int +pgagroal_read_limit_configuration(void* shmem, char* filename); + +/** + * Validate the LIMIT configuration from a file + * @param shmem The shared memory segment + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_validate_limit_configuration(void* shmem); + +/** + * Read the USERS configuration from a file + * @param shmem The shared memory segment + * @param filename The file name + * @return 0 (i.e, PGAGROAL_CONFIGURATION_STATUS_OK) upon success, otherwise + * - PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND if the file does not exists + * - PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG if the file contains too many users + * (i.e., more users than the number defined in the limits) + * - PGAGROAL_CONFIGURATION_STATUS_KO if the file has some problem (e.g., cannot be decrypted) + * - PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT to indicate a problem reading the content of the file + + */ +int +pgagroal_read_users_configuration(void* shmem, char* filename); + +/** + * Validate the USERS configuration from a file + * @param shmem The shared memory segment + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_validate_users_configuration(void* shmem); + +/** + * Read the FRONTEND USERS configuration from a file + * @param shmem The shared memory segment + * @param filename The file name + * @return 0 (i.e, PGAGROAL_CONFIGURATION_STATUS_OK) upon success, otherwise + * - PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND if the file does not exists + * - PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG if the file contains too many users + * (i.e., more users than the number defined in the limits) + * - PGAGROAL_CONFIGURATION_STATUS_KO if the file has some problem (e.g., cannot be decrypted) + * - PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT to indicate a problem reading the content of the file + */ +int +pgagroal_read_frontend_users_configuration(void* shmem, char* filename); + +/** + * Validate the FRONTEND USERS configuration from a file + * @param shmem The shared memory segment + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_validate_frontend_users_configuration(void* shmem); + +/** + * Read the ADMINS configuration from a file + * @param shmem The shared memory segment + * @param filename The file name + * @return 0 (i.e, PGAGROAL_CONFIGURATION_STATUS_OK) upon success, otherwise + * - PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND if the file does not exists + * - PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG if the file contains too many users + * (i.e., more users than the number defined in the limits) + * - PGAGROAL_CONFIGURATION_STATUS_KO if the file has some problem (e.g., cannot be decrypted) + * - PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT to indicate a problem reading the content of the file + */ +int +pgagroal_read_admins_configuration(void* shmem, char* filename); + +/** + * Read the USERS configuration of vault from a file + * @param shmem The shared memory segment + * @param filename The file name + * @return 0 (i.e, PGAGROAL_CONFIGURATION_STATUS_OK) upon success, otherwise + * - PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND if the file does not exists + * - PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG if the file contains too many users + * (i.e., more users than the number defined in the limits) + * - PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT to indicate a problem reading the content of the file + */ +int +pgagroal_vault_read_users_configuration(void* shmem, char* filename); + +/** + * Validate the ADMINS configuration from a file + * @param shmem The shared memory segment + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_validate_admins_configuration(void* shmem); + +/** + * Read the superuser from a file + * @param shmem The shared memory segment + * @param filename The file name + * @return 0 (i.e, PGAGROAL_CONFIGURATION_STATUS_OK) upon success, otherwise + * - PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND if the file does not exists + * - PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG if the file entry is to big + * - PGAGROAL_CONFIGURATION_STATUS_KO if the file has some problem (e.g., cannot be decrypted) + * - PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT to indicate a problem reading the content of the file + */ +int +pgagroal_read_superuser_configuration(void* shmem, char* filename); + +/** + * Validate the SUPERUSER configuration from a file + * @param shmem The shared memory segment + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_validate_superuser_configuration(void* shmem); + +/** + * Reload the configuration + * @param reload Should the server be reloaded + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_reload_configuration(bool* reload); + +/** + * Automatically initialize the 'pidfile' + * if none has been specified. + * This function is called as last step + * from pgagroal_validate_configuration + * because it builds the pidfile on the value + * of unix_socket_dir. + */ +void +pgagroal_init_pidfile_if_needed(void); + +/** + * Checks if the configuration has a min set of values to + * take into account doing a prefill. For example, there must + * be users and limits set, otherwise it does not + * make any sense to attempt a prefill. + * This can be used to wrap the condituion before calling + * other prefill functions, e.g., `pgagroal_prefill()`. + */ +bool +pgagroal_can_prefill(void); + +/** + * Gets a configuration parameter and places into the string pointer. + * This is used, for example, to get a writable string to send over the + * management socket. + * + * The key can contain words separated by a dot '.' to indicate different search criterias. + * A "dotted" key is made by a 'context', a 'section' and a 'search term', so that + * it can be written as 'section.context.search'. + * If both the section and the context are omitted, the 'search' is performed among the + * pgagroal global settings (i.e., those under the [pgagroal] main section). The same + * happens if the the section is specified as 'pgagroal', therefore the following two + * terms do the same search: + * - `update_process_title` + * - `pgagroal.update_process_title` + * + * Other possible sections are: + * - server to search for a specific server, the match is performed on the server name; + * - hba to search for a specific HBA entry, the match is performed on the username; + * - limit to search for a specific database in the limit (database) configuration file. + * + * When one the above sections is specified, the search is done identifying the entry to snoop + * by means of 'context', and within such the 'search' is performed. + * + * In the case of the `server` section, the `context` has to be the name of a server configured, + * while the `search` has to be the keyword to look for. AS an example: `server.venkman.port` provides + * the value of the 'port' setting under the server section '[venkman]'. + * + * In the case of the 'hba` section, the `context` has to be a username as it appears in a line + * of the pgagroal_hba.conf file, while the `search` has to be the column keyword to snoop. + * For example, `hba.luca.method` will seek for the `method` used to authenticate the user `luca`. + * Please note that, since the same user could be listed more than once, only the first matching + * entry is reported. + * + * In the case of the 'limit` section, the `context` has to be a database name as it appears in a line + * of the pgagroal_database.conf file, while the `search` has to be the column keyword to snoop. + * For example, `limit.pgbench.max_size` will seek for the `max_size` connection limit for the + * database 'pgbench'. + * Please note that, since the same database could be listed more than once, only the first matching + * entry is reported. + * + * @param buffer where to write the configuration value. The buffer must + * be already allocated. In case of failure, the buffer is zero filled. + * @param config_key the name of the configuration parameter + * @param buffer_size the max length of the buffer where the result will be stored + * @return 0 on success, 1 when the key cannot be found + */ +int +pgagroal_write_config_value(char* buffer, char* config_key, size_t buffer_size); + +/** + * Function to apply a single configuration parameter. + * + * This is the backbone function used when parsing the main configuration file + * and is used to set any of the allowed parameters. + * + * @param config the configuration to apply values onto + * @param srv the server to which the configuration parameter refers to, if needed + * @param section the section of the file, main or server + * @param key the parameter name of the configuration + * @param value the value of the configuration + * @return 0 on success + * + * Examples of usage: + * pgagroal_apply_main_configuration( config, NULL, PGAGROAL_MAIN_INI_SECTION, "log_level", "info" ); + */ +int +pgagroal_apply_main_configuration(struct main_configuration* config, + struct server* srv, + char* section, + char* key, + char* value); + +/** + * Function to apply a single configuration parameter. + * + * This is the backbone function used when parsing the main configuration file + * and is used to set any of the allowed parameters. + * + * @param config the configuration to apply values onto + * @param srv the server to which the configuration parameter refers to, if needed + * @param section the section of the file, main or server + * @param key the parameter name of the configuration + * @param value the value of the configuration + * @return 0 on success + * + * Examples of usage: + * pgagroal_apply_vault_configuration( config, NULL, PGAGROAL_VAULT_INI_SECTION, "log_level", "info" ); + */ +int +pgagroal_apply_vault_configuration(struct vault_configuration* config, + struct vault_server* srv, + char* section, + char* key, + char* value); + +/** + * Function to set a configuration value. + * + * This function accepts the same prefixes as the configuration get behavior, so + * a single parameter like 'max_connections' is managed as the main configuration file, + * a 'server' prefix will hit a specific server, a 'limit' prefix will set a limit, and so on. + * + * The idea behind the function is to "clone" the current configuration in use, and then + * apply the changes. In order to be coherent to what a "reload" operation would do, + * this function calls 'pgagroal_transfer_configuration' internally. + * + * @param config_key the string that contains the name of the parameter + * @param config_value the value to set + * @return 0 on success + */ +int +pgagroal_apply_configuration(char* config_key, char* config_value); + +/** + * Get a configuration parameter value + * @param ssl The SSL connection + * @param client_fd The client + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param payload The payload + */ +void +pgagroal_conf_get(SSL* ssl, int client_fd, uint8_t compression, uint8_t encryption, struct json* payload); + +/** + * Set a configuration parameter value + * @param ssl The SSL connection + * @param client_fd The client + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param payload The payload + */ +void +pgagroal_conf_set(SSL* ssl, int client_fd, uint8_t compression, uint8_t encryption, struct json* payload); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/connection.h b/src/include/connection.h new file mode 100644 index 00000000..72201d99 --- /dev/null +++ b/src/include/connection.h @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_CONNECTION_H +#define PGAGROAL_CONNECTION_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + +#include + +#define CONNECTION_TRANSFER 0 +#define CONNECTION_RETURN 1 +#define CONNECTION_KILL 2 +#define CONNECTION_CLIENT_FD 3 +#define CONNECTION_REMOVE_FD 4 +#define CONNECTION_CLIENT_DONE 5 + +/** + * Connection: Get a connection + * @param client_fd The client descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_connection_get(int* client_fd); + +/** + * Connection: Get a connection based on a PID + * @param client_fd The client descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_connection_get_pid(pid_t pid, int* client_fd); + +/** + * Connection: Opetation id write + * @param client_fd The client descriptor + * @param id The identifier + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_connection_id_write(int client_fd, int id); + +/** + * Connection: Opetation id read + * @param client_fd The client descriptor + * @param id The identifier + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_connection_id_read(int client_fd, int* id); + +/** + * Connection: Transfer write + * @param client_fd The client descriptor + * @param slot The slot + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_connection_transfer_write(int client_fd, int32_t slot); + +/** + * Connection: Transfer read + * @param client_fd The client descriptor + * @param slot The slot + * @param fd The file descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_connection_transfer_read(int client_fd, int32_t* slot, int* fd); + +/** + * Connection: Slot write + * @param slot The slot + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_connection_slot_write(int client_fd, int32_t slot); + +/** + * Connection: Slot read + * @param slot The slot + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_connection_slot_read(int client_fd, int32_t* slot); + +/** + * Connection: Socket write + * @param socket The socket + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_connection_socket_write(int client_fd, int socket); + +/** + * Connection: Socket read + * @param socket The socket + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_connection_socket_read(int client_fd, int* socket); + +/** + * Connection: PID write + * @param pid The PID + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_connection_pid_write(int client_fd, pid_t pid); + +/** + * Connection: PID read + * @param pid The PID + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_connection_pid_read(int client_fd, pid_t* pid); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/deque.h b/src/include/deque.h new file mode 100644 index 00000000..83fc15df --- /dev/null +++ b/src/include/deque.h @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_DEQUE_H +#define PGAGROAL_DEQUE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include +#include +#include + +/** @struct deque_node + * Defines a deque node + */ +struct deque_node +{ + struct value* data; /**< The value */ + char* tag; /**< The tag */ + struct deque_node* next; /**< The next pointer */ + struct deque_node* prev; /**< The previous pointer */ +}; + +/** @struct deque + * Defines a deque + */ +struct deque +{ + uint32_t size; /**< The size of the deque */ + bool thread_safe; /**< If the deque is thread safe */ + pthread_rwlock_t mutex; /**< The mutex of the deque */ + struct deque_node* start; /**< The start node */ + struct deque_node* end; /**< The end node */ +}; + +/** @struct deque_iterator + * Defines a deque iterator + */ +struct deque_iterator +{ + struct deque* deque; /**< The deque */ + struct deque_node* cur; /**< The current deque node */ + char* tag; /**< The current tag */ + struct value* value; /**< The current value */ +}; + +/** + * Create a deque + * @param thread_safe If the deque needs to be thread safe + * @param deque The deque + * @return 0 if success, otherwise 1 + */ +int +pgagroal_deque_create(bool thread_safe, struct deque** deque); + +/** + * Add a node to deque's tail, the tag will be copied + * This function is thread safe + * @param deque The deque + * @param tag The tag,optional + * @param data The data + * @param type The data type + * @return 0 if success, otherwise 1 + */ +int +pgagroal_deque_add(struct deque* deque, char* tag, uintptr_t data, enum value_type type); + +/** + * Remove all the nodes with the given tag + * @param deque The deque + * @param tag The tag + * @return Number of nodes removed + */ +int +pgagroal_deque_remove(struct deque* deque, char* tag); + +/** + * Add a node to deque's tail with custom to_string and data destroy callback, + * the type will be set to ValueRef + * This function is thread safe + * @param deque The deque + * @param tag The tag,optional + * @param data The data + * @return 0 if success, otherwise 1 + */ +int +pgagroal_deque_add_with_config(struct deque* deque, char* tag, uintptr_t data, struct value_config* config); + +/** + * Retrieve value and remove the node from deque's head. + * Note that if the value was copied into node, + * this function will return the original value and tag + * rather than making a copy of it. + * This function is thread safe, but the returned value is not protected + * @param deque The deque + * @param tag [out] Optional, tag will be returned through if not NULL + * @return The value data if deque's not empty, otherwise 0 + */ +uintptr_t +pgagroal_deque_poll(struct deque* deque, char** tag); + +/** + * Retrieve value and remove the node from deque's tail. + * Note that if the value was copied into node, + * this function will return the original value and tag + * rather than making a copy of it. + * This function is thread safe, but the returned value is not protected + * @param deque The deque + * @param tag [out] Optional, tag will be returned through if not NULL + * @return The value data if deque's not empty, otherwise 0 + */ +uintptr_t +pgagroal_deque_poll_last(struct deque* deque, char** tag); + +/** + * Retrieve value without removing the node from deque's head. + * Note that if the value was copied into node, + * this function will return the original value and tag + * rather than making a copy of it. + * This function is thread safe, but the returned value is not protected + * @param deque The deque + * @param tag [out] Optional, tag will be returned through if not NULL + * @return The value data if deque's not empty, otherwise 0 + */ +uintptr_t +pgagroal_deque_peek(struct deque* deque, char** tag); + +/** + * Retrieve value without removing the node from deque's tail. + * Note that if the value was copied into node, + * this function will return the original value and tag + * rather than making a copy of it. + * This function is thread safe, but the returned value is not protected + * @param deque The deque + * @param tag [out] Optional, tag will be returned through if not NULL + * @return The value data if deque's not empty, otherwise 0 + */ +uintptr_t +pgagroal_deque_peek_last(struct deque* deque, char** tag); + +/** + * Get the data for the specified tag + * @param deque The deque + * @param tag The tag + * @return The data, or 0 + */ +uintptr_t +pgagroal_deque_get(struct deque* deque, char* tag); + +/** + * Does the tag exists + * @param deque The deque + * @param tag The tag + * @return True if exists, otherwise false + */ +bool +pgagroal_deque_exists(struct deque* deque, char* tag); + +/** + * Create a deque iterator + * @param deque The deque + * @param iter [out] The iterator + * @return 0 on success, 1 if otherwise + */ +int +pgagroal_deque_iterator_create(struct deque* deque, struct deque_iterator** iter); + +/** + * Get the next deque value + * @param iter The iterator + * @return true if has next, false if otherwise + */ +bool +pgagroal_deque_iterator_next(struct deque_iterator* iter); + +/** + * Remove the current node iterator points to and place the iterator to the previous node + * @param iter The iterator + */ +void +pgagroal_deque_iterator_remove(struct deque_iterator* iter); + +/** + * Destroy a deque iterator + * @param iter The iterator + */ +void +pgagroal_deque_iterator_destroy(struct deque_iterator* iter); + +/** + * Get the size of the deque + * @param deque The deque + * @return The size + */ +uint32_t +pgagroal_deque_size(struct deque* deque); + +/** + * Check if the deque is empty + * @param deque The deque + * @return true if deque size is 0, otherwise false + */ +bool +pgagroal_deque_empty(struct deque* deque); + +/** + * List the nodes in the deque + * @param deque The deque + */ +void +pgagroal_deque_list(struct deque* deque); + +/** + * Sort the deque + * @param deque The deque + */ +void +pgagroal_deque_sort(struct deque* deque); + +/** + * Convert what's inside deque to string + * @param deque The deque + * @param format The format + * @param tag [Optional] The tag, which will be applied before the content if not null + * @param indent The current indentation + * @return The string + */ +char* +pgagroal_deque_to_string(struct deque* deque, int32_t format, char* tag, int indent); + +/** + * Destroy the deque and free its and its nodes' memory + * @param deque The deque + */ +void +pgagroal_deque_destroy(struct deque* deque); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/gzip_compression.h b/src/include/gzip_compression.h new file mode 100644 index 00000000..b90e9eb0 --- /dev/null +++ b/src/include/gzip_compression.h @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_GZIP_H +#define PGAGROAL_GZIP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * GZip a string + * @param s The original string + * @param buffer The point to the compressed data buffer + * @param buffer_size The size of the compressed buffer will be stored. + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_gzip_string(char* s, unsigned char** buffer, size_t* buffer_size); + +/** + * GUNZip a buffer to string + * @param compressed_buffer The buffer containing the GZIP compressed data + * @param compressed_size The size of the compressed buffer + * @param output_string The pointer to a string where the decompressed data will be stored + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_gunzip_string(unsigned char* compressed_buffer, size_t compressed_size, char** output_string); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/json.h b/src/include/json.h new file mode 100644 index 00000000..a94db8ce --- /dev/null +++ b/src/include/json.h @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_JSON_H +#define PGAGROAL_JSON_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* pgagroal */ +#include +#include +#include + +enum json_type { + JSONUnknown, + JSONItem, + JSONArray +}; + +/** @struct json + * Defines a JSON structure + */ +struct json +{ + // a json object can only be item or array + enum json_type type; /**< The json object type */ + // if the object is an array, it can have at most one json element + void* elements; /**< The json elements, could be an array or some kv pairs */ +}; + +/** @struct json_iterator + * Defines a JSON iterator + */ +struct json_iterator +{ + void* iter; /**< The internal iterator */ + struct json* obj; /**< The json object */ + char* key; /**< The current key, if it's json item */ + struct value* value; /**< The current value or entry */ +}; + +/** + * Create a json object + * @param item [out] The json item + * @return 0 if success, 1 if otherwise + */ +int +pgagroal_json_create(struct json** object); + +/** + * Put a key value pair into the json item, + * if the key exists, value will be overwritten, + * + * If the kv pair is put into an empty json object, it will be treated as json item, + * otherwise if the json object is an array, it will reject the kv pair + * @param item The json item + * @param key The json key + * @param val The value data + * @param type The value type + * @return 0 on success, otherwise 1 + */ +int +pgagroal_json_put(struct json* item, char* key, uintptr_t val, enum value_type type); + +/** + * Get the value data from json item + * @param item The item + * @param tag The tag + * @return The value data, 0 if not found + */ +uintptr_t +pgagroal_json_get(struct json* item, char* tag); + +/** + * Check if the json item contains the given key + * @param item The json item + * @param key The key + * @return True if the key exists, otherwise false + */ +bool +pgagroal_json_contains_key(struct json* item, char* key); + +/** + * Append an entry into the json array + * If the entry is put into an empty json object, it will be treated as json array, + * otherwise if the json object is an item, it will reject the entry + * @param array The json array + * @param entry The entry data + * @param type The entry value type + * @return 0 is successful, + * otherwise when the json object is an array, value is null, or value type conflicts with old value, 1 will be returned + */ +int +pgagroal_json_append(struct json* array, uintptr_t entry, enum value_type type); + +/** + * Get json array length + * @param array The json array + * @return The length + */ +uint32_t +pgagroal_json_array_length(struct json* array); + +/** + * Create a json iterator + * @param object The JSON object + * @param iter [out] The iterator + * @return 0 on success, 1 if otherwise + */ +int +pgagroal_json_iterator_create(struct json* object, struct json_iterator** iter); +/** + * Get the next kv pair/entry from JSON object + * @param iter The iterator + * @return true if has next, false if otherwise + */ +bool +pgagroal_json_iterator_next(struct json_iterator* iter); + +/** + * Destroy a iterator + * @param iter The iterator + */ +void +pgagroal_json_iterator_destroy(struct json_iterator* iter); + +/** + * Parse a string into json item + * @param str The string + * @param obj [out] The json object + * @return 0 if success, 1 if otherwise + */ +int +pgagroal_json_parse_string(char* str, struct json** obj); + +/** + * Clone a json object + * @param from The from object + * @param to [out] The to object + * @return 0 if success, 1 if otherwise + */ +int +pgagroal_json_clone(struct json* from, struct json** to); + +/** + * Convert a json to string + * @param object The json object + * @param format The format + * @param tag The optional tag + * @param indent The indent + * @return The json formatted string + */ +char* +pgagroal_json_to_string(struct json* object, int32_t format, char* tag, int indent); + +/** + * Print a json object + * @param object The object + * @param format The format + * @param indent_per_level The indent per level + */ +void +pgagroal_json_print(struct json* object, int32_t format); + +/** + * Destroy the json object + * @param item The json object + * @return 0 if success, 1 if otherwise + */ +int +pgagroal_json_destroy(struct json* object); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/logging.h b/src/include/logging.h new file mode 100644 index 00000000..d41ddd9a --- /dev/null +++ b/src/include/logging.h @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_LOGGING_H +#define PGAGROAL_LOGGING_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#define PGAGROAL_LOGGING_TYPE_CONSOLE 0 +#define PGAGROAL_LOGGING_TYPE_FILE 1 +#define PGAGROAL_LOGGING_TYPE_SYSLOG 2 + +#define PGAGROAL_LOGGING_LEVEL_DEBUG5 1 +#define PGAGROAL_LOGGING_LEVEL_DEBUG4 1 +#define PGAGROAL_LOGGING_LEVEL_DEBUG3 1 +#define PGAGROAL_LOGGING_LEVEL_DEBUG2 1 +#define PGAGROAL_LOGGING_LEVEL_DEBUG1 2 +#define PGAGROAL_LOGGING_LEVEL_INFO 3 +#define PGAGROAL_LOGGING_LEVEL_WARN 4 +#define PGAGROAL_LOGGING_LEVEL_ERROR 5 +#define PGAGROAL_LOGGING_LEVEL_FATAL 6 + +#define PGAGROAL_LOGGING_MODE_CREATE 0 +#define PGAGROAL_LOGGING_MODE_APPEND 1 + +#define PGAGROAL_LOGGING_ROTATION_DISABLED 0 + +#define PGAGROAL_LOGGING_DEFAULT_LOG_LINE_PREFIX "%Y-%m-%d %H:%M:%S" + +#define pgagroal_log_trace(...) pgagroal_log_line(PGAGROAL_LOGGING_LEVEL_DEBUG5, __FILE__, __LINE__, __VA_ARGS__) +#define pgagroal_log_debug(...) pgagroal_log_line(PGAGROAL_LOGGING_LEVEL_DEBUG1, __FILE__, __LINE__, __VA_ARGS__) +#define pgagroal_log_info(...) pgagroal_log_line(PGAGROAL_LOGGING_LEVEL_INFO, __FILE__, __LINE__, __VA_ARGS__) +#define pgagroal_log_warn(...) pgagroal_log_line(PGAGROAL_LOGGING_LEVEL_WARN, __FILE__, __LINE__, __VA_ARGS__) +#define pgagroal_log_error(...) pgagroal_log_line(PGAGROAL_LOGGING_LEVEL_ERROR, __FILE__, __LINE__, __VA_ARGS__) +#define pgagroal_log_fatal(...) pgagroal_log_line(PGAGROAL_LOGGING_LEVEL_FATAL, __FILE__, __LINE__, __VA_ARGS__) + +/** + * Initialize the logging system + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_init_logging(void); + +/** + * Start the logging system + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_start_logging(void); + +/** + * Stop the logging system + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_stop_logging(void); + +/** + * Log a line + * @param level The level + * @param file The file + * @param line The line number + * @param fmt The formatting code + * @return 0 upon success, otherwise 1 + */ +void +pgagroal_log_line(int level, char* file, int line, char* fmt, ...); + +/** + * Log a memory segment + * @param data The data + * @param size The size + * @return 0 upon success, otherwise 1 + */ +void +pgagroal_log_mem(void* data, size_t size); + +/** + * Is the logging level enabled + * @param level The level + * @return True if enabled, otherwise false + */ +bool +pgagroal_log_is_enabled(int level); + +/** + * Utility function to understand if log rotation + * is enabled or not. + * @return true if the rotation is enabled. + */ +bool +log_rotation_enabled(void); + +/** + * Forces a disabling of the log rotation. + * Useful when the system cannot determine how to rotate logs. + */ +void +log_rotation_disable(void); + +/** + * Checks if there are the requirements to perform a log rotation. + * It returns true in either the case of the size exceeded or + * the age exceeded. The age is contained into a global + * variable 'next_log_rotation_age' that express the number + * of seconds at which the next rotation will be performed. + * @return true if the log should be rotated + */ +bool +log_rotation_required(void); + +/** + * Function to compute the next instant at which a log rotation + * will happen. It computes only if the logging is to a file + * and only if the configuration tells to compute the rotation + * age. + * @return true on success + */ +bool +log_rotation_set_next_rotation_age(void); + +/** + * Opens the log file defined in the configuration. + * Works only for a real log file, i.e., the configuration + * must be set up to log to a file, not console. + * + * The function considers the settings in the configuration + * to determine the mode (append, create) and the filename + * to open. + * + * It sets the global variable 'log_file'. + * + * If it succeed in opening the log file, it calls + * the log_rotation_set_next_rotation_age() function to + * determine the next instant at which the log file + * must be rotated. Calling such function is safe + * because if the log rotation is disabled, the function + * does nothing. + * + * @return 0 on success, 1 on error. + */ +int +log_file_open(void); + +/** + * Performs a log file rotation. + * It flushes and closes the current log file, + * then re-opens it. + * + * DO NOT LOG WITHIN THIS FUNCTION as long as this + * is invoked by log_line + */ +void +log_file_rotate(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/lz4_compression.h b/src/include/lz4_compression.h new file mode 100644 index 00000000..4f11b67b --- /dev/null +++ b/src/include/lz4_compression.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_LZ4_H +#define PGAGROAL_LZ4_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#define BLOCK_BYTES 1024 * 4 + +/** + * LZ4 compress a string + * @param s The original string + * @param buffer The point to the compressed data buffer + * @param buffer_size The size of the compressed buffer will be stored. + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_lz4c_string(char* s, unsigned char** buffer, size_t* buffer_size); + +/** + * LZ4 decompress a buffer to string + * @param compressed_buffer The buffer containing the GZIP compressed data + * @param compressed_size The size of the compressed buffer + * @param output_string The pointer to a string where the decompressed data will be stored + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_lz4d_string(unsigned char* compressed_buffer, size_t compressed_size, char** output_string); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/management.h b/src/include/management.h new file mode 100644 index 00000000..3f6e0812 --- /dev/null +++ b/src/include/management.h @@ -0,0 +1,483 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_MANAGEMENT_H +#define PGAGROAL_MANAGEMENT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + +#include + +/** + * Management header + */ +#define MANAGEMENT_COMPRESSION_NONE 0 +#define MANAGEMENT_COMPRESSION_GZIP 1 +#define MANAGEMENT_COMPRESSION_ZSTD 2 +#define MANAGEMENT_COMPRESSION_LZ4 3 +#define MANAGEMENT_COMPRESSION_BZIP2 4 + +#define MANAGEMENT_ENCRYPTION_NONE 0 +#define MANAGEMENT_ENCRYPTION_AES256 1 +#define MANAGEMENT_ENCRYPTION_AES192 2 +#define MANAGEMENT_ENCRYPTION_AES128 3 + +/** + * Management categories + */ +#define MANAGEMENT_CATEGORY_HEADER "Header" +#define MANAGEMENT_CATEGORY_REQUEST "Request" +#define MANAGEMENT_CATEGORY_RESPONSE "Response" +#define MANAGEMENT_CATEGORY_OUTCOME "Outcome" + +/** + * Management commands + */ +#define MANAGEMENT_CANCEL_SHUTDOWN 1 +#define MANAGEMENT_CONFIG_LS 2 +#define MANAGEMENT_CONFIG_GET 3 +#define MANAGEMENT_CONFIG_SET 4 +#define MANAGEMENT_DETAILS 5 +#define MANAGEMENT_DISABLEDB 6 +#define MANAGEMENT_ENABLEDB 7 +#define MANAGEMENT_FLUSH 8 +#define MANAGEMENT_GET_PASSWORD 9 +#define MANAGEMENT_GRACEFULLY 10 +#define MANAGEMENT_PING 11 +#define MANAGEMENT_RELOAD 12 +#define MANAGEMENT_CLEAR 13 +#define MANAGEMENT_CLEAR_SERVER 14 +#define MANAGEMENT_SHUTDOWN 15 +#define MANAGEMENT_STATUS 16 +#define MANAGEMENT_SWITCH_TO 17 + +/** + * Management arguments + */ +#define MANAGEMENT_ARGUMENT_ACTIVE_CONNECTIONS "ActiveConnections" +#define MANAGEMENT_ARGUMENT_APPNAME "AppName" +#define MANAGEMENT_ARGUMENT_CLIENT_VERSION "ClientVersion" +#define MANAGEMENT_ARGUMENT_COMMAND "Command" +#define MANAGEMENT_ARGUMENT_COMPRESSION "Compression" +#define MANAGEMENT_ARGUMENT_CONFIG_KEY "ConfigKey" +#define MANAGEMENT_ARGUMENT_CONFIG_VALUE "ConfigValue" +#define MANAGEMENT_ARGUMENT_CONNECTIONS "Connections" +#define MANAGEMENT_ARGUMENT_DATABASE "Database" +#define MANAGEMENT_ARGUMENT_DATABASES "Databases" +#define MANAGEMENT_ARGUMENT_ENABLED "Enabled" +#define MANAGEMENT_ARGUMENT_ENCRYPTION "Encryption" +#define MANAGEMENT_ARGUMENT_ERROR "Error" +#define MANAGEMENT_ARGUMENT_FD "FD" +#define MANAGEMENT_ARGUMENT_HOST "Host" +#define MANAGEMENT_ARGUMENT_INITIAL_CONNECTIONS "InitialConnections" +#define MANAGEMENT_ARGUMENT_LIMITS "Limits" +#define MANAGEMENT_ARGUMENT_MAX_CONNECTIONS "MaxConnections" +#define MANAGEMENT_ARGUMENT_MIN_CONNECTIONS "MinConnections" +#define MANAGEMENT_ARGUMENT_MODE "Mode" +#define MANAGEMENT_ARGUMENT_NUMBER_OF_SERVERS "NumberOfServers" +#define MANAGEMENT_ARGUMENT_OUTPUT "Output" +#define MANAGEMENT_ARGUMENT_PASSWORD "Password" +#define MANAGEMENT_ARGUMENT_PID "PID" +#define MANAGEMENT_ARGUMENT_PORT "Port" +#define MANAGEMENT_ARGUMENT_RESTART "Restart" +#define MANAGEMENT_ARGUMENT_SERVER "Server" +#define MANAGEMENT_ARGUMENT_SERVERS "Servers" +#define MANAGEMENT_ARGUMENT_SERVER_VERSION "ServerVersion" +#define MANAGEMENT_ARGUMENT_START_TIME "StartTime" +#define MANAGEMENT_ARGUMENT_STATE "State" +#define MANAGEMENT_ARGUMENT_STATUS "Status" +#define MANAGEMENT_ARGUMENT_TIME "Time" +#define MANAGEMENT_ARGUMENT_TIMESTAMP "Timestamp" +#define MANAGEMENT_ARGUMENT_TIMESTAMP "Timestamp" +#define MANAGEMENT_ARGUMENT_TOTAL_CONNECTIONS "TotalConnections" +#define MANAGEMENT_ARGUMENT_USERNAME "Username" + +/** + * Management error + */ +#define MANAGEMENT_ERROR_BAD_PAYLOAD 1 +#define MANAGEMENT_ERROR_UNKNOWN_COMMAND 2 +#define MANAGEMENT_ERROR_ALLOCATION 3 + +#define MANAGEMENT_ERROR_METRICS_NOFORK 100 +#define MANAGEMENT_ERROR_METRICS_NETWORK 101 + +#define MANAGEMENT_ERROR_FLUSH_NOFORK 200 +#define MANAGEMENT_ERROR_FLUSH_NETWORK 201 + +#define MANAGEMENT_ERROR_STATUS_NOFORK 700 +#define MANAGEMENT_ERROR_STATUS_NETWORK 701 + +#define MANAGEMENT_ERROR_STATUS_DETAILS_NOFORK 800 +#define MANAGEMENT_ERROR_STATUS_DETAILS_NETWORK 801 + +#define MANAGEMENT_ERROR_CONF_GET_NOFORK 900 +#define MANAGEMENT_ERROR_CONF_GET_NETWORK 901 +#define MANAGEMENT_ERROR_CONF_GET_ERROR 902 + +#define MANAGEMENT_ERROR_CONF_SET_NOFORK 1000 +#define MANAGEMENT_ERROR_CONF_SET_NETWORK 1001 +#define MANAGEMENT_ERROR_CONF_SET_ERROR 1002 +#define MANAGEMENT_ERROR_CONF_SET_NOREQUEST 1003 +#define MANAGEMENT_ERROR_CONF_SET_NOCONFIG_KEY_OR_VALUE 1004 +#define MANAGEMENT_ERROR_CONF_SET_UNKNOWN_SERVER 1005 +#define MANAGEMENT_ERROR_CONF_SET_UNKNOWN_CONFIGURATION_KEY 1006 + +/** + * Output formats + */ +#define MANAGEMENT_OUTPUT_FORMAT_TEXT 0 +#define MANAGEMENT_OUTPUT_FORMAT_JSON 1 +#define MANAGEMENT_OUTPUT_FORMAT_RAW 2 + +/** + * Management operation: Flush the pool + * @param ssl The SSL connection + * @param socket The socket descriptor + * @param mode The flush mode + * @param database The database + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param output_format The output format + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_request_flush(SSL* ssl, int socket, int32_t mode, char* database, uint8_t compression, uint8_t encryption, int32_t output_format); + +/** + * Management operation: Enable database + * @param ssl The SSL connection + * @param socket The socket descriptor + * @param database The database name + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param output_format The output format + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_request_enabledb(SSL* ssl, int socket, char* database, uint8_t compression, uint8_t encryption, int32_t output_format); + +/** + * Management operation: Disable database + * @param ssl The SSL connection + * @param socket The socket descriptor + * @param database The database name + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param output_format The output format + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_request_disabledb(SSL* ssl, int socket, char* database, uint8_t compression, uint8_t encryption, int32_t output_format); + +/** + * Management operation: Gracefully + * @param ssl The SSL connection + * @param socket The socket descriptor + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param output_format The output format + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_request_gracefully(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); + +/** + * Management operation: Stop + * @param ssl The SSL connection + * @param socket The socket descriptor + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param output_format The output format + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_request_shutdown(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); + +/** + * Management operation: Cancel shutdown + * @param ssl The SSL connection + * @param socket The socket descriptor + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param output_format The output format + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_request_cancel_shutdown(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); + +/** + * Management operation: Status + * @param ssl The SSL connection + * @param socket The socket descriptor + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param output_format The output format + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_request_status(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); + +/** + * Management operation: Details + * @param ssl The SSL connection + * @param socket The socket + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param output_format The output format + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_request_details(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); + +/** + * Management operation: isalive + * @param socket The socket + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param output_format The output format + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_request_ping(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); + +/** + * Management operation: Clear + * @param ssl The SSL connection + * @param socket The socket + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param output_format The output format + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_request_clear(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); + +/** + * Management operation: Clear server + * @param ssl The SSL connection + * @param socket The socket + * @param server The server + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param output_format The output format + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_request_clear_server(SSL* ssl, int socket, char* server, uint8_t compression, uint8_t encryption, int32_t output_format); + +/** + * Management operation: Switch to + * @param ssl The SSL connection + * @param socket The socket + * @param server The server + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param output_format The output format + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_request_switch_to(SSL* ssl, int socket, char* server, uint8_t compression, uint8_t encryption, int32_t output_format); + +/** + * Management operation: Reload + * @param ssl The SSL connection + * @param socket The socket + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param output_format The output format + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_request_reload(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); + +/* /\** */ +/* * Management operation: get a configuration setting. */ +/* * This function sends over the socket the message to get a configuration */ +/* * value. */ +/* * In particular, the message block for the action config_get is sent, */ +/* * then the size of the configuration parameter to get (e.g., `max_connections`) */ +/* * and last the parameter name itself. */ +/* * */ +/* * @param ssl the SSL connection */ +/* * @param socket the socket file descriptor */ +/* * @param config_key the name of the configuration parameter to get back */ +/* * @param compression The compress method for wire protocol */ +/* * @param encryption The encrypt method for wire protocol */ +/* * @param output_format The output format */ +/* * @return 0 on success, 1 on error */ +/* *\/ */ +/* int */ +/* pgagroal_management_request_config_get(SSL* ssl, int socket, char* config_key, uint8_t compression, uint8_t encryption, int32_t output_format); */ + +/* /\** */ +/* * Management operation: set a configuration setting. */ +/* * This function sends over the socket the message to set a configuration */ +/* * value. */ +/* * In particular, the message block for the action config_set is sent, */ +/* * then the size of the configuration parameter to set (e.g., `max_connections`), */ +/* * then the parameter name. At this point another couple of "size" and "value" is */ +/* * sent with the size of the value to set and its value. */ +/* * */ +/* * @param ssl the SSL connection */ +/* * @param socket the socket file descriptor */ +/* * @param config_key the name of the configuration parameter to set */ +/* * @param config_value the value to set for the new parameter */ +/* * @param compression The compress method for wire protocol */ +/* * @param encryption The encrypt method for wire protocol */ +/* * @param output_format The output format */ +/* * @return 0 on success, 1 on error */ +/* *\/ */ +/* int */ +/* pgagroal_management_request_config_set(SSL* ssl, int socket, char* config_key, char* config_value, uint8_t compression, uint8_t encryption, int32_t output_format); */ + +/** + * Create a conf ls request + * @param ssl The SSL connection + * @param socket The socket descriptor + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param output_format The output format + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_request_conf_ls(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); + +/** + * Create a conf ls request + * @param ssl The SSL connection + * @param socket The socket descriptor + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param output_format The output format + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_request_conf_get(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format); + +/** + * Create a conf get request + * @param ssl The SSL connection + * @param socket The socket descriptor + * @param config_key The configuration key + * @param config_value The configuration value + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param output_format The output format + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_request_conf_set(SSL* ssl, int socket, char* config_key, char* config_value, uint8_t compression, uint8_t encryption, int32_t output_format); + +/** + * Get the frontend password of a user + * @param ssl The SSL connection + * @param socket The socket descriptor + * @param user The frontend user + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param output_format The output format + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_request_get_password(SSL* ssl, int socket, char* username, uint8_t compression, uint8_t encryption, int32_t output_format); + +/** + * Create an ok response + * @param ssl The SSL connection + * @param socket The socket descriptor + * @param start_time The start time + * @param end_time The end time + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param payload The full payload + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_response_ok(SSL* ssl, int socket, time_t start_time, time_t end_time, uint8_t compression, uint8_t encryption, struct json* payload); + +/** + * Create an error response + * @param ssl The SSL connection + * @param socket The socket descriptor + * @param server The server + * @param error The error code + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param payload The full payload + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_response_error(SSL* ssl, int socket, char* server, int32_t error, uint8_t compression, uint8_t encryption, struct json* payload); + +/** + * Create a response + * @param json The JSON structure + * @param server The server + * @param response The response + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_create_response(struct json* json, int server, struct json** response); + +/** + * Read the management JSON + * @param ssl The SSL connection + * @param socket The socket descriptor + * @param compression The pointer to an integer that will store the compress method + * @param json The JSON structure + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_read_json(SSL* ssl, int socket, uint8_t* compression, uint8_t* encryption, struct json** json); + +/** + * Write the management JSON + * @param ssl The SSL connection + * @param socket The socket descriptor + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param json The JSON structure + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_management_write_json(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, struct json* json); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/memory.h b/src/include/memory.h new file mode 100644 index 00000000..cc47e28b --- /dev/null +++ b/src/include/memory.h @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_MEMORY_H +#define PGAGROAL_MEMORY_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + +/** + * Initialize a memory segment for the process local message structure + */ +void +pgagroal_memory_init(void); + +/** + * Get the message structure + * @return The structure + */ +struct message* +pgagroal_memory_message(void); + +/** + * Free the memory segment + */ +void +pgagroal_memory_free(void); + +/** + * Destroy the memory segment + */ +void +pgagroal_memory_destroy(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/message.h b/src/include/message.h new file mode 100644 index 00000000..71dc027b --- /dev/null +++ b/src/include/message.h @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_MESSAGE_H +#define PGAGROAL_MESSAGE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include +#include + +#include + +#define MESSAGE_STATUS_ZERO 0 +#define MESSAGE_STATUS_OK 1 +#define MESSAGE_STATUS_ERROR 2 + +/** @struct message + * Defines a message + */ +struct message +{ + signed char kind; /**< The kind of the message */ + ssize_t length; /**< The length of the message */ + void* data; /**< The message data */ +} __attribute__ ((aligned (64))); + +/** + * Read a message in blocking mode + * @param ssl The SSL struct + * @param socket The socket descriptor + * @param msg The resulting message + * @return One of MESSAGE_STATUS_ZERO, MESSAGE_STATUS_OK or MESSAGE_STATUS_ERROR + */ +int +pgagroal_read_block_message(SSL* ssl, int socket, struct message** msg); + +/** + * Read a message with a timeout + * @param ssl The SSL struct + * @param socket The socket descriptor + * @param timeout The timeout in seconds + * @param msg The resulting message + * @return One of MESSAGE_STATUS_ZERO, MESSAGE_STATUS_OK or MESSAGE_STATUS_ERROR + */ +int +pgagroal_read_timeout_message(SSL* ssl, int socket, int timeout, struct message** msg); + +/** + * Write a message using a socket + * @param ssl The SSL struct + * @param socket The socket descriptor + * @param msg The message + * @return One of MESSAGE_STATUS_ZERO, MESSAGE_STATUS_OK or MESSAGE_STATUS_ERROR + */ +int +pgagroal_write_message(SSL* ssl, int socket, struct message* msg); + +/** + * Create a message + * @param data A pointer to the data + * @param length The length of the message + * @param msg The resulting message + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_create_message(void* data, ssize_t length, struct message** msg); + +/** + * Free a message + * @param msg The resulting message + */ +void +pgagroal_free_message(struct message* msg); + +/** + * Copy a message + * @param msg The resulting message + * @return The copy + */ +struct message* +pgagroal_copy_message(struct message* msg); + +/** + * Free a copy message + * @param msg The resulting message + */ +void +pgagroal_free_copy_message(struct message* msg); + +/** + * Write an empty message + * @param ssl The SSL struct + * @param socket The socket descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_write_empty(SSL* ssl, int socket); + +/** + * Write a notice message + * @param ssl The SSL struct + * @param socket The socket descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_write_notice(SSL* ssl, int socket); + +/** + * Write a pool is full message + * @param ssl The SSL struct + * @param socket The socket descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_write_pool_full(SSL* ssl, int socket); + +/** + * Write a connection refused message + * @param ssl The SSL struct + * @param socket The socket descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_write_connection_refused(SSL* ssl, int socket); + +/** + * Write a connection refused message (protocol 1 or 2) + * @param ssl The SSL struct + * @param socket The socket descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_write_connection_refused_old(SSL* ssl, int socket); + +/** + * Write a bad password message + * @param ssl The SSL struct + * @param socket The socket descriptor + * @param username The user name + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_write_bad_password(SSL* ssl, int socket, char* username); + +/** + * Write an unsupported security model message + * @param ssl The SSL struct + * @param socket The socket descriptor + * @param username The user name + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_write_unsupported_security_model(SSL* ssl, int socket, char* username); + +/** + * Write a no HBA entry message + * @param ssl The SSL struct + * @param socket The socket descriptor + * @param username The user name + * @param database The database + * @param address The client address + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_write_no_hba_entry(SSL* ssl, int socket, char* username, char* database, char* address); + +/** + * Write a deallocate all message + * @param ssl The SSL struct + * @param socket The socket descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_write_deallocate_all(SSL* ssl, int socket); + +/** + * Write a discard all message + * @param ssl The SSL struct + * @param socket The socket descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_write_discard_all(SSL* ssl, int socket); + +/** + * Write TLS response + * @param ssl The SSL struct + * @param socket The socket descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_write_tls(SSL* ssl, int socket); + +/** + * Write a terminate message + * @param ssl The SSL struct + * @param socket The socket descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_write_terminate(SSL* ssl, int socket); + +/** + * Write a failover message to the client + * @param ssl The SSL struct + * @param socket The socket descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_write_client_failover(SSL* ssl, int socket); + +/** + * Write an auth password message + * @param ssl The SSL struct + * @param socket The socket descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_write_auth_password(SSL* ssl, int socket); + +/** + * Write a rollback message + * @param ssl The SSL struct + * @param socket The socket descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_write_rollback(SSL* ssl, int socket); + +/** + * Create an auth password response message + * @param password The password + * @param msg The resulting message + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_create_auth_password_response(char* password, struct message** msg); + +/** + * Write an auth md5 message + * @param ssl The SSL struct + * @param socket The socket descriptor + * @param salt The salt + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_write_auth_md5(SSL* ssl, int socket, char salt[4]); + +/** + * Create an auth MD5 response message + * @param md5 The md5 + * @param msg The resulting message + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_create_auth_md5_response(char* md5, struct message** msg); + +/** + * Write an auth SCRAM-SHA-256 message + * @param ssl The SSL struct + * @param socket The socket descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_write_auth_scram256(SSL* ssl, int socket); + +/** + * Create an auth SCRAM-SHA-256 response message + * @param nounce The nounce + * @param msg The resulting message + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_create_auth_scram256_response(char* nounce, struct message** msg); + +/** + * Create an auth SCRAM-SHA-256/Continue message + * @param cn The client nounce + * @param sn The server nounce + * @param salt The salt + * @param msg The resulting message + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_create_auth_scram256_continue(char* cn, char* sn, char* salt, struct message** msg); + +/** + * Create an auth SCRAM-SHA-256/Continue response message + * @param wp The without proff + * @param p The proff + * @param msg The resulting message + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_create_auth_scram256_continue_response(char* wp, char* p, struct message** msg); + +/** + * Create an auth SCRAM-SHA-256/Final message + * @param ss The server signature (BASE64) + * @param msg The resulting message + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_create_auth_scram256_final(char* ss, struct message** msg); + +/** + * Write an auth success message + * @param ssl The SSL struct + * @param socket The socket descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_write_auth_success(SSL* ssl, int socket); + +/** + * Create a SSL message + * @param msg The resulting message + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_create_ssl_message(struct message** msg); + +/** + * Create a startup message + * @param username The user name + * @param database The database + * @param msg The resulting message + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_create_startup_message(char* username, char* database, struct message** msg); + +/** + * Create a cancel request message + * @param pid The pid + * @param secret The secret + * @param msg The resulting message + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_create_cancel_request_message(int pid, int secret, struct message** msg); + +/** + * Is the connection valid + * @param socket The socket descriptor + * @return true upon success, otherwise false + */ +bool +pgagroal_connection_isvalid(int socket); + +/** + * Log a message + * @param msg The message + */ +void +pgagroal_log_message(struct message* msg); + +/** + * Read a message using a socket + * @param socket The socket descriptor + * @param msg The resulting message + * @return One of MESSAGE_STATUS_ZERO, MESSAGE_STATUS_OK or MESSAGE_STATUS_ERROR + */ +int +pgagroal_read_socket_message(int socket, struct message** msg); + +/** + * Write a message using a socket + * @param socket The socket descriptor + * @param msg The message + * @return One of MESSAGE_STATUS_ZERO, MESSAGE_STATUS_OK or MESSAGE_STATUS_ERROR + */ +int +pgagroal_write_socket_message(int socket, struct message* msg); + +/** + * Read a message using SSL + * @param ssl The SSL descriptor + * @param msg The resulting message + * @return One of MESSAGE_STATUS_ZERO, MESSAGE_STATUS_OK or MESSAGE_STATUS_ERROR + */ +int +pgagroal_read_ssl_message(SSL* ssl, struct message** msg); + +/** + * Write a message using SSL + * @param ssl The SSL descriptor + * @param msg The message + * @return One of MESSAGE_STATUS_ZERO, MESSAGE_STATUS_OK or MESSAGE_STATUS_ERROR + */ +int +pgagroal_write_ssl_message(SSL* ssl, struct message* msg); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/network.h b/src/include/network.h new file mode 100644 index 00000000..5fe0e4d3 --- /dev/null +++ b/src/include/network.h @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_NETWORK_H +#define PGAGROAL_NETWORK_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * Bind sockets for a host + * @param hostname The host name + * @param port The port number + * @param fds The resulting descriptors + * @param length The resulting length of descriptors + * @param non_blocking Use non blocking + * @param no_delay Use NODELAY + * @param backlog the number of backlogs + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_bind(const char* hostname, int port, int** fds, int* length, bool no_blocking, bool no_delay, int backlog); + +/** + * Bind a Unix Domain Socket + * @param directory The directory + * @param file The file + * @param fd The resulting descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_bind_unix_socket(const char* directory, const char* file, int* fd); + +/** + * Remove Unix Domain Socket directory + * @param directory The directory + * @param file The file + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_remove_unix_socket(const char* directory, const char* file); + +/** + * Connect to a host + * @param hostname The host name + * @param port The port number + * @param fd The resulting descriptor + * @param keep_alive Use keep alive + * @param non_blocking Use non blocking + * @param no_delay Use NODELAY + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_connect(const char* hostname, int port, int* fd, bool keep_alive, bool non_blocking, bool no_delay); + +/** + * Connect to a Unix Domain Socket + * @param directory The directory + * @param file The file + * @param fd The resulting descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_connect_unix_socket(const char* directory, const char* file, int* fd); + +/** + * Is the socket valid + * @param fd The descriptor + * @return True upon success, otherwise false + */ +bool +pgagroal_socket_isvalid(int fd); + +/** + * Disconnect from a descriptor + * @param fd The descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_disconnect(int fd); + +/** + * Get the sockaddr_in structure + * @param sa The sockaddr structure + * @return The sockaddr_in / sockaddr_in6 structure + */ +void* +pgagroal_get_sockaddr(struct sockaddr* sa); + +/** + * Get the address of a sockaddr + * @param sa The sockaddr structure + * @param address The result address + * @param length The length + */ +void +pgagroal_get_address(struct sockaddr* sa, char* address, size_t length); + +/** + * Apply TCP/NODELAY to a descriptor + * @param fd The descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_tcp_nodelay(int fd); + +/** + * Apply O_NONBLOCK to a descriptor + * @param fd The descriptor + * @param value The value + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_socket_nonblocking(int fd, bool value); + +/** + * Does the descriptor have O_NONBLOCK + * @param fd The descriptor + * @return true if non blocking, otherwise false + */ +bool +pgagroal_socket_is_nonblocking(int fd); + +/** + * Does the socket have an error associated + * @param fd The descriptor + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_socket_has_error(int fd); + +/** + * Read bytes from a socket to buffer + * @param ssl The ssl + * @param fd The descriptor + * @param buffer The buffer to write to + * @param buffer_size Size of buffer + * @return The number of bytes read + */ +int +pgagroal_read_socket(SSL* ssl, int fd, char* buffer, size_t buffer_size); + +/** + * Write bytes from a buffer to socket + * @param ssl The ssl + * @param fd The descriptor + * @param buffer The buffer to write to + * @param buffer_size Size of buffer + * @return The number of bytes written + */ +int +pgagroal_write_socket(SSL* ssl, int fd, char* buffer, size_t buffer_size); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/pgagroal.h b/src/include/pgagroal.h new file mode 100644 index 00000000..4df183b9 --- /dev/null +++ b/src/include/pgagroal.h @@ -0,0 +1,592 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_H +#define PGAGROAL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include +#if HAVE_OPENBSD +#include +#endif +#include +#include + +#define PGAGROAL_HOMEPAGE "https://agroal.github.io/pgagroal/" +#define PGAGROAL_ISSUES "https://github.com/agroal/pgagroal/issues" + +#define MAIN_UDS ".s.pgagroal" +#define TRANSFER_UDS ".s.pgagroal.tu" + +#ifdef HAVE_FREEBSD + #define PGAGROAL_DEFAULT_CONFIGURATION_PATH "/usr/local/etc/pgagroal/" +#else + #define PGAGROAL_DEFAULT_CONFIGURATION_PATH "/etc/pgagroal/" +#endif + +#define PGAGROAL_DEFAULT_CONF_FILE PGAGROAL_DEFAULT_CONFIGURATION_PATH "pgagroal.conf" +#define PGAGROAL_DEFAULT_HBA_FILE PGAGROAL_DEFAULT_CONFIGURATION_PATH "pgagroal_hba.conf" +#define PGAGROAL_DEFAULT_LIMIT_FILE PGAGROAL_DEFAULT_CONFIGURATION_PATH "pgagroal_databases.conf" +#define PGAGROAL_DEFAULT_USERS_FILE PGAGROAL_DEFAULT_CONFIGURATION_PATH "pgagroal_users.conf" +#define PGAGROAL_DEFAULT_FRONTEND_USERS_FILE PGAGROAL_DEFAULT_CONFIGURATION_PATH "pgagroal_frontend_users.conf" +#define PGAGROAL_DEFAULT_ADMINS_FILE PGAGROAL_DEFAULT_CONFIGURATION_PATH "pgagroal_admins.conf" +#define PGAGROAL_DEFAULT_SUPERUSER_FILE PGAGROAL_DEFAULT_CONFIGURATION_PATH "pgagroal_superuser.conf" +#define PGAGROAL_DEFAULT_VAULT_CONF_FILE PGAGROAL_DEFAULT_CONFIGURATION_PATH "pgagroal_vault.conf" +#define PGAGROAL_DEFAULT_VAULT_USERS_FILE PGAGROAL_DEFAULT_CONFIGURATION_PATH "pgagroal_vault_users.conf" + +#define MAX_PROCESS_TITLE_LENGTH 256 + +#define DEFAULT_BUFFER_SIZE 131072 +#define SECURITY_BUFFER_SIZE 1024 +#define HTTP_BUFFER_SIZE 1024 + +#define MAX_USERNAME_LENGTH 128 +#define MAX_DATABASE_LENGTH 256 +#define MAX_TYPE_LENGTH 16 +#define MAX_ADDRESS_LENGTH 64 +#define DEFAULT_PASSWORD_LENGTH 64 +#define MIN_PASSWORD_LENGTH 8 +#define MAX_PASSWORD_LENGTH 1024 +#define MAX_APPLICATION_NAME 64 + +#define MAX_PATH 1024 +#define MISC_LENGTH 128 +#define NUMBER_OF_SERVERS 64 +#ifdef DEBUG +#define MAX_NUMBER_OF_CONNECTIONS 8 +#else +#define MAX_NUMBER_OF_CONNECTIONS 10000 +#endif +#define NUMBER_OF_HBAS 64 +#define NUMBER_OF_LIMITS 64 +#define NUMBER_OF_USERS 64 +#define NUMBER_OF_ADMINS 8 +#define NUMBER_OF_DISABLED 64 + +#define NUMBER_OF_SECURITY_MESSAGES 5 + +#define STATE_NOTINIT -2 +#define STATE_INIT -1 +#define STATE_FREE 0 +#define STATE_IN_USE 1 +#define STATE_GRACEFULLY 2 +#define STATE_FLUSH 3 +#define STATE_IDLE_CHECK 4 +#define STATE_MAX_CONNECTION_AGE 5 +#define STATE_VALIDATION 6 +#define STATE_REMOVE 7 + +#define SECURITY_INVALID -2 +#define SECURITY_REJECT -1 +#define SECURITY_TRUST 0 +#define SECURITY_PASSWORD 3 +#define SECURITY_MD5 5 +#define SECURITY_SCRAM256 10 +#define SECURITY_ALL 99 + +#define AUTH_SUCCESS 0 +#define AUTH_BAD_PASSWORD 1 +#define AUTH_ERROR 2 +#define AUTH_TIMEOUT 3 + +#define SERVER_NOTINIT -2 +#define SERVER_NOTINIT_PRIMARY -1 +#define SERVER_PRIMARY 0 +#define SERVER_REPLICA 1 +#define SERVER_FAILOVER 2 +#define SERVER_FAILED 3 + +#define FLUSH_IDLE 0 +#define FLUSH_GRACEFULLY 1 +#define FLUSH_ALL 2 + +#define VALIDATION_OFF 0 +#define VALIDATION_FOREGROUND 1 +#define VALIDATION_BACKGROUND 2 + +#define HISTOGRAM_BUCKETS 18 + +#define HUGEPAGE_OFF 0 +#define HUGEPAGE_TRY 1 +#define HUGEPAGE_ON 2 + +#define ENCRYPTION_NONE 0 +#define ENCRYPTION_AES_256_CBC 1 +#define ENCRYPTION_AES_192_CBC 2 +#define ENCRYPTION_AES_128_CBC 3 +#define ENCRYPTION_AES_256_CTR 4 +#define ENCRYPTION_AES_192_CTR 5 +#define ENCRYPTION_AES_128_CTR 6 + +#define COMPRESSION_NONE 0 +#define COMPRESSION_CLIENT_GZIP 1 +#define COMPRESSION_CLIENT_ZSTD 2 +#define COMPRESSION_CLIENT_LZ4 3 +#define COMPRESSION_CLIENT_BZIP2 4 +#define COMPRESSION_SERVER_GZIP 5 +#define COMPRESSION_SERVER_ZSTD 6 +#define COMPRESSION_SERVER_LZ4 7 + +#define UPDATE_PROCESS_TITLE_NEVER 0 +#define UPDATE_PROCESS_TITLE_STRICT 1 +#define UPDATE_PROCESS_TITLE_MINIMAL 2 +#define UPDATE_PROCESS_TITLE_VERBOSE 3 + +/** + * Constants used to refer to an HBA entry field. + */ +#define PGAGROAL_HBA_ENTRY_TYPE "type" +#define PGAGROAL_HBA_ENTRY_DATABASE "database" +#define PGAGROAL_HBA_ENTRY_USERNAME "username" +#define PGAGROAL_HBA_ENTRY_ADDRESS "address" +#define PGAGROAL_HBA_ENTRY_METHOD "method" + +/** + * Constants used to refer to the limit structure fields + */ +#define PGAGROAL_LIMIT_ENTRY_DATABASE "database" +#define PGAGROAL_LIMIT_ENTRY_USERNAME "username" +#define PGAGROAL_LIMIT_ENTRY_MAX_SIZE "max_size" +#define PGAGROAL_LIMIT_ENTRY_MIN_SIZE "min_size" +#define PGAGROAL_LIMIT_ENTRY_INITIAL_SIZE "initial_size" +#define PGAGROAL_LIMIT_ENTRY_LINENO "line_number" + +/** + * Constants used to manage the exit code + * of a command sent over the socket in the + * management stuff, e.g., `pgagroal-cli`. + */ +#define EXIT_STATUS_OK 0 +#define EXIT_STATUS_CONNECTION_ERROR 1 +#define EXIT_STATUS_DATA_ERROR 2 + +#define INDENT_PER_LEVEL 2 +#define FORMAT_JSON 0 +#define FORMAT_TEXT 1 +#define FORMAT_JSON_COMPACT 2 +#define BULLET_POINT "- " + +#define likely(x) __builtin_expect (!!(x), 1) +#define unlikely(x) __builtin_expect (!!(x), 0) + +#define EMPTY_STR(_s) (_s[0] == 0) + +#define MAX(a, b) \ + ({ __typeof__ (a) _a = (a); \ + __typeof__ (b) _b = (b); \ + _a > _b ? _a : _b; }) + +#define MIN(a, b) \ + ({ __typeof__ (a) _a = (a); \ + __typeof__ (b) _b = (b); \ + _a < _b ? _a : _b; }) + +/* + * Common piece of code to perform a sleeping. + * + * @param zzz the amount of time to + * sleep, expressed as nanoseconds. + * + * Example + SLEEP(5000000L) + * + */ +#define SLEEP(zzz) \ + do \ + { \ + struct timespec ts_private; \ + ts_private.tv_sec = 0; \ + ts_private.tv_nsec = zzz; \ + nanosleep(&ts_private, NULL); \ + } while (0); + +/* + * Commonly used block of code to sleep + * for a specified amount of time and + * then jump back to a specified label. + * + * @param zzz how much time to sleep (as long nanoseconds) + * @param goto_to the label to which jump to + * + * Example: + * + ... + else + SLEEP_AND_GOTO(100000L, retry) + */ +#define SLEEP_AND_GOTO(zzz, goto_to) \ + do \ + { \ + struct timespec ts_private; \ + ts_private.tv_sec = 0; \ + ts_private.tv_nsec = zzz; \ + nanosleep(&ts_private, NULL); \ + goto goto_to; \ + } while (0); + +/** + * The shared memory segment + */ +extern void* shmem; + +/** + * The shared memory segment for a pipeline + */ +extern void* pipeline_shmem; + +/** + * The shared memory segment for the Prometheus data + */ +extern void* prometheus_shmem; + +/** + * Shared memory used to contain the Prometheus + * response cache. + */ +extern void* prometheus_cache_shmem; + +/** @struct server + * Defines a server + */ +struct server +{ + char name[MISC_LENGTH]; /**< The name of the server */ + char host[MISC_LENGTH]; /**< The host name of the server */ + int port; /**< The port of the server */ + bool tls; /**< Use TLS if possible */ + char tls_cert_file[MISC_LENGTH]; /**< TLS certificate path */ + char tls_key_file[MISC_LENGTH]; /**< TLS key path */ + char tls_ca_file[MISC_LENGTH]; /**< TLS CA certificate path */ + atomic_schar state; /**< The state of the server */ + int lineno; /**< The line number within the configuration file */ +} __attribute__ ((aligned (64))); + +/** @struct connection + * Defines a connection + */ +struct connection +{ + char username[MAX_USERNAME_LENGTH]; /**< The user name */ + char database[MAX_DATABASE_LENGTH]; /**< The database */ + char appname[MAX_APPLICATION_NAME]; /**< The application_name */ + + bool new; /**< Is the connection new */ + signed char server; /**< The server identifier */ + bool tx_mode; /**< Connection in transaction mode */ + + signed char has_security; /**< The security identifier */ + ssize_t security_lengths[NUMBER_OF_SECURITY_MESSAGES]; /**< The lengths of the security messages */ + char security_messages[NUMBER_OF_SECURITY_MESSAGES][SECURITY_BUFFER_SIZE]; /**< The security messages */ + + int backend_pid; /**< The backend process id */ + int backend_secret; /**< The backend secret */ + + signed char limit_rule; /**< The limit rule used */ + time_t start_time; /**< The start timestamp */ + time_t timestamp; /**< The last used timestamp */ + pid_t pid; /**< The associated process id */ + int fd; /**< The descriptor */ +} __attribute__ ((aligned (64))); + +/** @struct hba + * Defines a HBA entry + */ +struct hba +{ + char type[MAX_TYPE_LENGTH]; /**< The type */ + char database[MAX_DATABASE_LENGTH]; /**< The database */ + char username[MAX_USERNAME_LENGTH]; /**< The user name */ + char address[MAX_ADDRESS_LENGTH]; /**< The address / mask */ + char method[MAX_ADDRESS_LENGTH]; /**< The access method */ + int lineno; /**< The line number within the configuration file */ +} __attribute__ ((aligned (64))); + +/** @struct limit + * Defines a limit entry + */ +struct limit +{ + char database[MAX_DATABASE_LENGTH]; /**< The database */ + char username[MAX_USERNAME_LENGTH]; /**< The user name */ + atomic_ushort active_connections; /**< The active number of connections */ + int max_size; /**< The maximum pool size */ + int initial_size; /**< The initial pool size */ + int min_size; /**< The minimum pool size */ + int lineno; /**< The line number within the configuration file */ +} __attribute__ ((aligned (64))); + +/** @struct user + * Defines a user + */ +struct user +{ + char username[MAX_USERNAME_LENGTH]; /**< The user name */ + char password[MAX_PASSWORD_LENGTH]; /**< The password */ +} __attribute__ ((aligned (64))); + +/** @struct vault_server + * Defines a vault server + */ +struct vault_server +{ + struct server server; /**< The server */ + struct user user; /**< The user */ +} __attribute__ ((aligned (64))); + +/** @struct prometheus_connection + * Defines the Prometheus connection metric + */ +struct prometheus_connection +{ + atomic_ullong query_count; /**< The number of queries per connection */ +} __attribute__ ((aligned (64))); + +/** @struct prometheus_cache + * A structure to handle the Prometheus response + * so that it is possible to serve the very same + * response over and over depending on the cache + * settings. + * + * The `valid_until` field stores the result + * of `time(2)`. + * + * The cache is protected by the `lock` field. + * + * The `size` field stores the size of the allocated + * `data` payload. + */ +struct prometheus_cache +{ + time_t valid_until; /**< when the cache will become not valid */ + atomic_schar lock; /**< lock to protect the cache */ + size_t size; /**< size of the cache */ + char data[]; /**< the payload */ +} __attribute__ ((aligned (64))); + +/** @struct prometheus + * Defines the common Prometheus metrics + */ +struct prometheus +{ + // logging + atomic_ulong logging_info; /**< Logging: INFO */ + atomic_ulong logging_warn; /**< Logging: WARN */ + atomic_ulong logging_error; /**< Logging: ERROR */ + atomic_ulong logging_fatal; /**< Logging: FATAL */ + + // internal connections + atomic_int client_sockets; /**< The number of sockets the client used */ + atomic_int self_sockets; /**< The number of sockets used by pgagroal itself */ + +} __attribute__ ((aligned (64))); + +/** @struct main_prometheus + * Defines the Main Prometheus metrics + */ +struct main_prometheus +{ + struct prometheus prometheus_base; /**< Common base class */ + atomic_ulong session_time[HISTOGRAM_BUCKETS]; /**< The histogram buckets */ + atomic_ulong session_time_sum; /**< Total session time */ + + atomic_ulong connection_error; /**< The number of error calls */ + atomic_ulong connection_kill; /**< The number of kill calls */ + atomic_ulong connection_remove; /**< The number of remove calls */ + atomic_ulong connection_timeout; /**< The number of timeout calls */ + atomic_ulong connection_return; /**< The number of return calls */ + atomic_ulong connection_invalid; /**< The number of invalid calls */ + atomic_ulong connection_get; /**< The number of get calls */ + atomic_ulong connection_idletimeout; /**< The number of idle timeout calls */ + atomic_ulong connection_max_connection_age; /**< The number of max connection age calls */ + atomic_ulong connection_flush; /**< The number of flush calls */ + atomic_ulong connection_success; /**< The number of success calls */ + + /**< The number of connection awaiting due to `blocking_timeout` */ + atomic_ulong connections_awaiting[NUMBER_OF_LIMITS]; /**< The number of connection waiting per limit */ + atomic_ulong connections_awaiting_total; /**< The number of connection waiting in total */ + + atomic_ulong auth_user_success; /**< The number of AUTH_SUCCESS calls */ + atomic_ulong auth_user_bad_password; /**< The number of AUTH_BAD_PASSWORD calls */ + atomic_ulong auth_user_error; /**< The number of AUTH_ERROR calls */ + + atomic_ulong client_wait; /**< The number of waiting clients */ + atomic_ulong client_active; /**< The number of active clients */ + atomic_ulong client_wait_time; /**< The time the client waits */ + + atomic_ullong query_count; /**< The number of queries */ + atomic_ullong tx_count; /**< The number of transactions */ + + atomic_ullong network_sent; /**< The bytes sent by clients */ + atomic_ullong network_received; /**< The bytes received from servers */ + + atomic_ulong server_error[NUMBER_OF_SERVERS]; /**< The number of errors for a server */ + atomic_ulong failed_servers; /**< The number of failed servers */ + struct prometheus_connection prometheus_connections[]; /**< The number of prometheus connections (FMA) */ + +} __attribute__ ((aligned (64))); + +/** @struct vault_prometheus + * Defines the Vault Prometheus metrics + */ +struct vault_prometheus +{ + struct prometheus prometheus_base; /**< The Prometheus base */ +} __attribute__ ((aligned (64))); + +/** @struct configuration + * Defines the common configurations between pgagroal and vault + */ +struct configuration +{ + char configuration_path[MAX_PATH]; /**< The configuration path */ + char host[MISC_LENGTH]; /**< The host */ + int port; /**< The port */ + int authentication_timeout; /**< The authentication timeout in seconds */ + + // Logging + int log_type; /**< The logging type */ + int log_level; /**< The logging level */ + char log_path[MISC_LENGTH]; /**< The logging path */ + bool log_connections; /**< Log successful logins */ + bool log_disconnections; /**< Log disconnects */ + int log_mode; /**< The logging mode */ + unsigned int log_rotation_size; /**< bytes to force log rotation */ + unsigned int log_rotation_age; /**< minutes for log rotation */ + char log_line_prefix[MISC_LENGTH]; /**< The logging prefix */ + atomic_schar log_lock; /**< The logging lock */ + char default_log_path[MISC_LENGTH]; /**< The default logging path */ + + // TLS support + bool tls; /**< Is TLS enabled */ + char tls_cert_file[MISC_LENGTH]; /**< TLS certificate path */ + char tls_key_file[MISC_LENGTH]; /**< TLS key path */ + char tls_ca_file[MISC_LENGTH]; /**< TLS CA certificate path */ + // Prometheus + unsigned char hugepage; /**< Huge page support */ + int metrics; /**< The metrics port */ + unsigned int metrics_cache_max_age; /**< Number of seconds to cache the Prometheus response */ + unsigned int metrics_cache_max_size; /**< Number of bytes max to cache the Prometheus response */ +}; + +/** @struct vault_configuration + * Defines the configuration of pgagroal-vault + */ +struct vault_configuration +{ + struct configuration common; /**< Common base class */ + char users_path[MAX_PATH]; /**< The configuration path */ + int number_of_users; /**< The number of users */ + struct vault_server vault_server; /**< The vault servers */ +} __attribute__ ((aligned (64))); + +/** @struct main_configuration + * Defines the configuration and state of pgagroal + */ +struct main_configuration +{ + struct configuration common; /**< Common configurations */ + char hba_path[MAX_PATH]; /**< The HBA path */ + char limit_path[MAX_PATH]; /**< The limit path */ + char users_path[MAX_PATH]; /**< The users path */ + char frontend_users_path[MAX_PATH];/**< The frontend users path */ + char admins_path[MAX_PATH]; /**< The admins path */ + char superuser_path[MAX_PATH]; /**< The superuser path */ + + int management; /**< The management port */ + bool gracefully; /**< Is pgagroal in gracefully mode */ + + bool all_disabled; /**< Are all databases disabled */ + char disabled[NUMBER_OF_DISABLED][MAX_DATABASE_LENGTH]; /**< Which databases are disabled */ + + int pipeline; /**< The pipeline type */ + + bool failover; /**< Is failover enabled */ + char failover_script[MISC_LENGTH]; /**< The failover script */ + + unsigned int update_process_title; /**< Behaviour for updating the process title */ + + bool authquery; /**< Is authentication query enabled */ + + atomic_ushort active_connections; /**< The active number of connections */ + int max_connections; /**< The maximum number of connections */ + bool allow_unknown_users; /**< Allow unknown users */ + + int blocking_timeout; /**< The blocking timeout in seconds */ + int idle_timeout; /**< The idle timeout in seconds */ + int rotate_frontend_password_timeout; /**< The rotation frontend password timeout in seconds */ + int rotate_frontend_password_length; /**< Length of randomised passwords */ + int max_connection_age; /**< The max connection age in seconds */ + int validation; /**< Validation mode */ + int background_interval; /**< Background validation timer in seconds */ + int max_retries; /**< The maximum number of retries */ + int disconnect_client; /**< Disconnect client if idle for more than the specified seconds */ + bool disconnect_client_force; /**< Force a disconnect client if active for more than the specified seconds */ + char pidfile[MAX_PATH]; /**< File containing the PID */ + + char libev[MISC_LENGTH]; /**< Name of libev mode */ + bool keep_alive; /**< Use keep alive */ + bool nodelay; /**< Use NODELAY */ + bool non_blocking; /**< Use non blocking */ + int backlog; /**< The backlog for listen */ + bool tracker; /**< Tracker support */ + bool track_prepared_statements; /**< Track prepared statements (transaction pooling) */ + + char unix_socket_dir[MISC_LENGTH]; /**< The directory for the Unix Domain Socket */ + + atomic_schar su_connection; /**< The superuser connection */ + + int number_of_servers; /**< The number of servers */ + int number_of_hbas; /**< The number of HBA entries */ + int number_of_limits; /**< The number of limit entries */ + int number_of_users; /**< The number of users */ + int number_of_frontend_users; /**< The number of users */ + int number_of_admins; /**< The number of admins */ + + atomic_schar states[MAX_NUMBER_OF_CONNECTIONS]; /**< The states */ + struct server servers[NUMBER_OF_SERVERS]; /**< The servers */ + struct hba hbas[NUMBER_OF_HBAS]; /**< The HBA entries */ + struct limit limits[NUMBER_OF_LIMITS]; /**< The limit entries */ + struct user users[NUMBER_OF_USERS]; /**< The users */ + struct user frontend_users[NUMBER_OF_USERS]; /**< The frontend users */ + struct user admins[NUMBER_OF_ADMINS]; /**< The admins */ + struct user superuser; /**< The superuser */ + struct connection connections[]; /**< The connections (FMA) */ +} __attribute__ ((aligned (64))); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/pipeline.h b/src/include/pipeline.h new file mode 100644 index 00000000..589c2e42 --- /dev/null +++ b/src/include/pipeline.h @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_PIPELINE_H +#define PGAGROAL_PIPELINE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include +#include + +#define PIPELINE_AUTO -1 +#define PIPELINE_PERFORMANCE 0 +#define PIPELINE_SESSION 1 +#define PIPELINE_TRANSACTION 2 + +typedef int (* initialize)(void*, void**, size_t*); +typedef void (* start)(struct ev_loop*, struct worker_io*); +typedef void (* callback)(struct ev_loop*, struct ev_io*, int); +typedef void (* stop)(struct ev_loop*, struct worker_io*); +typedef void (* destroy)(void*, size_t); +typedef void (* periodic)(void); + +/** @struct pipeline + * Define the structure for a pipeline + */ +struct pipeline +{ + initialize initialize; /**< The initialize function for the pipeline */ + start start; /**< The start function */ + callback client; /**< The callback for the client */ + callback server; /**< The callback for the server */ + stop stop; /**< The stop function */ + destroy destroy; /**< The destroy function for the pipeline */ + periodic periodic; /**< The periodic function for the pipeline */ +}; + +/** + * Get the performance pipeline + * @return The structure + */ +struct pipeline performance_pipeline(void); + +/** + * Get the session pipeline + * @return The structure + */ +struct pipeline session_pipeline(void); + +/** + * Get the transaction pipeline + * @return The structure + */ +struct pipeline transaction_pipeline(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/pool.h b/src/include/pool.h new file mode 100644 index 00000000..bfbf3180 --- /dev/null +++ b/src/include/pool.h @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_POOL_H +#define PGAGROAL_POOL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include +#include + +/** + * Get a connection + * @param username The user name + * @param database The database + * @param reuse Should a slot be reused + * @param transaction_mode Obtain a connection in transaction mode + * @param slot The resulting slot + * @param ssl The resulting SSL (can be NULL) + * @return 0 upon success, 1 if pool is full, otherwise 2 + */ +int +pgagroal_get_connection(char* username, char* database, bool reuse, bool transaction_mode, int* slot, SSL** ssl); + +/** + * Return a connection + * @param slot The slot + * @param ssl The SSL connection (can be NULL) + * @param transaction_mode Is the connection returned in transaction mode + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_return_connection(int slot, SSL* ssl, bool transaction_mode); + +/** + * Kill a connection + * @param slot The slot + * @param ssl The SSL connection (can be NULL) + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_kill_connection(int slot, SSL* ssl); + +/** + * Perform idle timeout + */ +void +pgagroal_idle_timeout(void); + +/** + * Perform max connection age check + */ +void +pgagroal_max_connection_age(void); + +/** + * Perform connection validation + */ +void +pgagroal_validation(void); + +/** + * Flush the pool (JSON) + * @param mode The mode + * @param database The database + */ +void +pgagroal_flush(int mode, char* database); + +/** + * Flush the pool for a specific server + * @param server The server + */ +void +pgagroal_flush_server(signed char server); + +/** + * Flush the pool (JSON) + * @param ssl The SSL connection + * @param client_fd The client + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param payload The payload + */ +void +pgagroal_request_flush(SSL* ssl, int client_fd, uint8_t compression, uint8_t encryption, struct json* payload); + +/** + * Prefill the pool + * @param initial Use initial size + */ +void +pgagroal_prefill(bool initial); + +/** + * Initialize the pool + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_pool_init(void); + +/** + * Shutdown the pool + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_pool_shutdown(void); + +/** + * Print the status of the pool + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_pool_status(void); + +/** + * This function wraps around the logic to call `pgagroal_prefill()`. + * In order to avoid code repetition, this function can be used safely + * wherever there is the possibility to activate the prefill. The function + * does check if the configuration allows for a prefill, and in such case + * tries to `fork(2)` and executes the prefill. + * Also, the function checks for the presence of a primary with + * `pgagroal_get_primary()` and refuses to do a prefill if there + * is no primary at all. + * + * @param do_fork Run the prefill in a separate process + * @param initial true if the prefill has to be done with the INITIAL + * value of the pgagroal_database.conf file, false if it has + * to be done with the MINIMAL value. + * + */ +void +pgagroal_prefill_if_can(bool do_fork, bool initial); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/prometheus.h b/src/include/prometheus.h new file mode 100644 index 00000000..091e15c1 --- /dev/null +++ b/src/include/prometheus.h @@ -0,0 +1,357 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_PROMETHEUS_H +#define PGAGROAL_PROMETHEUS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/* + * Value to disable the Prometheus cache, + * it is equivalent to set `metrics_cache` + * to 0 (seconds). + */ +#define PGAGROAL_PROMETHEUS_CACHE_DISABLED 0 + +/** + * Max size of the cache (in bytes). + * If the cache request exceeds this size + * the caching should be aborted in some way. + */ +#define PROMETHEUS_MAX_CACHE_SIZE (1024 * 1024) + +/** + * The default cache size in the case + * the user did not set any particular + * configuration option. + */ +#define PROMETHEUS_DEFAULT_CACHE_SIZE (256 * 1024) + +/** + * Create a prometheus instance + * @param fd The client descriptor + */ +void +pgagroal_prometheus(int fd); + +/** + * Create a prometheus instance for vault + * @param fd The client descriptor + */ +void +pgagroal_vault_prometheus(int fd); + +/** + * Initialize prometheus shmem + */ +int +pgagroal_init_prometheus(size_t* p_size, void** p_shmem); + +/** + * Initialize prometheus shmem for vault + */ +int +pgagroal_vault_init_prometheus(size_t* p_size, void** p_shmem); + +/** + * Add session time information + * @param time The time + */ +void +pgagroal_prometheus_session_time(double time); + +/** + * Connection error + */ +void +pgagroal_prometheus_connection_error(void); + +/** + * Connection kill + */ +void +pgagroal_prometheus_connection_kill(void); + +/** + * Connection remove + */ +void +pgagroal_prometheus_connection_remove(void); + +/** + * Connection timeout + */ +void +pgagroal_prometheus_connection_timeout(void); + +/** + * Connection return + */ +void +pgagroal_prometheus_connection_return(void); + +/** + * Connection invalid + */ +void +pgagroal_prometheus_connection_invalid(void); + +/** + * Connection get + */ +void +pgagroal_prometheus_connection_get(void); + +/** + * Connection idle timeout + */ +void +pgagroal_prometheus_connection_idletimeout(void); + +/** + * Connection max connection age + */ +void +pgagroal_prometheus_connection_max_connection_age(void); + +/** + * Connection awaiting due to `blocking_timeout`. + * Tracks the total awaiting connections and also the + * per-limit ones. + * + * + * Every call to this function should be paired + * by the same number of calls + * to `pgagroal_prometheus_connection_unawaiting()` + * + * + * @param limit_index if greater or equal to zero + * tracks the awaiting connection for the limits entry + * (i.e., per user and database) + */ +void +pgagroal_prometheus_connection_awaiting(int limit_index); + +/** + * An awaiting conection, i.e., one holded by `blocking_timeout` + * that is no more on hold and can restart its workflo. + * + * + * + * Every call to this function should be after + * the call + * to `pgagroal_prometheus_connection_awaiting()` + * + * + * The function decreases the total counter of the awaiting connections as + * well as the per-limit ones. + * + * @param limit_entry if greater or equal to zero + * it untracks the corresponding limit entry + */ +void +pgagroal_prometheus_connection_unawaiting(int limit_index); + +/** + * Connection flush + */ +void +pgagroal_prometheus_connection_flush(void); + +/** + * Connection success + */ +void +pgagroal_prometheus_connection_success(void); + +/** + * Increase AUTH_SUCCESS for a user + */ +void +pgagroal_prometheus_auth_user_success(void); + +/** + * Increase AUTH_BAD_PASSWORD for a user + */ +void +pgagroal_prometheus_auth_user_bad_password(void); + +/** + * Increase AUTH_ERROR for a user + */ +void +pgagroal_prometheus_auth_user_error(void); + +/** + * Increase client_wait by 1 + */ +void +pgagroal_prometheus_client_wait_add(void); + +/** + * Decrease client_wait by 1 + */ +void +pgagroal_prometheus_client_wait_sub(void); + +/** + * Increase client_active by 1 + */ +void +pgagroal_prometheus_client_active_add(void); + +/** + * Decrease client_active by 1 + */ +void +pgagroal_prometheus_client_active_sub(void); + +/** + * Increase query_count by 1 + */ +void +pgagroal_prometheus_query_count_add(void); + +/** + * Increase query_count for the specified connection by 1 + * @param slot The connection slot + */ +void +pgagroal_prometheus_query_count_specified_add(int slot); + +/** + * Reset query_count for the specified connection + * @param slot The connection slot + */ +void +pgagroal_prometheus_query_count_specified_reset(int slot); + +/** + * Increase tx_count by 1 + */ +void +pgagroal_prometheus_tx_count_add(void); + +/** + * Increase network_sent + * @param s The size + */ +void +pgagroal_prometheus_network_sent_add(ssize_t s); + +/** + * Increase network_received + * @param s The size + */ +void +pgagroal_prometheus_network_received_add(ssize_t s); + +/** + * Increase client_sockets by 1 + */ +void +pgagroal_prometheus_client_sockets_add(void); + +/** + * Decrease client_sockets by 1 + */ +void +pgagroal_prometheus_client_sockets_sub(void); + +/** + * Increase self_sockets by 1 + */ +void +pgagroal_prometheus_self_sockets_add(void); + +/** + * Decrease self_sockets by 1 + */ +void +pgagroal_prometheus_self_sockets_sub(void); + +/** + * Reset the counters and histograms + */ +void +pgagroal_prometheus_clear(void); + +/** + * Increase SERVER_ERROR for a server + * @param server The server + */ +void +pgagroal_prometheus_server_error(int server); + +/** + * Count failed servers + */ +void +pgagroal_prometheus_failed_servers(void); + +/** + * Add a logging count + * @param logging The logging type + */ +void +pgagroal_prometheus_logging(int logging); + +/** + * Allocates, for the first time, the Prometheus cache. + * + * The cache structure, as well as its dynamically sized payload, + * are created as shared memory chunks. + * + * Assumes the shared memory for the cofiguration is already set. + * + * The cache will be allocated as soon as this method is invoked, + * even if the cache has not been configured at all! + * + * If the memory cannot be allocated, the function issues errors + * in the logs and disables the caching machinaery. + * + * @param p_size a pointer to where to store the size of + * allocated chunk of memory + * @param p_shmem the pointer to the pointer at which the allocated chunk + * of shared memory is going to be inserted + * + * @return 0 on success + */ +int +pgagroal_init_prometheus_cache(size_t* p_size, void** p_shmem); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/remote.h b/src/include/remote.h new file mode 100644 index 00000000..3b3d8ca5 --- /dev/null +++ b/src/include/remote.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_REMOTE_H +#define PGAGROAL_REMOTE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * Create a remote management instance + * @param fd The client descriptor + * @param address The client address + */ +void +pgagroal_remote_management(int fd, char* address); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/security.h b/src/include/security.h new file mode 100644 index 00000000..fbb555db --- /dev/null +++ b/src/include/security.h @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_SECURITY_H +#define PGAGROAL_SECURITY_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include + +#include + +/** + * Authenticate a user + * @param client_fd The descriptor + * @param address The client address + * @param slot The resulting slot + * @param client_ssl The client SSL context + * @param server_ssl The server SSL context + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_authenticate(int client_fd, char* address, int* slot, SSL** client_ssl, SSL** server_ssl); + +/** + * Authenticate a prefill connection + * @param username The user name + * @param password The password + * @param database The database + * @param slot The resulting slot + * @param server_ssl The server SSL context + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_prefill_auth(char* username, char* password, char* database, int* slot, SSL** server_ssl); + +/** + * Authenticate a remote management user + * @param client_fd The descriptor + * @param address The client address + * @param client_ssl The client SSL context + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_remote_management_auth(int client_fd, char* address, SSL** client_ssl); + +/** + * Connect using SCRAM-SHA256 + * @param username The user name + * @param password The password + * @param server_fd The descriptor + * @param s_ssl The SSL context + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_remote_management_scram_sha256(char* username, char* password, int server_fd, SSL** s_ssl); + +/** + * Get the master key + * @param masterkey The master key + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_get_master_key(char** masterkey); + +/** + * MD5 a string + * @param str The string + * @param md5 The MD5 string + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_md5(char* str, int length, char** md5); + +/** + * Is the user known to the system + * @param user The user name + * @return True if known, otherwise false + */ +bool +pgagroal_user_known(char* user); + +/** + * Is the TLS configuration valid + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_tls_valid(void); + +/** + * @brief Generate a random ASCII password have size of pwd_length + * @param password the resultant password + * @param password_length length of the password + * @return 0 if success, otherwise 1 + */ +int +pgagroal_generate_password(int password_length, char** password); + +/** + * @brief Accept the SSL connection for the vault from client (curl) + * @param config the vault configuration + * @param client_fd the descriptor + * @param c_ssl the client SSL context + * @return 0 if success, otherwise 1 + */ +int +accept_ssl_vault(struct vault_configuration* config, int client_fd, SSL** c_ssl); + +/** + * @brief Initialize RNG + * + */ +void +pgagroal_initialize_random(void); +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/server.h b/src/include/server.h new file mode 100644 index 00000000..5e3c620a --- /dev/null +++ b/src/include/server.h @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_SERVER_H +#define PGAGROAL_SERVER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include +#include + +/** + * Get the primary server + * @param server The resulting server identifier + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_get_primary(int* server); + +/** + * Update the server state + * @param slot The slot + * @param socket The descriptor + * @param ssl The SSL connection + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_update_server_state(int slot, int socket, SSL* ssl); + +/** + * Print the state of the servers + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_server_status(void); + +/** + * Failover + * @param slot The slot + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_server_failover(int slot); + +/** + * Force failover + * @param server The server + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_server_force_failover(int server); + +/** + * Clear server + * @param server The server + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_server_clear(char* server); + +/** + * Switch server + * @param server The server + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_server_switch(char* server); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/shmem.h b/src/include/shmem.h new file mode 100644 index 00000000..25c9f73d --- /dev/null +++ b/src/include/shmem.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_SHMEM_H +#define PGAGROAL_SHMEM_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * Create a shared memory segment + * @param size The size of the segment + * @param hp Huge page value + * @parma shmem The shared memory segment + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_create_shared_memory(size_t size, unsigned char hp, void** shmem); + +/** + * Resize a shared memory segment + * @param size The size of the segment + * @param shmem The pointer to the segment + * @param new_size The size of the new segment + * @param new_shmem The pointer to the new segment + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_resize_shared_memory(size_t size, void* shmem, size_t* new_size, void** new_shmem); + +/** + * Destroy a shared memory segment + * @param shmem The shared memory segment + * @param size The size + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_destroy_shared_memory(void* shmem, size_t size); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/status.h b/src/include/status.h new file mode 100644 index 00000000..47d86dcc --- /dev/null +++ b/src/include/status.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_STATUS_H +#define PGAGROAL_STATUS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include + +/** + * Create an status + * @param ssl The SSL connection + * @param client_fd The client + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param payload The payload + */ +void +pgagroal_status(SSL* ssl, int client_fd, uint8_t compression, uint8_t encryption, struct json* payload); + +/** + * Create an status details + * @param ssl The SSL connection + * @param client_fd The client + * @param compression The compress method for wire protocol + * @param encryption The encrypt method for wire protocol + * @param payload The payload + */ +void +pgagroal_status_details(SSL* ssl, int client_fd, uint8_t compression, uint8_t encryption, struct json* payload); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/tracker.h b/src/include/tracker.h new file mode 100644 index 00000000..b3689f67 --- /dev/null +++ b/src/include/tracker.h @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_TRACKER_H +#define PGAGROAL_TRACKER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include + +#define TRACKER_CLIENT_START 0 +#define TRACKER_CLIENT_STOP 1 + +#define TRACKER_GET_CONNECTION_SUCCESS 2 +#define TRACKER_GET_CONNECTION_TIMEOUT 3 +#define TRACKER_GET_CONNECTION_ERROR 4 +#define TRACKER_RETURN_CONNECTION_SUCCESS 5 +#define TRACKER_RETURN_CONNECTION_KILL 6 +#define TRACKER_KILL_CONNECTION 7 + +#define TRACKER_AUTHENTICATE 8 + +#define TRACKER_BAD_CONNECTION 9 +#define TRACKER_IDLE_TIMEOUT 10 +#define TRACKER_MAX_CONNECTION_AGE 11 +#define TRACKER_INVALID_CONNECTION 12 +#define TRACKER_FLUSH 13 +#define TRACKER_REMOVE_CONNECTION 14 + +#define TRACKER_PREFILL 15 +#define TRACKER_PREFILL_RETURN 16 +#define TRACKER_PREFILL_KILL 17 +#define TRACKER_WORKER_RETURN1 18 +#define TRACKER_WORKER_RETURN2 19 +#define TRACKER_WORKER_KILL1 20 +#define TRACKER_WORKER_KILL2 21 + +#define TRACKER_TX_RETURN_CONNECTION_START 30 +#define TRACKER_TX_RETURN_CONNECTION_STOP 31 +#define TRACKER_TX_GET_CONNECTION 32 +#define TRACKER_TX_RETURN_CONNECTION 33 + +#define TRACKER_SOCKET_ASSOCIATE_CLIENT 100 +#define TRACKER_SOCKET_ASSOCIATE_SERVER 101 +#define TRACKER_SOCKET_DISASSOCIATE_CLIENT 102 +#define TRACKER_SOCKET_DISASSOCIATE_SERVER 103 + +/** + * Tracking event: Basic + * @param id The event identifier + * @param username The user name + * @param database The database + */ +void +pgagroal_tracking_event_basic(int id, char* username, char* database); + +/** + * Tracking event: Slot + * @param id The event identifier + * @param slot The slot + */ +void +pgagroal_tracking_event_slot(int id, int slot); + +/** + * Tracking event: Socket + * @param id The event identifier + * @param socket The socket + */ +void +pgagroal_tracking_event_socket(int id, int socket); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/utils.h b/src/include/utils.h new file mode 100644 index 00000000..6b26a245 --- /dev/null +++ b/src/include/utils.h @@ -0,0 +1,593 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_UTILS_H +#define PGAGROAL_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + +/** @struct signal_info + * Defines the signal structure + */ +struct signal_info +{ + struct ev_signal signal; /**< The libev base type */ + int slot; /**< The slot */ +}; + +/** @struct accept_io + * Defines the accept io structure + */ +struct accept_io +{ + struct ev_io io; /**< The I/O */ + int socket; /**< The socket */ + char** argv; /**< The argv */ +}; + +/** @struct client + * Defines the client structure + */ +struct client +{ + pid_t pid; /**< The process id */ + struct client* next; /**< The next client */ +}; + +/** @struct pgagroal_command + * Defines pgagroal commands. + * The necessary fields are marked with an ">". + * + * Fields: + * > command: The primary name of the command. + * > subcommand: The subcommand name. If there is no subcommand, it should be filled with an empty literal string. + * > accepted_argument_count: An array defining all the number of arguments this command accepts. + * Each entry represents a valid count of arguments, allowing the command to support overloads. + * - default_argument: A default value for the command argument, used when no explicit argument is provided. + * - log_message: A template string for logging command execution, which can include placeholders for dynamic values. + * > action: A value indicating the specific action. + * - mode: A value specifying the mode of operation or context in which the command applies. + * > deprecated: A flag indicating whether this command is deprecated. + * - deprecated_since_major: The major version number in which the command was deprecated. + * - deprecated_since_minor: The minor version number in which the command was deprecated. + * - deprecated_by: A string naming the command that replaces the deprecated command. + * + * This struct is key to extending and maintaining the command processing functionality in pgagroal, + * allowing for clear definition and handling of all supported commands. + */ +struct pgagroal_command +{ + const char* command; /**< The command */ + const char* subcommand; /**< The sub-command */ + const int accepted_argument_count[MISC_LENGTH]; /**< The array of accepted arguments */ + + const int action; /**< The action */ + const int mode; /**< The mode of the action */ + const char* default_argument; /**< The default argument */ + const char* log_message; /**< The log message */ + + /* Deprecation information */ + bool deprecated; /**< Is the command deprecated */ + unsigned int deprecated_since_major; /**< Deprecated since major */ + unsigned int deprecated_since_minor; /**< Deprecated since minor */ + const char* deprecated_by; /**< Deprecated by */ +}; + +/** @struct pgagroal_parsed_command + * Holds parsed command data. + * + * Fields: + * - cmd: A pointer to the command struct that was parsed. + * - args: An array of pointers to the parsed arguments of the command (points to argv). + */ +struct pgagroal_parsed_command +{ + const struct pgagroal_command* cmd; /**< The command */ + char* args[MISC_LENGTH]; /**< The command arguments */ +}; + +/** + * Get the request identifier + * @param msg The message + * @return The identifier + */ +int32_t +pgagroal_get_request(struct message* msg); + +/** + * Extract the user name and database from a message + * @param msg The message + * @param username The resulting user name + * @param database The resulting database + * @param appname The resulting application_name + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_extract_username_database(struct message* msg, char** username, char** database, char** appname); + +/** + * Extract a message from a message + * @param type The message type to be extracted + * @param msg The message + * @param extracted The resulting message + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_extract_message(char type, struct message* msg, struct message** extracted); + +/** + * Extract an error message from a message + * @param msg The message + * @param error The error + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_extract_error_message(struct message* msg, char** error); + +/** + * Read a byte + * @param data Pointer to the data + * @return The byte + */ +signed char +pgagroal_read_byte(void* data); + +/** + * Read an uint8 + * @param data Pointer to the data + * @return The uint8 + */ +uint8_t +pgagroal_read_uint8(void* data); + +/** + * Read an int16 + * @param data Pointer to the data + * @return The int16 + */ +int16_t +pgagroal_read_int16(void* data); + +/** + * Read an int32 + * @param data Pointer to the data + * @return The int32 + */ +int32_t +pgagroal_read_int32(void* data); + +/** + * Read an uint32 + * @param data Pointer to the data + * @return The uint32 + */ +uint32_t +pgagroal_read_uint32(void* data); + +/** + * Read a long + * @param data Pointer to the data + * @return The long + */ +long +pgagroal_read_long(void* data); + +/** + * Read a string + * @param data Pointer to the data + * @return The string + */ +char* +pgagroal_read_string(void* data); + +/** + * Write a byte + * @param data Pointer to the data + * @param b The byte + */ +void +pgagroal_write_byte(void* data, signed char b); + +/** + * Write a uint8 + * @param data Pointer to the data + * @param b The uint8 + */ +void +pgagroal_write_uint8(void* data, uint8_t b); + +/** + * Write an int32 + * @param data Pointer to the data + * @param i The int32 + */ +void +pgagroal_write_int32(void* data, int32_t i); + +/** + * Write an uint32 + * @param data Pointer to the data + * @param i The uint32 + */ +void +pgagroal_write_uint32(void* data, uint32_t i); + +/** + * Write a long + * @param data Pointer to the data + * @param l The long int + */ +void +pgagroal_write_long(void* data, long l); + +/** + * Write a string + * @param data Pointer to the data + * @param s The string + */ +void +pgagroal_write_string(void* data, char* s); + +/** + * Is the machine big endian ? + * @return True if big, otherwise false for little + */ +bool +pgagroal_bigendian(void); + +/** + * Swap + * @param i The value + * @return The swapped value + */ +unsigned int +pgagroal_swap(unsigned int i); + +/** + * Print the available libev engines + */ +void +pgagroal_libev_engines(void); + +/** + * Get the constant for a libev engine + * @param engine The name of the engine + * @return The constant + */ +unsigned int +pgagroal_libev(char* engine); + +/** + * Get the name for a libev engine + * @param val The constant + * @return The name + */ +char* +pgagroal_libev_engine(unsigned int val); + +/** + * Get the home directory + * @return The directory + */ +char* +pgagroal_get_home_directory(void); + +/** + * Get the user name + * @return The user name + */ +char* +pgagroal_get_user_name(void); + +/** + * Get a password from stdin + * @return The password + */ +char* +pgagroal_get_password(void); + +/** + * File/directory exists + * @param f The file/directory + * @return The result + */ +bool +pgagroal_exists(char* f); + +/** + * BASE64 encode a string + * @param raw The string + * @param raw_length The length of the raw string + * @param encoded The encoded string + * @param encoded_length The length of the encoded string + * @return 0 if success, otherwise 1 + */ +int +pgagroal_base64_encode(void* raw, size_t raw_length, char** encoded, size_t* encoded_length); + +/** + * BASE64 decode a string + * @param encoded The encoded string + * @param encoded_length The length of the encoded string + * @param raw The raw string + * @param raw_length The length of the raw string + * @return 0 if success, otherwise 1 + */ +int +pgagroal_base64_decode(char* encoded, size_t encoded_length, void** raw, size_t* raw_length); + +/** + * Set process title. + * + * The function will autonomously check the update policy set + * via the configuration option `update_process_title` and + * will do nothing if the setting is `never`. + * In the case the policy is set to `strict`, the process title + * will not overflow the initial command line length (i.e., strlen(argv[*])) + * otherwise it will do its best to set the title to the desired string. + * + * The policies `strict` and `minimal` will be honored only on Linux platforms + * where a native call to set the process title is not available. + * + * + * The resulting process title will be set to either `s1` or `s1/s2` if there + * both strings and the length is allowed by the policy. + * + * @param argc The number of arguments + * @param argv The argv pointer + * @param s1 The first string + * @param s2 The second string + */ +void +pgagroal_set_proc_title(int argc, char** argv, char* s1, char* s2); + +/** + * Sets the process title for a given connection. + * + * Uses `pgagroal_set_proc_title` to build an information string + * with the form + * user@host:port/database + * + * This means that all the policies honored by the latter function and + * set via the `update_process_title` configuration paramter will be + * honored. + * + * @param argc the number of arguments + * @param argv command line arguments + * @param connection the struct connection pointer for the established connection. + */ +void +pgagroal_set_connection_proc_title(int argc, char** argv, struct connection* connection); + +/** + * Get the timestramp difference as a string + * @param start_time The start time + * @param end_time The end time + * @param seconds The number of seconds + * @return The timestamp string + */ +char* +pgagroal_get_timestamp_string(time_t start_time, time_t end_time, int32_t* seconds); + +/** + * Provide the application version number as a unique value composed of the three + * specified parts. For example, when invoked with (1,5,0) it returns 10500. + * Every part of the number must be between 0 and 99, and the function + * applies a restriction on the values. For example passing 1 or 101 as one of the part + * will produce the same result. + * + * @param major the major version number + * @param minor the minor version number + * @param patch the patch level + * @returns a number made by (patch + minor * 100 + major * 10000 ) + */ +unsigned int +pgagroal_version_as_number(unsigned int major, unsigned int minor, unsigned int patch); + +/** + * Provides the current version number of the application. + * It relies on `pgagroal_version_as_number` and invokes it with the + * predefined constants. + * + * @returns the current version number + */ +unsigned int +pgagroal_version_number(void); + +/** + * Checks if the currently running version number is + * greater or equal than the specied one. + * + * @param major the major version number + * @param minor the minor version number + * @param patch the patch level + * @returns true if the current version is greater or equal to the specified one + */ +bool +pgagroal_version_ge(unsigned int major, unsigned int minor, unsigned int patch); + +/** + * Does a string end with another string + * @param str The string + * @param suffix The suffix + * @return The result + */ +bool +pgagroal_ends_with(char* str, char* suffix); + +/** + * Append a string + * + * @param orig The original string + * @param s The string + * @returns The new string + */ +char* +pgagroal_append(char* orig, char* s); + +/** + * Append an int + * + * @param orig The original string + * @param i The int + * @returns The new string + */ +char* +pgagroal_append_int(char* orig, int i); + +/** + * Append an unsigned long + * + * @param orig The original string + * @param l The long + * @returns The new string + */ +char* +pgagroal_append_ulong(char* orig, unsigned long l); + +/** + * Append an unsigned long long + * + * @param orig The original string + * @param l The long + * @returns The new string + */ +char* +pgagroal_append_ullong(char* orig, unsigned long long l); + +/** + * Append a char + * @param orig The original string + * @param s The string + * @return The resulting string + */ +char* +pgagroal_append_char(char* orig, char c); + +/** + * Indent a string + * @param str The string + * @param tag [Optional] The tag, which will be applied after indentation if not NULL + * @param indent The indent + * @return The indented string + */ +char* +pgagroal_indent(char* str, char* tag, int indent); + +/** + * Compare two strings + * @param str1 The first string + * @param str2 The second string + * @return true if the strings are the same, otherwise false + */ +bool +pgagroal_compare_string(const char* str1, const char* str2); + +/** + * Escape a string + * @param str The original string + * @return The escaped string + */ +char* +pgagroal_escape_string(char* str); + +#ifdef DEBUG + +/** + * Generate a backtrace in the log + * @return 0 if success, otherwise 1 + */ +int +pgagroal_backtrace(void); + +#endif + +/** + * Utility function to parse the command line + * and search for a command. + * + * The function tries to be smart, in helping to find out + * a command with the possible subcommand. + * + * @param argc the command line counter + * @param argv the command line as provided to the application + * @param offset the position at which the next token out of `argv` + * has to be read. This is usually the `optind` set by getopt_long(). + * @param parsed an `struct pgagroal_parsed_command` to hold the parsed + * data. It is modified inside the function to be accessed outside. + * @param command_table array containing one `struct pgagroal_command` for + * every possible command. + * @param command_count number of commands in `command_table`. + * @return true if the parsing of the command line was succesful, false + * otherwise + * + */ +bool +parse_command(int argc, + char** argv, + int offset, + struct pgagroal_parsed_command* parsed, + const struct pgagroal_command command_table[], + size_t command_count); + +/** + * Given a server state, it returns a string that + * described the state in a human-readable form. + * + * If the state cannot be determined, the numeric + * form of the state is returned as a string. + * + * @param state the value of the sate for the server + * @returns the string representing the state + */ +char* +pgagroal_server_state_as_string(signed char state); + +/** + * Utility function to convert the status of a connection + * into a descriptive string. Useful to spurt the status + * in command line output. + * + * @param state the actual state of the connection + * @returns the (allocated) buffer with the string + */ +char* +pgagroal_connection_state_as_string(signed char state); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/value.h b/src/include/value.h new file mode 100644 index 00000000..a2e86c81 --- /dev/null +++ b/src/include/value.h @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_VALUE_H +#define PGAGROAL_VALUE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +typedef void (*data_destroy_cb)(uintptr_t data); +typedef char* (*data_to_string_cb)(uintptr_t data, int32_t format, char* tag, int indent); + +enum value_type { + ValueInt8, + ValueUInt8, + ValueInt16, + ValueUInt16, + ValueInt32, + ValueUInt32, + ValueInt64, + ValueUInt64, + ValueChar, + ValueBool, + ValueString, + ValueFloat, + ValueDouble, + ValueBASE64, + ValueJSON, + ValueDeque, + ValueART, + ValueRef, + ValueMem, +}; + +/** + * @struct value + * Defines a universal value + */ +struct value +{ + enum value_type type; /**< The type of value data */ + uintptr_t data; /**< The data, could be passed by value or by reference */ + data_destroy_cb destroy_data; /**< The callback to destroy data */ + data_to_string_cb to_string; /**< The callback to convert data to string */ +}; + +/** + * @struct value_config + * Defines configuration for managing a value + */ +struct value_config +{ + data_destroy_cb destroy_data; /**< The callback to destroy data */ + data_to_string_cb to_string; /**< The callback to convert data to string */ +}; + +/** + * Create a value based on the data and value type + * @param type The value type, use ValueRef if you are only storing pointers without the need to manage memory, + * use ValueMem if you are storing pointers to a chunk of memory that needs to and can be simply freed + * (meaning it can't have pointers to other malloced memories) + * @param data The value data, type cast it to uintptr_t before passing into function + * @param value [out] The value + * @return 0 on success, 1 if otherwise + */ +int +pgagroal_value_create(enum value_type type, uintptr_t data, struct value** value); + +/** + * Create a value with a config for customized destroy or to_string callback, + * the type will default to ValueRef + * @param data The value data, type cast it to uintptr_t before passing into function + * @param config The configuration + * @param value [out] The value + * @return 0 on success, 1 if otherwise + */ +int +pgagroal_value_create_with_config(uintptr_t data, struct value_config* config, struct value** value); + +/** + * Destroy a value along with the data within + * @param value The value + * @return 0 on success, 1 if otherwise + */ +int +pgagroal_value_destroy(struct value* value); + +/** + * Get the raw data from the value, which can be casted back to its original type + * @param value The value + * @return The value data within + */ +uintptr_t +pgagroal_value_data(struct value* value); + +/** + * Convert a value to string + * @param value The value + * @param format The format + * @param tag The optional tag + * @param indent The indent + * @return The string + */ +char* +pgagroal_value_to_string(struct value* value, int32_t format, char* tag, int indent); + +/** + * Convert a double value to value data, since straight type cast discards the decimal part + * @param val The value + * @return The value data + */ +uintptr_t +pgagroal_value_from_double(double val); + +/** + * Convert a value data to double + * @param val The value + * @return + */ +double +pgagroal_value_to_double(uintptr_t val); + +/** + * Convert a float value to value data, since straight type cast discards the decimal part + * @param val The value + * @return The value data + */ +uintptr_t +pgagroal_value_from_float(float val); + +/** + * Convert a value data to float + * @param val The value + * @return + */ +float +pgagroal_value_to_float(uintptr_t val); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/worker.h b/src/include/worker.h new file mode 100644 index 00000000..5478293e --- /dev/null +++ b/src/include/worker.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_WORKER_H +#define PGAGROAL_WORKER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + +#define WORKER_SUCCESS 0 +#define WORKER_FAILURE 1 +#define WORKER_SHUTDOWN 2 +#define WORKER_CLIENT_FAILURE 3 +#define WORKER_SERVER_FAILURE 4 +#define WORKER_SERVER_FATAL 5 +#define WORKER_FAILOVER 6 + +/** @struct worker_io + * The worker structure for each IO event + */ +struct worker_io +{ + struct ev_io io; /**< The libev base type */ + int client_fd; /**< The client descriptor */ + int server_fd; /**< The server descriptor */ + int slot; /**< The slot */ + SSL* client_ssl; /**< The client SSL context */ + SSL* server_ssl; /**< The server SSL context */ +}; + +extern volatile int running; +extern volatile int exit_code; + +/** + * Create a worker instance + * @param fd The client descriptor + * @param address The client address + * @param argv The argv + */ +void +pgagroal_worker(int fd, char* address, char** argv); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/zstandard_compression.h b/src/include/zstandard_compression.h new file mode 100644 index 00000000..80b5f714 --- /dev/null +++ b/src/include/zstandard_compression.h @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PGAGROAL_ZSTANDARD_H +#define PGAGROAL_ZSTANDARD_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * ZSTD compress a string + * @param s The original string + * @param buffer The point to the compressed data buffer + * @param buffer_size The size of the compressed buffer will be stored. + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_zstdc_string(char* s, unsigned char** buffer, size_t* buffer_size); + +/** + * ZSTD decompress a buffer to string + * @param compressed_buffer The buffer containing the GZIP compressed data + * @param compressed_size The size of the compressed buffer + * @param output_string The pointer to a string where the decompressed data will be stored + * @return 0 upon success, otherwise 1 + */ +int +pgagroal_zstdd_string(unsigned char* compressed_buffer, size_t compressed_size, char** output_string); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/libpgagroal/aes.c b/src/libpgagroal/aes.c new file mode 100644 index 00000000..63beb1a2 --- /dev/null +++ b/src/libpgagroal/aes.c @@ -0,0 +1,362 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include + +static int derive_key_iv(char* password, unsigned char* key, unsigned char* iv, int mode); +static int aes_encrypt(char* plaintext, unsigned char* key, unsigned char* iv, char** ciphertext, int* ciphertext_length, int mode); +static int aes_decrypt(char* ciphertext, int ciphertext_length, unsigned char* key, unsigned char* iv, char** plaintext, int mode); +static const EVP_CIPHER* (*get_cipher(int mode))(void); + +static int encrypt_decrypt_buffer(unsigned char* origin_buffer, size_t origin_size, unsigned char** res_buffer, size_t* res_size, int enc, int mode); + +int +pgagroal_encrypt(char* plaintext, char* password, char** ciphertext, int* ciphertext_length, int mode) +{ + unsigned char key[EVP_MAX_KEY_LENGTH]; + unsigned char iv[EVP_MAX_IV_LENGTH]; + + memset(&key, 0, sizeof(key)); + memset(&iv, 0, sizeof(iv)); + + if (derive_key_iv(password, key, iv, mode) != 0) + { + return 1; + } + + return aes_encrypt(plaintext, key, iv, ciphertext, ciphertext_length, mode); +} + +int +pgagroal_decrypt(char* ciphertext, int ciphertext_length, char* password, char** plaintext, int mode) +{ + unsigned char key[EVP_MAX_KEY_LENGTH]; + unsigned char iv[EVP_MAX_IV_LENGTH]; + + memset(&key, 0, sizeof(key)); + memset(&iv, 0, sizeof(iv)); + + if (derive_key_iv(password, key, iv, mode) != 0) + { + return 1; + } + + return aes_decrypt(ciphertext, ciphertext_length, key, iv, plaintext, mode); +} + +// [private] +static int +derive_key_iv(char* password, unsigned char* key, unsigned char* iv, int mode) +{ + if (!EVP_BytesToKey(get_cipher(mode)(), EVP_sha1(), NULL, + (unsigned char*) password, strlen(password), 1, + key, iv)) + { + return 1; + } + + return 0; +} + +// [private] +static int +aes_encrypt(char* plaintext, unsigned char* key, unsigned char* iv, char** ciphertext, int* ciphertext_length, int mode) +{ + EVP_CIPHER_CTX* ctx = NULL; + int length; + size_t size; + unsigned char* ct = NULL; + int ct_length; + const EVP_CIPHER* (* cipher_fp)(void) = get_cipher(mode); + if (!(ctx = EVP_CIPHER_CTX_new())) + { + goto error; + } + + if (EVP_EncryptInit_ex(ctx, cipher_fp(), NULL, key, iv) != 1) + { + goto error; + } + + size = strlen(plaintext) + EVP_CIPHER_block_size(cipher_fp()); + ct = malloc(size); + + if (ct == NULL) + { + goto error; + } + + memset(ct, 0, size); + + if (EVP_EncryptUpdate(ctx, + ct, &length, + (unsigned char*)plaintext, strlen((char*)plaintext)) != 1) + { + goto error; + } + + ct_length = length; + + if (EVP_EncryptFinal_ex(ctx, ct + length, &length) != 1) + { + goto error; + } + + ct_length += length; + + EVP_CIPHER_CTX_free(ctx); + + *ciphertext = (char*)ct; + *ciphertext_length = ct_length; + + return 0; + +error: + if (ctx) + { + EVP_CIPHER_CTX_free(ctx); + } + + free(ct); + + return 1; +} + +// [private] +static int +aes_decrypt(char* ciphertext, int ciphertext_length, unsigned char* key, unsigned char* iv, char** plaintext, int mode) +{ + EVP_CIPHER_CTX* ctx = NULL; + int plaintext_length; + int length; + size_t size; + char* pt = NULL; + const EVP_CIPHER* (* cipher_fp)(void) = get_cipher(mode); + + if (!(ctx = EVP_CIPHER_CTX_new())) + { + goto error; + } + + if (EVP_DecryptInit_ex(ctx, cipher_fp(), NULL, key, iv) != 1) + { + goto error; + } + + size = ciphertext_length + EVP_CIPHER_block_size(cipher_fp()); + pt = malloc(size); + + if (pt == NULL) + { + goto error; + } + + memset(pt, 0, size); + + if (EVP_DecryptUpdate(ctx, + (unsigned char*)pt, &length, + (unsigned char*)ciphertext, ciphertext_length) != 1) + { + goto error; + } + + plaintext_length = length; + + if (EVP_DecryptFinal_ex(ctx, (unsigned char*)pt + length, &length) != 1) + { + goto error; + } + + plaintext_length += length; + + EVP_CIPHER_CTX_free(ctx); + + pt[plaintext_length] = 0; + *plaintext = pt; + + return 0; + +error: + if (ctx) + { + EVP_CIPHER_CTX_free(ctx); + } + + free(pt); + + return 1; +} + +static const EVP_CIPHER* (*get_cipher(int mode))(void) +{ + if (mode == ENCRYPTION_AES_256_CBC) + { + return &EVP_aes_256_cbc; + } + if (mode == ENCRYPTION_AES_192_CBC) + { + return &EVP_aes_192_cbc; + } + if (mode == ENCRYPTION_AES_128_CBC) + { + return &EVP_aes_128_cbc; + } + if (mode == ENCRYPTION_AES_256_CTR) + { + return &EVP_aes_256_ctr; + } + if (mode == ENCRYPTION_AES_192_CTR) + { + return &EVP_aes_192_ctr; + } + if (mode == ENCRYPTION_AES_128_CTR) + { + return &EVP_aes_128_ctr; + } + return &EVP_aes_256_cbc; +} + +int +pgagroal_encrypt_buffer(unsigned char* origin_buffer, size_t origin_size, unsigned char** enc_buffer, size_t* enc_size, int mode) +{ + return encrypt_decrypt_buffer(origin_buffer, origin_size, enc_buffer, enc_size, 1, mode); +} + +int +pgagroal_decrypt_buffer(unsigned char* origin_buffer, size_t origin_size, unsigned char** dec_buffer, size_t* dec_size, int mode) +{ + return encrypt_decrypt_buffer(origin_buffer, origin_size, dec_buffer, dec_size, 0, mode); +} + +static int +encrypt_decrypt_buffer(unsigned char* origin_buffer, size_t origin_size, unsigned char** res_buffer, size_t* res_size, int enc, int mode) +{ + unsigned char key[EVP_MAX_KEY_LENGTH]; + unsigned char iv[EVP_MAX_IV_LENGTH]; + char* master_key = NULL; + EVP_CIPHER_CTX* ctx = NULL; + const EVP_CIPHER* (*cipher_fp)(void) = NULL; + size_t cipher_block_size = 0; + size_t outbuf_size = 0; + size_t outl = 0; + size_t f_len = 0; + + cipher_fp = get_cipher(mode); + if (cipher_fp == NULL) + { + pgagroal_log_error("Invalid encryption method specified"); + goto error; + } + + cipher_block_size = EVP_CIPHER_block_size(cipher_fp()); + + if (enc == 1) + { + outbuf_size = origin_size + cipher_block_size; + } + else + { + outbuf_size = origin_size; + } + + *res_buffer = (unsigned char*)malloc(outbuf_size + 1); + if (*res_buffer == NULL) + { + pgagroal_log_error("pgagroal_encrypt_decrypt_buffer: Allocation failure"); + goto error; + } + + if (pgagroal_get_master_key(&master_key)) + { + pgagroal_log_error("pgagroal_get_master_key: Invalid master key"); + goto error; + } + + memset(&key, 0, sizeof(key)); + memset(&iv, 0, sizeof(iv)); + + if (derive_key_iv(master_key, key, iv, mode) != 0) + { + pgagroal_log_error("derive_key_iv: Failed to derive key and iv"); + goto error; + } + + if (!(ctx = EVP_CIPHER_CTX_new())) + { + pgagroal_log_error("EVP_CIPHER_CTX_new: Failed to create context"); + goto error; + } + + if (EVP_CipherInit_ex(ctx, cipher_fp(), NULL, key, iv, enc) == 0) + { + pgagroal_log_error("EVP_CipherInit_ex: Failed to initialize cipher context"); + goto error; + } + + if (EVP_CipherUpdate(ctx, *res_buffer, (int*)&outl, origin_buffer, origin_size) == 0) + { + pgagroal_log_error("EVP_CipherUpdate: Failed to process data"); + goto error; + } + + *res_size = outl; + + if (EVP_CipherFinal_ex(ctx, *res_buffer + outl, (int*)&f_len) == 0) + { + pgagroal_log_error("EVP_CipherFinal_ex: Failed to finalize operation"); + goto error; + } + + *res_size += f_len; + + if (enc == 0) + { + (*res_buffer)[*res_size] = '\0'; + } + + EVP_CIPHER_CTX_free(ctx); + free(master_key); + + return 0; + +error: + if (ctx) + { + EVP_CIPHER_CTX_free(ctx); + } + + if (master_key) + { + free(master_key); + } + + return 1; +} diff --git a/src/libpgagroal/art.c b/src/libpgagroal/art.c new file mode 100644 index 00000000..5eb0b5a8 --- /dev/null +++ b/src/libpgagroal/art.c @@ -0,0 +1,1668 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + +#include +#include +#include + +#define IS_LEAF(x) (((uintptr_t)(x) & 1)) +#define SET_LEAF(x) ((void*)((uintptr_t)(x) | 1)) +#define GET_LEAF(x) ((struct art_leaf*)((void*)((uintptr_t)(x) & ~1))) + +enum art_node_type { + Node4, + Node16, + Node48, + Node256 +}; + +/** + * This struct is included as part + * of all the various node sizes. + * All node types should be aligned + * because we need the last bit to be 0 as a flag bit for leaf. + * So that leaf can be treated as a node as well and stored in the children field, + * and only converted back when necessary + */ +struct art_node +{ + uint32_t prefix_len; /**< The actual length of the prefix segment */ + enum art_node_type type; /**< The node type */ + uint8_t num_children; /**< The number of children */ + unsigned char prefix[MAX_PREFIX_LEN]; /**< The (potentially partial) prefix, only record up to MAX_PREFIX_LEN characters */ +} __attribute__ ((aligned (64))); + +/** + * The ART leaf with key buffer of arbitrary size + */ +struct art_leaf +{ + struct value* value; + uint32_t key_len; + unsigned char key[]; +} __attribute__ ((aligned (64))); + +/** + * The ART node with only 4 children, + * the key character and the children pointer are stored + * in the same position of the corresponding array. + * The keys are stored in sorted order sequentially. + */ +struct art_node4 +{ + struct art_node node; + unsigned char keys[4]; + struct art_node* children[4]; +} __attribute__ ((aligned (64))); + +/** + * Similar structure as node4, but with 16 children. + * The keys are stored in sorted order sequentially. + */ +struct art_node16 +{ + struct art_node node; + unsigned char keys[16]; + struct art_node* children[16]; +} __attribute__ ((aligned (64))); + +/** + * A full key array that can be indexed by the key character directly, + * the array stores the index of the corresponding child in the array. + * Note that in practice 0 is used for invalid index, + * so the actual index is the index in key array - 1 + */ +struct art_node48 +{ + struct art_node node; + unsigned char keys[256]; + struct art_node* children[48]; +} __attribute__ ((aligned (64))); + +/** + * A direct array of children using key character as index + */ +struct art_node256 +{ + struct art_node node; + struct art_node* children[256]; +} __attribute__ ((aligned (64))); + +struct to_string_param +{ + char* str; + int indent; + int cnt; + char* tag; + struct art* t; +}; + +static struct art_node** +node_get_child(struct art_node* node, unsigned char ch); + +// Get the left most leaf of a child +static struct art_leaf* +node_get_minimum(struct art_node* node); + +static void +create_art_leaf(struct art_leaf** leaf, unsigned char* key, uint32_t key_len, uintptr_t value, enum value_type type, struct value_config* config); + +static void +create_art_node(struct art_node** node, enum art_node_type type); + +static void +create_art_node4(struct art_node4** node); + +static void +create_art_node16(struct art_node16** node); + +static void +create_art_node48(struct art_node48** node); + +static void +create_art_node256(struct art_node256** node); + +// Destroy ART nodes/leaves recursively +static void +destroy_art_node(struct art_node* node); + +static int +art_iterate(struct art* t, art_callback cb, void* data); + +/** + * Get where the keys diverge starting from depth. + * This function only compares within the partial prefix range + * @param node The node + * @param key The key + * @param depth The starting depth + * @param key_len The length of the key + * @return The length of the part of the prefix that matches + */ +static uint32_t +check_prefix_partial(struct art_node* node, unsigned char* key, uint32_t depth, uint32_t key_len); + +/** + * Get where the keys diverge starting from depth. + * This function compares with the complete key to determine if diverging point goes beyond current prefix or partial prefix + * @param node The node + * @param key The key + * @param depth The starting depth + * @param key_len The length of the key + * @return The length of the part of the prefix that matches + */ +static uint32_t +check_prefix(struct art_node* node, unsigned char* key, uint32_t depth, uint32_t key_len); + +/** + * Compare the key stored in leaf and the original key + * @param leaf + * @param key + * @param key_len + * @return true if the key matches + */ +static bool +leaf_match(struct art_leaf* leaf, unsigned char* key, uint32_t key_len); + +/** + * Find the index of the corresponding key character using binary search. + * If not found, the index of the largest element smaller than ch, + * or -1 if ch is the smallest, will be returned, + * @param ch + * @param keys + * @param length + * @return The index + */ +static int +find_index(unsigned char ch, const unsigned char* keys, int length); + +/** + * Insert a value into a node recursively, adopting lazy expansion and path compression -- + * Expand the leaf, or split inner node should keys diverge within node's prefix range + * @param node The node + * @param node_ref The reference to node pointer + * @param depth The depth into the node, which is the same as the total prefix length + * @param key The key + * @param key_len The length of the key + * @param value The value data + * @param type The value type + * @param config The config + * @param new If the key value is newly inserted (not replaced) + * @return Old value if the key exists, otherwise NULL + */ +static struct value* +art_node_insert(struct art_node* node, struct art_node** node_ref, uint32_t depth, unsigned char* key, uint32_t key_len, uintptr_t value, enum value_type type, struct value_config* config, bool* new); + +/** + * Delete a value from a node recursively. + * @param node The node + * @param node_ref The reference to node pointer + * @param depth The depth into the node + * @param key The key + * @param key_len The length of the key + * @return Deleted value if the key exists, otherwise NULL + */ +static struct art_leaf* +art_node_delete(struct art_node* node, struct art_node** node_ref, uint32_t depth, unsigned char* key, uint32_t key_len); + +static int +art_node_iterate(struct art_node* node, art_callback cb, void* data); + +static void +node_add_child(struct art_node* node, struct art_node** node_ref, unsigned char ch, void* child); + +/** + * Add a child to the node. The function assumes node is not NULL, + * nor the key character already exists. + * If node is full, a new node of type node16 will be created. The old + * node will be replaced by new node through node_ref. + * @param node The node + * @param node_ref The reference of the node pointer + * @param ch The key character + * @param child The child + */ +static void +node4_add_child(struct art_node4* node, struct art_node** node_ref, unsigned char ch, void* child); + +static void +node16_add_child(struct art_node16* node, struct art_node** node_ref, unsigned char ch, void* child); + +static void +node48_add_child(struct art_node48* node, struct art_node** node_ref, unsigned char ch, void* child); + +static void +node256_add_child(struct art_node256* node, unsigned char ch, void* child); + +// All removal functions assume the child to remove is leaf, meaning they don't try removing anything recursively. +// They also do not free the leaf node for bookkeeping purpose. The key insight is that due to path compression, +// no node will have only one child, if node has only one child after deletion, it merges with this child +static void +node_remove_child(struct art_node* node, struct art_node** node_ref, unsigned char ch); + +static void +node4_remove_child(struct art_node4* node, struct art_node** node_ref, unsigned char ch); + +static void +node16_remove_child(struct art_node16* node, struct art_node** node_ref, unsigned char ch); + +static void +node48_remove_child(struct art_node48* node, struct art_node** node_ref, unsigned char ch); + +static void +node256_remove_child(struct art_node256* node, struct art_node** node_ref, unsigned char ch); + +static void +copy_header(struct art_node* dest, struct art_node* src); + +static uint32_t +min(uint32_t a, uint32_t b); + +static struct value* +art_search(struct art* t, unsigned char* key, uint32_t key_len); + +static int +art_to_json_string_cb(void* param, const unsigned char* key, uint32_t key_len, struct value* value); + +static int +art_to_text_string_cb(void* param, const unsigned char* key, uint32_t key_len, struct value* value); + +static int +art_to_compact_json_string_cb(void* param, const unsigned char* key, uint32_t key_len, struct value* value); + +static char* +to_json_string(struct art* t, char* tag, int indent); + +static char* +to_compact_json_string(struct art* t, char* tag, int indent); + +static char* +to_text_string(struct art* t, char* tag, int indent); + +int +pgagroal_art_create(struct art** tree) +{ + struct art* t = NULL; + t = malloc(sizeof(struct art)); + t->size = 0; + t->root = NULL; + *tree = t; + return 0; +} + +int +pgagroal_art_destroy(struct art* tree) +{ + if (tree == NULL) + { + return 0; + } + destroy_art_node(tree->root); + free(tree); + return 0; +} + +uintptr_t +pgagroal_art_search(struct art* t, unsigned char* key, uint32_t key_len) +{ + struct value* val = art_search(t, key, key_len); + return pgagroal_value_data(val); +} + +bool +pgagroal_art_contains_key(struct art* t, unsigned char* key, uint32_t key_len) +{ + struct value* val = art_search(t, key, key_len); + return val != NULL; +} + +int +pgagroal_art_insert(struct art* t, unsigned char* key, uint32_t key_len, uintptr_t value, enum value_type type) +{ + struct value* old_val = NULL; + bool new = false; + if (t == NULL) + { + // c'mon, at least create a tree first... + goto error; + } + old_val = art_node_insert(t->root, &t->root, 0, key, key_len, value, type, NULL, &new); + pgagroal_value_destroy(old_val); + if (new) + { + t->size++; + } + return 0; +error: + return 1; +} + +int +pgagroal_art_insert_with_config(struct art* t, unsigned char* key, uint32_t key_len, uintptr_t value, struct value_config* config) +{ + struct value* old_val = NULL; + bool new = false; + if (t == NULL) + { + // c'mon, at least create a tree first... + goto error; + } + old_val = art_node_insert(t->root, &t->root, 0, key, key_len, value, ValueRef, config, &new); + pgagroal_value_destroy(old_val); + if (new) + { + t->size++; + } + return 0; +error: + return 1; +} + +int +pgagroal_art_delete(struct art* t, unsigned char* key, uint32_t key_len) +{ + struct art_leaf* l = NULL; + if (t == NULL) + { + return 1; + } + l = art_node_delete(t->root, &t->root, 0, key, key_len); + t->size--; + pgagroal_value_destroy(l->value); + free(l); + return 0; +} + +char* +pgagroal_art_to_string(struct art* t, int32_t format, char* tag, int indent) +{ + if (format == FORMAT_JSON) + { + return to_json_string(t, tag, indent); + } + else if (format == FORMAT_TEXT) + { + return to_text_string(t, tag, indent); + } + else if (format == FORMAT_JSON_COMPACT) + { + return to_compact_json_string(t, tag, indent); + } + return NULL; +} + +static uint32_t +min(uint32_t a, uint32_t b) +{ + if (a >= b) + { + return b; + } + return a; +} + +static void +create_art_leaf(struct art_leaf** leaf, unsigned char* key, uint32_t key_len, uintptr_t value, enum value_type type, struct value_config* config) +{ + struct art_leaf* l = NULL; + l = malloc(sizeof(struct art_leaf) + key_len); + memset(l, 0, sizeof(struct art_leaf) + key_len); + if (config != NULL) + { + pgagroal_value_create_with_config(value, config, &l->value); + } + else + { + pgagroal_value_create(type, value, &l->value); + } + + l->key_len = key_len; + memcpy(l->key, key, key_len); + *leaf = l; +} + +static void +create_art_node(struct art_node** node, enum art_node_type type) +{ + struct art_node* n = NULL; + switch (type) + { + case Node4: + { + struct art_node4* n4 = malloc(sizeof(struct art_node4)); + memset(n4, 0, sizeof(struct art_node4)); + n4->node.type = Node4; + n = (struct art_node*) n4; + break; + } + case Node16: + { + struct art_node16* n16 = malloc(sizeof(struct art_node16)); + memset(n16, 0, sizeof(struct art_node16)); + n16->node.type = Node16; + n = (struct art_node*) n16; + break; + } + case Node48: + { + struct art_node48* n48 = malloc(sizeof(struct art_node48)); + memset(n48, 0, sizeof(struct art_node48)); + n48->node.type = Node48; + n = (struct art_node*) n48; + break; + } + case Node256: + { + struct art_node256* n256 = malloc(sizeof(struct art_node256)); + memset(n256, 0, sizeof(struct art_node256)); + n256->node.type = Node256; + n = (struct art_node*) n256; + break; + } + } + *node = n; +} + +static void +create_art_node4(struct art_node4** node) +{ + struct art_node* n = NULL; + create_art_node(&n, Node4); + *node = (struct art_node4*)n; +} + +static void +create_art_node16(struct art_node16** node) +{ + struct art_node* n = NULL; + create_art_node(&n, Node16); + *node = (struct art_node16*)n; +} + +static void +create_art_node48(struct art_node48** node) +{ + struct art_node* n = NULL; + create_art_node(&n, Node48); + *node = (struct art_node48*)n; +} + +static void +create_art_node256(struct art_node256** node) +{ + struct art_node* n = NULL; + create_art_node(&n, Node256); + *node = (struct art_node256*)n; +} + +static void +destroy_art_node(struct art_node* node) +{ + if (node == NULL) + { + return; + } + if (IS_LEAF(node)) + { + pgagroal_value_destroy(GET_LEAF(node)->value); + free(GET_LEAF(node)); + return; + } + switch (node->type) + { + case Node4: + { + struct art_node4* n = (struct art_node4*) node; + for (int i = 0; i < node->num_children; i++) + { + destroy_art_node(n->children[i]); + } + break; + } + case Node16: + { + struct art_node16* n = (struct art_node16*) node; + for (int i = 0; i < node->num_children; i++) + { + destroy_art_node(n->children[i]); + } + break; + } + case Node48: + { + struct art_node48* n = (struct art_node48*) node; + for (int i = 0; i < 256; i++) + { + int idx = n->keys[i]; + if (idx == 0) + { + continue; + } + destroy_art_node(n->children[idx - 1]); + } + break; + } + + case Node256: + { + struct art_node256* n = (struct art_node256*) node; + for (int i = 0; i < 256; i++) + { + if (n->children[i] == NULL) + { + continue; + } + destroy_art_node(n->children[i]); + } + break; + } + } + free(node); +} + +static struct art_node** +node_get_child(struct art_node* node, unsigned char ch) +{ + switch (node->type) + { + case Node4: + { + struct art_node4* n = (struct art_node4*)node; + int idx = find_index(ch, n->keys, n->node.num_children); + if (idx == -1 || n->keys[idx] != ch) + { + goto error; + } + return &n->children[idx]; + } + case Node16: + { + struct art_node16* n = (struct art_node16*)node; + int idx = find_index(ch, n->keys, n->node.num_children); + if (idx == -1 || n->keys[idx] != ch) + { + goto error; + } + return &n->children[idx]; + } + case Node48: + { + struct art_node48* n = (struct art_node48*)node; + if (n->keys[ch] == 0) + { + goto error; + } + return &n->children[n->keys[ch] - 1]; + } + case Node256: + { + struct art_node256* n = (struct art_node256*)node; + return &n->children[ch]; + } + } +error: + return NULL; +} + +static struct value* +art_node_insert(struct art_node* node, struct art_node** node_ref, uint32_t depth, unsigned char* key, uint32_t key_len, uintptr_t value, enum value_type type, struct value_config* config, bool* new) +{ + struct art_leaf* leaf = NULL; + struct art_leaf* min_leaf = NULL; + uint32_t idx = 0; + uint32_t diff_len = 0; // where the keys diverge + struct art_node* new_node = NULL; + struct art_node** next = NULL; + unsigned char* leaf_key = NULL; + void* old_val = NULL; + if (node == NULL) + { + // Lazy expansion, skip creating an inner node since it currently will have only this one leaf. + // We will compare keys when reach leaf anyway, the path doesn't need to 100% match the key along the way + create_art_leaf(&leaf, key, key_len, value, type, config); + *node_ref = SET_LEAF(leaf); + *new = true; + return NULL; + } + // base case, reaching leaf, either replace or expand + if (IS_LEAF(node)) + { + // Lazy expansion, expand the leaf node to an inner node with 2 leaves + // If the key already exists, replace with new value and return old value + if (leaf_match(GET_LEAF(node), key, key_len)) + { + old_val = GET_LEAF(node)->value; + pgagroal_value_create(type, value, &(GET_LEAF(node)->value)); + return old_val; + } + // If the key does not match with existing key, old key and new key diverged some point after depth + // Even if we merely store a partial prefix for each node, it couldn't have diverged before depth. + // The reason is that when we find it diverged outside the partial prefix range, + // we compare with the existing key in the left most leaf and find an exact diverging point to split the node (see details below). + // This way we inductively guarantee that all children to a parent share the same prefix even if it's only partially stored + leaf_key = GET_LEAF(node)->key; + create_art_node(&new_node, Node4); + create_art_leaf(&leaf, key, key_len, value, type, config); + // Get the diverging index after point of depth + for (idx = depth; idx < min(key_len, GET_LEAF(node)->key_len); idx++) + { + if (key[idx] != leaf_key[idx]) + { + break; + } + if (idx - depth < MAX_PREFIX_LEN) + { + new_node->prefix[idx - depth] = key[idx]; + } + } + new_node->prefix_len = idx - depth; + depth += new_node->prefix_len; + node_add_child(new_node, &new_node, key[depth], SET_LEAF(leaf)); + node_add_child(new_node, &new_node, leaf_key[depth], (void*)node); + // replace with new node + *node_ref = new_node; + *new = true; + return NULL; + } + + // There are several cases, + // 1. The key diverges outside the current prefix (diff_len >= prefix_len) + // 2. The key diverges within the current prefix (diff_len < prefix_len) + // 2.1. The key diverges within the partial prefix range (diff_len < MAX_PREFIX_LEN) + // 2.2. The key diverges outside the partial prefix range (MAX_PREFIX_LEN <= diff_len < prefix_len) + // For case 1, go to the next child to add node recursively, or add leaf to current node in place + // For case 2, split the current node and add child to new node. + // Note that it's tricky to check case 2.2, or in that case know the exact diverging point, + // since we merely store the first 10 (MAX_PREFIX_LEN) bytes of the prefix. + // In this case we use the key in the left most leaf of the node to determine the diverging point. + // Theoretically we inductively guarantee that all children to the same parent share the same prefixes. + // So we can use the key inside any leaf under this node to see if the diverging point goes beyond the current prefix, + // but it's most convenient and efficient to reach the left most key. + + diff_len = check_prefix(node, key, depth, key_len); + if (diff_len < node->prefix_len) + { + // case 2, split the node + create_art_node(&new_node, Node4); + create_art_leaf(&leaf, key, key_len, value, type, config); + new_node->prefix_len = diff_len; + memcpy(new_node->prefix, node->prefix, min(MAX_PREFIX_LEN, diff_len)); + // We need to know if new bytes that were once outside the partial prefix range will now come into the range + // If original key didn't fill up the partial prefix buffer in the first place, + // no new bytes will come into buffer when prefix shifts left + if (node->prefix_len <= MAX_PREFIX_LEN) + { + node->prefix_len = node->prefix_len - (diff_len + 1); + node_add_child(new_node, &new_node, key[depth + diff_len], SET_LEAF(leaf)); + node_add_child(new_node, &new_node, node->prefix[diff_len], node); + // Update node's prefix info since we move it downwards + // The first diverging character serves as the key byte in keys array, + // so we don't duplicate store it in the prefix. + // In other words, if prefix is the starting point, + // prefix + prefix_len - 1 is the last byte of the prefix, + // prefix + prefix_len is the indexing byte + // prefix + prefix_len + 1 is the starting point of the next prefix + memmove(node->prefix, node->prefix + diff_len + 1, node->prefix_len); + } + else + { + node->prefix_len = node->prefix_len - (diff_len + 1); + min_leaf = node_get_minimum(node); + node_add_child(new_node, &new_node, key[depth + diff_len], SET_LEAF(leaf)); + node_add_child(new_node, &new_node, min_leaf->key[depth + diff_len], node); + // node is moved downwards + memmove(node->prefix, min_leaf->key + depth + diff_len + 1, min(MAX_PREFIX_LEN, node->prefix_len)); + } + // replace + *node_ref = new_node; + *new = true; + return NULL; + } + else + { + // case 1 + depth += node->prefix_len; + next = node_get_child(node, key[depth]); + if (next != NULL) + { + // recursively add node + if (*next == NULL) + { + node->num_children++; + } + return art_node_insert(*next, next, depth + 1, key, key_len, value, type, config, new); + } + else + { + // add a child to current node since the spot is available + create_art_leaf(&leaf, key, key_len, value, type, config); + node_add_child(node, node_ref, key[depth], SET_LEAF(leaf)); + *new = true; + return NULL; + } + } +} + +static struct art_leaf* +art_node_delete(struct art_node* node, struct art_node** node_ref, uint32_t depth, unsigned char* key, uint32_t key_len) +{ + struct art_leaf* l = NULL; + struct art_node** child = NULL; + uint32_t diff_len = 0; + if (node == NULL) + { + return NULL; + } + // Only one way we encounter this case, the tree only has one leaf + if (IS_LEAF(node)) + { + if (leaf_match(GET_LEAF(node), key, key_len)) + { + l = GET_LEAF(node); + *node_ref = NULL; + return l; + } + return NULL; + } + diff_len = check_prefix_partial(node, key, depth, key_len); + if (diff_len != min(MAX_PREFIX_LEN, node->prefix_len)) + { + return NULL; + } + else + { + depth += node->prefix_len; + child = node_get_child(node, key[depth]); + if (child == NULL) + { + // dead end + return NULL; + } + if (IS_LEAF(*child)) + { + if (leaf_match(GET_LEAF(*child), key, key_len)) + { + l = GET_LEAF(*child); + node_remove_child(node, node_ref, key[depth]); + return l; + } + else + { + return NULL; + } + } + else + { + return art_node_delete(*child, child, depth + 1, key, key_len); + } + } +} + +static int +art_node_iterate(struct art_node* node, art_callback cb, void* data) +{ + struct art_leaf* l = NULL; + struct art_node* child = NULL; + int idx = 0; + int res = 0; + if (node == NULL) + { + return 0; + } + if (IS_LEAF(node)) + { + l = GET_LEAF(node); + return cb(data, l->key, l->key_len, l->value); + } + switch (node->type) + { + case Node4: + { + struct art_node4* n = (struct art_node4*) node; + for (int i = 0; i < node->num_children; i++) + { + child = n->children[i]; + res = art_node_iterate(child, cb, data); + if (res) + { + return res; + } + } + break; + } + case Node16: + { + struct art_node16* n = (struct art_node16*) node; + for (int i = 0; i < node->num_children; i++) + { + child = n->children[i]; + res = art_node_iterate(child, cb, data); + if (res) + { + return res; + } + } + break; + } + case Node48: + { + struct art_node48* n = (struct art_node48*) node; + for (int i = 0; i < 256; i++) + { + idx = n->keys[i]; + if (idx == 0) + { + continue; + } + child = n->children[idx - 1]; + res = art_node_iterate(child, cb, data); + if (res) + { + return res; + } + } + break; + } + case Node256: + { + struct art_node256* n = (struct art_node256*) node; + for (int i = 0; i < 256; i++) + { + if (n->children[i] == NULL) + { + continue; + } + child = n->children[i]; + res = art_node_iterate(child, cb, data); + if (res) + { + return res; + } + } + break; + } + } + return 0; +} + +static void +node_add_child(struct art_node* node, struct art_node** node_ref, unsigned char ch, void* child) +{ + switch (node->type) + { + case Node4: + node4_add_child((struct art_node4*) node, node_ref, ch, child); + break; + case Node16: + node16_add_child((struct art_node16*) node, node_ref, ch, child); + break; + case Node48: + node48_add_child((struct art_node48*) node, node_ref, ch, child); + break; + case Node256: + node256_add_child((struct art_node256*) node, ch, child); + break; + } +} + +static void +node4_add_child(struct art_node4* node, struct art_node** node_ref, unsigned char ch, void* child) +{ + if (node->node.num_children < 4) + { + int idx = find_index(ch, node->keys, node->node.num_children); + // right shift the right part to make space for the key, so that we keep the keys in order + memmove(node->keys + (idx + 1) + 1, node->keys + (idx + 1), node->node.num_children - (idx + 1)); + memmove(node->children + (idx + 1) + 1, node->children + (idx + 1), (node->node.num_children - (idx + 1)) * sizeof(void*)); + + node->keys[idx + 1] = ch; + node->children[idx + 1] = (struct art_node*)child; + node->node.num_children++; + } + else + { + // expand + struct art_node16* new_node = NULL; + create_art_node16(&new_node); + copy_header((struct art_node*)new_node, (struct art_node*)node); + memcpy(new_node->children, node->children, node->node.num_children * sizeof(void*)); + memcpy(new_node->keys, node->keys, node->node.num_children); + // replace the node through node reference + *node_ref = (struct art_node*)new_node; + free(node); + + node16_add_child(new_node, node_ref, ch, child); + } +} + +static void +node16_add_child(struct art_node16* node, struct art_node** node_ref, unsigned char ch, void* child) +{ + if (node->node.num_children < 16) + { + int idx = find_index(ch, node->keys, node->node.num_children); + // right shift the right part to make space for the key, so that we keep the keys in order + memmove(node->keys + (idx + 1) + 1, node->keys + (idx + 1), node->node.num_children - (idx + 1)); + memmove(node->children + (idx + 1) + 1, node->children + (idx + 1), (node->node.num_children - (idx + 1)) * sizeof(void*)); + + node->keys[idx + 1] = ch; + node->children[idx + 1] = (struct art_node*)child; + node->node.num_children++; + } + else + { + // expand + struct art_node48* new_node = NULL; + create_art_node48(&new_node); + copy_header((struct art_node*)new_node, (struct art_node*)node); + memcpy(new_node->children, node->children, node->node.num_children * sizeof(void*)); + for (int i = 0; i < node->node.num_children; i++) + { + new_node->keys[node->keys[i]] = i + 1; + } + // replace the node through node reference + *node_ref = (struct art_node*)new_node; + free(node); + node48_add_child(new_node, node_ref, ch, child); + } +} + +static void +node48_add_child(struct art_node48* node, struct art_node** node_ref, unsigned char ch, void* child) +{ + if (node->node.num_children < 48) + { + // we cannot simply append to last because delete could have caused fragmentation + int pos = 0; + while (node->children[pos] != NULL) + { + pos++; + } + node->children[pos] = (struct art_node*) child; + node->keys[ch] = pos + 1; + node->node.num_children++; + } + else + { + // expand + struct art_node256* new_node = NULL; + create_art_node256(&new_node); + copy_header((struct art_node*)new_node, (struct art_node*)node); + for (int i = 0; i < 256; i++) + { + if (node->keys[i] == 0) + { + continue; + } + new_node->children[i] = node->children[node->keys[i] - 1]; + } + // replace the node through node reference + *node_ref = (struct art_node*)new_node; + free(node); + node256_add_child(new_node, ch, child); + } +} + +static void +node256_add_child(struct art_node256* node, unsigned char ch, void* child) +{ + node->node.num_children++; + node->children[ch] = (struct art_node*)child; +} + +static int +find_index(unsigned char ch, const unsigned char* keys, int length) +{ + int left = 0; + int right = length - 1; + int mid = 0; + if (length == 0) + { + return -1; + } + while (left + 1 < right) + { + mid = (left + right) / 2; + if (keys[mid] == ch) + { + return mid; + } + if (keys[mid] < ch) + { + left = mid; + } + else + { + right = mid; + } + } + if (keys[right] <= ch) + { + return right; + } + else if (keys[left] <= ch) + { + return left; + } + return -1; +} + +static void +copy_header(struct art_node* dest, struct art_node* src) +{ + dest->num_children = src->num_children; + dest->prefix_len = src->prefix_len; + memcpy(dest->prefix, src->prefix, min(MAX_PREFIX_LEN, src->prefix_len)); +} + +static uint32_t +check_prefix_partial(struct art_node* node, unsigned char* key, uint32_t depth, uint32_t key_len) +{ + uint32_t len = 0; + uint32_t max_cmp = min(min(node->prefix_len, MAX_PREFIX_LEN), key_len - depth); + while (len < max_cmp && key[depth + len] == node->prefix[len]) + { + len++; + } + return len; +} + +static uint32_t +check_prefix(struct art_node* node, unsigned char* key, uint32_t depth, uint32_t key_len) +{ + uint32_t len = 0; + struct art_leaf* leaf = NULL; + uint32_t max_cmp = min(min(node->prefix_len, MAX_PREFIX_LEN), key_len - depth); + while (len < max_cmp && key[depth + len] == node->prefix[len]) + { + len++; + } + // diverge within partial prefix range + if (len < MAX_PREFIX_LEN) + { + return len; + } + + leaf = node_get_minimum(node); + max_cmp = min(leaf->key_len, key_len) - depth; + // continue comparing the real keys + while (len < max_cmp && leaf->key[depth + len] == key[depth + len]) + { + len++; + } + return len; +} + +static bool +leaf_match(struct art_leaf* leaf, unsigned char* key, uint32_t key_len) +{ + if (leaf->key_len != key_len) + { + return false; + } + return memcmp(leaf->key, key, key_len) == 0; +} + +static struct art_leaf* +node_get_minimum(struct art_node* node) +{ + if (node == NULL) + { + return NULL; + } + while (node != NULL && !IS_LEAF(node)) + { + switch (node->type) + { + case Node4: + { + struct art_node4* n = (struct art_node4*)node; + node = n->children[0]; + break; + } + case Node16: + { + struct art_node16* n = (struct art_node16*)node; + node = n->children[0]; + break; + } + case Node48: + { + struct art_node48* n = (struct art_node48*) node; + int idx = 0; + while (n->keys[idx] == 0) + { + idx++; + } + node = n->children[n->keys[idx] - 1]; + break; + } + case Node256: + { + struct art_node256* n = (struct art_node256*) node; + int idx = 0; + while (n->children[idx] == NULL) + { + idx++; + } + node = n->children[idx]; + break; + } + } + } + if (node == NULL) + { + return NULL; + } + return GET_LEAF(node); +} + +static void +node_remove_child(struct art_node* node, struct art_node** node_ref, unsigned char ch) +{ + switch (node->type) + { + case Node4: + node4_remove_child((struct art_node4*)node, node_ref, ch); + break; + case Node16: + node16_remove_child((struct art_node16*)node, node_ref, ch); + break; + case Node48: + node48_remove_child((struct art_node48*)node, node_ref, ch); + break; + case Node256: + node256_remove_child((struct art_node256*)node, node_ref, ch); + break; + } +} + +static void +node4_remove_child(struct art_node4* node, struct art_node** node_ref, unsigned char ch) +{ + int idx = 0; + uint32_t len = 0; + struct art_node* child = NULL; + idx = find_index(ch, node->keys, node->node.num_children); + memmove(node->keys + idx, node->keys + idx + 1, node->node.num_children - (idx + 1)); + memmove(node->children + idx, node->children + idx + 1, sizeof(void*) * (node->node.num_children - (idx + 1))); + node->node.num_children--; + // path compression, merge the node with its child + if (node->node.num_children == 1) + { + child = node->children[0]; + if (IS_LEAF(child)) + { + // replace directly + *node_ref = child; + return; + } + // parent prefix bytes + byte index to child + child prefix bytes + len = node->node.prefix_len; + if (len < MAX_PREFIX_LEN) + { + node->node.prefix[len] = node->keys[0]; + len++; + } + // keep filling as much as we can + for (uint32_t i = 0; len + i < MAX_PREFIX_LEN && i < child->prefix_len; i++) + { + node->node.prefix[len + i] = child->prefix[i]; + } + child->prefix_len = node->node.prefix_len + 1 + child->prefix_len; + memcpy(child->prefix, node->node.prefix, min(child->prefix_len, MAX_PREFIX_LEN)); + free(node); + // replace + *node_ref = child; + } +} + +static void +node16_remove_child(struct art_node16* node, struct art_node** node_ref, unsigned char ch) +{ + int idx = 0; + struct art_node4* new_node = NULL; + idx = find_index(ch, node->keys, node->node.num_children); + memmove(node->keys + idx, node->keys + idx + 1, node->node.num_children - (idx + 1)); + memmove(node->children + idx, node->children + idx + 1, sizeof(void*) * (node->node.num_children - (idx + 1))); + node->node.num_children--; + // downgrade node + // Trick from libart, do not downgrade immediately to avoid jumping on 4/5 boundary + if (node->node.num_children <= 3) + { + create_art_node4(&new_node); + copy_header((struct art_node*)new_node, (struct art_node*)node); + memcpy(new_node->keys, node->keys, node->node.num_children); + memcpy(new_node->children, node->children, node->node.num_children * sizeof(void*)); + free(node); + *node_ref = (struct art_node*)new_node; + } +} + +static void +node48_remove_child(struct art_node48* node, struct art_node** node_ref, unsigned char ch) +{ + int idx = node->keys[ch]; + int cnt = 0; + struct art_node16* new_node = NULL; + node->children[idx - 1] = NULL; + node->keys[ch] = 0; + node->node.num_children--; + + if (node->node.num_children <= 12) + { + create_art_node16(&new_node); + copy_header((struct art_node*)new_node, (struct art_node*)node); + for (int i = 0; i < 256; i++) + { + if (node->keys[i] != 0) + { + new_node->children[cnt] = node->children[node->keys[i] - 1]; + new_node->keys[cnt] = i; + cnt++; + } + } + free(node); + *node_ref = (struct art_node*)new_node; + } +} + +static void +node256_remove_child(struct art_node256* node, struct art_node** node_ref, unsigned char ch) +{ + int num = 0; + for (int i = 0; i < 48; i++) + { + if (node->children[i] != NULL) + { + num++; + } + } + if (num != node->node.num_children) + { + num++; + } + struct art_node48* new_node = NULL; + int cnt = 0; + node->children[ch] = NULL; + node->node.num_children--; + + if (node->node.num_children <= 37) + { + create_art_node48(&new_node); + copy_header((struct art_node*)new_node, (struct art_node*)node); + for (int i = 0; i < 256; i++) + { + if (node->children[i] != NULL) + { + new_node->keys[i] = cnt + 1; + new_node->children[cnt] = node->children[i]; + cnt++; + } + } + free(node); + *node_ref = (struct art_node*)new_node; + } +} + +int +pgagroal_art_iterator_create(struct art* t, struct art_iterator** iter) +{ + struct art_iterator* i = NULL; + if (t == NULL) + { + return 1; + } + i = malloc(sizeof(struct art_iterator)); + i->count = 0; + i->tree = t; + i->key = NULL; + i->value = NULL; + pgagroal_deque_create(false, &i->que); + *iter = i; + return 0; +} + +bool +pgagroal_art_iterator_next(struct art_iterator* iter) +{ + struct deque* que = NULL; + struct art* tree = NULL; + struct art_node* node = NULL; + struct art_node* child = NULL; + int idx = 0; + if (iter == NULL || iter->que == NULL || iter->tree == NULL || iter->count == iter->tree->size) + { + return false; + } + que = iter->que; + tree = iter->tree; + if (iter->count == 0) + { + pgagroal_deque_add(que, NULL, (uintptr_t)tree->root, ValueRef); + } + while (!pgagroal_deque_empty(que)) + { + node = (struct art_node*)pgagroal_deque_poll(que, NULL); + if (IS_LEAF(node)) + { + iter->count++; + iter->key = GET_LEAF(node)->key; + iter->value = GET_LEAF(node)->value; + return true; + } + switch (node->type) + { + case Node4: + { + struct art_node4* n = (struct art_node4*) node; + for (int i = 0; i < node->num_children; i++) + { + child = n->children[i]; + pgagroal_deque_add(que, NULL, (uintptr_t)child, ValueRef); + } + break; + } + case Node16: + { + struct art_node16* n = (struct art_node16*) node; + for (int i = 0; i < node->num_children; i++) + { + child = n->children[i]; + pgagroal_deque_add(que, NULL, (uintptr_t)child, ValueRef); + } + break; + } + case Node48: + { + struct art_node48* n = (struct art_node48*) node; + for (int i = 0; i < 256; i++) + { + idx = n->keys[i]; + if (idx == 0) + { + continue; + } + child = n->children[idx - 1]; + pgagroal_deque_add(que, NULL, (uintptr_t)child, ValueRef); + } + break; + } + case Node256: + { + struct art_node256* n = (struct art_node256*) node; + for (int i = 0; i < 256; i++) + { + if (n->children[i] == NULL) + { + continue; + } + child = n->children[i]; + pgagroal_deque_add(que, NULL, (uintptr_t)child, ValueRef); + } + break; + } + } + } + return false; +} + +void +pgagroal_art_iterator_destroy(struct art_iterator* iter) +{ + if (iter == NULL) + { + return; + } + pgagroal_deque_destroy(iter->que); + free(iter); +} + +void +pgagroal_art_destroy_value_noop(void* val) +{ + (void)val; +} + +void +pgagroal_art_destroy_value_default(void* val) +{ + free(val); +} + +static struct value* +art_search(struct art* t, unsigned char* key, uint32_t key_len) +{ + struct art_node* node = NULL; + struct art_node** child = NULL; + uint32_t depth = 0; + if (t == NULL || t->root == NULL) + { + return NULL; + } + node = t->root; + while (node != NULL) + { + if (IS_LEAF(node)) + { + if (!leaf_match(GET_LEAF(node), key, key_len)) + { + return NULL; + } + return GET_LEAF(node)->value; + } + // optimistically check the prefix, + // we move forward as long as up to MAX_PREFIX_LEN characters match + if (check_prefix_partial(node, key, depth, key_len) != min(node->prefix_len, MAX_PREFIX_LEN)) + { + return NULL; + } + depth += node->prefix_len; + // you can't dereference what the function returns directly since it could be null + child = node_get_child(node, key[depth]); + node = child != NULL ? *child : NULL; + // child is indexed by key[depth], so the next round we should skip this byte and start checking at the next + depth++; + } + return NULL; +} + +static int +art_to_json_string_cb(void* param, const unsigned char* key, uint32_t key_len, struct value* value) +{ + struct to_string_param* p = (struct to_string_param*) param; + char* str = NULL; + char* tag = NULL; + char* translated_key = NULL; + p->cnt++; + bool has_next = p->cnt < p->t->size; + tag = pgagroal_append_char(tag, '"'); + translated_key = pgagroal_escape_string((char*)key); + tag = pgagroal_append(tag, translated_key); + free(translated_key); + tag = pgagroal_append_char(tag, '"'); + tag = pgagroal_append(tag, ": "); + str = pgagroal_value_to_string(value, FORMAT_JSON, tag, p->indent); + free(tag); + p->str = pgagroal_append(p->str, str); + p->str = pgagroal_append(p->str, has_next ? ",\n" : "\n"); + + free(str); + return 0; +} + +static int +art_to_compact_json_string_cb(void* param, const unsigned char* key, uint32_t key_len, struct value* value) +{ + struct to_string_param* p = (struct to_string_param*) param; + char* str = NULL; + char* tag = NULL; + char* translated_key = NULL; + p->cnt++; + bool has_next = p->cnt < p->t->size; + tag = pgagroal_append_char(tag, '"'); + translated_key = pgagroal_escape_string((char*)key); + tag = pgagroal_append(tag, (char*)translated_key); + free(translated_key); + tag = pgagroal_append_char(tag, '"'); + tag = pgagroal_append(tag, ":"); + str = pgagroal_value_to_string(value, FORMAT_JSON_COMPACT, tag, p->indent); + free(tag); + p->str = pgagroal_append(p->str, str); + p->str = pgagroal_append(p->str, has_next ? "," : ""); + + free(str); + return 0; +} + +static int +art_to_text_string_cb(void* param, const unsigned char* key, uint32_t key_len, struct value* value) +{ + struct to_string_param* p = (struct to_string_param*) param; + char* str = NULL; + char* tag = NULL; + p->cnt++; + bool has_next = p->cnt < p->t->size; + tag = pgagroal_append(tag, (char*)key); + tag = pgagroal_append(tag, ": "); + if (value->type == ValueJSON && ((struct json*) value->data)->type != JSONUnknown) + { + tag = pgagroal_append(tag, "\n"); + } + if (pgagroal_compare_string(p->tag, BULLET_POINT)) + { + if (p->cnt == 1) + { + if (value->type != ValueJSON || ((struct json*) value->data)->type == JSONUnknown) + { + str = pgagroal_value_to_string(value, FORMAT_TEXT, tag, 0); + } + else + { + p->str = pgagroal_indent(p->str, tag, 0); + str = pgagroal_value_to_string(value, FORMAT_TEXT, NULL, p->indent + INDENT_PER_LEVEL); + } + } + else + { + str = pgagroal_value_to_string(value, FORMAT_TEXT, tag, p->indent + INDENT_PER_LEVEL); + } + } + else + { + str = pgagroal_value_to_string(value, FORMAT_TEXT, tag, p->indent); + } + free(tag); + p->str = pgagroal_append(p->str, str); + p->str = pgagroal_append(p->str, has_next ? "\n" : ""); + + free(str); + return 0; +} + +static char* +to_json_string(struct art* t, char* tag, int indent) +{ + char* ret = NULL; + ret = pgagroal_indent(ret, tag, indent); + if (t == NULL || t->size == 0) + { + ret = pgagroal_append(ret, "{}"); + return ret; + } + ret = pgagroal_append(ret, "{\n"); + struct to_string_param param = { + .indent = indent + INDENT_PER_LEVEL, + .str = ret, + .t = t, + .cnt = 0, + }; + art_iterate(t, art_to_json_string_cb, ¶m); + ret = param.str; + ret = pgagroal_indent(ret, NULL, indent); + ret = pgagroal_append(ret, "}"); + return ret; +} + +static char* +to_compact_json_string(struct art* t, char* tag, int indent) +{ + char* ret = NULL; + ret = pgagroal_indent(ret, tag, indent); + if (t == NULL || t->size == 0) + { + ret = pgagroal_append(ret, "{}"); + return ret; + } + ret = pgagroal_append(ret, "{"); + struct to_string_param param = { + .indent = indent, + .str = ret, + .t = t, + .cnt = 0, + }; + art_iterate(t, art_to_compact_json_string_cb, ¶m); + ret = param.str; + ret = pgagroal_append(ret, "}"); + return ret; +} + +static char* +to_text_string(struct art* t, char* tag, int indent) +{ + char* ret = NULL; + int next_indent = indent; + if (tag != NULL && !pgagroal_compare_string(tag, BULLET_POINT)) + { + ret = pgagroal_indent(ret, tag, indent); + next_indent += INDENT_PER_LEVEL; + } + if (t == NULL || t->size == 0) + { + ret = pgagroal_append(ret, "{}"); + return ret; + } + struct to_string_param param = { + .indent = next_indent, + .str = ret, + .t = t, + .cnt = 0, + .tag = tag + }; + art_iterate(t, art_to_text_string_cb, ¶m); + ret = param.str; + return ret; +} + +static int +art_iterate(struct art* t, art_callback cb, void* data) +{ + return art_node_iterate(t->root, cb, data); +} \ No newline at end of file diff --git a/src/libpgagroal/bzip2_compression.c b/src/libpgagroal/bzip2_compression.c new file mode 100644 index 00000000..de5c83b9 --- /dev/null +++ b/src/libpgagroal/bzip2_compression.c @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BUFFER_LENGTH 8192 + +int +pgagroal_bzip2_string(char* s, unsigned char** buffer, size_t* buffer_size) +{ + size_t source_len; + unsigned int dest_len; + int bzip2_err; + + source_len = strlen(s); + dest_len = source_len + (source_len * 0.01) + 600; + + *buffer = (unsigned char*)malloc(dest_len); + if (!*buffer) + { + pgagroal_log_error("Bzip2: Allocation failed"); + return 1; + } + + bzip2_err = BZ2_bzBuffToBuffCompress((char*)(*buffer), &dest_len, s, source_len, 9, 0, 0); + if (bzip2_err != BZ_OK) + { + pgagroal_log_error("Bzip2: Compress failed"); + free(*buffer); + return 1; + } + + *buffer_size = dest_len; + return 0; +} + +int +pgagroal_bunzip2_string(unsigned char* compressed_buffer, size_t compressed_size, char** output_string) +{ + int bzip2_err; + unsigned int estimated_size = compressed_size * 10; + unsigned int new_size; + + *output_string = (char*)malloc(estimated_size); + if (!*output_string) + { + pgagroal_log_error("Bzip2: Allocation failed"); + return 1; + } + + bzip2_err = BZ2_bzBuffToBuffDecompress(*output_string, &estimated_size, (char*)compressed_buffer, compressed_size, 0, 0); + + if (bzip2_err == BZ_OUTBUFF_FULL) + { + new_size = estimated_size * 2; + char* temp = realloc(*output_string, new_size); + + if (!temp) + { + pgagroal_log_error("Bzip2: Reallocation failed"); + free(*output_string); + return 1; + } + + *output_string = temp; + + bzip2_err = BZ2_bzBuffToBuffDecompress(*output_string, &new_size, (char*)compressed_buffer, compressed_size, 0, 0); + if (bzip2_err != BZ_OK) + { + pgagroal_log_error("Bzip2: Decompress failed"); + free(*output_string); + return 1; + } + estimated_size = new_size; + } + else if (bzip2_err != BZ_OK) + { + pgagroal_log_error("Bzip2: Decompress failed"); + free(*output_string); + return 1; + } + + return 0; +} diff --git a/src/libpgagroal/configuration.c b/src/libpgagroal/configuration.c new file mode 100644 index 00000000..c8d68e2a --- /dev/null +++ b/src/libpgagroal/configuration.c @@ -0,0 +1,6061 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_LINUX +#include +#endif + +#define LINE_LENGTH 512 + +static int extract_key_value(char* str, char** key, char** value); +static int as_int(char* str, int* i); +static int as_bool(char* str, bool* b); +static int as_logging_type(char* str); +static int as_logging_level(char* str); +static int as_logging_mode(char* str); + +static int as_logging_rotation_size(char* str, unsigned int* size); +static int as_logging_rotation_age(char* str, unsigned int* age); +static int as_validation(char* str); +static int as_pipeline(char* str); +static int as_hugepage(char* str); +static unsigned int as_update_process_title(char* str, unsigned int* policy, unsigned int default_policy); +static int extract_value(char* str, int offset, char** value); +static void extract_hba(char* str, char** type, char** database, char** user, char** address, char** method); +static void extract_limit(char* str, int server_max, char** database, char** user, int* max_size, int* initial_size, int* min_size); +static unsigned int as_seconds(char* str, unsigned int* age, unsigned int default_age); +static unsigned int as_bytes(char* str, unsigned int* bytes, unsigned int default_bytes); + +static bool transfer_configuration(struct main_configuration* config, struct main_configuration* reload); +static void copy_server(struct server* dst, struct server* src); +static void copy_hba(struct hba* dst, struct hba* src); +static void copy_user(struct user* dst, struct user* src); +static int restart_int(char* name, int e, int n); +static int restart_bool(char* name, bool e, bool n); +static int restart_string(char* name, char* e, char* n, bool skip_non_existing); +static int restart_limit(char* name, struct main_configuration* config, struct main_configuration* reload); +static int restart_server(struct server* src, struct server* dst); + +static bool is_empty_string(char* s); +static bool is_same_server(struct server* s1, struct server* s2); +static bool is_same_tls(struct server* s1, struct server* s2); + +static bool key_in_section(char* wanted, char* section, char* key, bool global, bool* unknown); +static bool is_comment_line(char* line); +static bool section_line(char* line, char* section); + +static int pgagroal_write_server_config_value(char* buffer, char* server_name, char* config_key, size_t buffer_size); +static int pgagroal_write_hba_config_value(char* buffer, char* username, char* config_key, size_t buffer_size); +static int pgagroal_write_limit_config_value(char* buffer, char* database, char* config_key, size_t buffer_size); +static int pgagroal_apply_hba_configuration(struct hba* hba, char* context, char* value); +static int pgagroal_apply_limit_configuration_string(struct limit* limit, char* context, char* value); +static int pgagroal_apply_limit_configuration_int(struct limit* limit, char* context, int value); + +static int to_string(char* where, char* value, size_t max_length); +static int to_bool(char* where, bool value); +static int to_int(char* where, int value); +static int to_update_process_title(char* where, int value); +static int to_validation(char* where, int value); +static int to_pipeline(char* where, int value); +static int to_log_mode(char* where, int value); +static int to_log_level(char* where, int value); +static int to_log_type(char* where, int value); + +static void add_configuration_response(struct json* res); +static void add_servers_configuration_response(struct json* res); + +/** + * + */ +int +pgagroal_init_configuration(void* shm) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shm; + + atomic_init(&config->active_connections, 0); + + for (int i = 0; i < NUMBER_OF_SERVERS; i++) + { + atomic_init(&config->servers[i].state, SERVER_NOTINIT); + } + + config->failover = false; + config->common.tls = false; + config->gracefully = false; + config->pipeline = PIPELINE_AUTO; + config->authquery = false; + config->blocking_timeout = 30; + config->idle_timeout = 0; + config->rotate_frontend_password_timeout = 0; + config->rotate_frontend_password_length = MIN_PASSWORD_LENGTH; + config->max_connection_age = 0; + config->validation = VALIDATION_OFF; + config->background_interval = 300; + config->max_retries = 5; + config->common.authentication_timeout = 5; + config->disconnect_client = 0; + config->disconnect_client_force = false; + + config->all_disabled = false; + + config->keep_alive = true; + config->nodelay = true; + config->non_blocking = false; + config->backlog = -1; + config->common.hugepage = HUGEPAGE_TRY; + config->tracker = false; + config->track_prepared_statements = false; + + config->common.log_type = PGAGROAL_LOGGING_TYPE_CONSOLE; + config->common.log_level = PGAGROAL_LOGGING_LEVEL_INFO; + config->common.log_connections = false; + config->common.log_disconnections = false; + config->common.log_mode = PGAGROAL_LOGGING_MODE_APPEND; + atomic_init(&config->common.log_lock, STATE_FREE); + + memcpy(config->common.default_log_path, "pgagroal.log", strlen("pgagroal.log")); + + config->max_connections = 100; + config->allow_unknown_users = true; + + atomic_init(&config->su_connection, STATE_FREE); + + config->update_process_title = UPDATE_PROCESS_TITLE_VERBOSE; + + return 0; +} + +/** + * This struct is going to store the metadata + * about which sections have been parsed during + * the configuration read. + * This can be used to seek for duplicated sections + * at different positions in the configuration file. + */ +struct config_section +{ + char name[LINE_LENGTH]; /**< The name of the section */ + unsigned int lineno; /**< The line number for this section */ + bool main; /**< Is this the main configuration section or a server one? */ +}; + +/** + * + */ +int +pgagroal_read_configuration(void* shm, char* filename, bool emit_warnings) +{ + FILE* file; + char section[LINE_LENGTH]; + char line[LINE_LENGTH]; + char* key = NULL; + char* value = NULL; + struct main_configuration* config; + int idx_server = 0; + struct server srv; + bool has_main_section = false; + + // the max number of sections allowed in the configuration + // file is done by the max number of servers plus the main `pgagroal` + // configuration section + struct config_section sections[NUMBER_OF_SERVERS + 1]; + int idx_sections = 0; + int lineno = 0; + int return_value = 0; + + file = fopen(filename, "r"); + + if (!file) + { + return PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND; + } + + memset(§ion, 0, LINE_LENGTH); + memset(§ions, 0, sizeof(struct config_section) * NUMBER_OF_SERVERS + 1); + config = (struct main_configuration*)shm; + + while (fgets(line, sizeof(line), file)) + { + lineno++; + + if (!is_empty_string(line) && !is_comment_line(line)) + { + if (section_line(line, section)) + { + // check we don't overflow the number of available sections + if (idx_sections >= NUMBER_OF_SERVERS + 1) + { + warnx("Max number of sections (%d) in configuration file <%s> reached!", + NUMBER_OF_SERVERS + 1, + filename); + return PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG; + } + + // initialize the section structure + memset(sections[idx_sections].name, 0, LINE_LENGTH); + memcpy(sections[idx_sections].name, section, strlen(section)); + sections[idx_sections].lineno = lineno; + sections[idx_sections].main = !strncmp(section, PGAGROAL_MAIN_INI_SECTION, LINE_LENGTH); + if (sections[idx_sections].main) + { + has_main_section = true; + } + + idx_sections++; + + if (strcmp(section, PGAGROAL_MAIN_INI_SECTION)) + { + if (idx_server > 0 && idx_server <= NUMBER_OF_SERVERS) + { + memcpy(&(config->servers[idx_server - 1]), &srv, sizeof(struct server)); + } + else if (idx_server > NUMBER_OF_SERVERS) + { + printf("Maximum number of servers exceeded\n"); + } + + memset(&srv, 0, sizeof(struct server)); + atomic_init(&srv.state, SERVER_NOTINIT); + memcpy(&srv.name, §ion, strlen(section)); + srv.lineno = lineno; + idx_server++; + } + } + else + { + extract_key_value(line, &key, &value); + + if (key && value) + { + bool unknown = false; + + //printf("\nSection <%s> key <%s> = <%s>", section, key, value); + + // apply the configuration setting + if (pgagroal_apply_main_configuration(config, &srv, section, key, value)) + { + unknown = true; + } + + if (unknown && emit_warnings) + { + // we cannot use logging here... + // if we have a section, the key is not known, + // otherwise it is outside of a section at all + if (strlen(section) > 0) + { + warnx("Unknown key <%s> with value <%s> in section [%s] (line %d of file <%s>)", + key, + value, + section, + lineno, + filename); + } + else + { + warnx("Key <%s> with value <%s> out of any section (line %d of file <%s>)", + key, + value, + lineno, + filename); + } + } + + free(key); + free(value); + key = NULL; + value = NULL; + } + } + } + } + + if (strlen(srv.name) > 0) + { + memcpy(&(config->servers[idx_server - 1]), &srv, sizeof(struct server)); + } + + config->number_of_servers = idx_server; + + fclose(file); + + // check there is at least one main section + if (!has_main_section) + { + warnx("No main configuration section [%s] found in file <%s>", + PGAGROAL_MAIN_INI_SECTION, + filename); + return PGAGROAL_CONFIGURATION_STATUS_KO; + } + + // validate the sections: + // do a nested loop to scan over all the sections that have a duplicated + // name and warn the user about them. + for (int i = 0; i < NUMBER_OF_SERVERS + 1; i++) + { + for (int j = i + 1; j < NUMBER_OF_SERVERS + 1; j++) + { + // skip uninitialized sections + if (!strlen(sections[i].name) || !strlen(sections[j].name)) + { + continue; + } + + if (!strncmp(sections[i].name, sections[j].name, LINE_LENGTH)) + { + // cannot log here ... + warnx("%s section [%s] duplicated at lines %d and %d of file <%s>", + sections[i].main ? "Main" : "Server", + sections[i].name, + sections[i].lineno, + sections[j].lineno, + filename); + return_value++; // this is an error condition! + } + } + } + + return return_value; +} + +/** + * + */ +int +pgagroal_validate_configuration(void* shm, bool has_unix_socket, bool has_main_sockets) +{ + bool tls; + struct stat st; + struct main_configuration* config; + + tls = false; + + config = (struct main_configuration*)shm; + + if (!has_main_sockets) + { + if (strlen(config->common.host) == 0) + { + pgagroal_log_fatal("pgagroal: No host defined"); + return 1; + } + + if (config->common.port <= 0) + { + pgagroal_log_fatal("pgagroal: No port defined"); + return 1; + } + } + + if (!has_unix_socket) + { + if (strlen(config->unix_socket_dir) == 0) + { + pgagroal_log_fatal("pgagroal: No unix_socket_dir defined"); + return 1; + } + + if (stat(config->unix_socket_dir, &st) == 0 && S_ISDIR(st.st_mode)) + { + /* Ok */ + } + else + { + pgagroal_log_fatal("pgagroal: unix_socket_dir is not a directory (%s)", config->unix_socket_dir); + return 1; + } + } + + if (config->backlog <= 0) + { + config->backlog = MAX(config->max_connections / 4, 16); + } + + if (config->common.authentication_timeout <= 0) + { + config->common.authentication_timeout = 5; + } + + if (config->disconnect_client <= 0) + { + config->disconnect_client = 0; + } + + if (config->authquery) + { + if (strlen(config->superuser.username) == 0) + { + pgagroal_log_fatal("pgagroal: Authentication query requires a superuser"); + return 1; + } + else + { + config->allow_unknown_users = true; + + if (config->number_of_users > 0) + { + pgagroal_log_fatal("pgagroal: Users are not supported when using authentication query"); + return 1; + } + + if (config->number_of_frontend_users > 0) + { + pgagroal_log_fatal("pgagroal: Frontend users are not supported when using authentication query"); + return 1; + } + + if (config->number_of_limits > 0) + { + pgagroal_log_fatal("pgagroal: Limits are not supported when using authentication query"); + return 1; + } + } + } + + if (config->max_connections <= 0) + { + pgagroal_log_fatal("pgagroal: max_connections must be greater than 0"); + return 1; + } + + if (config->rotate_frontend_password_length < MIN_PASSWORD_LENGTH || config->rotate_frontend_password_length > MAX_PASSWORD_LENGTH) + { + pgagroal_log_fatal("pgagroal: rotate_frontend_password_length should be within [8-1024] characters"); + return 1; + } + + if (config->max_connections > MAX_NUMBER_OF_CONNECTIONS) + { + pgagroal_log_warn("pgagroal: max_connections (%d) is greater than allowed (%d)", config->max_connections, MAX_NUMBER_OF_CONNECTIONS); + config->max_connections = MAX_NUMBER_OF_CONNECTIONS; + } + + if (config->number_of_frontend_users > 0 && config->allow_unknown_users) + { + pgagroal_log_warn("pgagroal: Frontend users should not be used with allow_unknown_users"); + } + + if (config->number_of_frontend_users == 0 && config->number_of_users == 0 && config->rotate_frontend_password_timeout > 0) + { + pgagroal_log_fatal("pgagroal: Users must be defined for rotation frontend password to be enabled"); + return 1; + } + + if (config->failover) + { + if (strlen(config->failover_script) == 0) + { + pgagroal_log_fatal("pgagroal: Failover requires a script definition"); + return 1; + } + + memset(&st, 0, sizeof(struct stat)); + + if (stat(config->failover_script, &st) == -1) + { + pgagroal_log_error("pgagroal: Can't locate failover script: %s", config->failover_script); + return 1; + } + + if (!S_ISREG(st.st_mode)) + { + pgagroal_log_error("pgagroal: Failover script is not a regular file: %s", config->failover_script); + return 1; + } + + if (st.st_uid != geteuid()) + { + pgagroal_log_error("pgagroal: Failover script not owned by user: %s", config->failover_script); + return 1; + } + + if (!(st.st_mode & (S_IRUSR | S_IXUSR))) + { + pgagroal_log_error("pgagroal: Failover script must be executable: %s", config->failover_script); + return 1; + } + + if (config->number_of_servers <= 1) + { + pgagroal_log_fatal("pgagroal: Failover requires at least 2 servers defined"); + return 1; + } + } + + if (config->number_of_servers <= 0) + { + pgagroal_log_fatal("pgagroal: No servers defined"); + return 1; + } + + for (int i = 0; i < config->number_of_servers; i++) + { + if (strlen(config->servers[i].host) == 0) + { + pgagroal_log_fatal("pgagroal: No host defined for server [%s] (%s:%d)", + config->servers[i].name, + config->common.configuration_path[0], + config->servers[i].lineno); + return 1; + } + + if (config->servers[i].port == 0) + { + pgagroal_log_fatal("pgagroal: No port defined for server [%s] (%s:%d)", + config->servers[i].name, + config->common.configuration_path[0], + config->servers[i].lineno); + return 1; + } + } + + // check for duplicated servers + for (int i = 0; i < config->number_of_servers; i++) + { + for (int j = i + 1; j < config->number_of_servers; j++) + { + if (is_same_server(&config->servers[i], &config->servers[j])) + { + pgagroal_log_fatal("pgagroal: Servers [%s] and [%s] are duplicated! (%s:%d:%d)", + config->servers[i].name, + config->servers[j].name, + config->common.configuration_path[0], + config->servers[i].lineno, + config->servers[j].lineno); + return 1; + } + } + } + + if (config->pipeline == PIPELINE_AUTO) + { + if (config->common.tls && (strlen(config->common.tls_cert_file) > 0 || strlen(config->common.tls_key_file) > 0)) + { + tls = true; + } + + if (config->failover || tls || config->disconnect_client > 0) + { + config->pipeline = PIPELINE_SESSION; + } + else + { + config->pipeline = PIPELINE_PERFORMANCE; + } + } + + if (config->pipeline == PIPELINE_SESSION) + { + /* Checks */ + } + else if (config->pipeline == PIPELINE_TRANSACTION) + { + if (config->disconnect_client > 0) + { + pgagroal_log_fatal("pgagroal: Transaction pipeline does not support disconnect_client"); + return 1; + } + + if (!config->authquery) + { + if (config->number_of_users == 0) + { + pgagroal_log_fatal("pgagroal: Users must be defined for the transaction pipeline"); + return 1; + } + + if (config->allow_unknown_users) + { + pgagroal_log_fatal("pgagroal: Transaction pipeline does not support allow_unknown_users"); + return 1; + } + } + + for (int i = 0; i < config->number_of_servers; i++) + { + if (config->servers[i].tls) + { + pgagroal_log_fatal("pgagroal: Transaction pipeline does not support TLS to a server"); + return 1; + } + } + + if (config->number_of_limits == 0) + { + pgagroal_log_fatal("pgagroal: Defining limits for the transaction pipeline is mandatory"); + return 1; + } + + for (int i = 0; i < config->number_of_limits; i++) + { + if (config->limits[i].min_size <= 0) + { + pgagroal_log_fatal("pgagroal: min_size for transaction pipeline must be greater than 0"); + return 1; + } + + if (config->limits[i].initial_size <= 0) + { + pgagroal_log_fatal("pgagroal: initial_size for transaction pipeline must be greater than 0"); + return 1; + } + + if (config->limits[i].max_size <= 0) + { + pgagroal_log_fatal("pgagroal: max_size for transaction pipeline must be greater than 0"); + return 1; + } + } + + if (config->blocking_timeout > 0) + { + pgagroal_log_warn("pgagroal: Using blocking_timeout for the transaction pipeline is not recommended"); + } + + if (config->idle_timeout > 0) + { + pgagroal_log_warn("pgagroal: Using idle_timeout for the transaction pipeline is not recommended"); + } + + if (config->rotate_frontend_password_timeout > 0) + { + pgagroal_log_warn("pgagroal: Using rotate_frontend_password_timeout for the transaction pipeline is not recommended"); + } + + if (config->max_connection_age > 0) + { + pgagroal_log_warn("pgagroal: Using max_connection_age for the transaction pipeline is not recommended"); + } + + if (config->validation == VALIDATION_FOREGROUND) + { + pgagroal_log_warn("pgagroal: Using foreground validation for the transaction pipeline is not recommended"); + } + } + else if (config->pipeline == PIPELINE_PERFORMANCE) + { + if (config->common.tls && (strlen(config->common.tls_cert_file) > 0 || strlen(config->common.tls_key_file) > 0)) + { + tls = true; + } + + if (config->failover) + { + pgagroal_log_fatal("pgagroal: Performance pipeline does not support failover"); + return 1; + } + + if (tls) + { + pgagroal_log_fatal("pgagroal: Performance pipeline does not support TLS"); + return 1; + } + + if (config->disconnect_client > 0) + { + pgagroal_log_fatal("pgagroal: Performance pipeline does not support disconnect_client"); + return 1; + } + } + + // do some last initialization here, since the configuration + // looks good so far + pgagroal_init_pidfile_if_needed(); + + return 0; +} + +/** + * + */ +int +pgagroal_vault_init_configuration(void* shm) +{ + struct vault_configuration* config; + + config = (struct vault_configuration*)shm; + + config->common.port = 0; + config->common.tls = false; + + config->vault_server.server.port = 0; + config->vault_server.server.tls = false; + config->number_of_users = 0; + config->common.authentication_timeout = 5; + config->common.hugepage = HUGEPAGE_TRY; + config->common.log_type = PGAGROAL_LOGGING_TYPE_CONSOLE; + config->common.log_level = PGAGROAL_LOGGING_LEVEL_INFO; + config->common.log_connections = false; + config->common.log_disconnections = false; + config->common.log_mode = PGAGROAL_LOGGING_MODE_APPEND; + atomic_init(&config->common.log_lock, STATE_FREE); + memcpy(config->common.default_log_path, "pgagroal-vault.log", strlen("pgagroal-vault.log")); + + memset(config->vault_server.user.password, 0, MAX_PASSWORD_LENGTH); + + return 0; +} + +/** + * + */ +int +pgagroal_vault_read_configuration(void* shm, char* filename, bool emit_warnings) +{ + FILE* file; + char section[LINE_LENGTH]; + char line[LINE_LENGTH]; + char* key = NULL; + char* value = NULL; + struct vault_configuration* config; + int idx_server = 0; + struct vault_server srv; + bool has_vault_section = false; + + // the max number of sections allowed in the configuration + // file is done by the max number of servers plus the main `pgagroal` + // configuration section + struct config_section sections[1 + 1]; + int idx_sections = 0; + int lineno = 0; + int return_value = 0; + + file = fopen(filename, "r"); + + if (!file) + { + return PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND; + } + + memset(§ion, 0, LINE_LENGTH); + memset(§ions, 0, sizeof(struct config_section) * 2); + config = (struct vault_configuration*)shm; + + while (fgets(line, sizeof(line), file)) + { + lineno++; + if (!is_empty_string(line) && !is_comment_line(line)) + { + if (section_line(line, section)) + { + // check we don't overflow the number of available sections + if (idx_sections >= 2) + { + warnx("Max number of sections (%d) in configuration file <%s> reached!", + 2, + filename); + return PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG; + } + + // initialize the section structure + memset(sections[idx_sections].name, 0, LINE_LENGTH); + memcpy(sections[idx_sections].name, section, strlen(section)); + sections[idx_sections].lineno = lineno; + sections[idx_sections].main = !strncmp(section, PGAGROAL_VAULT_INI_SECTION, LINE_LENGTH); + if (sections[idx_sections].main) + { + has_vault_section = true; + } + + idx_sections++; + + if (strcmp(section, PGAGROAL_VAULT_INI_SECTION)) + { + if (idx_server > 0 && idx_server <= 2) + { + memcpy(&(config->vault_server), &srv, sizeof(struct vault_server)); + } + else if (idx_server > 1) + { + printf("Maximum number of servers exceeded\n"); + } + + memset(&srv, 0, sizeof(struct vault_server)); + memcpy(&srv.server.name, §ion, strlen(section)); + srv.server.lineno = lineno; + idx_server++; + } + } + else + { + extract_key_value(line, &key, &value); + + if (key && value) + { + bool unknown = false; + + //printf("\nSection <%s> key <%s> = <%s>", section, key, value); + + // apply the configuration setting + if (pgagroal_apply_vault_configuration(config, &srv, section, key, value)) + { + unknown = true; + } + + if (unknown && emit_warnings) + { + // we cannot use logging here... + // if we have a section, the key is not known, + // otherwise it is outside of a section at all + if (strlen(section) > 0) + { + warnx("Unknown key <%s> with value <%s> in section [%s] (line %d of file <%s>)", + key, + value, + section, + lineno, + filename); + } + else + { + warnx("Key <%s> with value <%s> out of any section (line %d of file <%s>)", + key, + value, + lineno, + filename); + } + } + + free(key); + free(value); + key = NULL; + value = NULL; + } + } + } + } + + if (strlen(srv.server.name) > 0) + { + memcpy(&(config->vault_server), &srv, sizeof(struct vault_server)); + } + + fclose(file); + + // check there is at least one main section + if (!has_vault_section) + { + warnx("No vault configuration section [%s] found in file <%s>", + PGAGROAL_VAULT_INI_SECTION, + filename); + return PGAGROAL_CONFIGURATION_STATUS_KO; + } + + return return_value; +} + +/** + * + */ +int +pgagroal_vault_validate_configuration (void* shm) +{ + struct vault_configuration* config; + config = (struct vault_configuration*)shm; + + if (strlen(config->common.host) == 0) + { + pgagroal_log_fatal("pgagroal-vault: No host defined"); + return 1; + } + + if (config->common.port <= 0) + { + pgagroal_log_fatal("pgagroal-vault: No port defined"); + return 1; + } + + if (config->common.authentication_timeout < 0) + { + config->common.authentication_timeout = 5; + } + + if (strlen(config->vault_server.server.host) == 0) + { + pgagroal_log_fatal("pgagroal-vault: No host defined for server [%s] (%s:%d)", + config->vault_server.server.name, + config->common.configuration_path, + config->vault_server.server.lineno); + return 1; + } + + if (config->vault_server.server.port == 0) + { + pgagroal_log_fatal("pgagroal-vault: No port defined for server [%s] (%s:%d)", + config->vault_server.server.name, + config->common.configuration_path, + config->vault_server.server.lineno); + return 1; + } + + if (strlen(config->vault_server.user.username) == 0) + { + pgagroal_log_fatal("pgagroal-vault: No user defined for server [%s] (%s:%d)", + config->vault_server.server.name, + config->common.configuration_path, + config->vault_server.server.lineno); + return 1; + } + + return 0; +} + +/** + * + */ +int +pgagroal_read_hba_configuration(void* shm, char* filename) +{ + FILE* file; + char line[LINE_LENGTH]; + int index; + char* type = NULL; + char* database = NULL; + char* username = NULL; + char* address = NULL; + char* method = NULL; + int lineno = 0; + struct main_configuration* config; + + file = fopen(filename, "r"); + + if (!file) + { + return PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND; + } + + index = 0; + config = (struct main_configuration*)shm; + + while (fgets(line, sizeof(line), file)) + { + lineno++; + + if (!is_empty_string(line) && !is_comment_line(line)) + { + extract_hba(line, &type, &database, &username, &address, &method); + + if (pgagroal_apply_hba_configuration(&config->hbas[index], PGAGROAL_HBA_ENTRY_TYPE, type) == 0 + && pgagroal_apply_hba_configuration(&config->hbas[index], PGAGROAL_HBA_ENTRY_DATABASE, database) == 0 + && pgagroal_apply_hba_configuration(&config->hbas[index], PGAGROAL_HBA_ENTRY_USERNAME, username) == 0 + && pgagroal_apply_hba_configuration(&config->hbas[index], PGAGROAL_HBA_ENTRY_ADDRESS, address) == 0 + && pgagroal_apply_hba_configuration(&config->hbas[index], PGAGROAL_HBA_ENTRY_METHOD, method) == 0) + { + // ok, this configuration has been applied + index++; + + if (index >= NUMBER_OF_HBAS) + { + warnx("Too many HBA entries (max is %d)\n", NUMBER_OF_HBAS); + fclose(file); + return PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG; + } + } + else + { + warnx("Invalid HBA entry (%s:%d)", filename, lineno); + } + + free(type); + free(database); + free(username); + free(address); + free(method); + + type = NULL; + database = NULL; + username = NULL; + address = NULL; + method = NULL; + } + } + + config->number_of_hbas = index; + + fclose(file); + + return PGAGROAL_CONFIGURATION_STATUS_OK; +} + +/** + * + */ +int +pgagroal_validate_hba_configuration(void* shm) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shm; + + if (config->number_of_hbas == 0) + { + pgagroal_log_fatal("pgagroal: No HBA entry defined"); + return 1; + } + + for (int i = 0; i < config->number_of_hbas; i++) + { + if (!strcasecmp("host", config->hbas[i].type) || + !strcasecmp("hostssl", config->hbas[i].type)) + { + /* Ok */ + } + else + { + pgagroal_log_fatal("Unknown HBA type: %s (%s:%d)", config->hbas[i].type, config->hba_path, config->hbas[i].lineno); + return 1; + } + + if (!strcasecmp("trust", config->hbas[i].method) || + !strcasecmp("reject", config->hbas[i].method) || + !strcasecmp("password", config->hbas[i].method) || + !strcasecmp("md5", config->hbas[i].method) || + !strcasecmp("scram-sha-256", config->hbas[i].method) || + !strcasecmp("all", config->hbas[i].method)) + { + /* Ok */ + } + else + { + pgagroal_log_fatal("Unknown HBA method: %s (%s:%d)", config->hbas[i].method, config->hba_path, config->hbas[i].lineno); + return 1; + } + } + + return 0; +} + +/** + * + */ +int +pgagroal_read_limit_configuration(void* shm, char* filename) +{ + FILE* file; + char line[LINE_LENGTH]; + int index; + char* database = NULL; + char* username = NULL; + int max_size; + int initial_size; + int min_size; + int server_max; + int lineno; + struct main_configuration* config; + + file = fopen(filename, "r"); + + if (!file) + { + return PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND; + } + + index = 0; + lineno = 0; + config = (struct main_configuration*)shm; + + server_max = config->max_connections; + + while (fgets(line, sizeof(line), file)) + { + lineno++; + + if (!is_empty_string(line) && !is_comment_line(line)) + { + initial_size = 0; + min_size = 0; + + extract_limit(line, server_max, &database, &username, &max_size, &initial_size, &min_size); + lineno++; + + if (database && username) + { + + // normalize the sizes + initial_size = initial_size > max_size ? max_size : initial_size; + min_size = min_size > max_size ? max_size : min_size; + + if (pgagroal_apply_limit_configuration_string(&config->limits[index], PGAGROAL_LIMIT_ENTRY_DATABASE, database) == 0 + && pgagroal_apply_limit_configuration_string(&config->limits[index], PGAGROAL_LIMIT_ENTRY_USERNAME, username) == 0 + && pgagroal_apply_limit_configuration_int(&config->limits[index], PGAGROAL_LIMIT_ENTRY_MAX_SIZE, max_size) == 0 + && pgagroal_apply_limit_configuration_int(&config->limits[index], PGAGROAL_LIMIT_ENTRY_MIN_SIZE, min_size) == 0 + && pgagroal_apply_limit_configuration_int(&config->limits[index], PGAGROAL_LIMIT_ENTRY_LINENO, lineno) == 0 + && pgagroal_apply_limit_configuration_int(&config->limits[index], PGAGROAL_LIMIT_ENTRY_INITIAL_SIZE, initial_size) == 0) + { + // configuration applied + server_max -= max_size; + + memcpy(&(config->limits[index].database), database, strlen(database)); + memcpy(&(config->limits[index].username), username, strlen(username)); + config->limits[index].max_size = max_size; + config->limits[index].initial_size = initial_size; + config->limits[index].min_size = min_size; + config->limits[index].lineno = lineno; + atomic_init(&config->limits[index].active_connections, 0); + + index++; + + if (index >= NUMBER_OF_LIMITS) + { + warnx("Too many LIMIT entries (max is %d)\n", NUMBER_OF_LIMITS); + fclose(file); + return PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG; + } + + } + else + { + warnx("Invalid LIMIT entry /%s:%d)", config->limit_path, lineno); + } + + free(database); + free(username); + + database = NULL; + username = NULL; + max_size = 0; + } + } + } + + config->number_of_limits = index; + + fclose(file); + + return PGAGROAL_CONFIGURATION_STATUS_OK; +} + +/** + * + */ +int +pgagroal_validate_limit_configuration(void* shm) +{ + int total_connections; + struct main_configuration* config; + + total_connections = 0; + config = (struct main_configuration*)shm; + + for (int i = 0; i < config->number_of_limits; i++) + { + total_connections += config->limits[i].max_size; + + if (config->limits[i].max_size <= 0) + { + pgagroal_log_fatal("max_size must be greater than 0 for limit entry %d (%s:%d)", i + 1, config->limit_path, config->limits[i].lineno); + return 1; + } + + if (config->limits[i].initial_size < 0) + { + pgagroal_log_fatal("initial_size must be greater or equal to 0 for limit entry %d (%s:%d)", i + 1, config->limit_path, config->limits[i].lineno); + return 1; + } + + if (config->limits[i].min_size < 0) + { + pgagroal_log_fatal("min_size must be greater or equal to 0 for limit entry %d (%s:%d)", i + 1, config->limit_path, config->limits[i].lineno); + return 1; + } + + if (config->limits[i].initial_size > 0 || config->limits[i].min_size > 0) + { + bool user_found = false; + + for (int j = 0; j < config->number_of_users; j++) + { + if (!strcmp(config->limits[i].username, config->users[j].username)) + { + user_found = true; + } + } + + if (!user_found) + { + pgagroal_log_fatal("Unknown user '%s' for limit entry %d (%s:%d)", config->limits[i].username, i + 1, config->limit_path, config->limits[i].lineno); + return 1; + } + + if (config->limits[i].initial_size != 0 && config->limits[i].initial_size < config->limits[i].min_size) + { + pgagroal_log_warn("initial_size smaller than min_size for limit entry %d (%s:%d)", i + 1, config->limit_path, config->limits[i].lineno); + pgagroal_log_info("Adjusting initial_size from %d to %d (min_size) for limit entry %d (%s:%d)", + config->limits[i].initial_size, + config->limits[i].min_size, i + 1, config->limit_path, config->limits[i].lineno); + config->limits[i].initial_size = config->limits[i].min_size; + } + + if (config->limits[i].initial_size != 0 && config->limits[i].initial_size > config->limits[i].max_size) + { + pgagroal_log_warn("initial_size greater than max_size for limit entry %d (%s:%d)", i + 1, config->limit_path, config->limits[i].lineno); + pgagroal_log_info("Adjusting initial_size from %d to %d (max_size) for limit entry %d (%s:%d)", + config->limits[i].initial_size, config->limits[i].max_size + , i + 1, config->limit_path, config->limits[i].lineno); + config->limits[i].initial_size = config->limits[i].max_size; + } + + if (config->limits[i].max_size < config->limits[i].min_size) + { + pgagroal_log_warn("max_size smaller than min_size for limit entry %d (%s:%d)", i + 1, config->limit_path, config->limits[i].lineno); + pgagroal_log_info("Adjusting min_size from %d to %d (max_size) for limit entry %d (%s:%d)", + config->limits[i].min_size, config->limits[i].max_size + , i + 1, config->limit_path, config->limits[i].lineno); + config->limits[i].min_size = config->limits[i].max_size; + } + + } + } + + if (total_connections > config->max_connections) + { + pgagroal_log_fatal("pgagroal: LIMIT: Too many connections defined %d (max_connections = %d)", total_connections, config->max_connections); + return 1; + } + + return 0; +} + +/** + * + */ +int +pgagroal_read_users_configuration(void* shm, char* filename) +{ + FILE* file; + char line[LINE_LENGTH]; + int index; + char* master_key = NULL; + char* username = NULL; + char* password = NULL; + char* decoded = NULL; + size_t decoded_length = 0; + char* ptr = NULL; + struct main_configuration* config; + int status; + + file = fopen(filename, "r"); + + if (!file) + { + status = PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND; + goto error; + } + + if (pgagroal_get_master_key(&master_key)) + { + status = PGAGROAL_CONFIGURATION_STATUS_KO; + goto error; + } + + index = 0; + config = (struct main_configuration*)shm; + + while (fgets(line, sizeof(line), file)) + { + if (!is_empty_string(line) && !is_comment_line(line)) + { + ptr = strtok(line, ":"); + + username = ptr; + + ptr = strtok(NULL, ":"); + + if (ptr == NULL) + { + status = PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT; + goto error; + } + + if (pgagroal_base64_decode(ptr, strlen(ptr), (void**)&decoded, &decoded_length)) + { + status = PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT; + goto error; + } + + if (pgagroal_decrypt(decoded, decoded_length, master_key, &password, ENCRYPTION_AES_256_CBC)) + { + status = PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT; + goto error; + } + + if (strlen(username) < MAX_USERNAME_LENGTH && + strlen(password) < MAX_PASSWORD_LENGTH) + { + memcpy(&config->users[index].username, username, strlen(username)); + memcpy(&config->users[index].password, password, strlen(password)); + } + else + { + printf("pgagroal: Invalid USER entry\n"); + printf("%s\n", line); + } + + free(password); + free(decoded); + + password = NULL; + decoded = NULL; + + index++; + } + } + + config->number_of_users = index; + + if (config->number_of_users > NUMBER_OF_USERS) + { + status = PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG; + goto error; + } + + free(master_key); + + fclose(file); + + return PGAGROAL_CONFIGURATION_STATUS_OK; + +error: + + free(master_key); + free(password); + free(decoded); + + if (file) + { + fclose(file); + } + + return status; +} + +/** + * + */ +int +pgagroal_validate_users_configuration(void* shm) +{ + return 0; +} + +/** + * + */ +int +pgagroal_read_frontend_users_configuration(void* shm, char* filename) +{ + FILE* file; + char line[LINE_LENGTH]; + int index; + char* master_key = NULL; + char* username = NULL; + char* password = NULL; + char* decoded = NULL; + size_t decoded_length = 0; + char* ptr = NULL; + struct main_configuration* config; + int status = PGAGROAL_CONFIGURATION_STATUS_OK; + + file = fopen(filename, "r"); + + if (!file) + { + status = PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND; + goto error; + } + + if (pgagroal_get_master_key(&master_key)) + { + status = PGAGROAL_CONFIGURATION_STATUS_KO; + goto error; + } + + index = 0; + config = (struct main_configuration*)shm; + + while (fgets(line, sizeof(line), file)) + { + if (!is_empty_string(line) && !is_comment_line(line)) + { + ptr = strtok(line, ":"); + + username = ptr; + + ptr = strtok(NULL, ":"); + + if (ptr == NULL) + { + status = PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT; + goto error; + } + + if (pgagroal_base64_decode(ptr, strlen(ptr), (void**)&decoded, &decoded_length)) + { + status = PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT; + goto error; + } + + if (pgagroal_decrypt(decoded, decoded_length, master_key, &password, ENCRYPTION_AES_256_CBC)) + { + status = PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT; + goto error; + } + + if (strlen(username) < MAX_USERNAME_LENGTH && + strlen(password) < MAX_PASSWORD_LENGTH) + { + memcpy(&config->frontend_users[index].username, username, strlen(username)); + memcpy(&config->frontend_users[index].password, password, strlen(password)); + } + else + { + printf("pgagroal: Invalid FRONTEND USER entry\n"); + printf("%s\n", line); + } + + free(password); + free(decoded); + + password = NULL; + decoded = NULL; + + index++; + } + } + + config->number_of_frontend_users = index; + + if (config->number_of_frontend_users > NUMBER_OF_USERS) + { + status = PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG; + goto error; + } + + free(master_key); + + fclose(file); + + return PGAGROAL_CONFIGURATION_STATUS_OK; + +error: + + free(master_key); + free(password); + free(decoded); + + if (file) + { + fclose(file); + } + + return status; +} + +/** + * + */ +int +pgagroal_validate_frontend_users_configuration(void* shm) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shm; + + for (int i = 0; i < config->number_of_frontend_users; i++) + { + bool found = false; + char* f = &config->frontend_users[i].username[0]; + + for (int i = 0; !found && i < config->number_of_users; i++) + { + char* u = &config->users[i].username[0]; + + if (!strcmp(f, u)) + { + found = true; + } + } + + if (!found) + { + return 1; + } + } + + return 0; +} + +/** + * + */ +int +pgagroal_read_admins_configuration(void* shm, char* filename) +{ + FILE* file; + char line[LINE_LENGTH]; + int index; + char* master_key = NULL; + char* username = NULL; + char* password = NULL; + char* decoded = NULL; + size_t decoded_length = 0; + char* ptr = NULL; + struct main_configuration* config; + int status = PGAGROAL_CONFIGURATION_STATUS_OK; + + file = fopen(filename, "r"); + + if (!file) + { + status = PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND; + goto error; + } + + if (pgagroal_get_master_key(&master_key)) + { + status = PGAGROAL_CONFIGURATION_STATUS_KO; + goto error; + } + + index = 0; + config = (struct main_configuration*)shm; + + while (fgets(line, sizeof(line), file)) + { + if (!is_empty_string(line) && !is_comment_line(line)) + { + ptr = strtok(line, ":"); + + username = ptr; + + ptr = strtok(NULL, ":"); + + if (ptr == NULL) + { + status = PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT; + goto error; + } + + if (pgagroal_base64_decode(ptr, strlen(ptr), (void**)&decoded, &decoded_length)) + { + status = PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT; + goto error; + } + + if (pgagroal_decrypt(decoded, decoded_length, master_key, &password, ENCRYPTION_AES_256_CBC)) + { + status = PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT; + goto error; + } + + if (strlen(username) < MAX_USERNAME_LENGTH && + strlen(password) < MAX_PASSWORD_LENGTH) + { + memcpy(&config->admins[index].username, username, strlen(username)); + memcpy(&config->admins[index].password, password, strlen(password)); + } + else + { + printf("pgagroal: Invalid ADMIN entry\n"); + printf("%s\n", line); + } + + free(password); + free(decoded); + + password = NULL; + decoded = NULL; + + index++; + } + } + + config->number_of_admins = index; + + if (config->number_of_admins > NUMBER_OF_ADMINS) + { + status = PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG; + goto error; + } + + free(master_key); + + fclose(file); + + return PGAGROAL_CONFIGURATION_STATUS_OK; + +error: + + free(master_key); + free(password); + free(decoded); + + if (file) + { + fclose(file); + } + + return status; +} + +/** + * + */ +int +pgagroal_vault_read_users_configuration(void* shm, char* filename) +{ + FILE* file; + char line[LINE_LENGTH]; + int index; + char* master_key = NULL; + char* username = NULL; + char* password = NULL; + char* decoded = NULL; + size_t decoded_length = 0; + char* ptr = NULL; + struct vault_configuration* config; + int status = PGAGROAL_CONFIGURATION_STATUS_OK; + + file = fopen(filename, "r"); + + if (!file) + { + status = PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND; + goto error; + } + + if (pgagroal_get_master_key(&master_key)) + { + status = PGAGROAL_CONFIGURATION_STATUS_KO; + goto error; + } + + index = 0; + config = (struct vault_configuration*)shm; + + while (fgets(line, sizeof(line), file)) + { + if (!is_empty_string(line) && !is_comment_line(line)) + { + ptr = strtok(line, ":"); + + username = ptr; + + ptr = strtok(NULL, ":"); + + if (ptr == NULL) + { + status = PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT; + goto error; + } + + if (pgagroal_base64_decode(ptr, strlen(ptr), (void**)&decoded, &decoded_length)) + { + status = PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT; + goto error; + } + + if (pgagroal_decrypt(decoded, decoded_length, master_key, &password, ENCRYPTION_AES_256_CBC)) + { + status = PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT; + goto error; + } + + if (strlen(username) < MAX_USERNAME_LENGTH && + strlen(password) < MAX_PASSWORD_LENGTH && + !strcmp(config->vault_server.user.username, username)) + { + memcpy(&config->vault_server.user.password, password, strlen(password)); + } + else + { + printf("pgagroal: Invalid ADMIN entry\n"); + printf("%s\n", line); + } + + free(password); + free(decoded); + + password = NULL; + decoded = NULL; + + index++; + } + } + + config->number_of_users = index; + + if (config->number_of_users > NUMBER_OF_ADMINS) + { + status = PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG; + goto error; + } + + free(master_key); + + fclose(file); + + return PGAGROAL_CONFIGURATION_STATUS_OK; + +error: + + free(master_key); + free(password); + free(decoded); + + if (file) + { + fclose(file); + } + + return status; +} + +/** + * + */ +int +pgagroal_validate_admins_configuration(void* shm) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shm; + + if (config->management > 0 && config->number_of_admins == 0) + { + pgagroal_log_warn("pgagroal: Remote management enabled, but no admins are defined"); + } + + return 0; +} + +int +pgagroal_read_superuser_configuration(void* shm, char* filename) +{ + FILE* file; + char line[LINE_LENGTH]; + int index; + char* master_key = NULL; + char* username = NULL; + char* password = NULL; + char* decoded = NULL; + size_t decoded_length = 0; + char* ptr = NULL; + struct main_configuration* config; + int status = PGAGROAL_CONFIGURATION_STATUS_OK; + + file = fopen(filename, "r"); + + if (!file) + { + status = PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND; + goto error; + } + + if (pgagroal_get_master_key(&master_key)) + { + status = PGAGROAL_CONFIGURATION_STATUS_KO; + goto error; + } + + index = 0; + config = (struct main_configuration*)shm; + + while (fgets(line, sizeof(line), file)) + { + if (!is_empty_string(line) && !is_comment_line(line)) + { + if (index > 0) + { + status = PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG; + goto error; + } + + ptr = strtok(line, ":"); + + username = ptr; + + ptr = strtok(NULL, ":"); + + if (ptr == NULL) + { + status = PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT; + goto error; + } + + if (pgagroal_base64_decode(ptr, strlen(ptr), (void**)&decoded, &decoded_length)) + { + status = PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT; + goto error; + + } + + if (pgagroal_decrypt(decoded, decoded_length, master_key, &password, ENCRYPTION_AES_256_CBC)) + { + status = PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT; + goto error; + + } + + if (strlen(username) < MAX_USERNAME_LENGTH && + strlen(password) < MAX_PASSWORD_LENGTH) + { + memcpy(&config->superuser.username, username, strlen(username)); + memcpy(&config->superuser.password, password, strlen(password)); + } + else + { + printf("pgagroal: Invalid SUPERUSER entry\n"); + printf("%s\n", line); + } + + free(password); + free(decoded); + + password = NULL; + decoded = NULL; + + index++; + } + } + + free(master_key); + + fclose(file); + + return PGAGROAL_CONFIGURATION_STATUS_OK; + +error: + + free(master_key); + free(password); + free(decoded); + + if (file) + { + fclose(file); + } + + return status; +} + +/** + * + */ +int +pgagroal_validate_superuser_configuration(void* shm) +{ + return 0; +} + +int +pgagroal_reload_configuration(bool* r) +{ + size_t reload_size; + struct main_configuration* reload = NULL; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + *r = false; + + pgagroal_log_trace("Configuration: %s", config->common.configuration_path); + pgagroal_log_trace("HBA: %s", config->hba_path); + pgagroal_log_trace("Limit: %s", config->limit_path); + pgagroal_log_trace("Users: %s", config->users_path); + pgagroal_log_trace("Frontend users: %s", config->frontend_users_path); + pgagroal_log_trace("Admins: %s", config->admins_path); + pgagroal_log_trace("Superuser: %s", config->superuser_path); + + reload_size = sizeof(struct main_configuration); + + if (pgagroal_create_shared_memory(reload_size, HUGEPAGE_OFF, (void**)&reload)) + { + goto error; + } + + pgagroal_init_configuration((void*)reload); + + if (pgagroal_read_configuration((void*)reload, config->common.configuration_path, true)) + { + goto error; + } + + if (pgagroal_read_hba_configuration((void*)reload, config->hba_path)) + { + goto error; + } + + if (strcmp("", config->limit_path)) + { + if (pgagroal_read_limit_configuration((void*)reload, config->limit_path)) + { + goto error; + } + } + + if (strcmp("", config->users_path)) + { + if (pgagroal_read_users_configuration((void*)reload, config->users_path)) + { + goto error; + } + } + + if (strcmp("", config->frontend_users_path)) + { + if (pgagroal_read_frontend_users_configuration((void*)reload, config->frontend_users_path)) + { + goto error; + } + } + + if (strcmp("", config->admins_path)) + { + if (pgagroal_read_admins_configuration((void*)reload, config->admins_path)) + { + goto error; + } + } + + if (strcmp("", config->superuser_path)) + { + if (pgagroal_read_superuser_configuration((void*)reload, config->superuser_path)) + { + goto error; + } + } + + if (pgagroal_validate_configuration(reload, false, false)) + { + goto error; + } + + if (pgagroal_validate_hba_configuration(reload)) + { + goto error; + } + + if (pgagroal_validate_limit_configuration(reload)) + { + goto error; + } + + if (pgagroal_validate_users_configuration(reload)) + { + goto error; + } + + if (pgagroal_validate_frontend_users_configuration(reload)) + { + goto error; + } + + if (pgagroal_validate_admins_configuration(reload)) + { + goto error; + } + + if (pgagroal_validate_superuser_configuration(reload)) + { + goto error; + } + + *r = transfer_configuration(config, reload); + + pgagroal_destroy_shared_memory((void*)reload, reload_size); + + pgagroal_log_debug("Reload: Success"); + + return 0; + +error: + if (reload != NULL) + { + pgagroal_destroy_shared_memory((void*)reload, reload_size); + } + + pgagroal_log_debug("Reload: Failure"); + + return 1; +} + +/** + * Given a line of text extracts the key part and the value. + * Valid lines must have the form = . + * + * The key must be unquoted and cannot have any spaces + * in front of it. + * + * Comments on the right side of a value are allowed. + * + * The value can be quoted, and this allows for inserting spaces + * and comment signs. Quotes are '""' and '\''. + * Example of valid lines are: + * + * foo = bar + * foo=bar + * foo= bar + * foo = "bar" + * foo = 'bar' + * foo = "#bar" + * foo = '#bar' + * foo = bar # bar set! + * foo = bar# bar set! + * + * + * @param str the line of text incoming from the configuration file + * @param key the pointer to where to store the key extracted from the line + * @param value the pointer to where to store the value (unquoted) + * @returns 1 if unable to parse the line, 0 if everything is ok + */ +static int +extract_key_value(char* str, char** key, char** value) +{ + int c = 0; + int offset = 0; + int length = strlen(str); + char* k; + char* v; + char quoting_begin = '\0'; + char quoting_end = '\0'; + + // the key does not allow spaces and is whatever is + // on the left of the '=' + while (str[c] != ' ' && str[c] != '=' && c < length) + c++; + + if (c < length) + { + k = calloc(1, c + 1); + if (k == NULL) + { + goto error; + } + memcpy(k, str, c); + *key = k; + + while ((str[c] == ' ' || str[c] == '\t' || str[c] == '=') && c < length) + c++; + + offset = c; + + // the value of the parameter starts from offset 'offset' + while (str[c] != '\r' && str[c] != '\n' && c < length) + { + if (str[c] == '\'' || str[c] == '"') + { + if (quoting_begin == '\0') + { + quoting_begin = str[c]; + offset = c + 1; // start at the very first character after the quote + } + else if (str[c] == quoting_begin && quoting_end == '\0') + { + quoting_end = str[c]; + // end at the last character before the quote + break; + } + } + else if (str[c] == '#' || str[c] == ';') + { + if (quoting_begin == '\0' || (quoting_begin != '\0' && quoting_end != '\0')) + { + // a comment outside of quoted string, ignore anything else + break; + } + } + else if (str[c] == ' ') + { + if (quoting_begin == '\0' || (quoting_begin != '\0' && quoting_end != '\0')) + { + // space outside a quoted string, stop here + break; + } + } + + c++; + } + + // quotes must be the same! + if (quoting_begin != '\0' && quoting_begin != quoting_end) + { + goto error; + } + + if (c <= length) + { + v = calloc(1, (c - offset) + 1); + if (v == NULL) + { + goto error; + } + memcpy(v, str + offset, (c - offset)); + *value = v; + return 0; + } + } +error: + return 1; +} + +static int +as_int(char* str, int* i) +{ + char* endptr; + long val; + + errno = 0; + val = strtol(str, &endptr, 10); + + if ((errno == ERANGE && (val == LONG_MAX || val == LONG_MIN)) || (errno != 0 && val == 0)) + { + goto error; + } + + if (str == endptr) + { + goto error; + } + + if (*endptr != '\0') + { + goto error; + } + + *i = (int)val; + + return 0; + +error: + + errno = 0; + + return 1; +} + +static int +as_bool(char* str, bool* b) +{ + if (!strcasecmp(str, "true") || !strcasecmp(str, "on") || !strcasecmp(str, "yes") || !strcasecmp(str, "1")) + { + *b = true; + return 0; + } + + if (!strcasecmp(str, "false") || !strcasecmp(str, "off") || !strcasecmp(str, "no") || !strcasecmp(str, "0")) + { + *b = false; + return 0; + } + + return 1; +} + +static int +as_logging_type(char* str) +{ + if (!strcasecmp(str, "console")) + { + return PGAGROAL_LOGGING_TYPE_CONSOLE; + } + + if (!strcasecmp(str, "file")) + { + return PGAGROAL_LOGGING_TYPE_FILE; + } + + if (!strcasecmp(str, "syslog")) + { + return PGAGROAL_LOGGING_TYPE_SYSLOG; + } + + return PGAGROAL_LOGGING_TYPE_CONSOLE; +} + +static int +as_logging_level(char* str) +{ + size_t size = 0; + int debug_level = 1; + char* debug_value = NULL; + + if (!strncasecmp(str, "debug", strlen("debug"))) + { + if (strlen(str) > strlen("debug")) + { + size = strlen(str) - strlen("debug"); + debug_value = (char*)calloc(1, size + 1); + if (debug_value == NULL) + { + return PGAGROAL_LOGGING_LEVEL_FATAL; + } + memcpy(debug_value, str + 5, size); + if (as_int(debug_value, &debug_level)) + { + // cannot parse, set it to 1 + debug_level = 1; + } + free(debug_value); + } + + if (debug_level < 1 || debug_level > 5) + { + warnx("Log level debug configuration %d not understood: %s - resetting to none", debug_level, str); + debug_level = 1; + } + + if (debug_level <= 1) + { + return PGAGROAL_LOGGING_LEVEL_DEBUG1; + } + else if (debug_level == 2) + { + return PGAGROAL_LOGGING_LEVEL_DEBUG2; + } + else if (debug_level == 3) + { + return PGAGROAL_LOGGING_LEVEL_DEBUG3; + } + else if (debug_level == 4) + { + return PGAGROAL_LOGGING_LEVEL_DEBUG4; + } + else if (debug_level >= 5) + { + return PGAGROAL_LOGGING_LEVEL_DEBUG5; + } + } + + if (!strcasecmp(str, "info")) + { + return PGAGROAL_LOGGING_LEVEL_INFO; + } + + if (!strcasecmp(str, "warn")) + { + return PGAGROAL_LOGGING_LEVEL_WARN; + } + + if (!strcasecmp(str, "error")) + { + return PGAGROAL_LOGGING_LEVEL_ERROR; + } + + if (!strcasecmp(str, "fatal")) + { + return PGAGROAL_LOGGING_LEVEL_FATAL; + } + + // "trace" is a synonim for "debug5" + if (!strcasecmp(str, "trace")) + { + return PGAGROAL_LOGGING_LEVEL_DEBUG5; + } + + return PGAGROAL_LOGGING_LEVEL_INFO; +} + +static int +as_logging_mode(char* str) +{ + if (!strcasecmp(str, "a") || !strcasecmp(str, "append")) + { + return PGAGROAL_LOGGING_MODE_APPEND; + } + + if (!strcasecmp(str, "c") || !strcasecmp(str, "create")) + { + return PGAGROAL_LOGGING_MODE_CREATE; + } + + return PGAGROAL_LOGGING_MODE_APPEND; +} + +static int +as_validation(char* str) +{ + if (!strcasecmp(str, "off")) + { + return VALIDATION_OFF; + } + + if (!strcasecmp(str, "foreground")) + { + return VALIDATION_FOREGROUND; + } + + if (!strcasecmp(str, "background")) + { + return VALIDATION_BACKGROUND; + } + + return VALIDATION_OFF; +} + +static int +as_pipeline(char* str) +{ + if (!strcasecmp(str, "auto")) + { + return PIPELINE_AUTO; + } + + if (!strcasecmp(str, "performance")) + { + return PIPELINE_PERFORMANCE; + } + + if (!strcasecmp(str, "session")) + { + return PIPELINE_SESSION; + } + + if (!strcasecmp(str, "transaction")) + { + return PIPELINE_TRANSACTION; + } + + return PIPELINE_AUTO; +} + +static int +as_hugepage(char* str) +{ + if (!strcasecmp(str, "off")) + { + return HUGEPAGE_OFF; + } + + if (!strcasecmp(str, "try")) + { + return HUGEPAGE_TRY; + } + + if (!strcasecmp(str, "on")) + { + return HUGEPAGE_ON; + } + + return HUGEPAGE_OFF; +} + +static void +extract_hba(char* str, char** type, char** database, char** user, char** address, char** method) +{ + int offset = 0; + int length = strlen(str); + + offset = extract_value(str, offset, type); + + if (offset == -1 || offset >= length) + { + return; + } + + offset = extract_value(str, offset, database); + + if (offset == -1 || offset >= length) + { + return; + } + + offset = extract_value(str, offset, user); + + if (offset == -1 || offset >= length) + { + return; + } + + offset = extract_value(str, offset, address); + + if (offset == -1 || offset >= length) + { + return; + } + + extract_value(str, offset, method); +} + +static void +extract_limit(char* str, int server_max, char** database, char** user, int* max_size, int* initial_size, int* min_size) +{ + int offset = 0; + int length = strlen(str); + char* value = NULL; + + *max_size = 0; + *initial_size = 0; + *min_size = 0; + + offset = extract_value(str, offset, database); + + if (offset == -1 || offset >= length) + { + return; + } + + offset = extract_value(str, offset, user); + + if (offset == -1 || offset >= length) + { + return; + } + + offset = extract_value(str, offset, &value); + + if (offset == -1) + { + return; + } + + if (!strcasecmp("all", value)) + { + *max_size = server_max; + } + else + { + if (as_int(value, max_size)) + { + *max_size = -1; + return; + } + } + + free(value); + value = NULL; + + offset = extract_value(str, offset, &value); + + if (offset == -1) + { + return; + } + + if (value != NULL && strcmp("", value) != 0) + { + if (!strcasecmp("all", value)) + { + *initial_size = server_max; + } + else + { + if (as_int(value, initial_size)) + { + *initial_size = 0; + return; + } + } + } + + free(value); + value = NULL; + + offset = extract_value(str, offset, &value); + + if (offset == -1) + { + return; + } + + if (value != NULL && strcmp("", value) != 0) + { + if (!strcasecmp("all", value)) + { + *min_size = server_max; + } + else + { + if (as_int(value, min_size)) + { + *min_size = 0; + return; + } + } + } + + free(value); +} + +static int +extract_value(char* str, int offset, char** value) +{ + int from; + int to; + int length = strlen(str); + char* v = NULL; + + while ((str[offset] == ' ' || str[offset] == '\t') && offset < length) + offset++; + + if (offset < length) + { + from = offset; + + while ((str[offset] != ' ' && str[offset] != '\t' && str[offset] != '\r' && str[offset] != '\n') && offset < length) + offset++; + + if (offset <= length) + { + to = offset; + + v = calloc(1, to - from + 1); + if (v == NULL) + { + return -1; + } + memcpy(v, str + from, to - from); + *value = v; + + return offset; + } + } + + return -1; +} + +/** + * Utility function to copy all the settings from the source configuration + * to the destination one. This is useful for example when a reload + * command is issued. + * + * @param config the new (clean) configuration + * @param reload the one loaded from the configuration (i.e., the one to apply) + * @return True, if restart or false if not + */ +static bool +transfer_configuration(struct main_configuration* config, struct main_configuration* reload) +{ + bool changed = false; + +#ifdef HAVE_LINUX + sd_notify(0, "RELOADING=1"); +#endif + + memcpy(config->common.host, reload->common.host, MISC_LENGTH); + config->common.port = reload->common.port; + config->common.metrics = reload->common.metrics; + config->common.metrics_cache_max_age = reload->common.metrics_cache_max_age; + if (restart_int("metrics_cache_max_size", config->common.metrics_cache_max_size, reload->common.metrics_cache_max_size)) + { + changed = true; + } + config->management = reload->management; + + config->update_process_title = reload->update_process_title; + + /* gracefully */ + + /* disabled */ + + /* pipeline */ + if (restart_int("pipeline", config->pipeline, reload->pipeline)) + { + changed = true; + } + + config->failover = reload->failover; + memcpy(config->failover_script, reload->failover_script, MISC_LENGTH); + + /* log_type */ + if (restart_int("log_type", config->common.log_type, reload->common.log_type)) + { + changed = true; + } + config->common.log_level = reload->common.log_level; + + /* log_path */ + // if the log main parameters have changed, we need + // to restart the logging system + if (strncmp(config->common.log_path, reload->common.log_path, MISC_LENGTH) + || config->common.log_rotation_size != reload->common.log_rotation_size + || config->common.log_rotation_age != reload->common.log_rotation_age + || config->common.log_mode != reload->common.log_mode) + { + pgagroal_log_debug("Log restart triggered!"); + pgagroal_stop_logging(); + config->common.log_rotation_size = reload->common.log_rotation_size; + config->common.log_rotation_age = reload->common.log_rotation_age; + config->common.log_mode = reload->common.log_mode; + memcpy(config->common.log_line_prefix, reload->common.log_line_prefix, MISC_LENGTH); + memcpy(config->common.log_path, reload->common.log_path, MISC_LENGTH); + pgagroal_start_logging(); + } + + config->common.log_connections = reload->common.log_connections; + config->common.log_disconnections = reload->common.log_disconnections; + + /* log_lock */ + + config->authquery = reload->authquery; + + config->common.tls = reload->common.tls; + memcpy(config->common.tls_cert_file, reload->common.tls_cert_file, MISC_LENGTH); + memcpy(config->common.tls_key_file, reload->common.tls_key_file, MISC_LENGTH); + memcpy(config->common.tls_ca_file, reload->common.tls_ca_file, MISC_LENGTH); + + if (config->common.tls && (config->pipeline == PIPELINE_SESSION || config->pipeline == PIPELINE_TRANSACTION)) + { + if (pgagroal_tls_valid()) + { + pgagroal_log_fatal("pgagroal: Invalid TLS configuration"); + exit(1); + } + } + + /* active_connections */ + /* max_connections */ + if (restart_int("max_connections", config->max_connections, reload->max_connections)) + { + changed = true; + } + config->allow_unknown_users = reload->allow_unknown_users; + + config->blocking_timeout = reload->blocking_timeout; + config->idle_timeout = reload->idle_timeout; + config->rotate_frontend_password_timeout = reload->rotate_frontend_password_timeout; + config->rotate_frontend_password_length = reload->rotate_frontend_password_length; + config->max_connection_age = reload->max_connection_age; + config->validation = reload->validation; + config->background_interval = reload->background_interval; + config->max_retries = reload->max_retries; + config->common.authentication_timeout = reload->common.authentication_timeout; + config->disconnect_client = reload->disconnect_client; + config->disconnect_client_force = reload->disconnect_client_force; + /* pidfile */ + if (restart_string("pidfile", config->pidfile, reload->pidfile, true)) + { + changed = true; + } + + /* libev */ + if (restart_string("libev", config->libev, reload->libev, true)) + { + changed = true; + } + config->keep_alive = reload->keep_alive; + config->nodelay = reload->nodelay; + config->non_blocking = reload->non_blocking; + config->backlog = reload->backlog; + /* hugepage */ + if (restart_int("hugepage", config->common.hugepage, reload->common.hugepage)) + { + changed = true; + } + config->tracker = reload->tracker; + config->track_prepared_statements = reload->track_prepared_statements; + + /* unix_socket_dir */ + + // does make sense to check for remote connections? Because in the case the Unix socket dir + // changes the pgagroal-cli probably will not be able to connect in any case! + if (restart_string("unix_socket_dir", config->unix_socket_dir, reload->unix_socket_dir, false)) + { + changed = true; + } + + /* su_connection */ + + /* states */ + + // decreasing the number of servers is probably a bad idea + if (config->number_of_servers > reload->number_of_servers) + { + if (restart_int("decreasing number of servers", config->number_of_servers, reload->number_of_servers)) + { + changed = true; + } + } + + for (int i = 0; i < reload->number_of_servers; i++) + { + // check and emit restart warning only for not-added servers + if (i < config->number_of_servers) + { + if (restart_server(&reload->servers[i], &config->servers[i])) + { + changed = true; + } + } + + copy_server(&config->servers[i], &reload->servers[i]); + } + config->number_of_servers = reload->number_of_servers; + + // zero fill remaining memory that is unused + memset(&config->servers[config->number_of_servers], 0, + sizeof(struct server) * (NUMBER_OF_SERVERS - config->number_of_servers)); + + memset(&config->hbas[0], 0, sizeof(struct hba) * NUMBER_OF_HBAS); + for (int i = 0; i < reload->number_of_hbas; i++) + { + copy_hba(&config->hbas[i], &reload->hbas[i]); + } + config->number_of_hbas = reload->number_of_hbas; + + /* number_of_limits */ + /* limits */ + if (restart_limit("limits", config, reload)) + { + changed = true; + } + + memset(&config->users[0], 0, sizeof(struct user) * NUMBER_OF_USERS); + for (int i = 0; i < reload->number_of_users; i++) + { + copy_user(&config->users[i], &reload->users[i]); + } + config->number_of_users = reload->number_of_users; + + memset(&config->frontend_users[0], 0, sizeof(struct user) * NUMBER_OF_USERS); + for (int i = 0; i < reload->number_of_frontend_users; i++) + { + copy_user(&config->frontend_users[i], &reload->frontend_users[i]); + } + config->number_of_frontend_users = reload->number_of_frontend_users; + + memset(&config->admins[0], 0, sizeof(struct user) * NUMBER_OF_ADMINS); + for (int i = 0; i < reload->number_of_admins; i++) + { + copy_user(&config->admins[i], &reload->admins[i]); + } + config->number_of_admins = reload->number_of_admins; + + memset(&config->superuser, 0, sizeof(struct user)); + copy_user(&config->superuser, &reload->superuser); + + /* prometheus */ + /* connections[] */ + +#ifdef HAVE_LINUX + sd_notify(0, "READY=1"); +#endif + + if (changed) + { + pgagroal_log_warn("Settings cannot be applied"); + } + + return changed; +} + +/** + * Checks if the configuration of the first server + * is the same as the configuration of the second server. + * So far it tests for the same connection string, meaning + * that the hostname and the port must be the same (i.e., + * pointing to the same endpoint). + * It does not resolve the hostname, therefore 'localhost' and '127.0.0.1' + * are considered as different hosts. + * @return true if the server configurations look the same + */ +static bool +is_same_server(struct server* s1, struct server* s2) +{ + if (!strncmp(s1->host, s2->host, MISC_LENGTH) && s1->port == s2->port) + { + return true; + } + else + { + return false; + } +} + +/** + * Checks if TLS configurations are same. + * @return true if the TLS configurations are same + */ +static bool +is_same_tls(struct server* src, struct server* dst) +{ + if (src->tls == dst->tls && + !strncmp(src->tls_cert_file, dst->tls_cert_file, MISC_LENGTH) && + !strncmp(src->tls_key_file, dst->tls_key_file, MISC_LENGTH) && + !strncmp(src->tls_ca_file, dst->tls_ca_file, MISC_LENGTH)) + { + return true; + } + else + { + return false; + } +} + +static void +copy_server(struct server* dst, struct server* src) +{ + atomic_schar state; + + // check the server cloned "seems" the same + if (is_same_server(dst, src)) + { + state = atomic_load(&dst->state); + } + else + { + state = SERVER_NOTINIT; + } + + memset(dst, 0, sizeof(struct server)); + memcpy(&dst->name[0], &src->name[0], MISC_LENGTH); + memcpy(&dst->host[0], &src->host[0], MISC_LENGTH); + dst->port = src->port; + atomic_init(&dst->state, state); +} + +static void +copy_hba(struct hba* dst, struct hba* src) +{ + memcpy(&dst->type[0], &src->type[0], MAX_TYPE_LENGTH); + memcpy(&dst->database[0], &src->database[0], MAX_DATABASE_LENGTH); + memcpy(&dst->username[0], &src->username[0], MAX_USERNAME_LENGTH); + memcpy(&dst->address[0], &src->address[0], MAX_ADDRESS_LENGTH); + memcpy(&dst->method[0], &src->method[0], MAX_ADDRESS_LENGTH); +} + +static void +copy_user(struct user* dst, struct user* src) +{ + memcpy(&dst->username[0], &src->username[0], MAX_USERNAME_LENGTH); + memcpy(&dst->password[0], &src->password[0], MAX_PASSWORD_LENGTH); +} + +static int +restart_int(char* name, int e, int n) +{ + if (e != n) + { + pgagroal_log_info("Restart required for %s - Existing %d New %d", name, e, n); + return 1; + } + + return 0; +} + +/** + * Utility function prints a line in the log when a restart is required. + * @return 0 when parameter values are same, 1 when a restart required. + */ +static int +restart_bool(char* name, bool e, bool n) +{ + if (e != n) + { + pgagroal_log_info("Restart required for %s - Existing %s New %s", name, e ? "true" : "false", n ? "true" : "false"); + return 1; + } + + return 0; +} + +/** + * Utility function to notify when a string parameter in the + * configuration requires a restart. + * Prints a line in the log when a restart is required. + * + * @param name the name of the parameter + * @param e the existing (current) value of the parameter + * @param n the new value + * @param skip_non_existing if true it will ignore when 'n' is empty, + * used when the parameter is automatically set + * @return 0 when the parameter values are the same, 1 when it is required + * a restart + */ +static int +restart_string(char* name, char* e, char* n, bool skip_non_existing) +{ + if (skip_non_existing && !strlen(n)) + { + return 0; + } + + if (strcmp(e, n)) + { + pgagroal_log_info("Restart required for %s - Existing %s New %s", name, e, n); + return 1; + } + + return 0; +} + +static int +restart_limit(char* name, struct main_configuration* config, struct main_configuration* reload) +{ + int ret; + + ret = restart_int("limits", config->number_of_limits, reload->number_of_limits); + if (ret == 1) + { + goto error; + } + + for (int i = 0; i < reload->number_of_limits; i++) + { + struct limit* e; + struct limit* n; + + e = &config->limits[i]; + n = &reload->limits[i]; + + if (strcmp(e->database, n->database) || + strcmp(e->username, n->username) || + e->max_size != n->max_size || + e->initial_size != n->initial_size || + e->min_size != n->min_size) + { + pgagroal_log_info("Restart required for limits"); + goto error; + } + } + + return 0; + +error: + + return 1; +} + +static int +restart_server(struct server* src, struct server* dst) +{ + char restart_message[2 * MISC_LENGTH]; + + if (!is_same_server(src, dst)) + { + snprintf(restart_message, sizeof(restart_message), "Server <%s>, parameter ", src->name); + restart_string(restart_message, dst->host, src->host, false); + snprintf(restart_message, sizeof(restart_message), "Server <%s>, parameter ", src->name); + restart_int(restart_message, dst->port, src->port); + return 1; + } + else if (!is_same_tls(src, dst)) + { + snprintf(restart_message, sizeof(restart_message), "Server <%s>, parameter ", src->name); + restart_bool(restart_message, dst->tls, src->tls); + snprintf(restart_message, sizeof(restart_message), "Server <%s>, parameter ", src->name); + restart_string(restart_message, dst->tls_cert_file, src->tls_cert_file, false); + snprintf(restart_message, sizeof(restart_message), "Server <%s>, parameter ", src->name); + restart_string(restart_message, dst->tls_key_file, src->tls_key_file, false); + snprintf(restart_message, sizeof(restart_message), "Server <%s>, parameter ", src->name); + restart_string(restart_message, dst->tls_ca_file, src->tls_ca_file, false); + return 1; + } + + return 0; +} + +static bool +is_empty_string(char* s) +{ + if (s == NULL) + { + return true; + } + + if (!strcmp(s, "")) + { + return true; + } + + for (int i = 0; i < strlen(s); i++) + { + if (s[i] == ' ' || s[i] == '\t' || s[i] == '\r' || s[i] == '\n') + { + /* Ok */ + } + else + { + return false; + } + } + + return true; +} + +/** + * Parses a string to see if it contains + * a valid value for log rotation size. + * Returns 0 if parsing ok, 1 otherwise. + * + */ +static int +as_logging_rotation_size(char* str, unsigned int* size) +{ + return as_bytes(str, size, PGAGROAL_LOGGING_ROTATION_DISABLED); +} + +/** + * Parses the log_rotation_age string. + * The string accepts + * - s for seconds + * - m for minutes + * - h for hours + * - d for days + * - w for weeks + * + * The default is expressed in seconds. + * The function sets the number of rotationg age as minutes. + * Returns 1 for errors, 0 for correct parsing. + * + */ +static int +as_logging_rotation_age(char* str, unsigned int* age) +{ + return as_seconds(str, age, PGAGROAL_LOGGING_ROTATION_DISABLED); +} + +void +pgagroal_init_pidfile_if_needed(void) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + if (strlen(config->pidfile) == 0) + { + // no pidfile set, use a default one + snprintf(config->pidfile, sizeof(config->pidfile), "%s/pgagroal.%d.pid", + config->unix_socket_dir, + config->common.port); + pgagroal_log_debug("PID file automatically set to: [%s]", config->pidfile); + } +} + +bool +pgagroal_can_prefill(void) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + if (config->number_of_users > 0 && config->number_of_limits > 0) + { + return true; + } + else + { + return false; + } +} + +/** + * Function to check if the specified key belongs to the right section. + * The idea is to pass all the values read from the configuration file, + * and a boolean parameter to check if the section the parameter belongs is global or not. + * A global section is the main `pgagroal` section, while a local section + * is a custom user section, i.e., a server section. + * + * @param wanted the key we want to match against + * @param section the section in which the key has been found + * @param key the key read from the configuration file + * @param global true if the `section` has to be `pgagroal` + * @param unknown set to true if the key does match but the section does not. + * For instance the key `host` found in a local section while required + * to be global will set `unknown` to true. + * This parameter can be omitted. + * + * @returns true if the key matches and the section is of the specified type. + * + * Example: + * key_in_section("host", section, key, true, &unknown); // search for [pgagroal] -> host + * key_in_section("port", section, key, false, &unknown); // search for server section -> port + */ +static bool +key_in_section(char* wanted, char* section, char* key, bool global, bool* unknown) +{ + + // first of all, look for a key match + if (strncmp(wanted, key, MISC_LENGTH)) + { + // no match at all + return false; + } + + // if here there is a match on the key, ensure the section is + // appropriate + if (global && (!strncmp(section, PGAGROAL_MAIN_INI_SECTION, MISC_LENGTH) || !strncmp(section, PGAGROAL_VAULT_INI_SECTION, MISC_LENGTH))) + { + return true; + } + else if (!global && strlen(section) > 0) + { + return true; + } + else + { + if (unknown) + { + *unknown = true; + } + + return false; + } +} + +/** + * Function to see if the specified line is a comment line + * and has to be ignored. + * A comment line is a line that starts with '#' or ';' or + * with spaces (or tabs) and a comment sign. + * + * @param line the line read from the file + * @return true if the line is a full comment line + */ +static bool +is_comment_line(char* line) +{ + int c = 0; + int length = strlen(line); + + while (c < length) + { + if (line[c] == '#' || line[c] == ';') + { + return true; + } + else if (line[c] != ' ' && line[c] != '\t') + { + break; + } + + c++; + } + + return false; +} + +/** + * Function to inspect a configuration line and detect if it handles a section. + * If the line handles a section name, like `[pgagroal]` the function does set + * the `section` argument, otherwise it does nothing. + * + * @param line the line to inspect + * @param section the pointer to the string that will contain + * the section name, only if the line handles a section, otherwise + * the pointer will not be changed. + * + * @returns true if the line handles a section and the `section` pointer + * has been changed + */ +static bool +section_line(char* line, char* section) +{ + size_t max; + char* ptr = NULL; + + // if does not appear to be a section line do nothing! + if (line[0] != '[') + { + return false; + } + + ptr = strchr(line, ']'); + if (ptr) + { + memset(section, 0, LINE_LENGTH); + max = ptr - line - 1; + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(section, line + 1, max); + return true; + } + + return false; + +} + +/** + * Parses an age string, providing the resulting value as seconds. + * An age string is expressed by a number and a suffix that indicates + * the multiplier. Accepted suffixes, case insensitive, are: + * - s for seconds + * - m for minutes + * - h for hours + * - d for days + * - w for weeks + * + * The default is expressed in seconds. + * + * @param str the value to parse as retrieved from the configuration + * @param age a pointer to the value that is going to store + * the resulting number of seconds + * @param default_age a value to set when the parsing is unsuccesful + + */ +static unsigned int +as_seconds(char* str, unsigned int* age, unsigned int default_age) +{ + int multiplier = 1; + int index; + char value[MISC_LENGTH]; + bool multiplier_set = false; + int i_value = default_age; + + if (is_empty_string(str)) + { + *age = default_age; + return 0; + } + + index = 0; + for (int i = 0; i < strlen(str); i++) + { + if (isdigit(str[i])) + { + value[index++] = str[i]; + } + else if (isalpha(str[i]) && multiplier_set) + { + // another extra char not allowed + goto error; + } + else if (isalpha(str[i]) && !multiplier_set) + { + if (str[i] == 's' || str[i] == 'S') + { + multiplier = 1; + multiplier_set = true; + } + else if (str[i] == 'm' || str[i] == 'M') + { + multiplier = 60; + multiplier_set = true; + } + else if (str[i] == 'h' || str[i] == 'H') + { + multiplier = 3600; + multiplier_set = true; + } + else if (str[i] == 'd' || str[i] == 'D') + { + multiplier = 24 * 3600; + multiplier_set = true; + } + else if (str[i] == 'w' || str[i] == 'W') + { + multiplier = 24 * 3600 * 7; + multiplier_set = true; + } + } + else + { + // do not allow alien chars + goto error; + } + } + + value[index] = '\0'; + if (!as_int(value, &i_value)) + { + // sanity check: the value + // must be a positive number! + if (i_value >= 0) + { + *age = i_value * multiplier; + } + else + { + goto error; + } + + return 0; + } + else + { +error: + *age = default_age; + return 1; + } +} + +/** + * Converts a "size string" into the number of bytes. + * + * Valid strings have one of the suffixes: + * - b for bytes (default) + * - k for kilobytes + * - m for megabytes + * - g for gigabytes + * + * The default is expressed always as bytes. + * Uppercase letters work too. + * If no suffix is specified, the value is expressed as bytes. + * + * @param str the string to parse (e.g., "2M") + * @param bytes the value to set as result of the parsing stage + * @param default_bytes the default value to set when the parsing cannot proceed + * @return 1 if parsing is unable to understand the string, 0 is parsing is + * performed correctly (or almost correctly, e.g., empty string) + */ +static unsigned int +as_bytes(char* str, unsigned int* bytes, unsigned int default_bytes) +{ + int multiplier = 1; + int index; + char value[MISC_LENGTH]; + bool multiplier_set = false; + int i_value = default_bytes; + + if (is_empty_string(str)) + { + *bytes = default_bytes; + return 0; + } + + index = 0; + for (int i = 0; i < strlen(str); i++) + { + if (isdigit(str[i])) + { + value[index++] = str[i]; + } + else if (isalpha(str[i]) && multiplier_set) + { + // allow a 'B' suffix on a multiplier + // like for instance 'MB', but don't allow it + // for bytes themselves ('BB') + if (multiplier == 1 + || (str[i] != 'b' && str[i] != 'B')) + { + // another non-digit char not allowed + goto error; + } + } + else if (isalpha(str[i]) && !multiplier_set) + { + if (str[i] == 'M' || str[i] == 'm') + { + multiplier = 1024 * 1024; + multiplier_set = true; + } + else if (str[i] == 'G' || str[i] == 'g') + { + multiplier = 1024 * 1024 * 1024; + multiplier_set = true; + } + else if (str[i] == 'K' || str[i] == 'k') + { + multiplier = 1024; + multiplier_set = true; + } + else if (str[i] == 'B' || str[i] == 'b') + { + multiplier = 1; + multiplier_set = true; + } + } + else + { + // do not allow alien chars + goto error; + } + } + + value[index] = '\0'; + if (!as_int(value, &i_value)) + { + // sanity check: the value + // must be a positive number! + if (i_value >= 0) + { + *bytes = i_value * multiplier; + } + else + { + goto error; + } + + return 0; + } + else + { +error: + *bytes = default_bytes; + return 1; + } +} + +/** + * Utility function to understand the setting for updating + * the process title. + * + * @param str the value obtained by the configuration parsing + * @param policy the pointer to the value where the setting will be stored + * @param default_policy a value to set when the configuration cannot be + * understood + * + * @return 0 on success, 1 on error. In any case the `policy` variable is set to + * `default_policy`. + */ +static unsigned int +as_update_process_title(char* str, unsigned int* policy, unsigned int default_policy) +{ + if (is_empty_string(str)) + { + *policy = default_policy; + return 1; + } + + if (!strncmp(str, "never", MISC_LENGTH) || !strncmp(str, "off", MISC_LENGTH)) + { + *policy = UPDATE_PROCESS_TITLE_NEVER; + return 0; + } + else if (!strncmp(str, "strict", MISC_LENGTH)) + { + *policy = UPDATE_PROCESS_TITLE_STRICT; + return 0; + } + else if (!strncmp(str, "minimal", MISC_LENGTH)) + { + *policy = UPDATE_PROCESS_TITLE_MINIMAL; + return 0; + } + else if (!strncmp(str, "verbose", MISC_LENGTH) || !strncmp(str, "full", MISC_LENGTH)) + { + *policy = UPDATE_PROCESS_TITLE_VERBOSE; + return 0; + } + else + { + // not a valid setting + *policy = default_policy; + return 1; + } + +} + +int +pgagroal_write_config_value(char* buffer, char* config_key, size_t buffer_size) +{ + struct main_configuration* config; + + char section[MISC_LENGTH]; + char context[MISC_LENGTH]; + char key[MISC_LENGTH]; + int begin = -1, end = -1; + bool main_section; + + config = (struct main_configuration*)shmem; + + memset(section, 0, MISC_LENGTH); + memset(context, 0, MISC_LENGTH); + memset(key, 0, MISC_LENGTH); + + for (int i = 0; i < strlen(config_key); i++) + { + if (config_key[i] == '.') + { + if (!strlen(section)) + { + memcpy(section, &config_key[begin], end - begin + 1); + section[end - begin + 1] = '\0'; + begin = end = -1; + continue; + } + else if (!strlen(context)) + { + memcpy(context, &config_key[begin], end - begin + 1); + context[end - begin + 1] = '\0'; + begin = end = -1; + continue; + } + else if (!strlen(key)) + { + memcpy(key, &config_key[begin], end - begin + 1); + key[end - begin + 1] = '\0'; + begin = end = -1; + continue; + } + + } + + if (begin < 0) + { + begin = i; + } + + end = i; + + } + + // if the key has not been found, since there is no ending dot, + // try to extract it from the string + if (!strlen(key)) + { + memcpy(key, &config_key[begin], end - begin + 1); + key[end - begin + 1] = '\0'; + } + + // force the main section, i.e., global parameters, if and only if + // there is no section or section is 'pgagroal' without any subsection + main_section = (!strlen(section) || !strncmp(section, "pgagroal", MISC_LENGTH)) + && !strlen(context); + + if (!strncmp(section, "server", MISC_LENGTH)) + { + return pgagroal_write_server_config_value(buffer, context, key, buffer_size); + } + else if (!strncmp(section, "hba", MISC_LENGTH)) + { + return pgagroal_write_hba_config_value(buffer, context, key, buffer_size); + } + else if (!strncmp(section, "limit", MISC_LENGTH)) + { + return pgagroal_write_limit_config_value(buffer, context, key, buffer_size); + } + else if (main_section) + { + + /* global configuration settings */ + + if (!strncmp(key, "host", MISC_LENGTH)) + { + return to_string(buffer, config->common.host, buffer_size); + } + else if (!strncmp(key, "port", MISC_LENGTH)) + { + return to_int(buffer, config->common.port); + } + else if (!strncmp(key, "log_type", MISC_LENGTH)) + { + return to_log_type(buffer, config->common.log_type); + } + else if (!strncmp(key, "log_mode", MISC_LENGTH)) + { + return to_log_mode(buffer, config->common.log_mode); + } + else if (!strncmp(key, "log_line_prefix", MISC_LENGTH)) + { + return to_string(buffer, config->common.log_line_prefix, buffer_size); + } + + else if (!strncmp(key, "log_level", MISC_LENGTH)) + { + return to_log_level(buffer, config->common.log_level); + } + else if (!strncmp(key, "log_rotation_size", MISC_LENGTH)) + { + return to_int(buffer, config->common.log_rotation_size); + + } + else if (!strncmp(key, "log_rotation_age", MISC_LENGTH)) + { + return to_int(buffer, config->common.log_rotation_age); + + } + else if (!strncmp(key, "log_connections", MISC_LENGTH)) + { + return to_bool(buffer, config->common.log_connections); + } + else if (!strncmp(key, "log_disconnections", MISC_LENGTH)) + { + return to_bool(buffer, config->common.log_disconnections); + } + else if (!strncmp(key, "log_path", MISC_LENGTH)) + { + return to_string(buffer, config->common.log_path, buffer_size); + } + else if (!strncmp(key, "metrics", MISC_LENGTH)) + { + return to_int(buffer, config->common.metrics); + } + else if (!strncmp(key, "metrics_cache_max_age", MISC_LENGTH)) + { + return to_int(buffer, config->common.metrics_cache_max_age); + } + else if (!strncmp(key, "metrics_cache_max_size", MISC_LENGTH)) + { + return to_int(buffer, config->common.metrics_cache_max_size); + } + else if (!strncmp(key, "management", MISC_LENGTH)) + { + return to_int(buffer, config->management); + } + else if (!strncmp(key, "pipeline", MISC_LENGTH)) + { + return to_pipeline(buffer, config->pipeline); + } + else if (!strncmp(key, "failover_script", MISC_LENGTH)) + { + return to_string(buffer, config->failover_script, buffer_size); + } + else if (!strncmp(key, "tls", MISC_LENGTH)) + { + return to_bool(buffer, config->common.tls); + } + else if (!strncmp(key, "auth_query", MISC_LENGTH)) + { + return to_bool(buffer, config->authquery); + } + else if (!strncmp(key, "tls_ca_file", MISC_LENGTH)) + { + return to_string(buffer, config->common.tls_ca_file, buffer_size); + } + else if (!strncmp(key, "tls_cert_file", MISC_LENGTH)) + { + return to_string(buffer, config->common.tls_cert_file, buffer_size); + } + else if (!strncmp(key, "tls_key_file", MISC_LENGTH)) + { + return to_string(buffer, config->common.tls_key_file, buffer_size); + } + else if (!strncmp(key, "blocking_timeout", MISC_LENGTH)) + { + return to_int(buffer, config->blocking_timeout); + } + else if (!strncmp(key, "idle_timeout", MISC_LENGTH)) + { + return to_int(buffer, config->idle_timeout); + } + else if (!strncmp(key, "rotate_frontend_password_timeout", MISC_LENGTH)) + { + return to_int(buffer, config->rotate_frontend_password_timeout); + } + else if (!strncmp(key, "rotate_frontend_password_length", MISC_LENGTH)) + { + return to_int(buffer, config->rotate_frontend_password_length); + } + else if (!strncmp(key, "max_connection_age", MISC_LENGTH)) + { + return to_int(buffer, config->max_connection_age); + } + else if (!strncmp(key, "validation", MISC_LENGTH)) + { + return to_validation(buffer, config->validation); + } + else if (!strncmp(key, "update_process_title", MISC_LENGTH)) + { + return to_update_process_title(buffer, config->update_process_title); + } + else if (!strncmp(key, "background_interval", MISC_LENGTH)) + { + return to_int(buffer, config->background_interval); + } + else if (!strncmp(key, "max_retries", MISC_LENGTH)) + { + return to_int(buffer, config->max_retries); + } + else if (!strncmp(key, "authentication_timeout", MISC_LENGTH)) + { + return to_int(buffer, config->common.authentication_timeout); + } + else if (!strncmp(key, "disconnect_client", MISC_LENGTH)) + { + return to_int(buffer, config->disconnect_client); + } + else if (!strncmp(key, "pidfile", MISC_LENGTH)) + { + return to_string(buffer, config->pidfile, buffer_size); + } + else if (!strncmp(key, "allow_unknown_users", MISC_LENGTH)) + { + return to_bool(buffer, config->allow_unknown_users); + } + else if (!strncmp(key, "max_connections", MISC_LENGTH)) + { + return to_int(buffer, config->max_connections); + } + else if (!strncmp(key, "unix_socket_dir", MISC_LENGTH)) + { + return to_string(buffer, config->unix_socket_dir, buffer_size); + } + else if (!strncmp(key, "keep_alive", MISC_LENGTH)) + { + return to_bool(buffer, config->keep_alive); + } + else if (!strncmp(key, "nodelay", MISC_LENGTH)) + { + return to_int(buffer, config->nodelay); + } + else if (!strncmp(key, "non_blocking", MISC_LENGTH)) + { + return to_bool(buffer, config->non_blocking); + } + else if (!strncmp(key, "backlog", MISC_LENGTH)) + { + return to_int(buffer, config->backlog); + } + else if (!strncmp(key, "hugepage", MISC_LENGTH)) + { + return to_bool(buffer, config->common.hugepage); + } + else if (!strncmp(key, "track_prepared_statements", MISC_LENGTH)) + { + return to_bool(buffer, config->track_prepared_statements); + } + else + { + goto error; + } + + } // end of global configuration settings + else + { + goto error; + } + + return 0; +error: + pgagroal_log_debug("Unknown configuration key <%s>", config_key); + return 1; + +} + +/** + * Function to extract a configuration value for a specific server. + * @param server_name the name of the server + * @param config_key one of the configuration keys allowed in the server section + * @param buffer the buffer where to write the stringified version of the value + * @param buffer_size the max size of the buffer where the result will be stored + * @return 0 on success + */ +static int +pgagroal_write_server_config_value(char* buffer, char* server_name, char* config_key, size_t buffer_size) +{ + int server_index = -1; + struct main_configuration* config; + int state; + + config = (struct main_configuration*)shmem; + + for (int i = 0; i < NUMBER_OF_SERVERS; i++) + { + if (!strncmp(config->servers[i].name, server_name, MISC_LENGTH)) + { + /* this is the right server */ + server_index = i; + break; + } + } + + if (server_index < 0 || server_index > NUMBER_OF_SERVERS) + { + pgagroal_log_debug("Unable to find a server named <%s> in the current configuration", server_name); + goto error; + } + + if (!strncmp(config_key, "host", MISC_LENGTH)) + { + return to_string(buffer, config->servers[server_index].host, buffer_size); + } + else if (!strncmp(config_key, "port", MISC_LENGTH)) + { + return to_int(buffer, config->servers[server_index].port); + } + else if (!strncmp(config_key, "primary", MISC_LENGTH)) + { + state = atomic_load(&config->servers[server_index].state); + bool primary = false; + switch (state) + { + case SERVER_NOTINIT_PRIMARY: + case SERVER_PRIMARY: + primary = true; + break; + default: + primary = false; + + } + + return to_bool(buffer, primary); + } + else if (!strncmp(config_key, "tls", MISC_LENGTH)) + { + return to_bool(buffer, config->servers[server_index].tls); + } + else if (!strncmp(config_key, "tls_cert_file", MISC_LENGTH)) + { + return to_string(buffer, config->servers[server_index].tls_cert_file, buffer_size); + } + else if (!strncmp(config_key, "tls_key_file", MISC_LENGTH)) + { + return to_string(buffer, config->servers[server_index].tls_key_file, buffer_size); + } + else if (!strncmp(config_key, "tls_ca_file", MISC_LENGTH)) + { + return to_string(buffer, config->servers[server_index].tls_ca_file, buffer_size); + } + else + { + goto error; + } + +error: + return 1; +} + +/** + * Method to extract a configuration value for an HBA entry. + * + * Please note that seeking for a username does not provide all the + * available configurations, since the same username could have been + * listed multiple times. Only the first match is returned. + * + * @param buffer where to write the stringified value + * @param username the username that must match the entry on the HBA entry line + * @param config_key the configuration parameter to search for + * @param buffer_size the max length of the destination buffer + * @return 0 on success + */ +static int +pgagroal_write_hba_config_value(char* buffer, char* username, char* config_key, size_t buffer_size) +{ + int hba_index = -1; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + for (int i = 0; i < NUMBER_OF_HBAS; i++) + { + if (!strncmp(config->hbas[i].username, username, MISC_LENGTH)) + { + /* this is the right hba entry */ + hba_index = i; + break; + } + } + + if (hba_index < 0 || hba_index > NUMBER_OF_HBAS) + { + pgagroal_log_warn("Unable to find a user named <%s> in the current configuration", username); + goto error; + } + + if (!strncmp(config_key, "type", MISC_LENGTH)) + { + return to_string(buffer, config->hbas[hba_index].type, buffer_size); + } + else if (!strncmp(config_key, "database", MISC_LENGTH)) + { + return to_string(buffer, config->hbas[hba_index].database, buffer_size); + } + else if (!strncmp(config_key, "username", MISC_LENGTH)) + { + return to_string(buffer, config->hbas[hba_index].username, buffer_size); + } + else if (!strncmp(config_key, "address", MISC_LENGTH)) + { + return to_string(buffer, config->hbas[hba_index].address, buffer_size); + } + else if (!strncmp(config_key, "method", MISC_LENGTH)) + { + return to_string(buffer, config->hbas[hba_index].method, buffer_size); + } + else + { + goto error; + } + +error: + return 1; +} + +/** + * Given a specific username, retrieves the informations about the limit + * configuration. The limit configuration is matched against a specific + * database. + * + * @param buffer where to write the information + * @param database the username to search for + * @param config_key the value to seek into the limits + * @param buffer_size the max size of the destination buffer where the result will be written + * @return 0 on success + */ +static int +pgagroal_write_limit_config_value(char* buffer, char* database, char* config_key, size_t buffer_size) +{ + int limit_index = -1; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + for (int i = 0; i < NUMBER_OF_LIMITS; i++) + { + if (!strncmp(config->limits[i].database, database, MISC_LENGTH)) + { + /* this is the right database entry */ + limit_index = i; + break; + } + } + + if (limit_index < 0 || limit_index > NUMBER_OF_LIMITS) + { + pgagroal_log_warn("Unable to find a database named <%s> in the current limit configuration", database); + goto error; + } + + if (!strncmp(config_key, "username", MISC_LENGTH)) + { + return to_string(buffer, config->limits[limit_index].username, buffer_size); + } + else if (!strncmp(config_key, "database", MISC_LENGTH)) + { + return to_string(buffer, config->limits[limit_index].database, buffer_size); + } + else if (!strncmp(config_key, "max_size", MISC_LENGTH)) + { + return to_int(buffer, config->limits[limit_index].max_size); + } + else if (!strncmp(config_key, "min_size", MISC_LENGTH)) + { + return to_int(buffer, config->limits[limit_index].min_size); + } + else if (!strncmp(config_key, "initial_size", MISC_LENGTH)) + { + return to_int(buffer, config->limits[limit_index].initial_size); + } + else + { + goto error; + } + +error: + return 1; +} + +/** + * An utility function to place an integer value into a string. + * @param where the string where to print the value, must be already allocated + * @param value the value to convert into a string + * @return 0 on success, 1 otherwise + */ +static int +to_int(char* where, int value) +{ + if (!where) + { + return 1; + } + + snprintf(where, MISC_LENGTH, "%d", value); + return 0; +} + +/** + * An utility function to place a boolean value into a string. + * The value is always converted in either "on" or "off". + * + * @param where the string where to print the value, must be already allocated + * @param value the value to convert into a string + * @return 0 on success, 1 otherwise + */ +static int +to_bool(char* where, bool value) +{ + if (!where) + { + return 1; + } + + snprintf(where, MISC_LENGTH, "%s", value ? "on" : "off"); + return 0; +} + +/** + * An utility function to place a string into another string. + * + * In the case the string has inner spaces, such spaces are quoted. The function + * tries to be as smart as possible identifying if there is the need for + * single or double quotes. + * + * The function accepts the size of the destination string, and before writing + * into such a string the result, it zero fills it. This means it is not mandatory + * to zero fill the destination string before calling this function. + * Also please note that if the string that is copied into the destination string + * has a length bigger than that specified, the function will not copy any data + * (and will not zero set the destination string, that will remain untouched!) + * + * @param where the string where to print the value, must be already allocated + * @param value the value to convert into a string + * @param max_length the max length of the 'where' destination string + * @return 0 on success, 1 otherwise + */ +static int +to_string(char* where, char* value, size_t max_length) +{ + bool needs_quotes = false; + bool has_double_quotes = false; + bool has_single_quotes = false; + char quoting_char = '\0'; + int index = 0; + + if (!where || !value || strlen(value) >= max_length) + { + return 1; + } + + // assume strings with spaces must be quoted + for (int i = 0; i < strlen(value); i++) + { + if (value[i] == ' ') + { + needs_quotes = true; + } + else if (value[i] == '"') + { + has_double_quotes = true; + } + else if (value[i] == '\'') + { + has_single_quotes = true; + } + + } + + needs_quotes = needs_quotes || has_double_quotes || has_single_quotes; + + if (needs_quotes) + { + // there must be space for quotes + if (strlen(value) > max_length - 2 - 1) + { + return 1; + } + + if (!has_single_quotes) + { + quoting_char = '\''; + } + else if (!has_double_quotes) + { + quoting_char = '"'; + } + + } + + // if here, the size of the string is appropriate, + // so do the copy + memset(where, 0, max_length); + + if (needs_quotes) + { + memcpy(&where[index], "ing_char, sizeof(quoting_char)); + index += sizeof(quoting_char); + } + + memcpy(&where[index], value, strlen(value)); + index += strlen(value); + + if (needs_quotes) + { + memcpy(&where[index], "ing_char, sizeof(quoting_char)); + index += sizeof(quoting_char); + } + + where[index] = '\0'; + + return 0; +} + +/** + * An utility function to convert the enumeration of values for the update_process_title + * into one of its possible string descriptions. + * + * @param where the buffer used to store the stringy thing + * @param value the config->update_process_title setting + * @return 0 on success, 1 otherwise + */ +static int +to_update_process_title(char* where, int value) +{ + if (!where || value < 0) + { + return 1; + } + + switch (value) + { + case UPDATE_PROCESS_TITLE_VERBOSE: + snprintf(where, MISC_LENGTH, "%s", "verbose"); + break; + case UPDATE_PROCESS_TITLE_MINIMAL: + snprintf(where, MISC_LENGTH, "%s", "minimal"); + + break; + case UPDATE_PROCESS_TITLE_STRICT: + snprintf(where, MISC_LENGTH, "%s", "strict"); + break; + case UPDATE_PROCESS_TITLE_NEVER: + snprintf(where, MISC_LENGTH, "%s", "never"); + break; + } + return 0; +} + +/** + * An utility function to convert the enumeration of values for the validation setting + * into one of its possible string descriptions. + * + * @param where the buffer used to store the stringy thing + * @param value the config->validation setting + * @return 0 on success, 1 otherwise + */ +static int +to_validation(char* where, int value) +{ + + if (!where || value < 0) + { + return 1; + } + + switch (value) + { + case VALIDATION_OFF: + snprintf(where, MISC_LENGTH, "%s", "off"); + break; + case VALIDATION_FOREGROUND: + snprintf(where, MISC_LENGTH, "%s", "foreground"); + break; + case VALIDATION_BACKGROUND: + snprintf(where, MISC_LENGTH, "%s", "background"); + break; + } + + return 0; + +} + +/** + * An utility function to convert the enumeration of values for the pipeline setting + * into one of its possible string descriptions. + * + * @param where the buffer used to store the stringy thing + * @param value the config->pipeline setting + * @return 0 on success, 1 otherwise + */ +static int +to_pipeline(char* where, int value) +{ + if (!where || value < 0) + { + return 1; + } + + switch (value) + { + case PIPELINE_AUTO: + snprintf(where, MISC_LENGTH, "%s", "auto"); + break; + case PIPELINE_SESSION: + snprintf(where, MISC_LENGTH, "%s", "session"); + break; + case PIPELINE_TRANSACTION: + snprintf(where, MISC_LENGTH, "%s", "transaction"); + break; + case PIPELINE_PERFORMANCE: + snprintf(where, MISC_LENGTH, "%s", "performance"); + break; + } + + return 0; +} + +/** + * An utility function to convert the enumeration of values for the log_level setting + * into one of its possible string descriptions. + * + * @param where the buffer used to store the stringy thing + * @param value the config->common.log_level setting + * @return 0 on success, 1 otherwise + */ +static int +to_log_level(char* where, int value) +{ + if (!where || value < 0) + { + return 1; + } + + switch (value) + { + + case PGAGROAL_LOGGING_LEVEL_DEBUG2: + snprintf(where, MISC_LENGTH, "%s", "debug2"); + break; + case PGAGROAL_LOGGING_LEVEL_DEBUG1: + snprintf(where, MISC_LENGTH, "%s", "debug"); + break; + case PGAGROAL_LOGGING_LEVEL_INFO: + snprintf(where, MISC_LENGTH, "%s", "info"); + break; + case PGAGROAL_LOGGING_LEVEL_WARN: + snprintf(where, MISC_LENGTH, "%s", "warn"); + break; + case PGAGROAL_LOGGING_LEVEL_ERROR: + snprintf(where, MISC_LENGTH, "%s", "error"); + break; + case PGAGROAL_LOGGING_LEVEL_FATAL: + snprintf(where, MISC_LENGTH, "%s", "fatal"); + break; + + } + + return 0; +} + +/** + * An utility function to convert the enumeration of values for the log_level setting + * into one of its possible string descriptions. + * + * @param where the buffer used to store the stringy thing + * @param value the config->common.log_mode setting + * @return 0 on success, 1 otherwise + */ +static int +to_log_mode(char* where, int value) +{ + if (!where || value < 0) + { + return 1; + } + + switch (value) + { + + case PGAGROAL_LOGGING_MODE_CREATE: + snprintf(where, MISC_LENGTH, "%s", "create"); + break; + case PGAGROAL_LOGGING_MODE_APPEND: + snprintf(where, MISC_LENGTH, "%s", "append"); + break; + } + + return 0; +} + +/** + * An utility function to convert the enumeration of values for the log_type setting + * into one of its possible string descriptions. + * + * @param where the buffer used to store the stringy thing + * @param value the config->common.log_type setting + * @return 0 on success, 1 otherwise + */ +static int +to_log_type(char* where, int value) +{ + if (!where || value < 0) + { + return 1; + } + + switch (value) + { + case PGAGROAL_LOGGING_TYPE_CONSOLE: + snprintf(where, MISC_LENGTH, "%s", "console"); + break; + case PGAGROAL_LOGGING_TYPE_FILE: + snprintf(where, MISC_LENGTH, "%s", "file"); + break; + case PGAGROAL_LOGGING_TYPE_SYSLOG: + snprintf(where, MISC_LENGTH, "%s", "syslog"); + break; + + } + + return 0; +} + +int +pgagroal_apply_main_configuration(struct main_configuration* config, + struct server* srv, + char* section, + char* key, + char* value) +{ + size_t max = 0; + bool unknown = false; + + // pgagroal_log_trace( "Configuration setting [%s] <%s> -> <%s>", section, key, value ); + + if (key_in_section("host", section, key, true, NULL)) + { + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->common.host, value, max); + } + else if (key_in_section("host", section, key, false, &unknown)) + { + max = strlen(section); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(&srv->name, section, max); + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(&srv->host, value, max); + atomic_store(&srv->state, SERVER_NOTINIT); + } + else if (key_in_section("port", section, key, true, NULL)) + { + if (as_int(value, &config->common.port)) + { + unknown = true; + } + } + else if (key_in_section("port", section, key, false, &unknown)) + { + memcpy(&srv->name, section, strlen(section)); + if (as_int(value, &srv->port)) + { + unknown = true; + } + atomic_store(&srv->state, SERVER_NOTINIT); + } + else if (key_in_section("primary", section, key, false, &unknown)) + { + bool b = false; + if (as_bool(value, &b)) + { + unknown = true; + } + if (b) + { + atomic_store(&srv->state, SERVER_NOTINIT_PRIMARY); + } + else + { + atomic_store(&srv->state, SERVER_NOTINIT); + } + } + else if (key_in_section("metrics", section, key, true, &unknown)) + { + if (as_int(value, &config->common.metrics)) + { + unknown = true; + } + } + else if (key_in_section("metrics_cache_max_age", section, key, true, &unknown)) + { + if (as_seconds(value, &config->common.metrics_cache_max_age, PGAGROAL_PROMETHEUS_CACHE_DISABLED)) + { + unknown = true; + } + } + else if (key_in_section("metrics_cache_max_size", section, key, true, &unknown)) + { + if (as_bytes(value, &config->common.metrics_cache_max_size, PROMETHEUS_DEFAULT_CACHE_SIZE)) + { + unknown = true; + } + } + else if (key_in_section("management", section, key, true, &unknown)) + { + if (as_int(value, &config->management)) + { + unknown = true; + } + } + else if (key_in_section("pipeline", section, key, true, &unknown)) + { + config->pipeline = as_pipeline(value); + } + else if (key_in_section("failover", section, key, true, &unknown)) + { + if (as_bool(value, &config->failover)) + { + unknown = true; + } + } + else if (key_in_section("failover_script", section, key, true, &unknown)) + { + + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->failover_script, value, max); + } + else if (key_in_section("auth_query", section, key, true, &unknown)) + { + if (as_bool(value, &config->authquery)) + { + unknown = true; + } + } + else if (key_in_section("tls", section, key, true, NULL)) + { + if (as_bool(value, &config->common.tls)) + { + unknown = true; + } + } + else if (key_in_section("tls", section, key, false, &unknown)) + { + if (as_bool(value, &srv->tls)) + { + unknown = true; + } + } + else if (key_in_section("tls_ca_file", section, key, true, NULL)) + { + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->common.tls_ca_file, value, max); + } + else if (key_in_section("tls_ca_file", section, key, false, &unknown)) + { + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(srv->tls_ca_file, value, max); + } + else if (key_in_section("tls_cert_file", section, key, true, NULL)) + { + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->common.tls_cert_file, value, max); + } + else if (key_in_section("tls_cert_file", section, key, false, &unknown)) + { + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(srv->tls_cert_file, value, max); + } + else if (key_in_section("tls_key_file", section, key, true, NULL)) + { + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->common.tls_key_file, value, max); + } + else if (key_in_section("tls_key_file", section, key, false, &unknown)) + { + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(srv->tls_key_file, value, max); + } + else if (key_in_section("blocking_timeout", section, key, true, &unknown)) + { + + if (as_int(value, &config->blocking_timeout)) + { + unknown = true; + } + } + else if (key_in_section("idle_timeout", section, key, true, &unknown)) + { + if (as_int(value, &config->idle_timeout)) + { + unknown = true; + } + } + else if (key_in_section("rotate_frontend_password_timeout", section, key, true, &unknown)) + { + if (as_int(value, &config->rotate_frontend_password_timeout)) + { + unknown = true; + } + } + else if (key_in_section("rotate_frontend_password_length", section, key, true, &unknown)) + { + if (as_int(value, &config->rotate_frontend_password_length)) + { + unknown = true; + } + } + else if (key_in_section("max_connection_age", section, key, true, &unknown)) + { + if (as_int(value, &config->max_connection_age)) + { + unknown = true; + } + } + else if (key_in_section("validation", section, key, true, &unknown)) + { + config->validation = as_validation(value); + } + else if (key_in_section("background_interval", section, key, true, &unknown)) + { + if (as_int(value, &config->background_interval)) + { + unknown = true; + } + } + else if (key_in_section("max_retries", section, key, true, &unknown)) + { + if (as_int(value, &config->max_retries)) + { + unknown = true; + } + } + else if (key_in_section("authentication_timeout", section, key, true, &unknown)) + { + if (as_int(value, &config->common.authentication_timeout)) + { + unknown = true; + } + } + else if (key_in_section("disconnect_client", section, key, true, &unknown)) + { + if (as_int(value, &config->disconnect_client)) + { + unknown = true; + } + } + else if (key_in_section("disconnect_client_force", section, key, true, &unknown)) + { + if (as_bool(value, &config->disconnect_client_force)) + { + unknown = true; + } + } + else if (key_in_section("pidfile", section, key, true, &unknown)) + { + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->pidfile, value, max); + } + else if (key_in_section("allow_unknown_users", section, key, true, &unknown)) + { + if (as_bool(value, &config->allow_unknown_users)) + { + unknown = true; + } + } + else if (key_in_section("log_type", section, key, true, &unknown)) + { + config->common.log_type = as_logging_type(value); + } + else if (key_in_section("log_level", section, key, true, &unknown)) + { + config->common.log_level = as_logging_level(value); + } + else if (key_in_section("log_path", section, key, true, &unknown)) + { + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->common.log_path, value, max); + } + else if (key_in_section("log_rotation_size", section, key, true, &unknown)) + { + if (as_logging_rotation_size(value, &config->common.log_rotation_size)) + { + unknown = true; + } + } + else if (key_in_section("log_rotation_age", section, key, true, &unknown)) + { + if (as_logging_rotation_age(value, &config->common.log_rotation_age)) + { + unknown = true; + } + } + else if (key_in_section("log_line_prefix", section, key, true, &unknown)) + { + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + + memcpy(config->common.log_line_prefix, value, max); + } + else if (key_in_section("log_connections", section, key, true, &unknown)) + { + + if (as_bool(value, &config->common.log_connections)) + { + unknown = true; + } + } + else if (key_in_section("log_disconnections", section, key, true, &unknown)) + { + if (as_bool(value, &config->common.log_disconnections)) + { + unknown = true; + } + } + else if (key_in_section("log_mode", section, key, true, &unknown)) + { + config->common.log_mode = as_logging_mode(value); + } + else if (key_in_section("max_connections", section, key, true, &unknown)) + { + if (as_int(value, &config->max_connections)) + { + unknown = true; + } + } + else if (key_in_section("unix_socket_dir", section, key, true, &unknown)) + { + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->unix_socket_dir, value, max); + } + else if (key_in_section("libev", section, key, true, &unknown)) + { + + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->libev, value, max); + } + else if (key_in_section("keep_alive", section, key, true, &unknown)) + { + if (as_bool(value, &config->keep_alive)) + { + unknown = true; + } + } + else if (key_in_section("nodelay", section, key, true, &unknown)) + { + if (as_bool(value, &config->nodelay)) + { + unknown = true; + } + } + else if (key_in_section("non_blocking", section, key, true, &unknown)) + { + if (as_bool(value, &config->non_blocking)) + { + unknown = true; + } + } + else if (key_in_section("backlog", section, key, true, &unknown)) + { + if (as_int(value, &config->backlog)) + { + unknown = true; + } + } + else if (key_in_section("hugepage", section, key, true, &unknown)) + { + config->common.hugepage = as_hugepage(value); + } + else if (key_in_section("tracker", section, key, true, &unknown)) + { + if (as_bool(value, &config->tracker)) + { + unknown = true; + } + } + else if (key_in_section("track_prepared_statements", section, key, true, &unknown)) + { + if (as_bool(value, &config->track_prepared_statements)) + { + unknown = true; + } + } + else if (key_in_section("update_process_title", section, key, true, &unknown)) + { + if (as_update_process_title(value, &config->update_process_title, UPDATE_PROCESS_TITLE_VERBOSE)) + { + unknown = false; + } + } + else + { + unknown = true; + } + + if (unknown) + { + return 1; + } + else + { + return 0; + } +} + +int +pgagroal_apply_vault_configuration(struct vault_configuration* config, + struct vault_server* srv, + char* section, + char* key, + char* value) +{ + size_t max = 0; + bool unknown = false; + + // pgagroal_log_trace( "Configuration setting [%s] <%s> -> <%s>", section, key, value ); + + if (key_in_section("host", section, key, true, NULL)) + { + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->common.host, value, max); + } + else if (key_in_section("host", section, key, false, &unknown)) + { + max = strlen(section); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(&srv->server.name, section, max); + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(&srv->server.host, value, max); + } + else if (key_in_section("port", section, key, true, NULL)) + { + if (as_int(value, &config->common.port)) + { + unknown = true; + } + } + else if (key_in_section("port", section, key, false, &unknown)) + { + memcpy(&srv->server.name, section, strlen(section)); + if (as_int(value, &srv->server.port)) + { + unknown = true; + } + } + else if (key_in_section("user", section, key, false, &unknown)) + { + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(&srv->user.username, value, max); + } + else if (key_in_section("tls", section, key, true, &unknown)) + { + if (as_bool(value, &config->common.tls)) + { + unknown = true; + } + } + else if (key_in_section("tls_ca_file", section, key, true, &unknown)) + { + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->common.tls_ca_file, value, max); + } + else if (key_in_section("tls_cert_file", section, key, true, &unknown)) + { + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->common.tls_cert_file, value, max); + } + else if (key_in_section("tls_key_file", section, key, true, &unknown)) + { + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->common.tls_key_file, value, max); + } + else if (key_in_section("metrics", section, key, true, &unknown)) + { + if (as_int(value, &config->common.metrics)) + { + unknown = true; + } + } + else if (key_in_section("metrics_cache_max_age", section, key, true, &unknown)) + { + if (as_seconds(value, &config->common.metrics_cache_max_age, PGAGROAL_PROMETHEUS_CACHE_DISABLED)) + { + unknown = true; + } + } + else if (key_in_section("metrics_cache_max_size", section, key, true, &unknown)) + { + if (as_bytes(value, &config->common.metrics_cache_max_size, PROMETHEUS_DEFAULT_CACHE_SIZE)) + { + unknown = true; + } + } + else if (key_in_section("authentication_timeout", section, key, true, &unknown)) + { + if (as_int(value, &config->common.authentication_timeout)) + { + unknown = true; + } + } + else if (key_in_section("log_type", section, key, true, &unknown)) + { + config->common.log_type = as_logging_type(value); + } + else if (key_in_section("log_level", section, key, true, &unknown)) + { + config->common.log_level = as_logging_level(value); + } + else if (key_in_section("log_path", section, key, true, &unknown)) + { + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->common.log_path, value, max); + } + else if (key_in_section("log_rotation_size", section, key, true, &unknown)) + { + if (as_logging_rotation_size(value, &config->common.log_rotation_size)) + { + unknown = true; + } + } + else if (key_in_section("log_rotation_age", section, key, true, &unknown)) + { + if (as_logging_rotation_age(value, &config->common.log_rotation_age)) + { + unknown = true; + } + } + else if (key_in_section("log_line_prefix", section, key, true, &unknown)) + { + max = strlen(value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + + memcpy(config->common.log_line_prefix, value, max); + } + else if (key_in_section("log_connections", section, key, true, &unknown)) + { + + if (as_bool(value, &config->common.log_connections)) + { + unknown = true; + } + } + else if (key_in_section("log_disconnections", section, key, true, &unknown)) + { + if (as_bool(value, &config->common.log_disconnections)) + { + unknown = true; + } + } + else if (key_in_section("log_mode", section, key, true, &unknown)) + { + config->common.log_mode = as_logging_mode(value); + } + else if (key_in_section("hugepage", section, key, true, &unknown)) + { + config->common.hugepage = as_hugepage(value); + } + return 0; +} + +int +pgagroal_apply_configuration(char* config_key, char* config_value) +{ + struct main_configuration* config; + struct main_configuration* current_config; + + char section[MISC_LENGTH]; + char context[MISC_LENGTH]; + char key[MISC_LENGTH]; + int begin = -1, end = -1; + bool main_section; + size_t config_size = 0; + struct server* srv_dst; + struct server* srv_src; + + // get the currently running configuration + current_config = (struct main_configuration*)shmem; + // create a new configuration that will be the clone of the previous one + config_size = sizeof(struct main_configuration); + if (pgagroal_create_shared_memory(config_size, HUGEPAGE_OFF, (void**)&config)) + { + goto error; + } + + // copy the configuration that is currently running + memcpy(config, current_config, config_size); + + memset(section, 0, MISC_LENGTH); + memset(context, 0, MISC_LENGTH); + memset(key, 0, MISC_LENGTH); + + for (int i = 0; i < strlen(config_key); i++) + { + if (config_key[i] == '.') + { + if (!strlen(section)) + { + memcpy(section, &config_key[begin], end - begin + 1); + section[end - begin + 1] = '\0'; + begin = end = -1; + continue; + } + else if (!strlen(context)) + { + memcpy(context, &config_key[begin], end - begin + 1); + context[end - begin + 1] = '\0'; + begin = end = -1; + continue; + } + else if (!strlen(key)) + { + memcpy(key, &config_key[begin], end - begin + 1); + key[end - begin + 1] = '\0'; + begin = end = -1; + continue; + } + + } + + if (begin < 0) + { + begin = i; + } + + end = i; + + } + + // if the key has not been found, since there is no ending dot, + // try to extract it from the string + if (!strlen(key)) + { + memcpy(key, &config_key[begin], end - begin + 1); + key[end - begin + 1] = '\0'; + } + + // force the main section, i.e., global parameters, if and only if + // there is no section or section is 'pgagroal' without any subsection + main_section = (!strlen(section) || !strncmp(section, PGAGROAL_MAIN_INI_SECTION, MISC_LENGTH)) + && !strlen(context); + + if (!strncmp(section, PGAGROAL_CONF_SERVER_PREFIX, MISC_LENGTH)) + { + srv_src = srv_dst = NULL; + + // server.. + // here the 'context' is the server name, so let's find it + for (int i = 0; i < config->number_of_servers; i++) + { + if (!strncmp(config->servers[i].name, context, MISC_LENGTH)) + { + pgagroal_log_debug("Changing configuration of server <%s>: (%s) %s -> %s", + config->servers[i].name, + config_key, + key, + config_value); + + srv_dst = calloc(1, sizeof(struct server)); + srv_src = &config->servers[i]; + // clone the current server + memcpy(srv_dst, srv_src, sizeof(struct server)); + if (pgagroal_apply_main_configuration(config, + srv_dst, + context, + key, + config_value)) + { + goto error; + } + + // now that changes have been applied, see if the server + // requires a restart: in such case abort the configuration + // change + if (restart_server(srv_dst, srv_src)) + { + goto error; + } + + break; // avoid searching for another server section + + } + } + + memcpy(srv_src, srv_dst, sizeof(struct server)); + srv_src = srv_dst = NULL; + + } + else if (!strncmp(section, PGAGROAL_CONF_HBA_PREFIX, MISC_LENGTH)) + { + // hba.. + // here the context is the username + // and the section is the 'hba', while the key is what the user wants to change + for (int i = 0; i < config->number_of_hbas; i++) + { + if (!strncmp(config->hbas[i].username, context, MISC_LENGTH)) + { + // this is the correct HBA entry, apply the changes + pgagroal_log_debug("Trying to change HBA configuration setting <%s> to <%s>", key, config_value); + if (pgagroal_apply_hba_configuration(&config->hbas[i], key, config_value)) + { + goto error; + } + + break; // avoid searching for another HBA entry + } + } + } + else if (!strncmp(section, PGAGROAL_CONF_LIMIT_PREFIX, MISC_LENGTH)) + { + // limit.. + // the context is the username and the key is what to change + for (int i = 0; i < config->number_of_limits; i++) + { + if (!strncmp(config->limits[i].username, context, MISC_LENGTH)) + { + // this is the correct limit entry, apply the changes + // WARNING: according to restart_limit() every change to a limit entry + // requires a restart, so it does not make a lot of sense to apply a configuration change + pgagroal_log_debug("Trying to change limit configuration setting <%s> to <%s>", key, config_value); + if (pgagroal_apply_limit_configuration_string(&config->limits[i], key, config_value)) + { + goto error; + } + + break; // avoid searching for another HBA entry + } + } + // return pgagroal_write_limit_config_value(buffer, context, key); + } + else if (main_section) + { + + pgagroal_log_debug("Trying to change main configuration setting <%s> to <%s>", config_key, config_value); + if (pgagroal_apply_main_configuration(config, + NULL, + PGAGROAL_MAIN_INI_SECTION, + config_key, + config_value)) + { + goto error; + } + } + else + { + // if here, an error happened! + goto error; + } + + if (pgagroal_validate_configuration(config, false, false)) + { + goto error; + } + + if (transfer_configuration(current_config, config)) + { + goto error; + } + + if (pgagroal_destroy_shared_memory((void*)config, config_size)) + { + goto error; + } + + // all done + return 0; +error: + + if (config != NULL) + { + memcpy(config, current_config, sizeof(struct main_configuration)); + pgagroal_destroy_shared_memory((void*)config, config_size); + } + + return 1; +} + +/** + * Utility function to set an HBA single entry. + * The HBA entry must be already allocated. + * + * Before applying a setting, the field is zeroed. + * + * @param hba the entry to modify + * @param context the entry to modify, e.g., "method" or a constant like PGAGRAOL_HBA_ENTRY_DATABASE + * @param value the value to set + * + * @return 0 on success, 1 on failure + */ +static int +pgagroal_apply_hba_configuration(struct hba* hba, + char* context, + char* value) +{ + + if (!hba || !context || !strlen(context) || !value || !strlen(value)) + { + goto error; + } + + if (!strncmp(context, PGAGROAL_HBA_ENTRY_TYPE, MAX_TYPE_LENGTH) + && strlen(value) < MAX_TYPE_LENGTH) + { + memset(&(hba->type), 0, strlen(hba->type)); + memcpy(&(hba->type), value, strlen(value)); + } + else if (!strncmp(context, PGAGROAL_HBA_ENTRY_DATABASE, MAX_DATABASE_LENGTH) + && strlen(value) < MAX_DATABASE_LENGTH) + { + memset(&(hba->database), 0, strlen(hba->database)); + memcpy(&(hba->database), value, strlen(value)); + } + else if (!strncmp(context, PGAGROAL_HBA_ENTRY_USERNAME, MAX_USERNAME_LENGTH) + && strlen(value) < MAX_USERNAME_LENGTH) + { + memset(&(hba->username), 0, strlen(hba->username)); + memcpy(&(hba->username), value, strlen(value)); + } + else if (!strncmp(context, PGAGROAL_HBA_ENTRY_ADDRESS, MAX_ADDRESS_LENGTH) + && strlen(value) < MAX_ADDRESS_LENGTH) + { + memset(&(hba->address), 0, strlen(hba->address)); + memcpy(&(hba->address), value, strlen(value)); + } + else if (!strncmp(context, PGAGROAL_HBA_ENTRY_METHOD, MAX_ADDRESS_LENGTH) + && strlen(value) < MAX_ADDRESS_LENGTH) + { + memset(&(hba->method), 0, strlen(hba->method)); + memcpy(&(hba->method), value, strlen(value)); + } + + return 0; + +error: + return 1; +} + +/** + * An utility function to set a single value for the limit struct. + * The structure must already be allocated. + * + * @param limit the structure to change + * @param context the key of the field to change, e.g., 'max_size' or a constant like PGAGROAL_LIMIT_ENTRY_DATABASE + * @param value the new value to set + * + * @return 0 on success. + */ +static int +pgagroal_apply_limit_configuration_string(struct limit* limit, + char* context, + char* value) +{ + + if (!strncmp(context, PGAGROAL_LIMIT_ENTRY_DATABASE, MAX_DATABASE_LENGTH) + && strlen(value) < MAX_DATABASE_LENGTH) + { + memset(&limit->database, 0, strlen(limit->database)); + memcpy(&limit->database, value, strlen(value)); + } + else if (!strncmp(context, PGAGROAL_LIMIT_ENTRY_USERNAME, MAX_USERNAME_LENGTH) + && strlen(value) < MAX_USERNAME_LENGTH) + { + memset(&limit->username, 0, strlen(limit->username)); + memcpy(&limit->username, value, strlen(value)); + } + else if (!strncmp(context, PGAGROAL_LIMIT_ENTRY_MAX_SIZE, MISC_LENGTH)) + { + return as_int(value, &limit->max_size); + } + else if (!strncmp(context, PGAGROAL_LIMIT_ENTRY_MIN_SIZE, MISC_LENGTH)) + { + return as_int(value, &limit->min_size); + } + else if (!strncmp(context, PGAGROAL_LIMIT_ENTRY_INITIAL_SIZE, MISC_LENGTH)) + { + return as_int(value, &limit->initial_size); + } + else if (!strncmp(context, PGAGROAL_LIMIT_ENTRY_LINENO, MISC_LENGTH)) + { + return as_int(value, &limit->lineno); + } + else + { + goto error; + } + + return 0; + +error: + return 1; + +} + +static int +pgagroal_apply_limit_configuration_int(struct limit* limit, + char* context, + int value) +{ + + if (!strncmp(context, PGAGROAL_LIMIT_ENTRY_MAX_SIZE, MISC_LENGTH)) + { + limit->max_size = value; + } + else if (!strncmp(context, PGAGROAL_LIMIT_ENTRY_MIN_SIZE, MISC_LENGTH)) + { + limit->min_size = value; + } + else if (!strncmp(context, PGAGROAL_LIMIT_ENTRY_INITIAL_SIZE, MISC_LENGTH)) + { + limit->initial_size = value; + } + else if (!strncmp(context, PGAGROAL_LIMIT_ENTRY_LINENO, MISC_LENGTH)) + { + limit->lineno = value; + } + else + { + goto error; + } + + return 0; + +error: + return 1; + +} + +static void +add_configuration_response(struct json* res) +{ + struct main_configuration* config = NULL; + + config = (struct main_configuration*)shmem; + + // JSON of main configuration + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_HOST, (uintptr_t)config->common.host, ValueString); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_PORT, (uintptr_t)config->common.port, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_UNIX_SOCKET_DIR, (uintptr_t)config->unix_socket_dir, ValueString); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_METRICS, (uintptr_t)config->common.metrics, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_METRICS_CACHE_MAX_AGE, (uintptr_t)config->common.metrics_cache_max_age, ValueString); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_METRICS_CACHE_MAX_SIZE, (uintptr_t)config->common.metrics_cache_max_size, ValueString); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_MANAGEMENT, (uintptr_t)config->management, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_LOG_TYPE, (uintptr_t)config->common.log_type, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_LOG_LEVEL, (uintptr_t)config->common.log_level, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_LOG_PATH, (uintptr_t)config->common.log_path, ValueString); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_LOG_ROTATION_AGE, (uintptr_t)config->common.log_rotation_age, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_LOG_ROTATION_SIZE, (uintptr_t)config->common.log_rotation_size, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_LOG_LINE_PREFIX, (uintptr_t)config->common.log_line_prefix, ValueString); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_LOG_MODE, (uintptr_t)config->common.log_mode, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_LOG_CONNECTIONS, (uintptr_t)config->common.log_connections, ValueBool); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_LOG_DISCONNECTIONS, (uintptr_t)config->common.log_disconnections, ValueBool); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_BLOCKING_TIMEOUT, (uintptr_t)config->blocking_timeout, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_IDLE_TIMEOUT, (uintptr_t)config->idle_timeout, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_ROTATE_FRONTEND_PASSWORD_TIMEOUT, (uintptr_t)config->rotate_frontend_password_timeout, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_ROTATE_FRONTEND_PASSWORD_LENGTH, (uintptr_t)config->rotate_frontend_password_length, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_MAX_CONNECTION_AGE, (uintptr_t)config->max_connection_age, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_VALIDATION, (uintptr_t)config->validation, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_BACKGROUND_INTERVAL, (uintptr_t)config->background_interval, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_MAX_RETRIES, (uintptr_t)config->max_retries, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_MAX_CONNECTIONS, (uintptr_t)config->max_connections, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_ALLOW_UNKNOWN_USERS, (uintptr_t)config->allow_unknown_users, ValueBool); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_AUTHENTICATION_TIMEOUT, (uintptr_t)config->common.authentication_timeout, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_PIPELINE, (uintptr_t)config->pipeline, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_AUTH_QUERY, (uintptr_t)config->authquery, ValueBool); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_FAILOVER, (uintptr_t)config->failover, ValueBool); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_FAILOVER_SCRIPT, (uintptr_t)config->failover_script, ValueString); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_TLS, (uintptr_t)config->common.tls, ValueBool); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_TLS_CERT_FILE, (uintptr_t)config->common.tls_cert_file, ValueString); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_TLS_KEY_FILE, (uintptr_t)config->common.tls_key_file, ValueString); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_TLS_CA_FILE, (uintptr_t)config->common.tls_ca_file, ValueString); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_LIBEV, (uintptr_t)config->libev, ValueString); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_KEEP_ALIVE, (uintptr_t)config->keep_alive, ValueBool); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_NODELAY, (uintptr_t)config->nodelay, ValueBool); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_NON_BLOCKING, (uintptr_t)config->non_blocking, ValueBool); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_BACKLOG, (uintptr_t)config->backlog, ValueInt64); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_HUGEPAGE, (uintptr_t)config->common.hugepage, ValueChar); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_TRACKER, (uintptr_t)config->tracker, ValueBool); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_TRACK_PREPARED_STATEMENTS, (uintptr_t)config->track_prepared_statements, ValueBool); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_PIDFILE, (uintptr_t)config->pidfile, ValueString); + pgagroal_json_put(res, CONFIGURATION_ARGUMENT_UPDATE_PROCESS_TITLE, (uintptr_t)config->update_process_title, ValueInt64); +} + +static void +add_servers_configuration_response(struct json* res) +{ + struct main_configuration* config = NULL; + + config = (struct main_configuration*)shmem; + + // JSON of server configuration + for (int i = 0; i < config->number_of_servers; i++) + { + struct json* server_conf = NULL; + + if (pgagroal_json_create(&server_conf)) + { + return; + } + + pgagroal_json_put(server_conf, CONFIGURATION_ARGUMENT_HOST, (uintptr_t)config->servers[i].host, ValueString); + pgagroal_json_put(server_conf, CONFIGURATION_ARGUMENT_PORT, (uintptr_t)config->servers[i].port, ValueInt64); + pgagroal_json_put(server_conf, CONFIGURATION_ARGUMENT_TLS, (uintptr_t)config->servers[i].tls, ValueBool); + pgagroal_json_put(server_conf, CONFIGURATION_ARGUMENT_TLS_CERT_FILE, (uintptr_t)config->servers[i].tls_cert_file, ValueString); + pgagroal_json_put(server_conf, CONFIGURATION_ARGUMENT_TLS_KEY_FILE, (uintptr_t)config->servers[i].tls_key_file, ValueString); + pgagroal_json_put(server_conf, CONFIGURATION_ARGUMENT_TLS_CA_FILE, (uintptr_t)config->servers[i].tls_ca_file, ValueString); + + pgagroal_json_put(res, config->servers[i].name, (uintptr_t)server_conf, ValueJSON); + } +} + +void +pgagroal_conf_get(SSL* ssl, int client_fd, uint8_t compression, uint8_t encryption, struct json* payload) +{ + struct json* response = NULL; + char* elapsed = NULL; + time_t start_time; + time_t end_time; + int total_seconds; + + pgagroal_start_logging(); + + start_time = time(NULL); + + if (pgagroal_management_create_response(payload, -1, &response)) + { + pgagroal_management_response_error(NULL, client_fd, NULL, MANAGEMENT_ERROR_CONF_GET_ERROR, compression, encryption, payload); + pgagroal_log_error("Conf Get: Error creating json object (%d)", MANAGEMENT_ERROR_CONF_GET_ERROR); + goto error; + } + + add_configuration_response(response); + add_servers_configuration_response(response); + + end_time = time(NULL); + + if (pgagroal_management_response_ok(NULL, client_fd, start_time, end_time, compression, encryption, payload)) + { + pgagroal_management_response_error(NULL, client_fd, NULL, MANAGEMENT_ERROR_CONF_GET_NETWORK, compression, encryption, payload); + pgagroal_log_error("Conf Get: Error sending response"); + + goto error; + } + + elapsed = pgagroal_get_timestamp_string(start_time, end_time, &total_seconds); + + pgagroal_log_info("Conf Get (Elapsed: %s)", elapsed); + + pgagroal_json_destroy(payload); + + pgagroal_disconnect(client_fd); + + pgagroal_stop_logging(); + + exit(0); +error: + + pgagroal_json_destroy(payload); + + pgagroal_disconnect(client_fd); + + pgagroal_stop_logging(); + + exit(1); +} + +void +pgagroal_conf_set(SSL* ssl, int client_fd, uint8_t compression, uint8_t encryption, struct json* payload) +{ + struct json* response = NULL; + struct json* request = NULL; + char* config_key = NULL; + char* config_value = NULL; + char* elapsed = NULL; + time_t start_time; + time_t end_time; + char section[MISC_LENGTH]; + char key[MISC_LENGTH]; + int total_seconds; + struct main_configuration* config = NULL; + struct json* server_j = NULL; + int server_index = -1; + int begin = -1, end = -1; + size_t max; + + pgagroal_start_logging(); + + start_time = time(NULL); + + config = (struct main_configuration*)shmem; + // Extract config_key and config_value from request + request = (struct json*)pgagroal_json_get(payload, MANAGEMENT_CATEGORY_REQUEST); + if (!request) + { + pgagroal_management_response_error(NULL, client_fd, NULL, MANAGEMENT_ERROR_CONF_SET_NOREQUEST, compression, encryption, payload); + pgagroal_log_error("Conf Set: No request category found in payload (%d)", MANAGEMENT_ERROR_CONF_SET_NOREQUEST); + goto error; + } + + config_key = (char*)pgagroal_json_get(request, MANAGEMENT_ARGUMENT_CONFIG_KEY); + config_value = (char*)pgagroal_json_get(request, MANAGEMENT_ARGUMENT_CONFIG_VALUE); + + if (!config_key || !config_value) + { + pgagroal_management_response_error(NULL, client_fd, NULL, MANAGEMENT_ERROR_CONF_SET_NOCONFIG_KEY_OR_VALUE, compression, encryption, payload); + pgagroal_log_error("Conf Set: No config key or config value in request (%d)", MANAGEMENT_ERROR_CONF_SET_NOCONFIG_KEY_OR_VALUE); + goto error; + } + + // Modify + memset(section, 0, MISC_LENGTH); + memset(key, 0, MISC_LENGTH); + + for (int i = 0; i < strlen(config_key); i++) + { + if (config_key[i] == '.') + { + if (!strlen(section)) + { + memcpy(section, &config_key[begin], end - begin + 1); + section[end - begin + 1] = '\0'; + begin = end = -1; + continue; + } + } + + if (begin < 0) + { + begin = i; + } + + end = i; + } + // if the key has not been found, since there is no ending dot, + // try to extract it from the string + if (!strlen(key)) + { + memcpy(key, &config_key[begin], end - begin + 1); + key[end - begin + 1] = '\0'; + } + + if (strlen(section) > 0) + { + if (pgagroal_json_create(&server_j)) + { + pgagroal_management_response_error(NULL, client_fd, NULL, MANAGEMENT_ERROR_CONF_SET_ERROR, compression, encryption, payload); + pgagroal_log_error("Conf Set: Error creating json object (%d)", MANAGEMENT_ERROR_CONF_SET_ERROR); + goto error; + } + + for (int i = 0; i < config->number_of_servers; i++) + { + if (!strcmp(config->servers[i].name, section)) + { + server_index = i; + break; + } + } + if (server_index == -1) + { + pgagroal_management_response_error(NULL, client_fd, NULL, MANAGEMENT_ERROR_CONF_SET_UNKNOWN_SERVER, compression, encryption, payload); + pgagroal_log_error("Conf Set: Unknown server value parsed (%d)", MANAGEMENT_ERROR_CONF_SET_UNKNOWN_SERVER); + goto error; + } + } + + if (pgagroal_management_create_response(payload, -1, &response)) + { + pgagroal_management_response_error(NULL, client_fd, NULL, MANAGEMENT_ERROR_CONF_SET_ERROR, compression, encryption, payload); + pgagroal_log_error("Conf Set: Error creating json object (%d)", MANAGEMENT_ERROR_CONF_SET_ERROR); + goto error; + } + + if (strlen(key) && config_value) + { + bool unknown = false; + if (!strcmp(key, "host")) + { + if (strlen(section) > 0) + { + max = strlen(config_value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(&config->servers[server_index].host, config_value, max); + config->servers[server_index].host[max] = '\0'; + pgagroal_json_put(server_j, key, (uintptr_t)config->servers[server_index].host, ValueString); + pgagroal_json_put(response, config->servers[server_index].name, (uintptr_t)server_j, ValueJSON); + } + else + { + max = strlen(config_value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->common.host, config_value, max); + config->common.host[max] = '\0'; + pgagroal_json_put(response, key, (uintptr_t)config->common.host, ValueString); + } + } + else if (!strcmp(key, "port")) + { + if (strlen(section) > 0) + { + if (as_int(config_value, &config->servers[server_index].port)) + { + unknown = true; + } + pgagroal_json_put(server_j, key, (uintptr_t)config->servers[server_index].port, ValueInt64); + pgagroal_json_put(response, config->servers[server_index].name, (uintptr_t)server_j, ValueJSON); + } + else + { + if (as_int(config_value, &config->common.port)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->common.port, ValueInt64); + } + } + else if (!strcmp(key, "tls")) + { + if (strlen(section) > 0) + { + if (as_bool(config_value, &config->servers[server_index].tls)) + { + unknown = true; + } + pgagroal_json_put(server_j, key, (uintptr_t)config->servers[server_index].tls, ValueBool); + pgagroal_json_put(response, config->servers[server_index].name, (uintptr_t)server_j, ValueJSON); + } + else + { + if (as_bool(config_value, &config->common.tls)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->common.tls, ValueBool); + } + } + else if (!strcmp(key, "tls_cert_file")) + { + if (strlen(section) > 0) + { + max = strlen(config_value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(&config->servers[server_index].tls_cert_file, config_value, max); + config->servers[server_index].tls_cert_file[max] = '\0'; + pgagroal_json_put(server_j, key, (uintptr_t)config->servers[server_index].tls_cert_file, ValueString); + pgagroal_json_put(response, config->servers[server_index].name, (uintptr_t)server_j, ValueJSON); + } + else + { + max = strlen(config_value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->common.tls_cert_file, config_value, max); + config->common.tls_cert_file[max] = '\0'; + pgagroal_json_put(response, key, (uintptr_t)config->common.tls_cert_file, ValueString); + } + } + else if (!strcmp(key, "tls_key_file")) + { + if (strlen(section) > 0) + { + max = strlen(config_value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(&config->servers[server_index].tls_key_file, config_value, max); + config->servers[server_index].tls_key_file[max] = '\0'; + pgagroal_json_put(server_j, key, (uintptr_t)config->servers[server_index].tls_key_file, ValueString); + pgagroal_json_put(response, config->servers[server_index].name, (uintptr_t)server_j, ValueJSON); + } + else + { + max = strlen(config_value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->common.tls_key_file, config_value, max); + config->common.tls_key_file[max] = '\0'; + pgagroal_json_put(response, key, (uintptr_t)config->common.tls_key_file, ValueString); + } + } + else if (!strcmp(key, "tls_ca_file")) + { + if (strlen(section) > 0) + { + max = strlen(config_value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(&config->servers[server_index].tls_ca_file, config_value, max); + config->servers[server_index].tls_ca_file[max] = '\0'; + pgagroal_json_put(server_j, key, (uintptr_t)config->servers[server_index].tls_ca_file, ValueString); + pgagroal_json_put(response, config->servers[server_index].name, (uintptr_t)server_j, ValueJSON); + } + else + { + max = strlen(config_value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->common.tls_ca_file, config_value, max); + config->common.tls_ca_file[max] = '\0'; + pgagroal_json_put(response, key, (uintptr_t)config->common.tls_ca_file, ValueString); + } + } + else if (!strcmp(key, "unix_socket_dir")) + { + max = strlen(config_value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->unix_socket_dir, config_value, max); + config->unix_socket_dir[max] = '\0'; + pgagroal_json_put(response, key, (uintptr_t)config->unix_socket_dir, ValueString); + } + else if (!strcmp(key, "metrics")) + { + if (as_int(config_value, &config->common.metrics)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->common.metrics, ValueInt64); + } + else if (!strcmp(key, "metrics_cache_max_age")) + { + if (as_seconds(config_value, &config->common.metrics_cache_max_age, PGAGROAL_PROMETHEUS_CACHE_DISABLED)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->common.metrics_cache_max_age, ValueInt64); + } + else if (!strcmp(key, "metrics_cache_max_size")) + { + if (as_bytes(config_value, &config->common.metrics_cache_max_size, PROMETHEUS_DEFAULT_CACHE_SIZE)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->common.metrics_cache_max_size, ValueInt64); + } + else if (!strcmp(key, "management")) + { + if (as_int(config_value, &config->management)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->management, ValueInt64); + } + else if (!strcmp(key, "log_type")) + { + config->common.log_type = as_logging_type(config_value); + pgagroal_json_put(response, key, (uintptr_t)config->common.log_type, ValueInt64); + } + else if (!strcmp(key, "log_level")) + { + config->common.log_level = as_logging_level(config_value); + pgagroal_json_put(response, key, (uintptr_t)config->common.log_level, ValueInt64); + } + else if (!strcmp(key, "log_path")) + { + max = strlen(config_value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->common.log_path, config_value, max); + config->common.log_path[max] = '\0'; + pgagroal_json_put(response, key, (uintptr_t)config->common.log_path, ValueString); + } + else if (!strcmp(key, "log_rotation_age")) + { + if (as_logging_rotation_age(config_value, &config->common.log_rotation_age)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->common.log_rotation_age, ValueInt64); + } + else if (!strcmp(key, "log_rotation_size")) + { + if (as_logging_rotation_size(config_value, &config->common.log_rotation_size)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->common.log_rotation_size, ValueInt64); + } + else if (!strcmp(key, "log_line_prefix")) + { + max = strlen(config_value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->common.log_path, config_value, max); + config->common.log_path[max] = '\0'; + pgagroal_json_put(response, key, (uintptr_t)config->common.log_path, ValueString); + } + else if (!strcmp(key, "log_mode")) + { + config->common.log_mode = as_logging_mode(config_value); + pgagroal_json_put(response, key, (uintptr_t)config->common.log_mode, ValueInt64); + } + else if (!strcmp(key, "log_connetions")) + { + if (as_bool(config_value, &config->common.log_connections)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->common.log_connections, ValueBool); + } + else if (!strcmp(key, "log_disconnetions")) + { + if (as_bool(config_value, &config->common.log_disconnections)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->common.log_disconnections, ValueBool); + } + else if (!strcmp(key, "blocking_timeout")) + { + if (as_int(config_value, &config->blocking_timeout)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->blocking_timeout, ValueInt64); + } + else if (!strcmp(key, "idle_timeout")) + { + if (as_int(config_value, &config->idle_timeout)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->idle_timeout, ValueInt64); + } + else if (!strcmp(key, "rotate_frontend_password_length")) + { + if (as_int(config_value, &config->rotate_frontend_password_length)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->rotate_frontend_password_length, ValueInt64); + } + else if (!strcmp(key, "rotate_frontend_password_timeout")) + { + if (as_int(config_value, &config->rotate_frontend_password_timeout)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->rotate_frontend_password_timeout, ValueInt64); + } + else if (!strcmp(key, "max_connection_age")) + { + if (as_int(config_value, &config->max_connection_age)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->max_connection_age, ValueInt64); + } + else if (!strcmp(key, "validation")) + { + config->validation = as_validation(config_value); + pgagroal_json_put(response, key, (uintptr_t)config->validation, ValueInt64); + } + else if (!strcmp(key, "background_interval")) + { + if (as_int(config_value, &config->background_interval)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->background_interval, ValueInt64); + } + else if (!strcmp(key, "max_retries")) + { + if (as_int(config_value, &config->max_retries)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->max_retries, ValueInt64); + } + else if (!strcmp(key, "max_connections")) + { + if (as_int(config_value, &config->max_connections)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->max_connections, ValueInt64); + } + else if (!strcmp(key, "allow_unknown_users")) + { + if (as_bool(config_value, &config->allow_unknown_users)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->allow_unknown_users, ValueBool); + } + else if (!strcmp(key, "authentication_timeout")) + { + if (as_int(config_value, &config->common.authentication_timeout)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->common.authentication_timeout, ValueInt64); + } + else if (!strcmp(key, "pipeline")) + { + if (as_int(config_value, &config->pipeline)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->pipeline, ValueInt64); + } + else if (!strcmp(key, "auth_query")) + { + if (as_bool(config_value, &config->authquery)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->authquery, ValueBool); + } + else if (!strcmp(key, "failover")) + { + if (as_bool(config_value, &config->failover)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->failover, ValueBool); + } + else if (!strcmp(key, "keep_alive")) + { + if (as_bool(config_value, &config->keep_alive)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->keep_alive, ValueBool); + } + else if (!strcmp(key, "nodelay")) + { + if (as_bool(config_value, &config->nodelay)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->nodelay, ValueBool); + } + else if (!strcmp(key, "non_blocking")) + { + if (as_bool(config_value, &config->non_blocking)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->non_blocking, ValueBool); + } + else if (!strcmp(key, "backlog")) + { + if (as_int(config_value, &config->backlog)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->backlog, ValueInt64); + } + else if (!strcmp(key, "tracker")) + { + if (as_bool(config_value, &config->tracker)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->tracker, ValueBool); + } + else if (!strcmp(key, "track_prepared_statements")) + { + if (as_bool(config_value, &config->track_prepared_statements)) + { + unknown = true; + } + pgagroal_json_put(response, key, (uintptr_t)config->track_prepared_statements, ValueBool); + } + else if (!strcmp(key, "hugepage")) + { + config->common.hugepage = as_hugepage(config_value); + pgagroal_json_put(response, key, (uintptr_t)config->common.hugepage, ValueInt64); + } + else if (!strcmp(key, "pidfile")) + { + max = strlen(config_value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->pidfile, config_value, max); + config->pidfile[max] = '\0'; + pgagroal_json_put(response, key, (uintptr_t)config->pidfile, ValueString); + } + else if (!strcmp(key, "failover_script")) + { + max = strlen(config_value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->failover_script, config_value, max); + config->failover_script[max] = '\0'; + pgagroal_json_put(response, key, (uintptr_t)config->failover_script, ValueString); + } + else if (!strcmp(key, "libev")) + { + max = strlen(config_value); + if (max > MISC_LENGTH - 1) + { + max = MISC_LENGTH - 1; + } + memcpy(config->libev, config_value, max); + config->libev[max] = '\0'; + pgagroal_json_put(response, key, (uintptr_t)config->libev, ValueString); + } + else if (!strcmp(key, "update_process_title")) + { + if (as_update_process_title(config_value, &config->update_process_title, UPDATE_PROCESS_TITLE_VERBOSE)) + { + unknown = false; + } + pgagroal_json_put(response, key, (uintptr_t)config->update_process_title, ValueInt64); + } + else + { + unknown = true; + } + + if (unknown) + { + pgagroal_management_response_error(NULL, client_fd, NULL, MANAGEMENT_ERROR_CONF_SET_UNKNOWN_CONFIGURATION_KEY, compression, encryption, payload); + pgagroal_log_error("Conf Set: Unknown configuration key found (%d)", MANAGEMENT_ERROR_CONF_SET_UNKNOWN_CONFIGURATION_KEY); + goto error; + } + } + + end_time = time(NULL); + + if (pgagroal_management_response_ok(NULL, client_fd, start_time, end_time, compression, encryption, payload)) + { + pgagroal_management_response_error(NULL, client_fd, NULL, MANAGEMENT_ERROR_CONF_SET_NETWORK, compression, encryption, payload); + pgagroal_log_error("Conf Set: Error sending response"); + goto error; + } + + elapsed = pgagroal_get_timestamp_string(start_time, end_time, &total_seconds); + + pgagroal_log_info("Conf Set (Elapsed: %s)", elapsed); + + pgagroal_json_destroy(payload); + + pgagroal_disconnect(client_fd); + + pgagroal_stop_logging(); + + exit(0); +error: + + pgagroal_json_destroy(payload); + + pgagroal_disconnect(client_fd); + + pgagroal_stop_logging(); + + exit(1); + +} \ No newline at end of file diff --git a/src/libpgagroal/connection.c b/src/libpgagroal/connection.c new file mode 100644 index 00000000..6153ea37 --- /dev/null +++ b/src/libpgagroal/connection.c @@ -0,0 +1,911 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static int read_complete(SSL* ssl, int socket, void* buf, size_t size); +static int write_complete(SSL* ssl, int socket, void* buf, size_t size); +static int write_socket(int socket, void* buf, size_t size); +static int write_ssl(SSL* ssl, void* buf, size_t size); + +int +pgagroal_connection_get(int* client_fd) +{ + int fd; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + *client_fd = -1; + + if (pgagroal_connect_unix_socket(config->unix_socket_dir, TRANSFER_UDS, &fd)) + { + pgagroal_log_warn("pgagroal_management_transfer_connection: connect: %d", fd); + errno = 0; + goto error; + } + + *client_fd = fd; + + return 0; + +error: + + return 1; +} + +int +pgagroal_connection_get_pid(pid_t pid, int* client_fd) +{ + char* f = NULL; + int fd; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + *client_fd = -1; + + f = pgagroal_append(f, ".s.pgagroal."); + f = pgagroal_append_int(f, (int)pid); + + if (pgagroal_connect_unix_socket(config->unix_socket_dir, f, &fd)) + { + pgagroal_log_warn("pgagroal_management_transfer_connection: connect: %d", fd); + errno = 0; + goto error; + } + + *client_fd = fd; + + free(f); + + return 0; + +error: + + free(f); + + return 1; +} + +int +pgagroal_connection_id_write(int client_fd, int id) +{ + char buf4[4]; + + memset(&buf4[0], 0, sizeof(buf4)); + pgagroal_write_int32(&buf4, id); + + if (write_complete(NULL, client_fd, &buf4, sizeof(buf4))) + { + pgagroal_log_warn("pgagroal_connection_id_write: %d %s", client_fd, strerror(errno)); + errno = 0; + goto error; + } + + return 0; + +error: + + return 1; +} + +int +pgagroal_connection_id_read(int client_fd, int* id) +{ + char buf4[4]; + + *id = -1; + + memset(&buf4[0], 0, sizeof(buf4)); + if (read_complete(NULL, client_fd, &buf4, sizeof(buf4))) + { + pgagroal_log_warn("pgagroal_connection_id_read: %d %s", client_fd, strerror(errno)); + errno = 0; + goto error; + } + + *id = pgagroal_read_int32(&buf4); + + return 0; + +error: + + return 1; +} + +int +pgagroal_connection_transfer_write(int client_fd, int32_t slot) +{ + struct cmsghdr* cmptr = NULL; + struct iovec iov[1]; + struct msghdr msg; + char buf2[2]; + char buf4[4]; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + memset(&buf4[0], 0, sizeof(buf4)); + pgagroal_write_int32(&buf4, slot); + + if (write_complete(NULL, client_fd, &buf4, sizeof(buf4))) + { + pgagroal_log_warn("pgagroal_management_transfer_connection: write: %d %s", client_fd, strerror(errno)); + errno = 0; + goto error; + } + + /* Write file descriptor */ + memset(&buf2[0], 0, sizeof(buf2)); + + iov[0].iov_base = &buf2[0]; + iov[0].iov_len = sizeof(buf2); + + cmptr = malloc(CMSG_SPACE(sizeof(int))); + memset(cmptr, 0, CMSG_SPACE(sizeof(int))); + cmptr->cmsg_level = SOL_SOCKET; + cmptr->cmsg_type = SCM_RIGHTS; + cmptr->cmsg_len = CMSG_LEN(sizeof(int)); + + msg.msg_name = NULL; + msg.msg_namelen = 0; + msg.msg_iov = iov; + msg.msg_iovlen = 1; + msg.msg_control = cmptr; + msg.msg_controllen = CMSG_SPACE(sizeof(int)); + msg.msg_flags = 0; + *(int*)CMSG_DATA(cmptr) = config->connections[slot].fd; + + if (sendmsg(client_fd, &msg, 0) != 2) + { + goto error; + } + + free(cmptr); + + return 0; + +error: + if (cmptr) + { + free(cmptr); + } + + return 1; +} + +int +pgagroal_connection_transfer_read(int client_fd, int* slot, int* fd) +{ + int nr; + char buf2[2]; + char buf4[4]; + struct cmsghdr* cmptr = NULL; + struct iovec iov[1]; + struct msghdr msg; + + *slot = -1; + *fd = -1; + + memset(&buf4[0], 0, sizeof(buf4)); + if (read_complete(NULL, client_fd, &buf4, sizeof(buf4))) + { + pgagroal_log_warn("pgagroal_connection_transfer_read: %d %s", client_fd, strerror(errno)); + errno = 0; + goto error; + } + + *slot = pgagroal_read_int32(&buf4); + + memset(&buf2[0], 0, sizeof(buf2)); + + iov[0].iov_base = &buf2[0]; + iov[0].iov_len = sizeof(buf2); + + cmptr = (struct cmsghdr*)calloc(1, CMSG_SPACE(sizeof(int))); + if (cmptr == NULL) + { + goto error; + } + cmptr->cmsg_len = CMSG_LEN(sizeof(int)); + cmptr->cmsg_level = SOL_SOCKET; + cmptr->cmsg_type = SCM_RIGHTS; + + msg.msg_name = NULL; + msg.msg_namelen = 0; + msg.msg_iov = iov; + msg.msg_iovlen = 1; + msg.msg_control = cmptr; + msg.msg_controllen = CMSG_SPACE(sizeof(int)); + msg.msg_flags = 0; + + if ((nr = recvmsg(client_fd, &msg, 0)) < 0) + { + goto error; + } + else if (nr == 0) + { + goto error; + } + + *fd = *(int*)CMSG_DATA(cmptr); + + free(cmptr); + + return 0; + +error: + + if (cmptr != NULL) + { + free(cmptr); + } + + return 1; +} + +int +pgagroal_connection_slot_write(int client_fd, int32_t slot) +{ + char buf4[4]; + + memset(&buf4[0], 0, sizeof(buf4)); + pgagroal_write_int32(&buf4, slot); + + if (write_complete(NULL, client_fd, &buf4, sizeof(buf4))) + { + pgagroal_log_warn("pgagroal_connection_slot_write: %d %s", client_fd, strerror(errno)); + errno = 0; + goto error; + } + + return 0; + +error: + + return 1; +} + +int +pgagroal_connection_slot_read(int client_fd, int32_t* slot) +{ + char buf4[4]; + + *slot = -1; + + memset(&buf4[0], 0, sizeof(buf4)); + if (read_complete(NULL, client_fd, &buf4, sizeof(buf4))) + { + pgagroal_log_warn("pgagroal_connection_id_read: %d %s", client_fd, strerror(errno)); + errno = 0; + goto error; + } + + *slot = (int32_t)pgagroal_read_int32(&buf4); + + return 0; + +error: + + return 1; +} + +int +pgagroal_connection_socket_write(int client_fd, int socket) +{ + char buf4[4]; + + memset(&buf4[0], 0, sizeof(buf4)); + pgagroal_write_int32(&buf4, socket); + + if (write_complete(NULL, client_fd, &buf4, sizeof(buf4))) + { + pgagroal_log_warn("pgagroal_connection_socket_write: %d %s", client_fd, strerror(errno)); + errno = 0; + goto error; + } + + return 0; + +error: + + return 1; +} + +int +pgagroal_connection_socket_read(int client_fd, int* socket) +{ + char buf4[4]; + + *socket = -1; + + memset(&buf4[0], 0, sizeof(buf4)); + if (read_complete(NULL, client_fd, &buf4, sizeof(buf4))) + { + pgagroal_log_warn("pgagroal_connection_id_read: %d %s", client_fd, strerror(errno)); + errno = 0; + goto error; + } + + *socket = pgagroal_read_int32(&buf4); + + return 0; + +error: + + return 1; +} + +int +pgagroal_connection_pid_write(int client_fd, pid_t pid) +{ + char buf4[4]; + + memset(&buf4[0], 0, sizeof(buf4)); + pgagroal_write_int32(&buf4, (int)pid); + + if (write_complete(NULL, client_fd, &buf4, sizeof(buf4))) + { + pgagroal_log_warn("pgagroal_connection_pid_write: %d %s", client_fd, strerror(errno)); + errno = 0; + goto error; + } + + return 0; + +error: + + return 1; +} + +int +pgagroal_connection_pid_read(int client_fd, pid_t* pid) +{ + char buf4[4]; + + *pid = -1; + + memset(&buf4[0], 0, sizeof(buf4)); + if (read_complete(NULL, client_fd, &buf4, sizeof(buf4))) + { + pgagroal_log_warn("pgagroal_connection_id_read: %d %s", client_fd, strerror(errno)); + errno = 0; + goto error; + } + + *pid = (pid_t)pgagroal_read_int32(&buf4); + + return 0; + +error: + + return 1; +} + +static int +read_complete(SSL* ssl, int socket, void* buf, size_t size) +{ + ssize_t r; + size_t offset; + size_t needs; + int retries; + + offset = 0; + needs = size; + retries = 0; + +read: + + if (ssl == NULL) + { + r = read(socket, buf + offset, needs); + } + else + { + r = SSL_read(ssl, buf + offset, needs); + } + + if (r == -1) + { + if (errno == EAGAIN || errno == EWOULDBLOCK) + { + errno = 0; + goto read; + } + + goto error; + } + else if (r < (ssize_t)needs) + { + /* Sleep for 10ms */ + SLEEP(10000000L); + + if (retries < 100) + { + offset += r; + needs -= r; + retries++; + goto read; + } + else + { + errno = EINVAL; + goto error; + } + } + + return 0; + +error: + + return 1; +} + +static int +write_complete(SSL* ssl, int socket, void* buf, size_t size) +{ + if (ssl == NULL) + { + return write_socket(socket, buf, size); + } + + return write_ssl(ssl, buf, size); +} + +static int +write_socket(int socket, void* buf, size_t size) +{ + bool keep_write = false; + ssize_t numbytes; + int offset; + ssize_t totalbytes; + ssize_t remaining; + + numbytes = 0; + offset = 0; + totalbytes = 0; + remaining = size; + + do + { + numbytes = write(socket, buf + offset, remaining); + + if (likely(numbytes == size)) + { + return 0; + } + else if (numbytes != -1) + { + offset += numbytes; + totalbytes += numbytes; + remaining -= numbytes; + + if (totalbytes == size) + { + return 0; + } + + pgagroal_log_trace("Write %d - %zd/%zd vs %zd", socket, numbytes, totalbytes, size); + keep_write = true; + errno = 0; + } + else + { + switch (errno) + { + case EAGAIN: + keep_write = true; + errno = 0; + break; + default: + keep_write = false; + break; + } + } + } + while (keep_write); + + return 1; +} + +static int +write_ssl(SSL* ssl, void* buf, size_t size) +{ + bool keep_write = false; + ssize_t numbytes; + int offset; + ssize_t totalbytes; + ssize_t remaining; + + numbytes = 0; + offset = 0; + totalbytes = 0; + remaining = size; + + do + { + numbytes = SSL_write(ssl, buf + offset, remaining); + + if (likely(numbytes == size)) + { + return 0; + } + else if (numbytes > 0) + { + offset += numbytes; + totalbytes += numbytes; + remaining -= numbytes; + + if (totalbytes == size) + { + return 0; + } + + pgagroal_log_trace("SSL/Write %d - %zd/%zd vs %zd", SSL_get_fd(ssl), numbytes, totalbytes, size); + keep_write = true; + errno = 0; + } + else + { + int err = SSL_get_error(ssl, numbytes); + + switch (err) + { + case SSL_ERROR_ZERO_RETURN: + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_CONNECT: + case SSL_ERROR_WANT_ACCEPT: + case SSL_ERROR_WANT_X509_LOOKUP: +#ifndef HAVE_OPENBSD + case SSL_ERROR_WANT_ASYNC: + case SSL_ERROR_WANT_ASYNC_JOB: + case SSL_ERROR_WANT_CLIENT_HELLO_CB: +#endif + errno = 0; + keep_write = true; + break; + case SSL_ERROR_SYSCALL: + pgagroal_log_error("SSL_ERROR_SYSCALL: %s (%d)", strerror(errno), SSL_get_fd(ssl)); + errno = 0; + keep_write = false; + break; + case SSL_ERROR_SSL: + pgagroal_log_error("SSL_ERROR_SSL: %s (%d)", strerror(errno), SSL_get_fd(ssl)); + errno = 0; + keep_write = false; + break; + } + ERR_clear_error(); + + if (!keep_write) + { + return 1; + } + } + } + while (keep_write); + + return 1; +} + +/* int */ +/* pgagroal_management_client_done(pid_t pid) */ +/* { */ +/* char buf[4]; */ +/* int fd; */ +/* struct main_configuration* config; */ + +/* config = (struct main_configuration*)shmem; */ + +/* if (pgagroal_connect_unix_socket(config->unix_socket_dir, TRANSFER_UDS, &fd)) */ +/* { */ +/* pgagroal_log_warn("pgagroal_management_client_done: connect: %d", fd); */ +/* errno = 0; */ +/* goto error; */ +/* } */ + +/* if (write_header(NULL, fd, MANAGEMENT_CLIENT_DONE, -1)) */ +/* { */ +/* pgagroal_log_warn("pgagroal_management_client_done: write: %d", fd); */ +/* errno = 0; */ +/* goto error; */ +/* } */ + +/* memset(&buf, 0, sizeof(buf)); */ +/* pgagroal_write_int32(buf, pid); */ + +/* if (write_complete(NULL, fd, &buf, sizeof(buf))) */ +/* { */ +/* pgagroal_log_warn("pgagroal_management_client_done: write: %d %s", fd, strerror(errno)); */ +/* errno = 0; */ +/* goto error; */ +/* } */ + +/* pgagroal_disconnect(fd); */ + +/* return 0; */ + +/* error: */ +/* pgagroal_disconnect(fd); */ + +/* return 1; */ +/* } */ + +/* int */ +/* pgagroal_management_client_fd(int32_t slot, pid_t pid) */ +/* { */ +/* char p[MISC_LENGTH]; */ +/* int fd; */ +/* struct main_configuration* config; */ +/* struct cmsghdr* cmptr = NULL; */ +/* struct iovec iov[1]; */ +/* struct msghdr msg; */ +/* char buf[2]; /\* send_fd()/recv_fd() 2-byte protocol *\/ */ + +/* config = (struct main_configuration*)shmem; */ + +/* memset(&p, 0, sizeof(p)); */ +/* snprintf(&p[0], sizeof(p), ".s.%d", pid); */ + +/* if (pgagroal_connect_unix_socket(config->unix_socket_dir, &p[0], &fd)) */ +/* { */ +/* pgagroal_log_debug("pgagroal_management_client_fd: connect: %d", fd); */ +/* errno = 0; */ +/* goto unavailable; */ +/* } */ + +/* if (write_header(NULL, fd, MANAGEMENT_CLIENT_FD, slot)) */ +/* { */ +/* pgagroal_log_warn("pgagroal_management_client_fd: write: %d", fd); */ +/* errno = 0; */ +/* goto error; */ +/* } */ + +/* /\* Write file descriptor *\/ */ +/* iov[0].iov_base = buf; */ +/* iov[0].iov_len = 2; */ +/* msg.msg_iov = iov; */ +/* msg.msg_iovlen = 1; */ +/* msg.msg_name = NULL; */ +/* msg.msg_namelen = 0; */ + +/* cmptr = malloc(CMSG_LEN(sizeof(int))); */ +/* cmptr->cmsg_level = SOL_SOCKET; */ +/* cmptr->cmsg_type = SCM_RIGHTS; */ +/* cmptr->cmsg_len = CMSG_LEN(sizeof(int)); */ +/* msg.msg_control = cmptr; */ +/* msg.msg_controllen = CMSG_LEN(sizeof(int)); */ +/* *(int*)CMSG_DATA(cmptr) = config->connections[slot].fd; */ +/* buf[1] = 0; /\* zero status means OK *\/ */ +/* buf[0] = 0; /\* null byte flag to recv_fd() *\/ */ + +/* if (sendmsg(fd, &msg, 0) != 2) */ +/* { */ +/* goto error; */ +/* } */ + +/* free(cmptr); */ +/* pgagroal_disconnect(fd); */ + +/* return 0; */ + +/* unavailable: */ +/* free(cmptr); */ +/* pgagroal_disconnect(fd); */ + +/* return 1; */ + +/* error: */ +/* free(cmptr); */ +/* pgagroal_disconnect(fd); */ +/* pgagroal_kill_connection(slot, NULL); */ + +/* return 1; */ +/* } */ + +/* int */ +/* pgagroal_management_remove_fd(int32_t slot, int socket, pid_t pid) */ +/* { */ +/* char p[MISC_LENGTH]; */ +/* int fd; */ +/* char buf[4]; */ +/* struct main_configuration* config; */ + +/* config = (struct main_configuration*)shmem; */ + +/* if (atomic_load(&config->states[slot]) == STATE_NOTINIT) */ +/* { */ +/* return 0; */ +/* } */ + +/* memset(&p, 0, sizeof(p)); */ +/* snprintf(&p[0], sizeof(p), ".s.%d", pid); */ + +/* if (pgagroal_connect_unix_socket(config->unix_socket_dir, &p[0], &fd)) */ +/* { */ +/* pgagroal_log_debug("pgagroal_management_remove_fd: slot %d state %d database %s user %s socket %d pid %d connect: %d", */ +/* slot, atomic_load(&config->states[slot]), */ +/* config->connections[slot].database, config->connections[slot].username, socket, pid, fd); */ +/* errno = 0; */ +/* goto error; */ +/* } */ + +/* if (write_header(NULL, fd, MANAGEMENT_REMOVE_FD, slot)) */ +/* { */ +/* pgagroal_log_warn("pgagroal_management_remove_fd: write: %d", fd); */ +/* errno = 0; */ +/* goto error; */ +/* } */ + +/* pgagroal_write_int32(&buf, socket); */ +/* if (write_complete(NULL, fd, &buf, sizeof(buf))) */ +/* { */ +/* pgagroal_log_warn("pgagroal_management_remove_fd: write: %d %s", fd, strerror(errno)); */ +/* errno = 0; */ +/* goto error; */ +/* } */ + +/* pgagroal_disconnect(fd); */ + +/* return 0; */ + +/* error: */ +/* pgagroal_disconnect(fd); */ + +/* return 1; */ +/* } */ + +/* /\* */ +/* * Utility function to convert PGAGROAL_VERSION into a number. */ +/* * The major version is represented by a single digit. */ +/* * For minor and patch, a leading 0 is added if they are single digits. */ +/* *\/ */ +/* static int */ +/* pgagroal_executable_version_number(char* version, size_t version_size) */ +/* { */ +/* int major; */ +/* int minor; */ +/* int patch; */ + +/* long val; */ + +/* char* del = "."; */ +/* char* endptr; */ + +/* if (version == NULL) */ +/* { */ +/* version = PGAGROAL_VERSION; */ +/* version_size = sizeof(version); */ +/* } */ + +/* char buf[version_size]; */ + +/* memcpy(buf, version, sizeof(buf)); */ + +/* char* token = strtok(buf, del); */ +/* val = strtol(token, &endptr, 10); */ +/* if (errno == ERANGE || val <= LONG_MIN || val >= LONG_MAX) */ +/* { */ +/* goto error; */ +/* } */ +/* major = (int)val; */ + +/* token = strtok(NULL, del); */ +/* val = strtol(token, &endptr, 10); */ +/* if (errno == ERANGE || val <= LONG_MIN || val >= LONG_MAX) */ +/* { */ +/* goto error; */ +/* } */ +/* minor = (int)val; */ + +/* token = strtok(NULL, del); */ +/* val = strtol(token, &endptr, 10); */ +/* if (errno == ERANGE || val <= LONG_MIN || val >= LONG_MAX) */ +/* { */ +/* goto error; */ +/* } */ +/* patch = (int)val; */ + +/* int version_number = (major % 10) * 10000 + (minor / 10) * 1000 + (minor % 10) * 100 + patch; */ + +/* if (version_number < INT_MIN || version_number > INT_MAX || version_number < 10700) */ +/* { */ +/* goto error; */ +/* } */ + +/* return version_number; */ + +/* error: */ +/* pgagroal_log_debug("pgagroal_get_executable_number got overflowed or suspicious value: %s %s", version, strerror(errno)); */ +/* errno = 0; */ +/* return 1; */ +/* } */ + +/* /\* */ +/* * Utility function to convert back version_number into a string. */ +/* *\/ */ +/* static int */ +/* pgagroal_executable_version_string(char** version_string, int version_number) */ +/* { */ +/* char* v = NULL; */ +/* int major = version_number / 10000; */ +/* int minor = (version_number / 100) % 100; */ +/* int patch = version_number % 100; */ + +/* v = pgagroal_append_int(v, major); */ +/* v = pgagroal_append(v, "."); */ +/* v = pgagroal_append_int(v, minor); */ +/* v = pgagroal_append(v, "."); */ +/* v = pgagroal_append_int(v, patch); */ + +/* *version_string = v; */ + +/* return 0; */ +/* } */ + +/* /\* */ +/* * Utility function to convert command into a string. */ +/* *\/ */ +/* static char* */ +/* pgagroal_executable_name(int command) */ +/* { */ +/* switch (command) */ +/* { */ +/* case PGAGROAL_EXECUTABLE: */ +/* return "pgagroal"; */ +/* case PGAGROAL_EXECUTABLE_CLI: */ +/* return "pgagroal-cli"; */ +/* case PGAGROAL_EXECUTABLE_VAULT: */ +/* return "pgagroal-vault"; */ +/* default: */ +/* pgagroal_log_debug("pgagroal_get_command_name got unexpected value: %d", command); */ +/* return NULL; */ +/* } */ +/* } */ diff --git a/src/libpgagroal/deque.c b/src/libpgagroal/deque.c new file mode 100644 index 00000000..ebd44793 --- /dev/null +++ b/src/libpgagroal/deque.c @@ -0,0 +1,847 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include + +#include +#include + +// tag is copied if not NULL +static void +deque_offer(struct deque* deque, char* tag, uintptr_t data, enum value_type type, struct value_config* config); + +// tag is copied if not NULL +static void +deque_node_create(uintptr_t data, enum value_type type, char* tag, struct value_config* config, struct deque_node** node); + +// tag will always be freed +static void +deque_node_destroy(struct deque_node* node); + +static void +deque_read_lock(struct deque* deque); + +static void +deque_write_lock(struct deque* deque); + +static void +deque_unlock(struct deque* deque); + +static struct deque_node* +deque_next(struct deque* deque, struct deque_node* node); + +static struct deque_node* +deque_find(struct deque* deque, char* tag); + +static char* +to_json_string(struct deque* deque, char* tag, int indent); + +static char* +to_compact_json_string(struct deque* deque, char* tag, int indent); + +static char* +to_text_string(struct deque* deque, char* tag, int indent); + +static struct deque_node* +deque_remove(struct deque* deque, struct deque_node* node); + +static struct deque_node* +get_middle(struct deque_node* node); + +static struct deque_node* +deque_sort(struct deque_node* node); + +static struct deque_node* +deque_merge(struct deque_node* node1, struct deque_node* node2); + +static int +tag_compare(char* tag1, char* tag2); + +int +pgagroal_deque_create(bool thread_safe, struct deque** deque) +{ + struct deque* q = NULL; + q = malloc(sizeof(struct deque)); + q->size = 0; + q->thread_safe = thread_safe; + if (thread_safe) + { + pthread_rwlock_init(&q->mutex, NULL); + } + deque_node_create(0, ValueInt32, NULL, NULL, &q->start); + deque_node_create(0, ValueInt32, NULL, NULL, &q->end); + q->start->next = q->end; + q->end->prev = q->start; + *deque = q; + return 0; +} + +int +pgagroal_deque_add(struct deque* deque, char* tag, uintptr_t data, enum value_type type) +{ + deque_offer(deque, tag, data, type, NULL); + return 0; +} + +int +pgagroal_deque_remove(struct deque* deque, char* tag) +{ + int cnt = 0; + struct deque_iterator* iter = NULL; + if (deque == NULL || tag == NULL) + { + return 0; + } + pgagroal_deque_iterator_create(deque, &iter); + while (pgagroal_deque_iterator_next(iter)) + { + if (pgagroal_compare_string(iter->tag, tag)) + { + pgagroal_deque_iterator_remove(iter); + cnt++; + } + } + pgagroal_deque_iterator_destroy(iter); + return cnt; +} + +int +pgagroal_deque_add_with_config(struct deque* deque, char* tag, uintptr_t data, struct value_config* config) +{ + deque_offer(deque, tag, data, ValueRef, config); + return 0; +} + +uintptr_t +pgagroal_deque_poll(struct deque* deque, char** tag) +{ + struct deque_node* head = NULL; + struct value* val = NULL; + uintptr_t data = 0; + if (deque == NULL || pgagroal_deque_size(deque) == 0) + { + return 0; + } + deque_write_lock(deque); + head = deque->start->next; + // this should not happen when size is not 0, but just in case + if (head == deque->end) + { + deque_unlock(deque); + return 0; + } + // remove node + deque->start->next = head->next; + head->next->prev = deque->start; + deque->size--; + val = head->data; + if (tag != NULL) + { + *tag = head->tag; + } + free(head); + + data = pgagroal_value_data(val); + free(val); + + deque_unlock(deque); + return data; +} + +uintptr_t +pgagroal_deque_poll_last(struct deque* deque, char** tag) +{ + struct deque_node* tail = NULL; + struct value* val = NULL; + uintptr_t data = 0; + if (deque == NULL || pgagroal_deque_size(deque) == 0) + { + return 0; + } + deque_write_lock(deque); + tail = deque->end->prev; + if (tail == deque->start) + { + deque_unlock(deque); + return 0; + } + // remove node + deque->end->prev = tail->prev; + tail->prev->next = deque->end; + deque->size--; + + val = tail->data; + if (tag != NULL) + { + *tag = tail->tag; + } + free(tail); + + data = pgagroal_value_data(val); + free(val); + + deque_unlock(deque); + return data; +} + +uintptr_t +pgagroal_deque_peek(struct deque* deque, char** tag) +{ + struct deque_node* head = NULL; + struct value* val = NULL; + if (deque == NULL || pgagroal_deque_size(deque) == 0) + { + return 0; + } + deque_read_lock(deque); + head = deque->start->next; + // this should not happen when size is not 0, but just in case + if (head == deque->end) + { + deque_unlock(deque); + return 0; + } + val = head->data; + if (tag != NULL) + { + *tag = head->tag; + } + deque_unlock(deque); + return pgagroal_value_data(val); +} + +uintptr_t +pgagroal_deque_peek_last(struct deque* deque, char** tag) +{ + struct deque_node* tail = NULL; + struct value* val = NULL; + if (deque == NULL || pgagroal_deque_size(deque) == 0) + { + return 0; + } + deque_read_lock(deque); + tail = deque->end->prev; + // this should not happen when size is not 0, but just in case + if (tail == deque->start) + { + deque_unlock(deque); + return 0; + } + val = tail->data; + if (tag != NULL) + { + *tag = tail->tag; + } + deque_unlock(deque); + return pgagroal_value_data(val); +} + +uintptr_t +pgagroal_deque_get(struct deque* deque, char* tag) +{ + struct deque_node* n = NULL; + uintptr_t ret = 0; + deque_read_lock(deque); + n = deque_find(deque, tag); + if (n == NULL) + { + goto error; + } + ret = pgagroal_value_data(n->data); + deque_unlock(deque); + return ret; +error: + deque_unlock(deque); + return 0; +} + +bool +pgagroal_deque_exists(struct deque* deque, char* tag) +{ + bool ret = false; + struct deque_node* n = NULL; + + deque_read_lock(deque); + + n = deque_find(deque, tag); + if (n != NULL) + { + ret = true; + } + + deque_unlock(deque); + + return ret; +} + +bool +pgagroal_deque_empty(struct deque* deque) +{ + return pgagroal_deque_size(deque) == 0; +} + +void +pgagroal_deque_list(struct deque* deque) +{ + char* str = NULL; + if (pgagroal_log_is_enabled(PGAGROAL_LOGGING_LEVEL_DEBUG5)) + { + str = pgagroal_deque_to_string(deque, FORMAT_JSON, NULL, 0); + pgagroal_log_trace("Deque: %s", str); + free(str); + } +} + +void +pgagroal_deque_sort(struct deque* deque) +{ + deque_write_lock(deque); + if (deque == NULL || deque->start == NULL || deque->end == NULL || deque->size <= 1) + { + deque_unlock(deque); + return; + } + // break the connection to start and end node since we are going to move nodes around + struct deque_node* first = deque->start->next; + struct deque_node* last = deque->end->prev; + struct deque_node* node = NULL; + first->prev = NULL; + last->next = NULL; + deque->start->next = NULL; + deque->end->prev = NULL; + node = deque_sort(first); + deque->start->next = node; + node->prev = deque->start; + while (node->next != NULL) + { + node = node->next; + } + deque->end->prev = node; + node->next = deque->end; + deque_unlock(deque); +} + +void +pgagroal_deque_destroy(struct deque* deque) +{ + struct deque_node* n = NULL; + struct deque_node* next = NULL; + if (deque == NULL) + { + return; + } + n = deque->start; + while (n != NULL) + { + next = n->next; + deque_node_destroy(n); + n = next; + } + if (deque->thread_safe) + { + pthread_rwlock_destroy(&deque->mutex); + } + free(deque); +} + +char* +pgagroal_deque_to_string(struct deque* deque, int32_t format, char* tag, int indent) +{ + if (format == FORMAT_JSON) + { + return to_json_string(deque, tag, indent); + } + else if (format == FORMAT_TEXT) + { + return to_text_string(deque, tag, indent); + } + else if (format == FORMAT_JSON_COMPACT) + { + return to_compact_json_string(deque, tag, indent); + } + return NULL; +} + +uint32_t +pgagroal_deque_size(struct deque* deque) +{ + uint32_t size = 0; + if (deque == NULL) + { + return 0; + } + deque_read_lock(deque); + size = deque->size; + deque_unlock(deque); + return size; +} + +int +pgagroal_deque_iterator_create(struct deque* deque, struct deque_iterator** iter) +{ + struct deque_iterator* i = NULL; + if (deque == NULL) + { + return 1; + } + i = malloc(sizeof(struct deque_iterator)); + i->deque = deque; + i->cur = deque->start; + i->tag = NULL; + i->value = NULL; + *iter = i; + return 0; +} + +void +pgagroal_deque_iterator_remove(struct deque_iterator* iter) +{ + if (iter == NULL || iter->cur == NULL || iter->deque == NULL || + iter->cur == iter->deque->start || iter->cur == iter->deque->end) + { + return; + } + iter->cur = deque_remove(iter->deque, iter->cur); + if (iter->cur == iter->deque->start) + { + iter->value = NULL; + iter->tag = NULL; + return; + } + iter->value = iter->cur->data; + iter->tag = iter->cur->tag; + return; +} + +void +pgagroal_deque_iterator_destroy(struct deque_iterator* iter) +{ + if (iter == NULL) + { + return; + } + free(iter); +} + +bool +pgagroal_deque_iterator_next(struct deque_iterator* iter) +{ + if (iter == NULL) + { + return false; + } + iter->cur = deque_next(iter->deque, iter->cur); + if (iter->cur == NULL) + { + return false; + } + iter->value = iter->cur->data; + iter->tag = iter->cur->tag; + return true; +} + +static void +deque_offer(struct deque* deque, char* tag, uintptr_t data, enum value_type type, struct value_config* config) +{ + struct deque_node* n = NULL; + struct deque_node* last = NULL; + deque_node_create(data, type, tag, config, &n); + deque_write_lock(deque); + deque->size++; + last = deque->end->prev; + last->next = n; + n->prev = last; + n->next = deque->end; + deque->end->prev = n; + deque_unlock(deque); +} + +static void +deque_node_create(uintptr_t data, enum value_type type, char* tag, struct value_config* config, struct deque_node** node) +{ + struct deque_node* n = NULL; + n = malloc(sizeof(struct deque_node)); + memset(n, 0, sizeof(struct deque_node)); + if (config != NULL) + { + pgagroal_value_create_with_config(data, config, &n->data); + } + else + { + pgagroal_value_create(type, data, &n->data); + } + if (tag != NULL) + { + n->tag = pgagroal_append(NULL, tag); + } + else + { + n->tag = NULL; + } + *node = n; +} + +static void +deque_node_destroy(struct deque_node* node) +{ + if (node == NULL) + { + return; + } + pgagroal_value_destroy(node->data); + free(node->tag); + free(node); +} + +static void +deque_read_lock(struct deque* deque) +{ + if (deque == NULL || !deque->thread_safe) + { + return; + } + pthread_rwlock_rdlock(&deque->mutex); +} + +static void +deque_write_lock(struct deque* deque) +{ + if (deque == NULL || !deque->thread_safe) + { + return; + } + pthread_rwlock_wrlock(&deque->mutex); +} + +static void +deque_unlock(struct deque* deque) +{ + if (deque == NULL || !deque->thread_safe) + { + return; + } + pthread_rwlock_unlock(&deque->mutex); +} + +static struct deque_node* +deque_next(struct deque* deque, struct deque_node* node) +{ + struct deque_node* next = NULL; + if (deque == NULL || deque->size == 0 || node == NULL) + { + return NULL; + } + if (node->next == deque->end) + { + return NULL; + } + next = node->next; + return next; +} + +static struct deque_node* +deque_find(struct deque* deque, char* tag) +{ + struct deque_node* n = NULL; + if (tag == NULL || strlen(tag) == 0 || deque == NULL || deque->size == 0) + { + return NULL; + } + n = deque_next(deque, deque->start); + + while (n != NULL) + { + if (pgagroal_compare_string(tag, n->tag)) + { + return n; + } + + n = deque_next(deque, n); + } + return NULL; +} + +static char* +to_json_string(struct deque* deque, char* tag, int indent) +{ + char* ret = NULL; + ret = pgagroal_indent(ret, tag, indent); + struct deque_node* cur = NULL; + if (deque == NULL || pgagroal_deque_empty(deque)) + { + ret = pgagroal_append(ret, "[]"); + return ret; + } + deque_read_lock(deque); + ret = pgagroal_append(ret, "[\n"); + cur = deque_next(deque, deque->start); + while (cur != NULL) + { + bool has_next = cur->next != deque->end; + char* str = NULL; + char* t = NULL; + if (cur->tag != NULL) + { + t = pgagroal_append(t, cur->tag); + t = pgagroal_append(t, ": "); + } + str = pgagroal_value_to_string(cur->data, FORMAT_JSON, t, indent + INDENT_PER_LEVEL); + free(t); + ret = pgagroal_append(ret, str); + ret = pgagroal_append(ret, has_next ? ",\n" : "\n"); + free(str); + cur = deque_next(deque, cur); + } + ret = pgagroal_indent(ret, NULL, indent); + ret = pgagroal_append(ret, "]"); + deque_unlock(deque); + return ret; +} + +static char* +to_compact_json_string(struct deque* deque, char* tag, int indent) +{ + char* ret = NULL; + ret = pgagroal_indent(ret, tag, indent); + struct deque_node* cur = NULL; + if (deque == NULL || pgagroal_deque_empty(deque)) + { + ret = pgagroal_append(ret, "[]"); + return ret; + } + deque_read_lock(deque); + ret = pgagroal_append(ret, "["); + cur = deque_next(deque, deque->start); + while (cur != NULL) + { + bool has_next = cur->next != deque->end; + char* str = NULL; + char* t = NULL; + if (cur->tag != NULL) + { + t = pgagroal_append(t, cur->tag); + t = pgagroal_append(t, ":"); + } + str = pgagroal_value_to_string(cur->data, FORMAT_JSON_COMPACT, t, indent); + free(t); + ret = pgagroal_append(ret, str); + ret = pgagroal_append(ret, has_next ? "," : ""); + free(str); + cur = deque_next(deque, cur); + } + ret = pgagroal_append(ret, "]"); + deque_unlock(deque); + return ret; +} + +static char* +to_text_string(struct deque* deque, char* tag, int indent) +{ + char* ret = NULL; + int cnt = 0; + int next_indent = pgagroal_compare_string(tag, BULLET_POINT) ? 0 : indent; + // we have a tag and it's not the bullet point, so that means another line + if (tag != NULL && !pgagroal_compare_string(tag, BULLET_POINT)) + { + ret = pgagroal_indent(ret, tag, indent); + next_indent += INDENT_PER_LEVEL; + } + struct deque_node* cur = NULL; + if (deque == NULL || pgagroal_deque_empty(deque)) + { + ret = pgagroal_append(ret, "[]"); + return ret; + } + deque_read_lock(deque); + cur = deque_next(deque, deque->start); + while (cur != NULL) + { + bool has_next = cur->next != deque->end; + char* str = NULL; + str = pgagroal_value_to_string(cur->data, FORMAT_TEXT, BULLET_POINT, next_indent); + if (cnt == 0) + { + cnt++; + if (pgagroal_compare_string(tag, BULLET_POINT)) + { + next_indent = indent + INDENT_PER_LEVEL; + } + } + if (cur->data->type == ValueJSON) + { + ret = pgagroal_indent(ret, BULLET_POINT, next_indent); + } + ret = pgagroal_append(ret, str); + ret = pgagroal_append(ret, has_next ? "\n" : ""); + free(str); + cur = deque_next(deque, cur); + } + deque_unlock(deque); + return ret; +} + +static struct deque_node* +deque_remove(struct deque* deque, struct deque_node* node) +{ + if (deque == NULL || node == NULL || node == deque->start || node == deque->end) + { + return NULL; + } + struct deque_node* prev = node->prev; + struct deque_node* next = node->next; + prev->next = next; + next->prev = prev; + deque_node_destroy(node); + deque->size--; + return prev; +} + +static struct deque_node* +get_middle(struct deque_node* node) +{ + struct deque_node* slow = node; + struct deque_node* fast = node; + while (fast != NULL && fast->next != NULL) + { + slow = slow->next; + fast = fast->next->next; + } + return slow; +} + +static struct deque_node* +deque_sort(struct deque_node* node) +{ + struct deque_node* mid = NULL; + struct deque_node* prevmid = NULL; + struct deque_node* node1 = NULL; + struct deque_node* node2 = NULL; + if (node == NULL || node->next == NULL) + { + return node; + } + mid = get_middle(node); + prevmid = mid->prev; + mid->prev = NULL; + prevmid->next = NULL; + node1 = deque_sort(node); + node2 = deque_sort(mid); + return deque_merge(node1, node2); +} + +static struct deque_node* +deque_merge(struct deque_node* node1, struct deque_node* node2) +{ + struct deque_node* node = NULL; + struct deque_node* left = node1; + struct deque_node* right = node2; + struct deque_node* next = NULL; + struct deque_node* start = NULL; + if (node1 == NULL) + { + return node2; + } + if (node2 == NULL) + { + return node1; + } + while (left != NULL && right != NULL) + { + if (tag_compare(left->tag, right->tag) <= 0) + { + next = left->next; + if (node == NULL) + { + start = left; + node = left; + node->prev = NULL; + node->next = NULL; + } + else + { + node->next = left; + left->prev = node; + left->next = NULL; + node = node->next; + } + left = next; + } + else + { + next = right->next; + if (node == NULL) + { + start = right; + node = right; + node->prev = NULL; + node->next = NULL; + } + else + { + node->next = right; + right->prev = node; + right->next = NULL; + node = node->next; + } + right = next; + } + } + while (left != NULL) + { + next = left->next; + node->next = left; + left->prev = node; + left = next; + node = node->next; + } + while (right != NULL) + { + next = right->next; + node->next = right; + right->prev = node; + right = next; + node = node->next; + } + return start; +} +static int +tag_compare(char* tag1, char* tag2) +{ + if (tag1 == NULL) + { + return tag2 == NULL ? 0 : 1; + } + if (tag2 == NULL) + { + return tag1 == NULL ? 0 : -1; + } + return strcmp(tag1, tag2); +} diff --git a/src/libpgagroal/gzip_compression.c b/src/libpgagroal/gzip_compression.c new file mode 100644 index 00000000..a43b1fea --- /dev/null +++ b/src/libpgagroal/gzip_compression.c @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include +#include + +#define BUFFER_LENGTH 8192 + +int +pgagroal_gzip_string(char* s, unsigned char** buffer, size_t* buffer_size) +{ + int ret; + z_stream stream; + size_t source_len; + size_t chunk_size; + unsigned char* temp_buffer; + unsigned char* final_buffer; + + source_len = strlen(s); + chunk_size = BUFFER_LENGTH; + + temp_buffer = (unsigned char*)malloc(chunk_size); + if (temp_buffer == NULL) + { + pgagroal_log_error("Gzip: Allocation error"); + return 1; + } + + memset(&stream, 0, sizeof(stream)); + stream.next_in = (unsigned char*)s; + stream.avail_in = source_len; + + ret = deflateInit2(&stream, Z_BEST_COMPRESSION, Z_DEFLATED, MAX_WBITS + 16, 8, Z_DEFAULT_STRATEGY); + if (ret != Z_OK) + { + free(temp_buffer); + pgagroal_log_error("Gzip: Initialization failed"); + return 1; + } + + size_t total_out = 0; + do + { + if (stream.total_out >= chunk_size) + { + chunk_size *= 2; + unsigned char* new_buffer = (unsigned char*)realloc(temp_buffer, chunk_size); + if (new_buffer == NULL) + { + free(temp_buffer); + deflateEnd(&stream); + pgagroal_log_error("Gzip: Allocation error"); + return 1; + } + temp_buffer = new_buffer; + } + + stream.next_out = temp_buffer + stream.total_out; + stream.avail_out = chunk_size - stream.total_out; + + ret = deflate(&stream, Z_FINISH); + } + while (ret == Z_OK || ret == Z_BUF_ERROR); + + if (ret != Z_STREAM_END) + { + free(temp_buffer); + deflateEnd(&stream); + pgagroal_log_error("Gzip: Compression failed"); + return 1; + } + + total_out = stream.total_out; + + final_buffer = (unsigned char*)realloc(temp_buffer, total_out); + if (final_buffer == NULL) + { + *buffer = temp_buffer; + } + else + { + *buffer = final_buffer; + } + *buffer_size = total_out; + + deflateEnd(&stream); + + return 0; +} + +int +pgagroal_gunzip_string(unsigned char* compressed_buffer, size_t compressed_size, char** output_string) +{ + int ret; + z_stream stream; + size_t chunk_size; + size_t total_out = 0; + + chunk_size = BUFFER_LENGTH; + + char* temp_buffer = (char*)malloc(chunk_size); + if (temp_buffer == NULL) + { + pgagroal_log_error("GUNzip: Allocation failed"); + return 1; + } + + memset(&stream, 0, sizeof(stream)); + stream.next_in = (unsigned char*)compressed_buffer; + stream.avail_in = compressed_size; + + ret = inflateInit2(&stream, MAX_WBITS + 16); + if (ret != Z_OK) + { + free(temp_buffer); + pgagroal_log_error("GUNzip: Initialization failed"); + return 1; + } + + do + { + if (stream.total_out >= chunk_size) + { + chunk_size *= 2; + char* new_buffer = (char*)realloc(temp_buffer, chunk_size); + if (new_buffer == NULL) + { + free(temp_buffer); + inflateEnd(&stream); + pgagroal_log_error("GUNzip: Allocation error"); + return 1; + } + temp_buffer = new_buffer; + } + + stream.next_out = (unsigned char*)(temp_buffer + stream.total_out); + stream.avail_out = chunk_size - stream.total_out; + + ret = inflate(&stream, Z_NO_FLUSH); + } + while (ret == Z_OK || ret == Z_BUF_ERROR); + + if (ret != Z_STREAM_END) + { + free(temp_buffer); + inflateEnd(&stream); + pgagroal_log_error("GUNzip: Decompression failed"); + return 1; + } + + total_out = stream.total_out; + + char* final_buffer = (char*)realloc(temp_buffer, total_out + 1); + if (final_buffer == NULL) + { + free(temp_buffer); + inflateEnd(&stream); + pgagroal_log_error("GUNzip: Allocation failed"); + return 1; + } + temp_buffer = final_buffer; + + temp_buffer[total_out] = '\0'; + + *output_string = temp_buffer; + + inflateEnd(&stream); + + return 0; +} diff --git a/src/libpgagroal/json.c b/src/libpgagroal/json.c new file mode 100644 index 00000000..9338c2f8 --- /dev/null +++ b/src/libpgagroal/json.c @@ -0,0 +1,650 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#include +#include +/* System */ +#include +#include +#include +#include + +static bool type_allowed(enum value_type type); +static char* item_to_string(struct json* item, int32_t format, char* tag, int indent); +static char* array_to_string(struct json* array, int32_t format, char* tag, int indent); +static int parse_string(char* str, uint64_t* index, struct json** obj); +static int json_add(struct json* obj, char* key, uintptr_t val, enum value_type type); +static int fill_value(char* str, char* key, uint64_t* index, struct json* o); +static bool value_start(char ch); +static int handle_escape_char(char* str, uint64_t* index, uint64_t len, char* ch); + +int +pgagroal_json_append(struct json* array, uintptr_t entry, enum value_type type) +{ + if (array != NULL && array->type == JSONUnknown) + { + array->type = JSONArray; + pgagroal_deque_create(false, (struct deque**)&array->elements); + } + if (array == NULL || array->type != JSONArray || !type_allowed(type)) + { + goto error; + } + return pgagroal_deque_add(array->elements, NULL, entry, type); +error: + return 1; +} + +int +pgagroal_json_put(struct json* item, char* key, uintptr_t val, enum value_type type) +{ + if (item != NULL && item->type == JSONUnknown) + { + item->type = JSONItem; + pgagroal_art_create((struct art**)&item->elements); + } + if (item == NULL || item->type != JSONItem || !type_allowed(type) || key == NULL || strlen(key) == 0) + { + goto error; + } + return pgagroal_art_insert((struct art*)item->elements, (unsigned char*)key, strlen(key) + 1, val, type); +error: + return 1; +} + +int +pgagroal_json_create(struct json** object) +{ + struct json* o = malloc(sizeof(struct json)); + memset(o, 0, sizeof(struct json)); + o->type = JSONUnknown; + *object = o; + return 0; +} + +int +pgagroal_json_destroy(struct json* object) +{ + if (object == NULL) + { + return 0; + } + if (object->type == JSONArray) + { + pgagroal_deque_destroy(object->elements); + } + else if (object->type == JSONItem) + { + pgagroal_art_destroy(object->elements); + } + free(object); + return 0; +} + +char* +pgagroal_json_to_string(struct json* object, int32_t format, char* tag, int indent) +{ + char* str = NULL; + if (object == NULL || (object->type == JSONUnknown || object->elements == NULL)) + { + str = pgagroal_indent(str, tag, indent); + str = pgagroal_append(str, "{}"); + return str; + } + if (object->type != JSONArray) + { + return item_to_string(object, format, tag, indent); + } + else + { + return array_to_string(object, format, tag, indent); + } +} + +void +pgagroal_json_print(struct json* object, int32_t format) +{ + char* str = pgagroal_json_to_string(object, format, NULL, 0); + printf("%s\n", str); + free(str); +} + +uint32_t +pgagroal_json_array_length(struct json* array) +{ + if (array == NULL || array->type != JSONArray) + { + goto error; + } + return pgagroal_deque_size(array->elements); +error: + return 0; +} + +uintptr_t +pgagroal_json_get(struct json* item, char* tag) +{ + if (item == NULL || item->type != JSONItem || tag == NULL || strlen(tag) == 0) + { + return 0; + } + return pgagroal_art_search(item->elements, (unsigned char*)tag, strlen(tag) + 1); +} + +bool +pgagroal_json_contains_key(struct json* item, char* key) +{ + if (item == NULL || item->type != JSONItem || key == NULL || strlen(key) == 0) + { + return false; + } + return pgagroal_art_contains_key(item->elements, (unsigned char*)key, strlen(key) + 1); +} + +int +pgagroal_json_iterator_create(struct json* object, struct json_iterator** iter) +{ + struct json_iterator* i = NULL; + if (object == NULL || object->type == JSONUnknown) + { + return 1; + } + i = malloc(sizeof (struct json_iterator)); + memset(i, 0, sizeof (struct json_iterator)); + i->obj = object; + if (object->type == JSONItem) + { + pgagroal_art_iterator_create(object->elements, (struct art_iterator**)(&i->iter)); + } + else + { + pgagroal_deque_iterator_create(object->elements, (struct deque_iterator**)(&i->iter)); + } + *iter = i; + return 0; +} + +void +pgagroal_json_iterator_destroy(struct json_iterator* iter) +{ + if (iter == NULL) + { + return; + } + if (iter->obj->type == JSONArray) + { + pgagroal_deque_iterator_destroy((struct deque_iterator*)iter->iter); + } + else + { + pgagroal_art_iterator_destroy((struct art_iterator*)iter->iter); + } + free(iter); +} + +bool +pgagroal_json_iterator_next(struct json_iterator* iter) +{ + bool has_next = false; + if (iter == NULL || iter->iter == NULL) + { + return false; + } + if (iter->obj->type == JSONArray) + { + has_next = pgagroal_deque_iterator_next((struct deque_iterator*)iter->iter); + if (has_next) + { + iter->value = ((struct deque_iterator*)iter->iter)->value; + } + } + else + { + has_next = pgagroal_art_iterator_next((struct art_iterator*)iter->iter); + if (has_next) + { + iter->value = ((struct art_iterator*)iter->iter)->value; + iter->key = (char*)((struct art_iterator*)iter->iter)->key; + } + } + return has_next; +} + +int +pgagroal_json_parse_string(char* str, struct json** obj) +{ + uint64_t idx = 0; + if (str == NULL || strlen(str) < 2) + { + return 1; + } + + return parse_string(str, &idx, obj); +} + +int +pgagroal_json_clone(struct json* from, struct json** to) +{ + struct json* o = NULL; + char* str = NULL; + str = pgagroal_json_to_string(from, FORMAT_JSON, NULL, 0); + if (pgagroal_json_parse_string(str, &o)) + { + goto error; + } + *to = o; + free(str); + return 0; +error: + free(str); + return 1; +} + +static int +parse_string(char* str, uint64_t* index, struct json** obj) +{ + enum json_type type; + struct json* o = NULL; + uint64_t idx = *index; + char ch = str[idx]; + char* key = NULL; + uint64_t len = strlen(str); + + if (ch == '{') + { + type = JSONItem; + } + else if (ch == '[') + { + type = JSONArray; + } + else + { + goto error; + } + idx++; + pgagroal_json_create(&o); + if (type == JSONItem) + { + while (idx < len) + { + // pre key + while (idx < len && isspace(str[idx])) + { + idx++; + } + if (idx == len) + { + goto error; + } + if (str[idx] == ',') + { + idx++; + } + else if (str[idx] == '}') + { + idx++; + break; + } + else if (!(str[idx] == '"' && o->type == JSONUnknown)) + { + // if it's first key we won't see comma, otherwise we must see comma + goto error; + } + while (idx < len && str[idx] != '"') + { + idx++; + } + if (idx == len) + { + goto error; + } + idx++; + // The key + while (idx < len && str[idx] != '"') + { + char ec_ch; + // handle escape character + if (str[idx] == '\\') + { + if (handle_escape_char(str, &idx, len, &ec_ch)) + { + goto error; + } + key = pgagroal_append_char(key, ec_ch); + continue; + } + + key = pgagroal_append_char(key, str[idx++]); + } + if (idx == len || key == NULL) + { + goto error; + } + // The lands between + while (idx < len && (str[idx] == '"' || isspace(str[idx]))) + { + idx++; + } + if (idx == len || str[idx] != ':') + { + goto error; + } + while (idx < len && (str[idx] == ':' || isspace(str[idx]))) + { + idx++; + } + if (idx == len) + { + goto error; + } + // The value + if (fill_value(str, key, &idx, o)) + { + goto error; + } + free(key); + key = NULL; + } + } + else + { + while (idx < len) + { + while (idx < len && isspace(str[idx])) + { + idx++; + } + if (idx == len) + { + goto error; + } + if (str[idx] == ',') + { + idx++; + } + else if (str[idx] == ']') + { + idx++; + break; + } + else if (!(value_start(str[idx]) && o->type == JSONUnknown)) + { + // if it's first key we won't see comma, otherwise we must see comma + goto error; + } + while (idx < len && !value_start(str[idx])) + { + idx++; + } + if (idx == len) + { + goto error; + } + + if (fill_value(str, key, &idx, o)) + { + goto error; + } + } + } + + *index = idx; + *obj = o; + return 0; +error: + pgagroal_json_destroy(o); + free(key); + return 1; +} + +static int +json_add(struct json* obj, char* key, uintptr_t val, enum value_type type) +{ + if (obj == NULL) + { + return 1; + } + if (key == NULL) + { + return pgagroal_json_append(obj, val, type); + } + return pgagroal_json_put(obj, key, val, type); +} + +static bool +value_start(char ch) +{ + return (isdigit(ch) || ch == '-' || ch == '+') || // number + (ch == '[') || // array + (ch == '{') || // item + (ch == '"' || ch == 'n') || // string or null string + (ch == 't' || ch == 'f'); // potential boolean value +} + +static int +fill_value(char* str, char* key, uint64_t* index, struct json* o) +{ + uint64_t idx = *index; + uint64_t len = strlen(str); + if (str[idx] == '"') + { + char* val = NULL; + idx++; + while (idx < len && str[idx] != '"') + { + char ec_ch; + if (str[idx] == '\\') + { + if (handle_escape_char(str, &idx, len, &ec_ch)) + { + goto error; + } + val = pgagroal_append_char(val, ec_ch); + continue; + } + + val = pgagroal_append_char(val, str[idx++]); + } + if (idx == len) + { + goto error; + } + if (val == NULL) + { + json_add(o, key, (uintptr_t)"", ValueString); + } + else + { + json_add(o, key, (uintptr_t)val, ValueString); + } + idx++; + free(val); + } + else if (str[idx] == '-' || str[idx] == '+' || isdigit(str[idx])) + { + bool has_digit = false; + char* val_str = NULL; + while (idx < len && (isdigit(str[idx]) || str[idx] == '.' || str[idx] == '-' || str[idx] == '+')) + { + if (str[idx] == '.') + { + has_digit = true; + } + val_str = pgagroal_append_char(val_str, str[idx++]); + } + if (has_digit) + { + double val = 0.; + if (sscanf(val_str, "%lf", &val) != 1) + { + free(val_str); + goto error; + } + json_add(o, key, pgagroal_value_from_double(val), ValueDouble); + free(val_str); + } + else + { + int64_t val = 0; + if (sscanf(val_str, "%" PRId64, &val) != 1) + { + free(val_str); + goto error; + } + json_add(o, key, (uintptr_t)val, ValueInt64); + free(val_str); + } + } + else if (str[idx] == '{') + { + struct json* val = NULL; + if (parse_string(str, &idx, &val)) + { + goto error; + } + json_add(o, key, (uintptr_t)val, ValueJSON); + } + else if (str[idx] == '[') + { + struct json* val = NULL; + if (parse_string(str, &idx, &val)) + { + goto error; + } + json_add(o, key, (uintptr_t)val, ValueJSON); + } + else if (str[idx] == 'n' || str[idx] == 't' || str[idx] == 'f') + { + char* val = NULL; + while (idx < len && str[idx] >= 'a' && str[idx] <= 'z') + { + val = pgagroal_append_char(val, str[idx++]); + } + if (pgagroal_compare_string(val, "null")) + { + json_add(o, key, 0, ValueString); + } + else if (pgagroal_compare_string(val, "true")) + { + json_add(o, key, true, ValueBool); + } + else if (pgagroal_compare_string(val, "false")) + { + json_add(o, key, false, ValueBool); + } + else + { + free(val); + goto error; + } + free(val); + } + else + { + goto error; + } + *index = idx; + return 0; +error: + return 1; +} + +static int +handle_escape_char(char* str, uint64_t* index, uint64_t len, char* ch) +{ + uint64_t idx = *index; + idx++; + if (idx == len) // security check + { + return 1; + } + // Check the next character after checking '\' character + switch (str[idx]) + { + case '\"': + case '\\': + *ch = str[idx]; + break; + case 'n': + *ch = '\n'; + break; + case 't': + *ch = '\t'; + break; + case 'r': + *ch = '\r'; + break; + default: + return 1; + } + *index = idx + 1; + return 0; +} + +static bool +type_allowed(enum value_type type) +{ + switch (type) + { + case ValueInt8: + case ValueUInt8: + case ValueInt16: + case ValueUInt16: + case ValueInt32: + case ValueUInt32: + case ValueInt64: + case ValueUInt64: + case ValueBool: + case ValueString: + case ValueBASE64: + case ValueFloat: + case ValueDouble: + case ValueJSON: + return true; + default: + return false; + } +} + +static char* +item_to_string(struct json* item, int32_t format, char* tag, int indent) +{ + return pgagroal_art_to_string(item->elements, format, tag, indent); +} + +static char* +array_to_string(struct json* array, int32_t format, char* tag, int indent) +{ + return pgagroal_deque_to_string(array->elements, format, tag, indent); +} diff --git a/src/libpgagroal/logging.c b/src/libpgagroal/logging.c new file mode 100644 index 00000000..0995ed09 --- /dev/null +++ b/src/libpgagroal/logging.c @@ -0,0 +1,531 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LINE_LENGTH 32 + +FILE* log_file; + +time_t next_log_rotation_age; /* number of seconds at which the next location will happen */ + +char current_log_path[MAX_PATH]; /* the current log file */ + +static const char* levels[] = +{ + "TRACE", + "DEBUG", + "INFO", + "WARN", + "ERROR", + "FATAL" +}; + +static const char* colors[] = +{ + "\x1b[37m", + "\x1b[36m", + "\x1b[32m", + "\x1b[91m", + "\x1b[31m", + "\x1b[35m" +}; + +bool +log_rotation_enabled(void) +{ + struct configuration* config; + config = (struct configuration*)shmem; + + // disable log rotation in the case + // logging is not to a file + if (config->log_type != PGAGROAL_LOGGING_TYPE_FILE) + { + log_rotation_disable(); + return false; + } + + // log rotation is enabled if either log_rotation_age or + // log_rotation_size is enabled + return config->log_rotation_age != PGAGROAL_LOGGING_ROTATION_DISABLED + || config->log_rotation_size != PGAGROAL_LOGGING_ROTATION_DISABLED; +} + +void +log_rotation_disable(void) +{ + struct configuration* config; + config = (struct configuration*)shmem; + + config->log_rotation_age = PGAGROAL_LOGGING_ROTATION_DISABLED; + config->log_rotation_size = PGAGROAL_LOGGING_ROTATION_DISABLED; + next_log_rotation_age = 0; +} + +bool +log_rotation_required(void) +{ + struct stat log_stat; + struct configuration* config; + + config = (struct configuration*)shmem; + + if (!log_rotation_enabled()) + { + return false; + } + + if (stat(current_log_path, &log_stat)) + { + return false; + } + + if (config->log_rotation_size > 0 && log_stat.st_size >= config->log_rotation_size) + { + return true; + } + + if (config->log_rotation_age > 0 && next_log_rotation_age > 0 && next_log_rotation_age <= log_stat.st_ctime) + { + return true; + } + + return false; +} + +bool +log_rotation_set_next_rotation_age(void) +{ + struct configuration* config; + time_t now; + + config = (struct configuration*)shmem; + + if (config->log_type == PGAGROAL_LOGGING_TYPE_FILE && config->log_rotation_age > 0) + { + now = time(NULL); + if (!now) + { + config->log_rotation_age = PGAGROAL_LOGGING_ROTATION_DISABLED; + return false; + } + + next_log_rotation_age = now + config->log_rotation_age; + return true; + } + else + { + config->log_rotation_age = PGAGROAL_LOGGING_ROTATION_DISABLED; + return false; + } +} + +/** + * + */ +int +pgagroal_init_logging(void) +{ + struct configuration* config; + + config = (struct configuration*)shmem; + + if (config->log_type == PGAGROAL_LOGGING_TYPE_FILE) + { + log_file_open(); + + if (!log_file) + { + printf("Failed to open log file %s due to %s\n", strlen(config->log_path) > 0 ? config->log_path : config->default_log_path, strerror(errno)); + errno = 0; + log_rotation_disable(); + return 1; + } + } + + return 0; +} + +/** + * + */ +int +pgagroal_start_logging(void) +{ + struct configuration* config; + + config = (struct configuration*)shmem; + + if (config->log_type == PGAGROAL_LOGGING_TYPE_FILE && !log_file) + { + log_file_open(); + + if (!log_file) + { + printf("Failed to open log file %s due to %s\n", strlen(config->log_path) > 0 ? config->log_path : config->default_log_path, strerror(errno)); + errno = 0; + return 1; + } + } + else if (config->log_type == PGAGROAL_LOGGING_TYPE_SYSLOG) + { + openlog("pgagroal", LOG_CONS | LOG_PERROR | LOG_PID, LOG_USER); + } + + return 0; +} + +int +log_file_open(void) +{ + struct configuration* config; + time_t htime; + struct tm* tm; + + config = (struct configuration*)shmem; + + if (config->log_type == PGAGROAL_LOGGING_TYPE_FILE) + { + htime = time(NULL); + if (!htime) + { + log_file = NULL; + return 1; + } + + tm = localtime(&htime); + if (tm == NULL) + { + log_file = NULL; + return 1; + } + + if (strftime(current_log_path, sizeof(current_log_path), config->log_path, tm) <= 0) + { + // cannot parse the format string, fallback to default logging + memcpy(current_log_path, config->default_log_path, strlen(config->default_log_path)); + log_rotation_disable(); + } + + log_file = fopen(current_log_path, config->log_mode == PGAGROAL_LOGGING_MODE_APPEND ? "a" : "w"); + + if (!log_file) + { + return 1; + } + + log_rotation_set_next_rotation_age(); + return 0; + } + + return 1; +} + +void +log_file_rotate(void) +{ + if (log_rotation_enabled()) + { + fflush(log_file); + fclose(log_file); + log_file_open(); + } +} + +/** + * + */ +int +pgagroal_stop_logging(void) +{ + struct configuration* config; + + config = (struct configuration*)shmem; + + if (config->log_type == PGAGROAL_LOGGING_TYPE_FILE) + { + if (log_file != NULL) + { + return fclose(log_file); + } + else + { + return 1; + } + } + else if (config->log_type == PGAGROAL_LOGGING_TYPE_SYSLOG) + { + closelog(); + } + + return 0; +} + +void +pgagroal_log_line(int level, char* file, int line, char* fmt, ...) +{ + signed char isfree; + struct configuration* config; + + config = (struct configuration*)shmem; + + if (config == NULL) + { + return; + } + + if (level >= config->log_level) + { + switch (level) + { + case PGAGROAL_LOGGING_LEVEL_INFO: + pgagroal_prometheus_logging(PGAGROAL_LOGGING_LEVEL_INFO); + break; + case PGAGROAL_LOGGING_LEVEL_WARN: + pgagroal_prometheus_logging(PGAGROAL_LOGGING_LEVEL_WARN); + break; + case PGAGROAL_LOGGING_LEVEL_ERROR: + pgagroal_prometheus_logging(PGAGROAL_LOGGING_LEVEL_ERROR); + break; + case PGAGROAL_LOGGING_LEVEL_FATAL: + pgagroal_prometheus_logging(PGAGROAL_LOGGING_LEVEL_FATAL); + break; + default: + break; + } + +retry: + isfree = STATE_FREE; + + if (atomic_compare_exchange_strong(&config->log_lock, &isfree, STATE_IN_USE)) + { + char buf[256]; + va_list vl; + struct tm* tm; + time_t t; + char* filename; + + t = time(NULL); + tm = localtime(&t); + + filename = strrchr(file, '/'); + if (filename != NULL) + { + filename = filename + 1; + } + else + { + filename = file; + } + + if (strlen(config->log_line_prefix) == 0) + { + memcpy(config->log_line_prefix, PGAGROAL_LOGGING_DEFAULT_LOG_LINE_PREFIX, strlen(PGAGROAL_LOGGING_DEFAULT_LOG_LINE_PREFIX)); + } + + va_start(vl, fmt); + + if (config->log_type == PGAGROAL_LOGGING_TYPE_CONSOLE) + { + buf[strftime(buf, sizeof(buf), config->log_line_prefix, tm)] = '\0'; + fprintf(stdout, "%s %s%-5s\x1b[0m \x1b[90m%s:%d\x1b[0m ", + buf, colors[level - 1], levels[level - 1], + filename, line); + vfprintf(stdout, fmt, vl); + fprintf(stdout, "\n"); + fflush(stdout); + } + else if (config->log_type == PGAGROAL_LOGGING_TYPE_FILE) + { + buf[strftime(buf, sizeof(buf), config->log_line_prefix, tm)] = '\0'; + fprintf(log_file, "%s %-5s %s:%d ", + buf, levels[level - 1], filename, line); + vfprintf(log_file, fmt, vl); + fprintf(log_file, "\n"); + fflush(log_file); + + if (log_rotation_required()) + { + log_file_rotate(); + } + } + else if (config->log_type == PGAGROAL_LOGGING_TYPE_SYSLOG) + { + switch (level) + { + case PGAGROAL_LOGGING_LEVEL_DEBUG5: + vsyslog(LOG_DEBUG, fmt, vl); + break; + case PGAGROAL_LOGGING_LEVEL_DEBUG1: + vsyslog(LOG_DEBUG, fmt, vl); + break; + case PGAGROAL_LOGGING_LEVEL_INFO: + vsyslog(LOG_INFO, fmt, vl); + break; + case PGAGROAL_LOGGING_LEVEL_WARN: + vsyslog(LOG_WARNING, fmt, vl); + break; + case PGAGROAL_LOGGING_LEVEL_ERROR: + vsyslog(LOG_ERR, fmt, vl); + break; + case PGAGROAL_LOGGING_LEVEL_FATAL: + vsyslog(LOG_CRIT, fmt, vl); + break; + default: + vsyslog(LOG_INFO, fmt, vl); + break; + } + } + + va_end(vl); + + atomic_store(&config->log_lock, STATE_FREE); + } + else + { + SLEEP_AND_GOTO(1000000L, retry) + } + } +} + +void +pgagroal_log_mem(void* data, size_t size) +{ + signed char isfree; + struct configuration* config; + + config = (struct configuration*)shmem; + + if (config == NULL) + { + return; + } + + if (config->log_level == PGAGROAL_LOGGING_LEVEL_DEBUG5 && + size > 0 && + (config->log_type == PGAGROAL_LOGGING_TYPE_CONSOLE || config->log_type == PGAGROAL_LOGGING_TYPE_FILE)) + { +retry: + isfree = STATE_FREE; + + if (atomic_compare_exchange_strong(&config->log_lock, &isfree, STATE_IN_USE)) + { + char buf[(3 * size) + (2 * ((size / LINE_LENGTH) + 1)) + 1 + 1]; + int j = 0; + int k = 0; + + memset(&buf, 0, sizeof(buf)); + + for (int i = 0; i < size; i++) + { + if (k == LINE_LENGTH) + { + buf[j] = '\n'; + j++; + k = 0; + } + sprintf(&buf[j], "%02X", (signed char) *((char*)data + i)); + j += 2; + k++; + } + + buf[j] = '\n'; + j++; + k = 0; + + for (int i = 0; i < size; i++) + { + signed char c = (signed char) *((char*)data + i); + if (k == LINE_LENGTH) + { + buf[j] = '\n'; + j++; + k = 0; + } + if (c >= 32 && c <= 127) + { + buf[j] = c; + } + else + { + buf[j] = '?'; + } + j++; + k++; + } + + if (config->log_type == PGAGROAL_LOGGING_TYPE_CONSOLE) + { + fprintf(stdout, "%s", buf); + fprintf(stdout, "\n"); + fflush(stdout); + } + else if (config->log_type == PGAGROAL_LOGGING_TYPE_FILE) + { + fprintf(log_file, "%s", buf); + fprintf(log_file, "\n"); + fflush(log_file); + } + + atomic_store(&config->log_lock, STATE_FREE); + } + else + { + SLEEP_AND_GOTO(1000000L, retry) + } + } +} + +bool +pgagroal_log_is_enabled(int level) +{ + struct configuration* config; + + config = (struct configuration*)shmem; + + if (level >= config->log_level) + { + return true; + } + + return false; +} \ No newline at end of file diff --git a/src/libpgagroal/lz4_compression.c b/src/libpgagroal/lz4_compression.c new file mode 100644 index 00000000..8a189f1a --- /dev/null +++ b/src/libpgagroal/lz4_compression.c @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2021 Red Hat + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include + +/* system */ +#include +#include "lz4.h" +#include +#include +#include +#include +#include +#include + +int +pgagroal_lz4c_string(char* s, unsigned char** buffer, size_t* buffer_size) +{ + size_t input_size; + size_t max_compressed_size; + int compressed_size; + + input_size = strlen(s); + max_compressed_size = LZ4_compressBound(input_size); + + *buffer = (unsigned char*)malloc(max_compressed_size); + if (*buffer == NULL) + { + pgagroal_log_error("LZ4: Allocation failed"); + return 1; + } + + compressed_size = LZ4_compress_default(s, (char*)*buffer, input_size, max_compressed_size); + if (compressed_size <= 0) + { + pgagroal_log_error("LZ4: Compress failed"); + free(*buffer); + return 1; + } + + *buffer_size = (size_t)compressed_size; + + return 0; +} + +int +pgagroal_lz4d_string(unsigned char* compressed_buffer, size_t compressed_size, char** output_string) +{ + size_t max_decompressed_size; + int decompressed_size; + + max_decompressed_size = compressed_size * 4; + + *output_string = (char*)malloc(max_decompressed_size); + if (*output_string == NULL) + { + pgagroal_log_error("LZ4: Allocation failed"); + return 1; + } + + decompressed_size = LZ4_decompress_safe((const char*)compressed_buffer, *output_string, compressed_size, max_decompressed_size); + if (decompressed_size < 0) + { + pgagroal_log_error("LZ4: Decompress failed"); + free(*output_string); + return 1; + } + + (*output_string)[decompressed_size] = '\0'; + + return 0; +} diff --git a/src/libpgagroal/management.c b/src/libpgagroal/management.c new file mode 100644 index 00000000..6f7365b6 --- /dev/null +++ b/src/libpgagroal/management.c @@ -0,0 +1,1576 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static int create_header(int32_t command, uint8_t compression, uint8_t encryption, int32_t output_format, struct json** json); +static int create_request(struct json* json, struct json** request); +static int create_outcome_success(struct json* json, time_t start_time, time_t end_time, struct json** outcome); +static int create_outcome_failure(struct json* json, int32_t error, struct json** outcome); + +static int read_uint8(char* prefix, SSL* ssl, int socket, uint8_t* i); +static int read_string(char* prefix, SSL* ssl, int socket, char** str); +static int read_complete(SSL* ssl, int socket, void* buf, size_t size); +static int write_uint8(char* prefix, SSL* ssl, int socket, uint8_t i); +static int write_string(char* prefix, SSL* ssl, int socket, char* str); +static int write_complete(SSL* ssl, int socket, void* buf, size_t size); +static int write_socket(int socket, void* buf, size_t size); +static int write_ssl(SSL* ssl, void* buf, size_t size); + +int +pgagroal_management_request_flush(SSL* ssl, int socket, int32_t mode, char* database, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + struct json* j = NULL; + struct json* request = NULL; + + if (create_header(MANAGEMENT_FLUSH, compression, encryption, output_format, &j)) + { + goto error; + } + + if (create_request(j, &request)) + { + goto error; + } + + pgagroal_json_put(request, MANAGEMENT_ARGUMENT_MODE, (uintptr_t)mode, ValueInt32); + pgagroal_json_put(request, MANAGEMENT_ARGUMENT_DATABASE, (uintptr_t)database, ValueString); + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, j)) + { + goto error; + } + + pgagroal_json_destroy(j); + + return 0; + +error: + + pgagroal_json_destroy(j); + + return 1; +} + +int +pgagroal_management_request_enabledb(SSL* ssl, int socket, char* database, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + struct json* j = NULL; + struct json* request = NULL; + + if (create_header(MANAGEMENT_ENABLEDB, compression, encryption, output_format, &j)) + { + goto error; + } + + if (create_request(j, &request)) + { + goto error; + } + + pgagroal_json_put(request, MANAGEMENT_ARGUMENT_DATABASE, (uintptr_t)database, ValueString); + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, j)) + { + goto error; + } + + pgagroal_json_destroy(j); + + return 0; + +error: + + pgagroal_json_destroy(j); + + return 1; +} + +int +pgagroal_management_request_get_password(SSL* ssl, int socket, char* username, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + struct json* j = NULL; + struct json* request = NULL; + + if (create_header(MANAGEMENT_GET_PASSWORD, compression, encryption, output_format, &j)) + { + goto error; + } + + if (create_request(j, &request)) + { + goto error; + } + + pgagroal_json_put(request, MANAGEMENT_ARGUMENT_USERNAME, (uintptr_t)username, ValueString); + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, j)) + { + goto error; + } + + pgagroal_json_destroy(j); + + return 0; + +error: + + pgagroal_json_destroy(j); + + return 1; +} + +int +pgagroal_management_request_disabledb(SSL* ssl, int socket, char* database, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + struct json* j = NULL; + struct json* request = NULL; + + if (create_header(MANAGEMENT_DISABLEDB, compression, encryption, output_format, &j)) + { + goto error; + } + + if (create_request(j, &request)) + { + goto error; + } + + pgagroal_json_put(request, MANAGEMENT_ARGUMENT_DATABASE, (uintptr_t)database, ValueString); + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, j)) + { + goto error; + } + + pgagroal_json_destroy(j); + + return 0; + +error: + + pgagroal_json_destroy(j); + + return 1; +} + +int +pgagroal_management_request_gracefully(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + struct json* j = NULL; + struct json* request = NULL; + + if (create_header(MANAGEMENT_GRACEFULLY, compression, encryption, output_format, &j)) + { + goto error; + } + + if (create_request(j, &request)) + { + goto error; + } + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, j)) + { + goto error; + } + + pgagroal_json_destroy(j); + + return 0; + +error: + + pgagroal_json_destroy(j); + + return 1; +} + +int +pgagroal_management_request_shutdown(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + struct json* j = NULL; + struct json* request = NULL; + + if (create_header(MANAGEMENT_SHUTDOWN, compression, encryption, output_format, &j)) + { + goto error; + } + + if (create_request(j, &request)) + { + goto error; + } + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, j)) + { + goto error; + } + + pgagroal_json_destroy(j); + + return 0; + +error: + + pgagroal_json_destroy(j); + + return 1; +} + +int +pgagroal_management_request_cancel_shutdown(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + struct json* j = NULL; + struct json* request = NULL; + + if (create_header(MANAGEMENT_CANCEL_SHUTDOWN, compression, encryption, output_format, &j)) + { + goto error; + } + + if (create_request(j, &request)) + { + goto error; + } + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, j)) + { + goto error; + } + + pgagroal_json_destroy(j); + + return 0; + +error: + + pgagroal_json_destroy(j); + + return 1; +} + +int +pgagroal_management_request_status(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + struct json* j = NULL; + struct json* request = NULL; + + if (create_header(MANAGEMENT_STATUS, compression, encryption, output_format, &j)) + { + goto error; + } + + if (create_request(j, &request)) + { + goto error; + } + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, j)) + { + goto error; + } + + pgagroal_json_destroy(j); + + return 0; + +error: + + pgagroal_json_destroy(j); + + return 1; +} + +int +pgagroal_management_request_details(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + struct json* j = NULL; + struct json* request = NULL; + + if (create_header(MANAGEMENT_DETAILS, compression, encryption, output_format, &j)) + { + goto error; + } + + if (create_request(j, &request)) + { + goto error; + } + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, j)) + { + goto error; + } + + pgagroal_json_destroy(j); + + return 0; + +error: + + pgagroal_json_destroy(j); + + return 1; +} + +int +pgagroal_management_request_ping(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + struct json* j = NULL; + struct json* request = NULL; + + if (create_header(MANAGEMENT_PING, compression, encryption, output_format, &j)) + { + goto error; + } + + if (create_request(j, &request)) + { + goto error; + } + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, j)) + { + goto error; + } + + pgagroal_json_destroy(j); + + return 0; + +error: + + pgagroal_json_destroy(j); + + return 1; +} + +int +pgagroal_management_request_clear(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + struct json* j = NULL; + struct json* request = NULL; + + if (create_header(MANAGEMENT_CLEAR, compression, encryption, output_format, &j)) + { + goto error; + } + + if (create_request(j, &request)) + { + goto error; + } + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, j)) + { + goto error; + } + + pgagroal_json_destroy(j); + + return 0; + +error: + + pgagroal_json_destroy(j); + + return 1; +} + +int +pgagroal_management_request_clear_server(SSL* ssl, int socket, char* server, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + struct json* j = NULL; + struct json* request = NULL; + + if (create_header(MANAGEMENT_CLEAR_SERVER, compression, encryption, output_format, &j)) + { + goto error; + } + + if (create_request(j, &request)) + { + goto error; + } + + pgagroal_json_put(request, MANAGEMENT_ARGUMENT_SERVER, (uintptr_t)server, ValueString); + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, j)) + { + goto error; + } + + pgagroal_json_destroy(j); + + return 0; + +error: + + pgagroal_json_destroy(j); + + return 1; +} + +int +pgagroal_management_request_switch_to(SSL* ssl, int socket, char* server, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + struct json* j = NULL; + struct json* request = NULL; + + if (create_header(MANAGEMENT_SWITCH_TO, compression, encryption, output_format, &j)) + { + goto error; + } + + if (create_request(j, &request)) + { + goto error; + } + + pgagroal_json_put(request, MANAGEMENT_ARGUMENT_SERVER, (uintptr_t)server, ValueString); + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, j)) + { + goto error; + } + + pgagroal_json_destroy(j); + + return 0; + +error: + + pgagroal_json_destroy(j); + + return 1; +} + +int +pgagroal_management_request_reload(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + struct json* j = NULL; + struct json* request = NULL; + + if (create_header(MANAGEMENT_RELOAD, compression, encryption, output_format, &j)) + { + goto error; + } + + if (create_request(j, &request)) + { + goto error; + } + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, j)) + { + goto error; + } + + pgagroal_json_destroy(j); + + return 0; + +error: + + pgagroal_json_destroy(j); + + return 1; +} + +int +pgagroal_management_request_conf_ls(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + struct json* j = NULL; + struct json* request = NULL; + + if (create_header(MANAGEMENT_CONFIG_LS, compression, encryption, output_format, &j)) + { + goto error; + } + + if (create_request(j, &request)) + { + goto error; + } + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, j)) + { + goto error; + } + + pgagroal_json_destroy(j); + + return 0; + +error: + + pgagroal_json_destroy(j); + + return 1; +} + +int +pgagroal_management_request_conf_get(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + struct json* j = NULL; + struct json* request = NULL; + + if (create_header(MANAGEMENT_CONFIG_GET, compression, encryption, output_format, &j)) + { + goto error; + } + + if (create_request(j, &request)) + { + goto error; + } + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, j)) + { + goto error; + } + + pgagroal_json_destroy(j); + + return 0; + +error: + + pgagroal_json_destroy(j); + + return 1; +} + +int +pgagroal_management_request_conf_set(SSL* ssl, int socket, char* config_key, char* config_value, uint8_t compression, uint8_t encryption, int32_t output_format) +{ + struct json* j = NULL; + struct json* request = NULL; + + if (create_header(MANAGEMENT_CONFIG_SET, compression, encryption, output_format, &j)) + { + goto error; + } + + if (create_request(j, &request)) + { + goto error; + } + + pgagroal_json_put(request, MANAGEMENT_ARGUMENT_CONFIG_KEY, (uintptr_t)config_key, ValueString); + pgagroal_json_put(request, MANAGEMENT_ARGUMENT_CONFIG_VALUE, (uintptr_t)config_value, ValueString); + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, j)) + { + goto error; + } + + pgagroal_json_destroy(j); + + return 0; + +error: + + pgagroal_json_destroy(j); + + return 1; +} + +static int +create_header(int32_t command, uint8_t compression, uint8_t encryption, int32_t output_format, struct json** json) +{ + time_t t; + char timestamp[128]; + struct tm* time_info; + struct json* j = NULL; + struct json* header = NULL; + + *json = NULL; + + if (pgagroal_json_create(&j)) + { + goto error; + } + + if (pgagroal_json_create(&header)) + { + goto error; + } + + time(&t); + time_info = localtime(&t); + strftime(×tamp[0], sizeof(timestamp), "%Y%m%d%H%M%S", time_info); + + pgagroal_json_put(header, MANAGEMENT_ARGUMENT_COMMAND, (uintptr_t)command, ValueInt32); + pgagroal_json_put(header, MANAGEMENT_ARGUMENT_CLIENT_VERSION, (uintptr_t)PGAGROAL_VERSION, ValueString); + pgagroal_json_put(header, MANAGEMENT_ARGUMENT_OUTPUT, (uintptr_t)output_format, ValueUInt8); + pgagroal_json_put(header, MANAGEMENT_ARGUMENT_TIMESTAMP, (uintptr_t)timestamp, ValueString); + pgagroal_json_put(header, MANAGEMENT_ARGUMENT_COMPRESSION, (uintptr_t)compression, ValueUInt8); + pgagroal_json_put(header, MANAGEMENT_ARGUMENT_ENCRYPTION, (uintptr_t)encryption, ValueUInt8); + + pgagroal_json_put(j, MANAGEMENT_CATEGORY_HEADER, (uintptr_t)header, ValueJSON); + + *json = j; + + return 0; + +error: + + pgagroal_json_destroy(header); + pgagroal_json_destroy(j); + + *json = NULL; + + return 1; +} + +static int +create_request(struct json* json, struct json** request) +{ + struct json* r = NULL; + + *request = NULL; + + if (pgagroal_json_create(&r)) + { + goto error; + } + + pgagroal_json_put(json, MANAGEMENT_CATEGORY_REQUEST, (uintptr_t)r, ValueJSON); + + *request = r; + + return 0; + +error: + + pgagroal_json_destroy(r); + + return 1; +} + +static int +create_outcome_success(struct json* json, time_t start_time, time_t end_time, struct json** outcome) +{ + int32_t total_seconds = 0; + char* elapsed = NULL; + struct json* r = NULL; + + *outcome = NULL; + + if (pgagroal_json_create(&r)) + { + goto error; + } + + elapsed = pgagroal_get_timestamp_string(start_time, end_time, &total_seconds); + + pgagroal_json_put(r, MANAGEMENT_ARGUMENT_STATUS, (uintptr_t)true, ValueBool); + pgagroal_json_put(r, MANAGEMENT_ARGUMENT_TIME, (uintptr_t)elapsed, ValueString); + + pgagroal_json_put(json, MANAGEMENT_CATEGORY_OUTCOME, (uintptr_t)r, ValueJSON); + + *outcome = r; + + free(elapsed); + + return 0; + +error: + + free(elapsed); + + pgagroal_json_destroy(r); + + return 1; +} + +static int +create_outcome_failure(struct json* json, int32_t error, struct json** outcome) +{ + struct json* r = NULL; + + *outcome = NULL; + + if (pgagroal_json_create(&r)) + { + goto error; + } + + pgagroal_json_put(r, MANAGEMENT_ARGUMENT_STATUS, (uintptr_t)false, ValueBool); + pgagroal_json_put(r, MANAGEMENT_ARGUMENT_ERROR, (uintptr_t)error, ValueInt32); + + pgagroal_json_put(json, MANAGEMENT_CATEGORY_OUTCOME, (uintptr_t)r, ValueJSON); + + *outcome = r; + + return 0; + +error: + + pgagroal_json_destroy(r); + + return 1; +} + +int +pgagroal_management_create_response(struct json* json, int server, struct json** response) +{ + struct json* r = NULL; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + *response = NULL; + + if (pgagroal_json_create(&r)) + { + goto error; + } + + pgagroal_json_put(json, MANAGEMENT_CATEGORY_RESPONSE, (uintptr_t)r, ValueJSON); + + if (server >= 0) + { + pgagroal_json_put(r, MANAGEMENT_ARGUMENT_SERVER, (uintptr_t)config->servers[server].name, ValueString); + } + + pgagroal_json_put(r, MANAGEMENT_ARGUMENT_SERVER_VERSION, (uintptr_t)PGAGROAL_VERSION, ValueString); + + *response = r; + + return 0; + +error: + + pgagroal_json_destroy(r); + + return 1; +} + +int +pgagroal_management_response_ok(SSL* ssl, int socket, time_t start_time, time_t end_time, uint8_t compression, uint8_t encryption, struct json* payload) +{ + struct json* outcome = NULL; + + if (create_outcome_success(payload, start_time, end_time, &outcome)) + { + goto error; + } + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, payload)) + { + goto error; + } + + return 0; + +error: + + return 1; +} + +int +pgagroal_management_response_error(SSL* ssl, int socket, char* server, int32_t error, uint8_t compression, uint8_t encryption, struct json* payload) +{ + int srv = -1; + struct json* response = NULL; + struct json* outcome = NULL; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + if (create_outcome_failure(payload, error, &outcome)) + { + goto error; + } + + if (server != NULL && strlen(server) > 0) + { + if (pgagroal_json_get(payload, MANAGEMENT_CATEGORY_RESPONSE) != 0) + { + response = (struct json*)pgagroal_json_get(payload, MANAGEMENT_CATEGORY_RESPONSE); + } + else + { + for (int i = 0; i < config->number_of_servers; i++) + { + if (!strcmp(server, config->servers[i].name)) + { + srv = i; + } + } + + if (pgagroal_management_create_response(payload, srv, &response)) + { + goto error; + } + + pgagroal_json_put(response, MANAGEMENT_ARGUMENT_SERVER, (uintptr_t)server, ValueString); + } + } + + if (pgagroal_management_write_json(ssl, socket, compression, encryption, payload)) + { + goto error; + } + + return 0; + +error: + + return 1; +} + +int +pgagroal_management_read_json(SSL* ssl, int socket, uint8_t* compression, uint8_t* encryption, struct json** json) +{ + uint8_t compress_method = MANAGEMENT_COMPRESSION_NONE; + uint8_t encrypt_method = MANAGEMENT_ENCRYPTION_NONE; + char* s = NULL; + struct json* r = NULL; + + unsigned char* transfer_buffer = NULL; + unsigned char* decoded_buffer = NULL; + unsigned char* decrypted_buffer = NULL; + char* decompressed = NULL; + size_t transfer_size = 0; + size_t decoded_size = 0; + size_t decrypted_size = 0; + + if (read_uint8("pgagroal-cli", ssl, socket, &compress_method)) + { + goto error; + } + + if (compression != NULL) + { + *compression = compress_method; + } + + if (read_uint8("pgagroal-cli", ssl, socket, &encrypt_method)) + { + goto error; + } + + if (encryption != NULL) + { + *encryption = encrypt_method; + } + + if (read_string("pgagroal-cli", ssl, socket, &s)) + { + goto error; + } + + if (compress_method || encrypt_method) + { + // First, perform decode + if (pgagroal_base64_decode(s, strlen(s), (void**)&decoded_buffer, &decoded_size) != 0) + { + pgagroal_log_error("pgagroal_management_read_json: Decoding failedg"); + goto error; + } + free(s); + s = NULL; + transfer_buffer = decoded_buffer; + transfer_size = decoded_size; + decoded_buffer = NULL; + + // Second, perform dencrypt + switch (encrypt_method) + { + case MANAGEMENT_ENCRYPTION_AES256: + if (pgagroal_decrypt_buffer(transfer_buffer, transfer_size, &decrypted_buffer, &decrypted_size, MANAGEMENT_ENCRYPTION_AES256)) + { + pgagroal_log_error("pgagroal_management_read_json: Failed to aes256 dencrypt the string"); + goto error; + } + free(transfer_buffer); + transfer_buffer = decrypted_buffer; + transfer_size = decrypted_size; + decrypted_buffer = NULL; + + break; + case MANAGEMENT_ENCRYPTION_AES192: + if (pgagroal_decrypt_buffer(transfer_buffer, transfer_size, &decrypted_buffer, &decrypted_size, MANAGEMENT_ENCRYPTION_AES192)) + { + pgagroal_log_error("pgagroal_management_read_json: Failed to aes192 dencrypt the string"); + goto error; + } + free(transfer_buffer); + transfer_buffer = decrypted_buffer; + transfer_size = decrypted_size; + decrypted_buffer = NULL; + + break; + case MANAGEMENT_ENCRYPTION_AES128: + if (pgagroal_decrypt_buffer(transfer_buffer, transfer_size, &decrypted_buffer, &decrypted_size, MANAGEMENT_ENCRYPTION_AES128)) + { + pgagroal_log_error("pgagroal_management_read_json: Failed to aes128 dencrypt the string"); + goto error; + } + free(transfer_buffer); + transfer_buffer = decrypted_buffer; + transfer_size = decrypted_size; + decrypted_buffer = NULL; + + break; + default: + break; + } + + // Third, perform decompress + switch (compress_method) + { + case MANAGEMENT_COMPRESSION_GZIP: + if (pgagroal_gunzip_string(transfer_buffer, transfer_size, &decompressed)) + { + pgagroal_log_error("pgagroal_management_read_json: GZIP decompress failed"); + goto error; + } + free(transfer_buffer); + transfer_buffer = NULL; + s = decompressed; + decompressed = NULL; + break; + case MANAGEMENT_COMPRESSION_ZSTD: + if (pgagroal_zstdd_string(transfer_buffer, transfer_size, &decompressed)) + { + pgagroal_log_error("pgagroal_management_read_json: ZSTD decompress failed"); + goto error; + } + free(transfer_buffer); + transfer_buffer = NULL; + s = decompressed; + decompressed = NULL; + break; + case MANAGEMENT_COMPRESSION_LZ4: + if (pgagroal_lz4d_string(transfer_buffer, transfer_size, &decompressed)) + { + pgagroal_log_error("pgagroal_management_read_json: LZ4 decompress failed"); + goto error; + } + free(transfer_buffer); + transfer_buffer = NULL; + s = decompressed; + decompressed = NULL; + break; + case MANAGEMENT_COMPRESSION_BZIP2: + if (pgagroal_bunzip2_string(transfer_buffer, transfer_size, &decompressed)) + { + pgagroal_log_error("pgagroal_management_read_json: bzip2 decompress failed"); + goto error; + } + free(transfer_buffer); + transfer_buffer = NULL; + s = decompressed; + decompressed = NULL; + break; + default: + s = (char*) transfer_buffer; + transfer_buffer = NULL; + break; + } + } + + if (pgagroal_json_parse_string(s, &r)) + { + goto error; + } + + *json = r; + + free(s); + + return 0; + +error: + + pgagroal_json_destroy(r); + + if (s != NULL) + { + free(s); + } + if (transfer_buffer != NULL) + { + free(transfer_buffer); + } + if (decoded_buffer != NULL) + { + free(decoded_buffer); + } + if (decrypted_buffer != NULL) + { + free(decrypted_buffer); + } + if (decompressed != NULL) + { + free(decompressed); + } + + return 1; +} + +int +pgagroal_management_write_json(SSL* ssl, int socket, uint8_t compression, uint8_t encryption, struct json* json) +{ + char* s = NULL; + + unsigned char* transfer_buffer = NULL; + unsigned char* compressed_buffer = NULL; + unsigned char* encrypted_buffer = NULL; + char* encoded = NULL; + size_t transfer_size = 0; + size_t compressed_size = 0; + size_t encrypted_size = 0; + size_t encoded_size = 0; + + s = pgagroal_json_to_string(json, FORMAT_JSON_COMPACT, NULL, 0); + + if (write_uint8("pgagroal-cli", ssl, socket, compression)) + { + goto error; + } + + if (write_uint8("pgagroal-cli", ssl, socket, encryption)) + { + goto error; + } + + if (compression || encryption) + { + // First, perform compress + switch (compression) + { + case MANAGEMENT_COMPRESSION_GZIP: + if (pgagroal_gzip_string(s, &compressed_buffer, &compressed_size)) + { + pgagroal_log_error("pgagroal_management_write_json: Failed to gzip the string"); + goto error; + } + transfer_buffer = compressed_buffer; + transfer_size = compressed_size; + + free(s); + compressed_buffer = NULL; + s = NULL; + break; + case MANAGEMENT_COMPRESSION_ZSTD: + if (pgagroal_zstdc_string(s, &compressed_buffer, &compressed_size)) + { + pgagroal_log_error("pgagroal_management_write_json: Failed to zstd the string"); + goto error; + } + transfer_buffer = compressed_buffer; + transfer_size = compressed_size; + + free(s); + compressed_buffer = NULL; + s = NULL; + break; + case MANAGEMENT_COMPRESSION_LZ4: + if (pgagroal_lz4c_string(s, &compressed_buffer, &compressed_size)) + { + pgagroal_log_error("pgagroal_management_write_json: Failed to lz4 the string"); + goto error; + } + transfer_buffer = compressed_buffer; + transfer_size = compressed_size; + + free(s); + compressed_buffer = NULL; + s = NULL; + break; + case MANAGEMENT_COMPRESSION_BZIP2: + if (pgagroal_bzip2_string(s, &compressed_buffer, &compressed_size)) + { + pgagroal_log_error("pgagroal_management_write_json: Failed to bzip2 the string"); + goto error; + } + transfer_buffer = compressed_buffer; + transfer_size = compressed_size; + + free(s); + compressed_buffer = NULL; + s = NULL; + break; + default: + transfer_buffer = (unsigned char*)s; + transfer_size = strlen(s); + s = NULL; + break; + } + + // Second, perform encrypt + switch (encryption) + { + case MANAGEMENT_ENCRYPTION_AES256: + if (pgagroal_encrypt_buffer(transfer_buffer, transfer_size, &encrypted_buffer, &encrypted_size, MANAGEMENT_ENCRYPTION_AES256)) + { + pgagroal_log_error("pgagroal_management_write_json: Failed to aes256 encrypt the string"); + goto error; + } + free(transfer_buffer); + transfer_buffer = encrypted_buffer; + transfer_size = encrypted_size; + encrypted_buffer = NULL; + + break; + case MANAGEMENT_ENCRYPTION_AES192: + if (pgagroal_encrypt_buffer(transfer_buffer, transfer_size, &encrypted_buffer, &encrypted_size, MANAGEMENT_ENCRYPTION_AES192)) + { + pgagroal_log_error("pgagroal_management_write_json: Failed to aes192 encrypt the string"); + goto error; + } + free(transfer_buffer); + transfer_buffer = encrypted_buffer; + transfer_size = encrypted_size; + encrypted_buffer = NULL; + + break; + case MANAGEMENT_ENCRYPTION_AES128: + if (pgagroal_encrypt_buffer(transfer_buffer, transfer_size, &encrypted_buffer, &encrypted_size, MANAGEMENT_ENCRYPTION_AES128)) + { + pgagroal_log_error("pgagroal_management_write_json: Failed to aes128 encrypt the string"); + goto error; + } + free(transfer_buffer); + transfer_buffer = encrypted_buffer; + transfer_size = encrypted_size; + encrypted_buffer = NULL; + + break; + default: + break; + } + + // Third, perform base64 encode + if (pgagroal_base64_encode(transfer_buffer, transfer_size, &encoded, &encoded_size) != 0) + { + pgagroal_log_error("pgagroal_management_write_json: Encoding failed"); + goto error; + } + + free(transfer_buffer); + s = encoded; + encoded = NULL; + } + + if (write_string("pgagroal-cli", ssl, socket, s)) + { + goto error; + } + + free(s); + + return 0; + +error: + if (s != NULL) + { + free(s); + } + if (transfer_buffer != NULL) + { + free(transfer_buffer); + } + if (compressed_buffer != NULL) + { + free(compressed_buffer); + } + if (encrypted_buffer != NULL) + { + free(encrypted_buffer); + } + if (encoded != NULL) + { + free(encoded); + } + + return 1; +} + +static int +read_uint8(char* prefix, SSL* ssl, int socket, uint8_t* i) +{ + char buf1[1] = {0}; + + *i = 0; + + if (read_complete(ssl, socket, &buf1[0], sizeof(buf1))) + { + pgagroal_log_warn("%s: read_byte: %p %d %s", prefix, ssl, socket, strerror(errno)); + errno = 0; + goto error; + } + + *i = pgagroal_read_uint8(&buf1); + + return 0; + +error: + + return 1; +} + +static int +read_string(char* prefix, SSL* ssl, int socket, char** str) +{ + char* s = NULL; + char buf4[4] = {0}; + uint32_t size; + + *str = NULL; + + if (read_complete(ssl, socket, &buf4[0], sizeof(buf4))) + { + pgagroal_log_warn("%s: read_string: %p %d %s", prefix, ssl, socket, strerror(errno)); + errno = 0; + goto error; + } + + size = pgagroal_read_uint32(&buf4); + if (size > 0) + { + s = malloc(size + 1); + + if (s == NULL) + { + goto error; + } + + memset(s, 0, size + 1); + + if (read_complete(ssl, socket, s, size)) + { + pgagroal_log_warn("%s: read_string: %p %d %s", prefix, ssl, socket, strerror(errno)); + errno = 0; + goto error; + } + + *str = s; + } + + return 0; + +error: + + free(s); + + return 1; +} + +static int +write_uint8(char* prefix, SSL* ssl, int socket, uint8_t i) +{ + char buf1[1] = {0}; + + pgagroal_write_uint8(&buf1, i); + if (write_complete(ssl, socket, &buf1, sizeof(buf1))) + { + pgagroal_log_warn("%s: write_string: %p %d %s", prefix, ssl, socket, strerror(errno)); + errno = 0; + goto error; + } + + return 0; + +error: + + return 1; +} + +static int +write_string(char* prefix, SSL* ssl, int socket, char* str) +{ + char buf4[4] = {0}; + + pgagroal_write_uint32(&buf4, str != NULL ? strlen(str) : 0); + if (write_complete(ssl, socket, &buf4, sizeof(buf4))) + { + pgagroal_log_warn("%s: write_string: %p %d %s", prefix, ssl, socket, strerror(errno)); + errno = 0; + goto error; + } + + if (str != NULL) + { + if (write_complete(ssl, socket, str, strlen(str))) + { + pgagroal_log_warn("%s: write_string: %p %d %s", prefix, ssl, socket, strerror(errno)); + errno = 0; + goto error; + } + } + + return 0; + +error: + + return 1; +} + +static int +read_complete(SSL* ssl, int socket, void* buf, size_t size) +{ + ssize_t r; + size_t offset; + size_t needs; + int retries; + + offset = 0; + needs = size; + retries = 0; + +read: + if (ssl == NULL) + { + r = read(socket, buf + offset, needs); + } + else + { + r = SSL_read(ssl, buf + offset, needs); + } + + if (r == -1) + { + if (errno == EAGAIN || errno == EWOULDBLOCK) + { + errno = 0; + goto read; + } + + goto error; + } + else if (r < needs) + { + SLEEP(10000000L) + + pgagroal_log_trace("Got: %ld, needs: %ld", r, needs); + + if (retries < 100) + { + offset += r; + needs -= r; + retries++; + goto read; + } + else + { + errno = EINVAL; + goto error; + } + } + + return 0; + +error: + + return 1; +} + +static int +write_complete(SSL* ssl, int socket, void* buf, size_t size) +{ + if (ssl == NULL) + { + return write_socket(socket, buf, size); + } + + return write_ssl(ssl, buf, size); +} + +static int +write_socket(int socket, void* buf, size_t size) +{ + bool keep_write = false; + ssize_t numbytes; + int offset; + ssize_t totalbytes; + ssize_t remaining; + + numbytes = 0; + offset = 0; + totalbytes = 0; + remaining = size; + + do + { + numbytes = write(socket, buf + offset, remaining); + + if (likely(numbytes == (ssize_t)size)) + { + return 0; + } + else if (numbytes != -1) + { + offset += numbytes; + totalbytes += numbytes; + remaining -= numbytes; + + if (totalbytes == (ssize_t)size) + { + return 0; + } + + pgagroal_log_debug("Write %d - %zd/%zd vs %zd", socket, numbytes, totalbytes, size); + keep_write = true; + errno = 0; + } + else + { + switch (errno) + { + case EAGAIN: + keep_write = true; + errno = 0; + break; + default: + keep_write = false; + break; + } + } + } + while (keep_write); + + return 1; +} + +static int +write_ssl(SSL* ssl, void* buf, size_t size) +{ + bool keep_write = false; + ssize_t numbytes; + int offset; + ssize_t totalbytes; + ssize_t remaining; + + numbytes = 0; + offset = 0; + totalbytes = 0; + remaining = size; + + do + { + numbytes = SSL_write(ssl, buf + offset, remaining); + + if (likely(numbytes == size)) + { + return 0; + } + else if (numbytes > 0) + { + offset += numbytes; + totalbytes += numbytes; + remaining -= numbytes; + + if (totalbytes == size) + { + return 0; + } + + pgagroal_log_debug("SSL/Write %d - %zd/%zd vs %zd", SSL_get_fd(ssl), numbytes, totalbytes, size); + keep_write = true; + errno = 0; + } + else + { + int err = SSL_get_error(ssl, numbytes); + + switch (err) + { + case SSL_ERROR_ZERO_RETURN: + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_CONNECT: + case SSL_ERROR_WANT_ACCEPT: + case SSL_ERROR_WANT_X509_LOOKUP: +#ifndef HAVE_OPENBSD + case SSL_ERROR_WANT_ASYNC: + case SSL_ERROR_WANT_ASYNC_JOB: + case SSL_ERROR_WANT_CLIENT_HELLO_CB: +#endif + errno = 0; + keep_write = true; + break; + case SSL_ERROR_SYSCALL: + pgagroal_log_error("SSL_ERROR_SYSCALL: %s (%d)", strerror(errno), SSL_get_fd(ssl)); + errno = 0; + keep_write = false; + break; + case SSL_ERROR_SSL: + pgagroal_log_error("SSL_ERROR_SSL: %s (%d)", strerror(errno), SSL_get_fd(ssl)); + errno = 0; + keep_write = false; + break; + } + ERR_clear_error(); + + if (!keep_write) + { + return 1; + } + } + } + while (keep_write); + + return 1; +} diff --git a/src/libpgagroal/memory.c b/src/libpgagroal/memory.c new file mode 100644 index 00000000..a85ac53a --- /dev/null +++ b/src/libpgagroal/memory.c @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#ifdef DEBUG +#include +#endif + +/* system */ +#ifdef DEBUG +#include +#endif +#include +#include +#include + +static struct message* message = NULL; +static void* data = NULL; + +/** + * + */ +void +pgagroal_memory_init(void) +{ +#ifdef DEBUG + assert(shmem != NULL); +#endif + + if (message == NULL) + { + message = (struct message*)calloc(1, sizeof(struct message)); + if (message == NULL) + { + goto error; + } + } + + if (data == NULL) + { + data = calloc(1, DEFAULT_BUFFER_SIZE); + if (data == NULL) + { + goto error; + } + } + + message->kind = 0; + message->length = 0; + message->data = data; + + return; + +error: + + pgagroal_log_fatal("Unable to allocate memory"); + +#ifdef DEBUG + pgagroal_backtrace(); +#endif + + errno = 0; +} + +/** + * + */ +struct message* +pgagroal_memory_message(void) +{ +#ifdef DEBUG + assert(message != NULL); + assert(data != NULL); +#endif + + return message; +} + +/** + * + */ +void +pgagroal_memory_free(void) +{ +#ifdef DEBUG + assert(message != NULL); + assert(data != NULL); +#endif + + memset(message, 0, sizeof(struct message)); + memset(data, 0, DEFAULT_BUFFER_SIZE); + + message->kind = 0; + message->length = 0; + message->data = data; +} + +/** + * + */ +void +pgagroal_memory_destroy(void) +{ + free(data); + free(message); + + data = NULL; + message = NULL; +} diff --git a/src/libpgagroal/message.c b/src/libpgagroal/message.c new file mode 100644 index 00000000..fb8f2b14 --- /dev/null +++ b/src/libpgagroal/message.c @@ -0,0 +1,1518 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +static int read_message(int socket, bool block, int timeout, struct message** msg); +static int write_message(int socket, struct message* msg); + +static int ssl_read_message(SSL* ssl, int timeout, struct message** msg); +static int ssl_write_message(SSL* ssl, struct message* msg); + +int +pgagroal_read_block_message(SSL* ssl, int socket, struct message** msg) +{ + if (ssl == NULL) + { + return read_message(socket, true, 0, msg); + } + + return ssl_read_message(ssl, 0, msg); +} + +int +pgagroal_read_timeout_message(SSL* ssl, int socket, int timeout, struct message** msg) +{ + if (ssl == NULL) + { + return read_message(socket, true, timeout, msg); + } + + return ssl_read_message(ssl, timeout, msg); +} + +int +pgagroal_write_message(SSL* ssl, int socket, struct message* msg) +{ + if (ssl == NULL) + { + return write_message(socket, msg); + } + + return ssl_write_message(ssl, msg); +} + +int +pgagroal_read_socket_message(int socket, struct message** msg) +{ + return read_message(socket, false, 0, msg); +} + +int +pgagroal_write_socket_message(int socket, struct message* msg) +{ + return write_message(socket, msg); +} + +int +pgagroal_read_ssl_message(SSL* ssl, struct message** msg) +{ + return ssl_read_message(ssl, 0, msg); +} + +int +pgagroal_write_ssl_message(SSL* ssl, struct message* msg) +{ + return ssl_write_message(ssl, msg); +} + +int +pgagroal_create_message(void* data, ssize_t length, struct message** msg) +{ + struct message* copy = NULL; + + copy = (struct message*)malloc(sizeof(struct message)); + if (copy == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating message"); + return MESSAGE_STATUS_ERROR; + } + copy->data = malloc(length); + if (copy->data == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating message"); + free(copy); + return MESSAGE_STATUS_ERROR; + } + + copy->kind = pgagroal_read_byte(data); + copy->length = length; + memcpy(copy->data, data, length); + + *msg = copy; + + return MESSAGE_STATUS_OK; +} + +void +pgagroal_free_message(struct message* msg) +{ + pgagroal_memory_free(); +} + +struct message* +pgagroal_copy_message(struct message* msg) +{ + struct message* copy = NULL; + +#ifdef DEBUG + assert(msg != NULL); + assert(msg->data != NULL); + assert(msg->length > 0); +#endif + + copy = (struct message*)malloc(sizeof(struct message)); + if (copy == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while copying message"); + return NULL; + } + + copy->data = malloc(msg->length); + if (copy->data == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while copying message"); + free(copy); + return NULL; + } + + copy->kind = msg->kind; + copy->length = msg->length; + memcpy(copy->data, msg->data, msg->length); + + return copy; +} + +void +pgagroal_free_copy_message(struct message* msg) +{ + if (msg) + { + if (msg->data) + { + free(msg->data); + msg->data = NULL; + } + + free(msg); + msg = NULL; + } +} + +int +pgagroal_write_empty(SSL* ssl, int socket) +{ + char zero[1]; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(&zero, 0, sizeof(zero)); + + msg.kind = 0; + msg.length = 1; + msg.data = &zero; + + if (ssl == NULL) + { + return write_message(socket, &msg); + } + + return ssl_write_message(ssl, &msg); +} + +int +pgagroal_write_notice(SSL* ssl, int socket) +{ + char notice[1]; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(¬ice, 0, sizeof(notice)); + + notice[0] = 'N'; + + msg.kind = 'N'; + msg.length = 1; + msg.data = ¬ice; + + if (ssl == NULL) + { + return write_message(socket, &msg); + } + + return ssl_write_message(ssl, &msg); +} + +int +pgagroal_write_tls(SSL* ssl, int socket) +{ + char tls[1]; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(&tls, 0, sizeof(tls)); + + tls[0] = 'S'; + + msg.kind = 'S'; + msg.length = 1; + msg.data = &tls; + + if (ssl == NULL) + { + return write_message(socket, &msg); + } + + return ssl_write_message(ssl, &msg); +} + +int +pgagroal_write_pool_full(SSL* ssl, int socket) +{ + int size = 51; + char pool_full[size]; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(&pool_full, 0, sizeof(pool_full)); + + pgagroal_write_byte(&pool_full, 'E'); + pgagroal_write_int32(&(pool_full[1]), size - 1); + pgagroal_write_string(&(pool_full[5]), "SFATAL"); + pgagroal_write_string(&(pool_full[12]), "VFATAL"); + pgagroal_write_string(&(pool_full[19]), "C53300"); + pgagroal_write_string(&(pool_full[26]), "Mconnection pool is full"); + + msg.kind = 'E'; + msg.length = size; + msg.data = &pool_full; + + if (ssl == NULL) + { + return write_message(socket, &msg); + } + + return ssl_write_message(ssl, &msg); +} + +int +pgagroal_write_connection_refused(SSL* ssl, int socket) +{ + int size = 46; + char connection_refused[size]; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(&connection_refused, 0, sizeof(connection_refused)); + + pgagroal_write_byte(&connection_refused, 'E'); + pgagroal_write_int32(&(connection_refused[1]), size - 1); + pgagroal_write_string(&(connection_refused[5]), "SFATAL"); + pgagroal_write_string(&(connection_refused[12]), "VFATAL"); + pgagroal_write_string(&(connection_refused[19]), "C53300"); + pgagroal_write_string(&(connection_refused[26]), "Mconnection refused"); + + msg.kind = 'E'; + msg.length = size; + msg.data = &connection_refused; + + if (ssl == NULL) + { + return write_message(socket, &msg); + } + + return ssl_write_message(ssl, &msg); +} + +int +pgagroal_write_connection_refused_old(SSL* ssl, int socket) +{ + int size = 20; + char connection_refused[size]; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(&connection_refused, 0, sizeof(connection_refused)); + + pgagroal_write_byte(&connection_refused, 'E'); + pgagroal_write_string(&(connection_refused[1]), "connection refused"); + + msg.kind = 'E'; + msg.length = size; + msg.data = &connection_refused; + + if (ssl == NULL) + { + return write_message(socket, &msg); + } + + return ssl_write_message(ssl, &msg); +} + +int +pgagroal_write_bad_password(SSL* ssl, int socket, char* username) +{ + int size = strlen(username); + size += 84; + + char badpassword[size]; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(&badpassword, 0, sizeof(badpassword)); + + pgagroal_write_byte(&badpassword, 'E'); + pgagroal_write_int32(&(badpassword[1]), size - 1); + pgagroal_write_string(&(badpassword[5]), "SFATAL"); + pgagroal_write_string(&(badpassword[12]), "VFATAL"); + pgagroal_write_string(&(badpassword[19]), "C28P01"); + pgagroal_write_string(&(badpassword[26]), "Mpassword authentication failed for user \""); + pgagroal_write_string(&(badpassword[68]), username); + pgagroal_write_string(&(badpassword[68 + strlen(username)]), "\""); + pgagroal_write_string(&(badpassword[68 + strlen(username) + 2]), "Rauth_failed"); + + msg.kind = 'E'; + msg.length = size; + msg.data = &badpassword; + + if (ssl == NULL) + { + return write_message(socket, &msg); + } + + return ssl_write_message(ssl, &msg); +} + +int +pgagroal_write_unsupported_security_model(SSL* ssl, int socket, char* username) +{ + int size = strlen(username); + size += 66; + + char unsupported[size]; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(&unsupported, 0, sizeof(unsupported)); + + pgagroal_write_byte(&unsupported, 'E'); + pgagroal_write_int32(&(unsupported[1]), size - 1); + pgagroal_write_string(&(unsupported[5]), "SFATAL"); + pgagroal_write_string(&(unsupported[12]), "VFATAL"); + pgagroal_write_string(&(unsupported[19]), "C28000"); + pgagroal_write_string(&(unsupported[26]), "Munsupported security model for user \""); + pgagroal_write_string(&(unsupported[64]), username); + pgagroal_write_string(&(unsupported[size - 2]), "\""); + + msg.kind = 'E'; + msg.length = size; + msg.data = &unsupported; + + if (ssl == NULL) + { + return write_message(socket, &msg); + } + + return ssl_write_message(ssl, &msg); +} + +int +pgagroal_write_no_hba_entry(SSL* ssl, int socket, char* username, char* database, char* address) +{ + int size = strlen(username); + size += strlen(database); + size += strlen(address); + size += 88; + + char no_hba[size]; + struct message msg; + int offset = 64; + + memset(&msg, 0, sizeof(struct message)); + memset(&no_hba, 0, sizeof(no_hba)); + + pgagroal_write_byte(&no_hba, 'E'); + pgagroal_write_int32(&(no_hba[1]), size - 1); + pgagroal_write_string(&(no_hba[5]), "SFATAL"); + pgagroal_write_string(&(no_hba[12]), "VFATAL"); + pgagroal_write_string(&(no_hba[19]), "C28000"); + pgagroal_write_string(&(no_hba[26]), "Mno pgagroal_hba.conf entry for host \""); + pgagroal_write_string(&(no_hba[64]), address); + + offset += strlen(address); + + pgagroal_write_string(&(no_hba[offset]), "\", user \""); + + offset += 9; + + pgagroal_write_string(&(no_hba[offset]), username); + + offset += strlen(username); + + pgagroal_write_string(&(no_hba[offset]), "\", database \""); + + offset += 13; + + pgagroal_write_string(&(no_hba[offset]), database); + + offset += strlen(database); + + pgagroal_write_string(&(no_hba[offset]), "\""); + + msg.kind = 'E'; + msg.length = size; + msg.data = &no_hba; + + if (ssl == NULL) + { + return write_message(socket, &msg); + } + + return ssl_write_message(ssl, &msg); +} + +int +pgagroal_write_deallocate_all(SSL* ssl, int socket) +{ + int status; + int size = 21; + + char deallocate[size]; + struct message msg; + struct message* reply = NULL; + + memset(&msg, 0, sizeof(struct message)); + memset(&deallocate, 0, sizeof(deallocate)); + + pgagroal_write_byte(&deallocate, 'Q'); + pgagroal_write_int32(&(deallocate[1]), size - 1); + pgagroal_write_string(&(deallocate[5]), "DEALLOCATE ALL;"); + + msg.kind = 'Q'; + msg.length = size; + msg.data = &deallocate; + + if (ssl == NULL) + { + status = write_message(socket, &msg); + } + else + { + status = ssl_write_message(ssl, &msg); + } + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (ssl == NULL) + { + status = read_message(socket, true, 0, &reply); + } + else + { + status = ssl_read_message(ssl, 0, &reply); + } + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (reply->kind == 'E') + { + goto error; + } + + pgagroal_free_message(reply); + + return 0; + +error: + if (reply) + { + pgagroal_free_message(reply); + } + + return 1; +} + +int +pgagroal_write_discard_all(SSL* ssl, int socket) +{ + int status; + int size = 18; + + char discard[size]; + struct message msg; + struct message* reply = NULL; + + memset(&msg, 0, sizeof(struct message)); + memset(&discard, 0, sizeof(discard)); + + pgagroal_write_byte(&discard, 'Q'); + pgagroal_write_int32(&(discard[1]), size - 1); + pgagroal_write_string(&(discard[5]), "DISCARD ALL;"); + + msg.kind = 'Q'; + msg.length = size; + msg.data = &discard; + + if (ssl == NULL) + { + status = write_message(socket, &msg); + } + else + { + status = ssl_write_message(ssl, &msg); + } + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (ssl == NULL) + { + status = read_message(socket, true, 0, &reply); + } + else + { + status = ssl_read_message(ssl, 0, &reply); + } + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (reply->kind == 'E') + { + goto error; + } + + pgagroal_free_message(reply); + + return 0; + +error: + if (reply) + { + pgagroal_free_message(reply); + } + + return 1; +} + +int +pgagroal_write_terminate(SSL* ssl, int socket) +{ + char terminate[5]; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(&terminate, 0, sizeof(terminate)); + + terminate[0] = 'X'; + terminate[4] = 4; + + msg.kind = 'X'; + msg.length = 5; + msg.data = &terminate; + + if (ssl == NULL) + { + return write_message(socket, &msg); + } + + return ssl_write_message(ssl, &msg); +} + +int +pgagroal_write_client_failover(SSL* ssl, int socket) +{ + int size = 57; + char failover[size]; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(&failover, 0, sizeof(failover)); + + pgagroal_write_byte(&failover, 'E'); + pgagroal_write_int32(&(failover[1]), size - 1); + pgagroal_write_string(&(failover[5]), "SFATAL"); + pgagroal_write_string(&(failover[12]), "VFATAL"); + pgagroal_write_string(&(failover[19]), "C53300"); + pgagroal_write_string(&(failover[26]), "Mserver failover"); + pgagroal_write_string(&(failover[43]), "Rauth_failed"); + + msg.kind = 'E'; + msg.length = size; + msg.data = &failover; + + if (ssl == NULL) + { + return write_message(socket, &msg); + } + + return ssl_write_message(ssl, &msg); +} + +int +pgagroal_write_auth_password(SSL* ssl, int socket) +{ + char password[9]; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(&password, 0, sizeof(password)); + + password[0] = 'R'; + pgagroal_write_int32(&(password[1]), 8); + pgagroal_write_int32(&(password[5]), 3); + + msg.kind = 'R'; + msg.length = 9; + msg.data = &password; + + if (ssl == NULL) + { + return write_message(socket, &msg); + } + + return ssl_write_message(ssl, &msg); +} + +int +pgagroal_write_rollback(SSL* ssl, int socket) +{ + int status; + int size = 15; + + char rollback[size]; + struct message msg; + struct message* reply = NULL; + + memset(&msg, 0, sizeof(struct message)); + memset(&rollback, 0, sizeof(rollback)); + + pgagroal_write_byte(&rollback, 'Q'); + pgagroal_write_int32(&(rollback[1]), size - 1); + pgagroal_write_string(&(rollback[5]), "ROLLBACK;"); + + msg.kind = 'Q'; + msg.length = size; + msg.data = &rollback; + + if (ssl == NULL) + { + status = write_message(socket, &msg); + } + else + { + status = ssl_write_message(ssl, &msg); + } + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (ssl == NULL) + { + status = read_message(socket, true, 0, &reply); + } + else + { + status = ssl_read_message(ssl, 0, &reply); + } + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + pgagroal_free_message(reply); + + return 0; + +error: + if (reply) + { + pgagroal_free_message(reply); + } + + return 1; +} + +int +pgagroal_create_auth_password_response(char* password, struct message** msg) +{ + struct message* m = NULL; + size_t size; + + size = 6 + strlen(password); + + m = (struct message*)malloc(sizeof(struct message)); + if (m == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating auth_password_response"); + return MESSAGE_STATUS_ERROR; + } + m->data = calloc(1, size); + if (m->data == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating auth_password_response"); + free(m); + return MESSAGE_STATUS_ERROR; + } + + m->kind = 'p'; + m->length = size; + + pgagroal_write_byte(m->data, 'p'); + pgagroal_write_int32(m->data + 1, size - 1); + pgagroal_write_string(m->data + 5, password); + + *msg = m; + + return MESSAGE_STATUS_OK; +} + +int +pgagroal_write_auth_md5(SSL* ssl, int socket, char salt[4]) +{ + char md5[13]; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(&md5, 0, sizeof(md5)); + + md5[0] = 'R'; + pgagroal_write_int32(&(md5[1]), 12); + pgagroal_write_int32(&(md5[5]), 5); + pgagroal_write_byte(&(md5[9]), salt[0]); + pgagroal_write_byte(&(md5[10]), salt[1]); + pgagroal_write_byte(&(md5[11]), salt[2]); + pgagroal_write_byte(&(md5[12]), salt[3]); + + msg.kind = 'R'; + msg.length = 13; + msg.data = &md5; + + if (ssl == NULL) + { + return write_message(socket, &msg); + } + + return ssl_write_message(ssl, &msg); +} + +int +pgagroal_create_auth_md5_response(char* md5, struct message** msg) +{ + struct message* m = NULL; + size_t size; + + size = 1 + 4 + strlen(md5) + 1; + + m = (struct message*)malloc(sizeof(struct message)); + if (m == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating auth_md5_response"); + return MESSAGE_STATUS_ERROR; + } + m->data = calloc(1, size); + if (m->data == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating auth_md5_response"); + free(m); + return MESSAGE_STATUS_ERROR; + } + + m->kind = 'p'; + m->length = size; + + pgagroal_write_byte(m->data, 'p'); + pgagroal_write_int32(m->data + 1, size - 1); + pgagroal_write_string(m->data + 5, md5); + + *msg = m; + + return MESSAGE_STATUS_OK; +} + +int +pgagroal_write_auth_scram256(SSL* ssl, int socket) +{ + char scram[24]; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(&scram, 0, sizeof(scram)); + + scram[0] = 'R'; + pgagroal_write_int32(&(scram[1]), 23); + pgagroal_write_int32(&(scram[5]), 10); + pgagroal_write_string(&(scram[9]), "SCRAM-SHA-256"); + + msg.kind = 'R'; + msg.length = 24; + msg.data = &scram; + + if (ssl == NULL) + { + return write_message(socket, &msg); + } + + return ssl_write_message(ssl, &msg); +} + +int +pgagroal_create_auth_scram256_response(char* nounce, struct message** msg) +{ + struct message* m = NULL; + size_t size; + + size = 1 + 4 + 13 + 4 + 9 + strlen(nounce); + + m = (struct message*)malloc(sizeof(struct message)); + if (m == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating auth_scram256_response"); + return MESSAGE_STATUS_ERROR; + } + m->data = calloc(1, size); + if (m->data == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating auth_scram256_response"); + free(m); + return MESSAGE_STATUS_ERROR; + } + + m->kind = 'p'; + m->length = size; + + pgagroal_write_byte(m->data, 'p'); + pgagroal_write_int32(m->data + 1, size - 1); + pgagroal_write_string(m->data + 5, "SCRAM-SHA-256"); + pgagroal_write_string(m->data + 22, " n,,n=,r="); + pgagroal_write_string(m->data + 31, nounce); + + *msg = m; + + return MESSAGE_STATUS_OK; +} + +int +pgagroal_create_auth_scram256_continue(char* cn, char* sn, char* salt, struct message** msg) +{ + struct message* m = NULL; + size_t size; + + size = 1 + 4 + 4 + 2 + strlen(cn) + strlen(sn) + 3 + strlen(salt) + 7; + + m = (struct message*)malloc(sizeof(struct message)); + if (m == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating auth_scram256_continue"); + return MESSAGE_STATUS_ERROR; + } + m->data = calloc(1, size); + + if (m->data == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating auth_scram256_continue"); + free(m); + return MESSAGE_STATUS_ERROR; + } + + m->kind = 'R'; + m->length = size; + + pgagroal_write_byte(m->data, 'R'); + pgagroal_write_int32(m->data + 1, size - 1); + pgagroal_write_int32(m->data + 5, 11); + pgagroal_write_string(m->data + 9, "r="); + pgagroal_write_string(m->data + 11, cn); + pgagroal_write_string(m->data + 11 + strlen(cn), sn); + pgagroal_write_string(m->data + 11 + strlen(cn) + strlen(sn), ",s="); + pgagroal_write_string(m->data + 11 + strlen(cn) + strlen(sn) + 3, salt); + pgagroal_write_string(m->data + 11 + strlen(cn) + strlen(sn) + 3 + strlen(salt), ",i=4096"); + + *msg = m; + + return MESSAGE_STATUS_OK; +} + +int +pgagroal_create_auth_scram256_continue_response(char* wp, char* p, struct message** msg) +{ + struct message* m = NULL; + size_t size; + + size = 1 + 4 + strlen(wp) + 3 + strlen(p); + + m = (struct message*)malloc(sizeof(struct message)); + if (m == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating auth_scram256_continue_response"); + return MESSAGE_STATUS_ERROR; + } + + m->data = calloc(1, size); + if (m->data == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating auth_scram256_continue_response"); + free(m); + return MESSAGE_STATUS_ERROR; + } + + m->kind = 'p'; + m->length = size; + + pgagroal_write_byte(m->data, 'p'); + pgagroal_write_int32(m->data + 1, size - 1); + pgagroal_write_string(m->data + 5, wp); + pgagroal_write_string(m->data + 5 + strlen(wp), ",p="); + pgagroal_write_string(m->data + 5 + strlen(wp) + 3, p); + + *msg = m; + + return MESSAGE_STATUS_OK; +} + +int +pgagroal_create_auth_scram256_final(char* ss, struct message** msg) +{ + struct message* m = NULL; + size_t size; + + size = 1 + 4 + 4 + 2 + strlen(ss); + + m = (struct message*)malloc(sizeof(struct message)); + if (m == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating auth_scram256_final"); + return MESSAGE_STATUS_ERROR; + } + m->data = calloc(1, size); + if (m->data == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating auth_scram256_final"); + free(m); + return MESSAGE_STATUS_ERROR; + } + + m->kind = 'R'; + m->length = size; + + pgagroal_write_byte(m->data, 'R'); + pgagroal_write_int32(m->data + 1, size - 1); + pgagroal_write_int32(m->data + 5, 12); + pgagroal_write_string(m->data + 9, "v="); + pgagroal_write_string(m->data + 11, ss); + + *msg = m; + + return MESSAGE_STATUS_OK; +} + +int +pgagroal_write_auth_success(SSL* ssl, int socket) +{ + char success[9]; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(&success, 0, sizeof(success)); + + success[0] = 'R'; + pgagroal_write_int32(&(success[1]), 8); + pgagroal_write_int32(&(success[5]), 0); + + msg.kind = 'R'; + msg.length = 9; + msg.data = &success; + + if (ssl == NULL) + { + return write_message(socket, &msg); + } + + return ssl_write_message(ssl, &msg); +} + +int +pgagroal_create_ssl_message(struct message** msg) +{ + struct message* m = NULL; + size_t size; + + size = 8; + + m = (struct message*)malloc(sizeof(struct message)); + if (m == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating ssl_message"); + return MESSAGE_STATUS_ERROR; + } + m->data = calloc(1, size); + if (m->data == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating ssl_message"); + free(m); + return MESSAGE_STATUS_ERROR; + } + + m->kind = 0; + m->length = size; + + pgagroal_write_int32(m->data, size); + pgagroal_write_int32(m->data + 4, 80877103); + + *msg = m; + + return MESSAGE_STATUS_OK; +} + +int +pgagroal_create_startup_message(char* username, char* database, struct message** msg) +{ + struct message* m = NULL; + size_t size; + size_t us; + size_t ds; + + us = strlen(username); + ds = strlen(database); + size = 4 + 4 + 4 + 1 + us + 1 + 8 + 1 + ds + 1 + 17 + 9 + 1; + + m = (struct message*)malloc(sizeof(struct message)); + if (m == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating startup_message"); + return MESSAGE_STATUS_ERROR; + } + m->data = calloc(1, size); + if (m->data == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating startup_message"); + free(m); + return MESSAGE_STATUS_ERROR; + } + + m->kind = 0; + m->length = size; + + pgagroal_write_int32(m->data, size); + pgagroal_write_int32(m->data + 4, 196608); + pgagroal_write_string(m->data + 8, "user"); + pgagroal_write_string(m->data + 13, username); + pgagroal_write_string(m->data + 13 + us + 1, "database"); + pgagroal_write_string(m->data + 13 + us + 1 + 9, database); + pgagroal_write_string(m->data + 13 + us + 1 + 9 + ds + 1, "application_name"); + pgagroal_write_string(m->data + 13 + us + 1 + 9 + ds + 1 + 17, "pgagroal"); + + *msg = m; + + return MESSAGE_STATUS_OK; +} + +int +pgagroal_create_cancel_request_message(int pid, int secret, struct message** msg) +{ + struct message* m = NULL; + size_t size; + + size = 16; + + m = (struct message*)malloc(sizeof(struct message)); + if (m == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating cancel_request_message"); + return MESSAGE_STATUS_ERROR; + } + m->data = calloc(1, size); + if (m == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while creating cancel_request_message"); + free(m); + return MESSAGE_STATUS_ERROR; + } + + m->kind = 0; + m->length = size; + + pgagroal_write_int32(m->data, size); + pgagroal_write_int32(m->data + 4, 80877102); + pgagroal_write_int32(m->data + 8, pid); + pgagroal_write_int32(m->data + 12, secret); + + *msg = m; + + return MESSAGE_STATUS_OK; +} + +bool +pgagroal_connection_isvalid(int socket) +{ + int status; + int size = 15; + + char valid[size]; + struct message msg; + struct message* reply = NULL; + + memset(&msg, 0, sizeof(struct message)); + memset(&valid, 0, sizeof(valid)); + + pgagroal_write_byte(&valid, 'Q'); + pgagroal_write_int32(&(valid[1]), size - 1); + pgagroal_write_string(&(valid[5]), "SELECT 1;"); + + msg.kind = 'Q'; + msg.length = size; + msg.data = &valid; + + status = write_message(socket, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = read_message(socket, true, 0, &reply); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (reply->kind == 'E') + { + goto error; + } + + pgagroal_free_message(reply); + + return true; + +error: + if (reply) + { + pgagroal_free_message(reply); + } + + return false; +} + +void +pgagroal_log_message(struct message* msg) +{ + if (msg == NULL) + { + pgagroal_log_info("Message is NULL"); + } + else if (msg->data == NULL) + { + pgagroal_log_info("Message DATA is NULL"); + } + else + { + pgagroal_log_mem(msg->data, msg->length); + } +} + +static int +read_message(int socket, bool block, int timeout, struct message** msg) +{ + bool keep_read; + ssize_t numbytes; + struct timeval tv; + struct message* m = NULL; + + if (unlikely(timeout > 0)) + { + tv.tv_sec = timeout; + tv.tv_usec = 0; + setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)); + } + + do + { + keep_read = false; + + m = pgagroal_memory_message(); + + numbytes = read(socket, m->data, DEFAULT_BUFFER_SIZE); + + if (likely(numbytes > 0)) + { + m->kind = (signed char)(*((char*)m->data)); + m->length = numbytes; + *msg = m; + + if (unlikely(timeout > 0)) + { + tv.tv_sec = 0; + tv.tv_usec = 0; + setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)); + } + + return MESSAGE_STATUS_OK; + } + else if (numbytes == 0) + { + if ((errno == EAGAIN || errno == EWOULDBLOCK) && block) + { + keep_read = true; + errno = 0; + } + else + { + if (unlikely(timeout > 0)) + { + tv.tv_sec = 0; + tv.tv_usec = 0; + setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)); + } + + return MESSAGE_STATUS_ZERO; + } + } + else + { + if ((errno == EAGAIN || errno == EWOULDBLOCK) && block) + { + keep_read = true; + errno = 0; + } + } + } + while (keep_read); + + if (unlikely(timeout > 0)) + { + tv.tv_sec = 0; + tv.tv_usec = 0; + setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)); + } + + return MESSAGE_STATUS_ERROR; +} + +static int +write_message(int socket, struct message* msg) +{ + bool keep_write; + ssize_t numbytes; + int offset; + ssize_t totalbytes; + ssize_t remaining; + +#ifdef DEBUG + assert(msg != NULL); +#endif + + numbytes = 0; + offset = 0; + totalbytes = 0; + remaining = msg->length; + + do + { + keep_write = false; + + numbytes = write(socket, msg->data + offset, remaining); + + if (likely(numbytes == msg->length)) + { + return MESSAGE_STATUS_OK; + } + else if (numbytes != -1) + { + offset += numbytes; + totalbytes += numbytes; + remaining -= numbytes; + + if (totalbytes == msg->length) + { + return MESSAGE_STATUS_OK; + } + + pgagroal_log_debug("Write %d - %zd/%zd vs %zd", socket, numbytes, totalbytes, msg->length); + keep_write = true; + errno = 0; + } + else + { + switch (errno) + { + case EAGAIN: + keep_write = true; + errno = 0; + break; + default: + keep_write = false; + break; + } + } + } + while (keep_write); + + return MESSAGE_STATUS_ERROR; +} + +static int +ssl_read_message(SSL* ssl, int timeout, struct message** msg) +{ + bool keep_read; + ssize_t numbytes; + time_t start_time; + struct message* m = NULL; + + if (unlikely(timeout > 0)) + { + start_time = time(NULL); + } + + do + { + keep_read = false; + + m = pgagroal_memory_message(); + + numbytes = SSL_read(ssl, m->data, DEFAULT_BUFFER_SIZE); + + if (likely(numbytes > 0)) + { + m->kind = (signed char)(*((char*)m->data)); + m->length = numbytes; + *msg = m; + + return MESSAGE_STATUS_OK; + } + else + { + int err; + + err = SSL_get_error(ssl, numbytes); + switch (err) + { + case SSL_ERROR_NONE: + break; + case SSL_ERROR_ZERO_RETURN: + if (timeout > 0) + { + + if (difftime(time(NULL), start_time) >= timeout) + { + return MESSAGE_STATUS_ZERO; + } + + /* Sleep for 100ms */ + SLEEP(100000000L) + + } + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_CONNECT: + case SSL_ERROR_WANT_ACCEPT: + case SSL_ERROR_WANT_X509_LOOKUP: +#ifndef HAVE_OPENBSD + case SSL_ERROR_WANT_ASYNC: + case SSL_ERROR_WANT_ASYNC_JOB: + case SSL_ERROR_WANT_CLIENT_HELLO_CB: +#endif + keep_read = true; + break; + case SSL_ERROR_SYSCALL: + pgagroal_log_error("SSL_ERROR_SYSCALL: %s (%d)", strerror(errno), SSL_get_fd(ssl)); + errno = 0; + break; + case SSL_ERROR_SSL: + pgagroal_log_error("SSL_ERROR_SSL: %s (%d)", strerror(errno), SSL_get_fd(ssl)); + break; + } + ERR_clear_error(); + } + } + while (keep_read); + + return MESSAGE_STATUS_ERROR; +} + +static int +ssl_write_message(SSL* ssl, struct message* msg) +{ + bool keep_write; + ssize_t numbytes; + int offset; + ssize_t totalbytes; + ssize_t remaining; + +#ifdef DEBUG + assert(msg != NULL); +#endif + + numbytes = 0; + offset = 0; + totalbytes = 0; + remaining = msg->length; + + do + { + keep_write = false; + + numbytes = SSL_write(ssl, msg->data + offset, remaining); + + if (likely(numbytes == msg->length)) + { + return MESSAGE_STATUS_OK; + } + else if (numbytes > 0) + { + offset += numbytes; + totalbytes += numbytes; + remaining -= numbytes; + + if (totalbytes == msg->length) + { + return MESSAGE_STATUS_OK; + } + + pgagroal_log_debug("SSL/Write %d - %zd/%zd vs %zd", SSL_get_fd(ssl), numbytes, totalbytes, msg->length); + keep_write = true; + errno = 0; + } + else + { + int err = SSL_get_error(ssl, numbytes); + + switch (err) + { + case SSL_ERROR_NONE: + break; + case SSL_ERROR_ZERO_RETURN: + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_CONNECT: + case SSL_ERROR_WANT_ACCEPT: + case SSL_ERROR_WANT_X509_LOOKUP: +#ifndef HAVE_OPENBSD + case SSL_ERROR_WANT_ASYNC: + case SSL_ERROR_WANT_ASYNC_JOB: + case SSL_ERROR_WANT_CLIENT_HELLO_CB: +#endif + errno = 0; + keep_write = true; + break; + case SSL_ERROR_SYSCALL: + pgagroal_log_error("SSL_ERROR_SYSCALL: FD %d", SSL_get_fd(ssl)); + pgagroal_log_error("%s", ERR_error_string(err, NULL)); + pgagroal_log_error("%s", ERR_lib_error_string(err)); + pgagroal_log_error("%s", ERR_reason_error_string(err)); + errno = 0; + break; + case SSL_ERROR_SSL: + pgagroal_log_error("SSL_ERROR_SSL: FD %d", SSL_get_fd(ssl)); + pgagroal_log_error("%s", ERR_error_string(err, NULL)); + pgagroal_log_error("%s", ERR_lib_error_string(err)); + pgagroal_log_error("%s", ERR_reason_error_string(err)); + errno = 0; + break; + } + ERR_clear_error(); + + if (!keep_write) + { + return MESSAGE_STATUS_ERROR; + } + } + } + while (keep_write); + + return MESSAGE_STATUS_ERROR; +} diff --git a/src/libpgagroal/network.c b/src/libpgagroal/network.c new file mode 100644 index 00000000..dcedced3 --- /dev/null +++ b/src/libpgagroal/network.c @@ -0,0 +1,712 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int bind_host(const char* hostname, int port, int** fds, int* length, bool non_blocking, int* buffer_size, bool no_delay, int backlog); +static int socket_buffers(int fd); + +/** + * + */ +int +pgagroal_bind(const char* hostname, int port, int** fds, int* length, bool non_blocking, bool no_delay, int backlog) +{ + int default_buffer_size = DEFAULT_BUFFER_SIZE; + struct ifaddrs* ifaddr, * ifa; + struct sockaddr_in* sa4; + struct sockaddr_in6* sa6; + char addr[50]; + int* star_fds = NULL; + int star_length = 0; + + if (!strcmp("*", hostname)) + { + if (getifaddrs(&ifaddr) == -1) + { + pgagroal_log_warn("getifaddrs: %s", strerror(errno)); + errno = 0; + return 1; + } + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) + { + if (ifa->ifa_addr != NULL && + (ifa->ifa_addr->sa_family == AF_INET || ifa->ifa_addr->sa_family == AF_INET6) && + (ifa->ifa_flags & IFF_UP)) + { + int* new_fds = NULL; + int new_length = 0; + + memset(addr, 0, sizeof(addr)); + + if (ifa->ifa_addr->sa_family == AF_INET) + { + sa4 = (struct sockaddr_in*) ifa->ifa_addr; + inet_ntop(AF_INET, &sa4->sin_addr, addr, sizeof(addr)); + } + else + { + sa6 = (struct sockaddr_in6*) ifa->ifa_addr; + inet_ntop(AF_INET6, &sa6->sin6_addr, addr, sizeof(addr)); + } + + if (bind_host(addr, port, &new_fds, &new_length, non_blocking, &default_buffer_size, no_delay, backlog)) + { + free(new_fds); + continue; + } + + if (star_fds == NULL) + { + star_fds = malloc(new_length * sizeof(int)); + memcpy(star_fds, new_fds, new_length * sizeof(int)); + star_length = new_length; + } + else + { + star_fds = realloc(star_fds, (star_length + new_length) * sizeof(int)); + memcpy(star_fds + star_length, new_fds, new_length * sizeof(int)); + star_length += new_length; + } + + free(new_fds); + } + } + + freeifaddrs(ifaddr); + + if (star_length == 0) + { + // cannot bind to anything! + return 1; + } + + *fds = star_fds; + *length = star_length; + + return 0; + } + + return bind_host(hostname, port, fds, length, non_blocking, &default_buffer_size, no_delay, backlog); +} + +/** + * + */ +int +pgagroal_bind_unix_socket(const char* directory, const char* file, int* fd) +{ + int status; + char buf[107]; + struct stat st = {0}; + struct sockaddr_un addr; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + if ((*fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) + { + pgagroal_log_error("pgagroal_bind_unix_socket: socket: %s %s", directory, strerror(errno)); + errno = 0; + goto error; + } + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + + if (!directory) + { + directory = "/tmp/"; + } + + memset(&buf, 0, sizeof(buf)); + snprintf(&buf[0], sizeof(buf), "%s", directory); + + if (stat(&buf[0], &st) == -1) + { + status = mkdir(&buf[0], S_IRWXU); + if (status == -1) + { + pgagroal_log_error("pgagroal_bind_unix_socket: permission defined for %s (%s)", directory, strerror(errno)); + errno = 0; + goto error; + } + } + + memset(&buf, 0, sizeof(buf)); + if (!pgagroal_ends_with(&buf[0], "/")) + { + snprintf(&buf[0], sizeof(buf), "%s/%s", directory, file); + } + else + { + snprintf(&buf[0], sizeof(buf), "%s%s", directory, file); + } + + strncpy(addr.sun_path, &buf[0], sizeof(addr.sun_path) - 1); + unlink(&buf[0]); + + if (bind(*fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) + { + pgagroal_log_error("pgagroal_bind_unix_socket: bind: %s/%s %s", directory, file, strerror(errno)); + errno = 0; + goto error; + } + + if (listen(*fd, config->backlog) == -1) + { + pgagroal_log_error("pgagroal_bind_unix_socket: listen: %s/%s %s", directory, file, strerror(errno)); + errno = 0; + goto error; + } + + return 0; + +error: + + return 1; +} + +/** + * + */ +int +pgagroal_remove_unix_socket(const char* directory, const char* file) +{ + char buf[MISC_LENGTH]; + + memset(&buf, 0, sizeof(buf)); + snprintf(&buf[0], sizeof(buf), "%s/%s", directory, file); + + unlink(&buf[0]); + + return 0; +} + +/** + * + */ +int +pgagroal_connect(const char* hostname, int port, int* fd, bool keep_alive, bool non_blocking, bool no_delay) +{ + int default_buffer_size = DEFAULT_BUFFER_SIZE; + struct addrinfo hints = {0}; + struct addrinfo* servinfo = NULL; + struct addrinfo* p = NULL; + int yes = 1; + socklen_t optlen = sizeof(int); + int rv; + char sport[5]; + int error = 0; + + memset(&sport, 0, sizeof(sport)); + sprintf(&sport[0], "%d", port); + + /* Connect to server */ + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + if ((rv = getaddrinfo(hostname, &sport[0], &hints, &servinfo)) != 0) + { + pgagroal_log_debug("getaddrinfo: %s", gai_strerror(rv)); + if (servinfo != NULL) + { + freeaddrinfo(servinfo); + } + return 1; + } + + *fd = -1; + + /* Loop through all the results and connect to the first we can */ + for (p = servinfo; *fd == -1 && p != NULL; p = p->ai_next) + { + if ((*fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) + { + error = errno; + errno = 0; + } + + if (*fd != -1) + { + if (keep_alive) + { + if (setsockopt(*fd, SOL_SOCKET, SO_KEEPALIVE, &yes, optlen) == -1) + { + error = errno; + pgagroal_disconnect(*fd); + errno = 0; + *fd = -1; + continue; + } + } + + if (no_delay) + { + if (setsockopt(*fd, IPPROTO_TCP, TCP_NODELAY, &yes, optlen) == -1) + { + error = errno; + pgagroal_disconnect(*fd); + errno = 0; + *fd = -1; + continue; + } + } + + if (setsockopt(*fd, SOL_SOCKET, SO_RCVBUF, &default_buffer_size, optlen) == -1) + { + error = errno; + pgagroal_disconnect(*fd); + errno = 0; + *fd = -1; + continue; + } + + if (setsockopt(*fd, SOL_SOCKET, SO_SNDBUF, &default_buffer_size, optlen) == -1) + { + error = errno; + pgagroal_disconnect(*fd); + errno = 0; + *fd = -1; + continue; + } + + if (connect(*fd, p->ai_addr, p->ai_addrlen) == -1) + { + error = errno; + pgagroal_disconnect(*fd); + errno = 0; + *fd = -1; + continue; + } + } + } + + if (*fd == -1) + { + goto error; + } + + freeaddrinfo(servinfo); + + /* Set O_NONBLOCK on the socket */ + if (non_blocking) + { + pgagroal_socket_nonblocking(*fd, true); + } + + return 0; + +error: + + pgagroal_log_debug("pgagroal_connect: %s", strerror(error)); + + if (servinfo != NULL) + { + freeaddrinfo(servinfo); + } + + return 1; +} + +/** + * + */ +int +pgagroal_connect_unix_socket(const char* directory, const char* file, int* fd) +{ + char buf[107]; + struct sockaddr_un addr; + + if ((*fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) + { + pgagroal_log_warn("pgagroal_connect_unix_socket: socket: %s %s", directory, strerror(errno)); + errno = 0; + return 1; + } + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + + memset(&buf, 0, sizeof(buf)); + snprintf(&buf[0], sizeof(buf), "%s/%s", directory, file); + + strncpy(addr.sun_path, &buf[0], sizeof(addr.sun_path) - 1); + + if (connect(*fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) + { + pgagroal_log_trace("pgagroal_connect_unix_socket: connect: %s/%s %s", directory, file, strerror(errno)); + errno = 0; + return 1; + } + + return 0; +} + +bool +pgagroal_socket_isvalid(int fd) +{ + int error = 0; + socklen_t length; + int r; + + r = fcntl(fd, F_GETFL); + + if (r == -1) + { + errno = 0; + return false; + } + + length = sizeof(error); + r = getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &length); + + if (r != 0 || error != 0) + { + errno = 0; + return false; + } + + return true; +} + +/** + * + */ +int +pgagroal_disconnect(int fd) +{ + if (fd == -1) + { + return 1; + } + + return close(fd); +} + +void* +pgagroal_get_sockaddr(struct sockaddr* sa) +{ + if (sa->sa_family == AF_INET) + { + return &(((struct sockaddr_in*)sa)->sin_addr); + } + + return &(((struct sockaddr_in6*)sa)->sin6_addr); +} + +void +pgagroal_get_address(struct sockaddr* sa, char* address, size_t length) +{ + if (sa->sa_family == AF_INET) + { + inet_ntop(AF_INET, &(((struct sockaddr_in*)sa)->sin_addr), address, length); + } + else + { + inet_ntop(AF_INET6, &(((struct sockaddr_in6*)sa)->sin6_addr), address, length); + } +} + +int +pgagroal_socket_nonblocking(int fd, bool value) +{ + int flags; + + flags = fcntl(fd, F_GETFL); + + if (value) + { + fcntl(fd, F_SETFL, flags | O_NONBLOCK); + } + else + { + fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); + } + + return 0; +} + +bool +pgagroal_socket_is_nonblocking(int fd) +{ + int flags; + + flags = fcntl(fd, F_GETFL); + + return flags & O_NONBLOCK; +} + +int +pgagroal_socket_has_error(int fd) +{ + int error = 0; + socklen_t length = sizeof(int); + + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &length) == -1) + { + pgagroal_log_trace("error getting socket error code: %s (%d)", strerror(errno), fd); + errno = 0; + goto error; + } + + if (error != 0) + { + pgagroal_log_trace("socket error: %s (%d)", strerror(error), fd); + errno = 0; + goto error; + } + + return 0; + +error: + + return 1; +} + +int +pgagroal_tcp_nodelay(int fd) +{ + int yes = 1; + socklen_t optlen = sizeof(int); + + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &yes, optlen) == -1) + { + pgagroal_log_warn("tcp_nodelay: %d %s", fd, strerror(errno)); + errno = 0; + return 1; + } + + return 0; +} + +int +pgagroal_read_socket(SSL* ssl, int fd, char* buffer, size_t buffer_size) +{ + int byte_read; + if (ssl) + { + byte_read = SSL_read(ssl, buffer, buffer_size); + } + else + { + byte_read = read(fd, buffer, buffer_size); + } + + return byte_read; +} + +int +pgagroal_write_socket(SSL* ssl, int fd, char* buffer, size_t buffer_size) +{ + int byte_write; + if (ssl) + { + byte_write = SSL_write(ssl, buffer, buffer_size); + } + else + { + byte_write = write(fd, buffer, buffer_size); + } + + return byte_write; +} + +static int +socket_buffers(int fd) +{ + int default_buffer_size = DEFAULT_BUFFER_SIZE; + socklen_t optlen = sizeof(int); + + if (setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &default_buffer_size, optlen) == -1) + { + pgagroal_log_warn("socket_buffers: SO_RCVBUF %d %s", fd, strerror(errno)); + errno = 0; + return 1; + } + + if (setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &default_buffer_size, optlen) == -1) + { + pgagroal_log_warn("socket_buffers: SO_SNDBUF %d %s", fd, strerror(errno)); + errno = 0; + return 1; + } + + return 0; +} + +/** + * + */ +static int +bind_host(const char* hostname, int port, int** fds, int* length, bool non_blocking, int* buffer_size, bool no_delay, int backlog) +{ + int* result = NULL; + int index, size; + int sockfd; + struct addrinfo hints, * servinfo, * addr; + int yes = 1; + int rv; + char* sport; + + index = 0; + size = 0; + + sport = calloc(1, 6); + if (sport == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while binding host"); + return 1; + } + sprintf(sport, "%d", port); + + /* Find all SOCK_STREAM addresses */ + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; + + if ((rv = getaddrinfo(hostname, sport, &hints, &servinfo)) != 0) + { + free(sport); + pgagroal_log_error("getaddrinfo: %s:%d (%s)", hostname, port, gai_strerror(rv)); + return 1; + } + + free(sport); + + for (addr = servinfo; addr != NULL; addr = addr->ai_next) + { + size++; + } + + result = calloc(1, size * sizeof(int)); + if (result == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while binding host"); + return 1; + } + + /* Loop through all the results and bind to the first we can */ + for (addr = servinfo; addr != NULL; addr = addr->ai_next) + { + if ((sockfd = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol)) == -1) + { + pgagroal_log_debug("server: socket: %s:%d (%s)", hostname, port, strerror(errno)); + continue; + } + + if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) + { + pgagroal_log_debug("server: so_reuseaddr: %d %s", sockfd, strerror(errno)); + pgagroal_disconnect(sockfd); + continue; + } + + if (non_blocking) + { + if (pgagroal_socket_nonblocking(sockfd, true)) + { + pgagroal_disconnect(sockfd); + continue; + } + } + + if (socket_buffers(sockfd)) + { + pgagroal_disconnect(sockfd); + continue; + } + + if (no_delay) + { + if (pgagroal_tcp_nodelay(sockfd)) + { + pgagroal_disconnect(sockfd); + continue; + } + } + + if (bind(sockfd, addr->ai_addr, addr->ai_addrlen) == -1) + { + pgagroal_disconnect(sockfd); + pgagroal_log_debug("server: bind: %s:%d (%s)", hostname, port, strerror(errno)); + continue; + } + + if (listen(sockfd, backlog) == -1) + { + pgagroal_disconnect(sockfd); + pgagroal_log_debug("server: listen: %s:%d (%s)", hostname, port, strerror(errno)); + continue; + } + + *(result + index) = sockfd; + index++; + } + + freeaddrinfo(servinfo); + + if (index == 0) + { + free(result); + return 1; + } + + *fds = result; + *length = index; + + return 0; +} diff --git a/src/libpgagroal/pipeline_perf.c b/src/libpgagroal/pipeline_perf.c new file mode 100644 index 00000000..a8a01bc9 --- /dev/null +++ b/src/libpgagroal/pipeline_perf.c @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include + +static int performance_initialize(void*, void**, size_t*); +static void performance_start(struct ev_loop* loop, struct worker_io*); +static void performance_client(struct ev_loop* loop, struct ev_io* watcher, int revents); +static void performance_server(struct ev_loop* loop, struct ev_io* watcher, int revents); +static void performance_stop(struct ev_loop* loop, struct worker_io*); +static void performance_destroy(void*, size_t); +static void performance_periodic(void); + +static bool saw_x = false; + +struct pipeline +performance_pipeline(void) +{ + struct pipeline pipeline; + + pipeline.initialize = &performance_initialize; + pipeline.start = &performance_start; + pipeline.client = &performance_client; + pipeline.server = &performance_server; + pipeline.stop = &performance_stop; + pipeline.destroy = &performance_destroy; + pipeline.periodic = &performance_periodic; + + return pipeline; +} + +static int +performance_initialize(void* shmem, void** pipeline_shmem, size_t* pipeline_shmem_size) +{ + return 0; +} + +static void +performance_start(struct ev_loop* loop, struct worker_io* w) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + for (int i = 0; i < config->max_connections; i++) + { + if (i != w->slot && !config->connections[i].new && config->connections[i].fd > 0) + { + pgagroal_disconnect(config->connections[i].fd); + } + } + + return; +} + +static void +performance_stop(struct ev_loop* loop, struct worker_io* w) +{ +} + +static void +performance_destroy(void* pipeline_shmem, size_t pipeline_shmem_size) +{ +} + +static void +performance_periodic(void) +{ +} + +static void +performance_client(struct ev_loop* loop, struct ev_io* watcher, int revents) +{ + int status = MESSAGE_STATUS_ERROR; + struct worker_io* wi = NULL; + struct message* msg = NULL; + struct main_configuration* config = NULL; + + wi = (struct worker_io*)watcher; + + status = pgagroal_read_socket_message(wi->client_fd, &msg); + if (likely(status == MESSAGE_STATUS_OK)) + { + if (likely(msg->kind != 'X')) + { + if (wi->server_ssl == NULL) + { + status = pgagroal_write_socket_message(wi->server_fd, msg); + } + else + { + status = pgagroal_write_ssl_message(wi->server_ssl, msg); + } + if (unlikely(status != MESSAGE_STATUS_OK)) + { + goto server_error; + } + } + else if (msg->kind == 'X') + { + saw_x = true; + running = 0; + } + } + else if (status == MESSAGE_STATUS_ZERO) + { + goto client_done; + } + else + { + goto client_error; + } + + ev_break (loop, EVBREAK_ONE); + return; + +client_done: + config = (struct main_configuration*)shmem; + pgagroal_log_debug("[C] Client done (slot %d database %s user %s): %s (socket %d status %d)", + wi->slot, config->connections[wi->slot].database, config->connections[wi->slot].username, + strerror(errno), wi->client_fd, status); + errno = 0; + + if (saw_x) + { + exit_code = WORKER_SUCCESS; + } + else + { + exit_code = WORKER_SERVER_FAILURE; + } + + running = 0; + ev_break(loop, EVBREAK_ALL); + return; + +client_error: + config = (struct main_configuration*)shmem; + pgagroal_log_warn("[C] Client error (slot %d database %s user %s): %s (socket %d status %d)", + wi->slot, config->connections[wi->slot].database, config->connections[wi->slot].username, + strerror(errno), wi->client_fd, status); + pgagroal_log_message(msg); + errno = 0; + + exit_code = WORKER_CLIENT_FAILURE; + running = 0; + ev_break(loop, EVBREAK_ALL); + return; + +server_error: + config = (struct main_configuration*)shmem; + pgagroal_log_warn("[C] Server error (slot %d database %s user %s): %s (socket %d status %d)", + wi->slot, config->connections[wi->slot].database, config->connections[wi->slot].username, + strerror(errno), wi->server_fd, status); + pgagroal_log_message(msg); + errno = 0; + + exit_code = WORKER_SERVER_FAILURE; + running = 0; + ev_break(loop, EVBREAK_ALL); + return; +} + +static void +performance_server(struct ev_loop* loop, struct ev_io* watcher, int revents) +{ + int status = MESSAGE_STATUS_ERROR; + bool fatal = false; + struct worker_io* wi = NULL; + struct message* msg = NULL; + struct main_configuration* config = NULL; + + wi = (struct worker_io*)watcher; + + if (wi->server_ssl == NULL) + { + status = pgagroal_read_socket_message(wi->server_fd, &msg); + } + else + { + status = pgagroal_read_ssl_message(wi->server_ssl, &msg); + } + if (likely(status == MESSAGE_STATUS_OK)) + { + status = pgagroal_write_socket_message(wi->client_fd, msg); + if (unlikely(status != MESSAGE_STATUS_OK)) + { + goto client_error; + } + + if (unlikely(msg->kind == 'E')) + { + fatal = false; + + if (!strncmp(msg->data + 6, "FATAL", 5) || !strncmp(msg->data + 6, "PANIC", 5)) + { + fatal = true; + } + + if (fatal) + { + exit_code = WORKER_SERVER_FATAL; + running = 0; + } + } + } + else if (status == MESSAGE_STATUS_ZERO) + { + goto server_done; + } + else + { + goto server_error; + } + + ev_break(loop, EVBREAK_ONE); + return; + +client_error: + config = (struct main_configuration*)shmem; + pgagroal_log_warn("[S] Client error (slot %d database %s user %s): %s (socket %d status %d)", + wi->slot, config->connections[wi->slot].database, config->connections[wi->slot].username, + strerror(errno), wi->client_fd, status); + pgagroal_log_message(msg); + errno = 0; + + exit_code = WORKER_CLIENT_FAILURE; + running = 0; + ev_break(loop, EVBREAK_ALL); + return; + +server_done: + config = (struct main_configuration*)shmem; + pgagroal_log_debug("[S] Server done (slot %d database %s user %s): %s (socket %d status %d)", + wi->slot, config->connections[wi->slot].database, config->connections[wi->slot].username, + strerror(errno), wi->server_fd, status); + errno = 0; + + running = 0; + ev_break(loop, EVBREAK_ALL); + return; + +server_error: + config = (struct main_configuration*)shmem; + pgagroal_log_warn("[S] Server error (slot %d database %s user %s): %s (socket %d status %d)", + wi->slot, config->connections[wi->slot].database, config->connections[wi->slot].username, + strerror(errno), wi->server_fd, status); + pgagroal_log_message(msg); + errno = 0; + + exit_code = WORKER_SERVER_FAILURE; + running = 0; + ev_break(loop, EVBREAK_ALL); + return; +} diff --git a/src/libpgagroal/pipeline_session.c b/src/libpgagroal/pipeline_session.c new file mode 100644 index 00000000..66136cf1 --- /dev/null +++ b/src/libpgagroal/pipeline_session.c @@ -0,0 +1,624 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include +#include + +static int session_initialize(void*, void**, size_t*); +static void session_start(struct ev_loop* loop, struct worker_io*); +static void session_client(struct ev_loop* loop, struct ev_io* watcher, int revents); +static void session_server(struct ev_loop* loop, struct ev_io* watcher, int revents); +static void session_stop(struct ev_loop* loop, struct worker_io*); +static void session_destroy(void*, size_t); +static void session_periodic(void); + +static bool in_tx; +static int next_client_message; +static int next_server_message; +static bool saw_x = false; + +#define CLIENT_INIT 0 +#define CLIENT_IDLE 1 +#define CLIENT_ACTIVE 2 +#define CLIENT_CHECK 3 + +struct client_session +{ + atomic_schar state; /**< The state */ + time_t timestamp; /**< The last used timestamp */ +}; + +static void client_active(int); +static void client_inactive(int); + +struct pipeline +session_pipeline(void) +{ + struct pipeline pipeline; + + pipeline.initialize = &session_initialize; + pipeline.start = &session_start; + pipeline.client = &session_client; + pipeline.server = &session_server; + pipeline.stop = &session_stop; + pipeline.destroy = &session_destroy; + pipeline.periodic = &session_periodic; + + return pipeline; +} + +static int +session_initialize(void* shmem, void** pipeline_shmem, size_t* pipeline_shmem_size) +{ + void* session_shmem = NULL; + size_t session_shmem_size; + struct client_session* client; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + *pipeline_shmem = NULL; + *pipeline_shmem_size = 0; + + if (config->disconnect_client > 0) + { + session_shmem_size = config->max_connections * sizeof(struct client_session); + if (pgagroal_create_shared_memory(session_shmem_size, config->common.hugepage, &session_shmem)) + { + return 1; + } + memset(session_shmem, 0, session_shmem_size); + + for (int i = 0; i < config->max_connections; i++) + { + client = session_shmem + (i * sizeof(struct client_session)); + + atomic_init(&client->state, CLIENT_INIT); + client->timestamp = time(NULL); + } + + *pipeline_shmem = session_shmem; + *pipeline_shmem_size = session_shmem_size; + } + + return 0; +} + +static void +session_start(struct ev_loop* loop, struct worker_io* w) +{ + struct client_session* client; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + in_tx = false; + next_client_message = 0; + next_server_message = 0; + + for (int i = 0; i < config->max_connections; i++) + { + if (i != w->slot && !config->connections[i].new && config->connections[i].fd > 0) + { + pgagroal_disconnect(config->connections[i].fd); + } + } + + if (pipeline_shmem != NULL) + { + client = pipeline_shmem + (w->slot * sizeof(struct client_session)); + + atomic_store(&client->state, CLIENT_IDLE); + client->timestamp = time(NULL); + } + + return; +} + +static void +session_stop(struct ev_loop* loop, struct worker_io* w) +{ + struct client_session* client; + + if (pipeline_shmem != NULL) + { + client = pipeline_shmem + (w->slot * sizeof(struct client_session)); + + atomic_store(&client->state, CLIENT_INIT); + client->timestamp = time(NULL); + } +} + +static void +session_destroy(void* pipeline_shmem, size_t pipeline_shmem_size) +{ + if (pipeline_shmem != NULL) + { + pgagroal_destroy_shared_memory(pipeline_shmem, pipeline_shmem_size); + } +} + +static void +session_periodic(void) +{ + signed char state; + signed char idle; + bool do_kill; + time_t now; + int ret; + signed char server; + int socket; + struct message* cancel_msg = NULL; + struct client_session* client; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + if (config->disconnect_client > 0 && pipeline_shmem != NULL) + { + now = time(NULL); + + for (int i = 0; i < config->max_connections; i++) + { + client = pipeline_shmem + (i * sizeof(struct client_session)); + + if (difftime(now, client->timestamp) > config->disconnect_client) + { + if (config->connections[i].pid != 0) + { + state = atomic_load(&client->state); + do_kill = false; + + if (config->disconnect_client_force) + { + do_kill = true; + } + else + { + idle = CLIENT_IDLE; + + if (atomic_compare_exchange_strong(&client->state, &idle, CLIENT_CHECK)) + { + do_kill = true; + } + } + + if (do_kill) + { + pgagroal_create_cancel_request_message(config->connections[i].backend_pid, config->connections[i].backend_secret, &cancel_msg); + + server = config->connections[i].server; + + if (config->servers[server].host[0] == '/') + { + char pgsql[MISC_LENGTH]; + + memset(&pgsql, 0, sizeof(pgsql)); + snprintf(&pgsql[0], sizeof(pgsql), ".s.PGSQL.%d", config->servers[server].port); + ret = pgagroal_connect_unix_socket(config->servers[server].host, &pgsql[0], &socket); + } + else + { + ret = pgagroal_connect(config->servers[server].host, config->servers[server].port, &socket, config->keep_alive, config->non_blocking, config->nodelay); + } + + if (ret == 0) + { + pgagroal_log_debug("Cancel request for %s/%s using slot %d (pid %d secret %d)", + config->connections[i].database, config->connections[i].username, + i, config->connections[i].backend_pid, config->connections[i].backend_secret); + + pgagroal_write_message(NULL, socket, cancel_msg); + } + + pgagroal_disconnect(socket); + + atomic_store(&config->states[i], STATE_GRACEFULLY); + + pgagroal_log_info("Disconnect client %s/%s using slot %d (pid %d socket %d)", + config->connections[i].database, config->connections[i].username, + i, config->connections[i].pid, config->connections[i].fd); + kill(config->connections[i].pid, SIGQUIT); + + pgagroal_free_copy_message(cancel_msg); + cancel_msg = NULL; + } + else + { + atomic_store(&client->state, state); + } + } + } + } + } + + exit(0); +} + +static void +session_client(struct ev_loop* loop, struct ev_io* watcher, int revents) +{ + int status = MESSAGE_STATUS_ERROR; + struct worker_io* wi = NULL; + struct message* msg = NULL; + struct main_configuration* config = NULL; + + wi = (struct worker_io*)watcher; + config = (struct main_configuration*)shmem; + + client_active(wi->slot); + + if (wi->client_ssl == NULL) + { + status = pgagroal_read_socket_message(wi->client_fd, &msg); + } + else + { + status = pgagroal_read_ssl_message(wi->client_ssl, &msg); + } + if (likely(status == MESSAGE_STATUS_OK)) + { + pgagroal_prometheus_network_sent_add(msg->length); + + if (likely(msg->kind != 'X')) + { + int offset = 0; + + while (offset < msg->length) + { + if (next_client_message == 0) + { + char kind = pgagroal_read_byte(msg->data + offset); + int length = pgagroal_read_int32(msg->data + offset + 1); + + /* The Q and E message tell us the execute of the simple query and the prepared statement */ + if (kind == 'Q' || kind == 'E') + { + pgagroal_prometheus_query_count_add(); + pgagroal_prometheus_query_count_specified_add(wi->slot); + } + + /* Calculate the offset to the next message */ + if (offset + length + 1 <= msg->length) + { + next_client_message = 0; + offset += length + 1; + } + else + { + next_client_message = length + 1 - (msg->length - offset); + offset = msg->length; + } + } + else + { + offset = MIN(next_client_message, msg->length); + next_client_message -= offset; + } + } + + if (wi->server_ssl == NULL) + { + status = pgagroal_write_socket_message(wi->server_fd, msg); + } + else + { + status = pgagroal_write_ssl_message(wi->server_ssl, msg); + } + if (unlikely(status == MESSAGE_STATUS_ERROR)) + { + if (config->failover) + { + pgagroal_server_failover(wi->slot); + pgagroal_write_client_failover(wi->client_ssl, wi->client_fd); + pgagroal_prometheus_failed_servers(); + + goto failover; + } + else + { + goto server_error; + } + } + } + else if (msg->kind == 'X') + { + saw_x = true; + running = 0; + } + } + else if (status == MESSAGE_STATUS_ZERO) + { + goto client_done; + } + else + { + goto client_error; + } + + client_inactive(wi->slot); + + ev_break(loop, EVBREAK_ONE); + return; + +client_done: + pgagroal_log_debug("[C] Client done (slot %d database %s user %s): %s (socket %d status %d)", + wi->slot, config->connections[wi->slot].database, config->connections[wi->slot].username, + strerror(errno), wi->client_fd, status); + errno = 0; + + client_inactive(wi->slot); + + if (saw_x) + { + exit_code = WORKER_SUCCESS; + } + else + { + exit_code = WORKER_SERVER_FAILURE; + } + + running = 0; + ev_break(loop, EVBREAK_ALL); + return; + +client_error: + pgagroal_log_warn("[C] Client error (slot %d database %s user %s): %s (socket %d status %d)", + wi->slot, config->connections[wi->slot].database, config->connections[wi->slot].username, + strerror(errno), wi->client_fd, status); + pgagroal_log_message(msg); + errno = 0; + + client_inactive(wi->slot); + + exit_code = WORKER_CLIENT_FAILURE; + running = 0; + ev_break(loop, EVBREAK_ALL); + return; + +server_error: + pgagroal_log_warn("[C] Server error (slot %d database %s user %s): %s (socket %d status %d)", + wi->slot, config->connections[wi->slot].database, config->connections[wi->slot].username, + strerror(errno), wi->server_fd, status); + pgagroal_log_message(msg); + errno = 0; + + client_inactive(wi->slot); + + exit_code = WORKER_SERVER_FAILURE; + running = 0; + ev_break(loop, EVBREAK_ALL); + return; + +failover: + + client_inactive(wi->slot); + + exit_code = WORKER_FAILOVER; + running = 0; + ev_break(loop, EVBREAK_ALL); + return; +} + +static void +session_server(struct ev_loop* loop, struct ev_io* watcher, int revents) +{ + int status = MESSAGE_STATUS_ERROR; + bool fatal = false; + struct worker_io* wi = NULL; + struct message* msg = NULL; + struct main_configuration* config = NULL; + + wi = (struct worker_io*)watcher; + + client_active(wi->slot); + + if (wi->server_ssl == NULL) + { + status = pgagroal_read_socket_message(wi->server_fd, &msg); + } + else + { + status = pgagroal_read_ssl_message(wi->server_ssl, &msg); + } + if (likely(status == MESSAGE_STATUS_OK)) + { + pgagroal_prometheus_network_received_add(msg->length); + + int offset = 0; + + while (offset < msg->length) + { + if (next_server_message == 0) + { + char kind = pgagroal_read_byte(msg->data + offset); + int length = pgagroal_read_int32(msg->data + offset + 1); + + /* The Z message tell us the transaction state */ + if (kind == 'Z') + { + char tx_state = pgagroal_read_byte(msg->data + offset + 5); + + if (tx_state != 'I' && !in_tx) + { + pgagroal_prometheus_tx_count_add(); + } + + in_tx = tx_state != 'I'; + } + + /* Calculate the offset to the next message */ + if (offset + length + 1 <= msg->length) + { + next_server_message = 0; + offset += length + 1; + } + else + { + next_server_message = length + 1 - (msg->length - offset); + offset = msg->length; + } + } + else + { + offset = MIN(next_server_message, msg->length); + next_server_message -= offset; + } + } + if (wi->client_ssl == NULL) + { + status = pgagroal_write_socket_message(wi->client_fd, msg); + } + else + { + status = pgagroal_write_ssl_message(wi->client_ssl, msg); + } + if (unlikely(status != MESSAGE_STATUS_OK)) + { + goto client_error; + } + + if (unlikely(msg->kind == 'E')) + { + fatal = false; + + if (!strncmp(msg->data + 6, "FATAL", 5) || !strncmp(msg->data + 6, "PANIC", 5)) + { + fatal = true; + } + + if (fatal) + { + exit_code = WORKER_SERVER_FATAL; + running = 0; + } + } + } + else if (status == MESSAGE_STATUS_ZERO) + { + goto server_done; + } + else + { + goto server_error; + } + + client_inactive(wi->slot); + + ev_break(loop, EVBREAK_ONE); + return; + +client_error: + config = (struct main_configuration*)shmem; + pgagroal_log_warn("[S] Client error (slot %d database %s user %s): %s (socket %d status %d)", + wi->slot, config->connections[wi->slot].database, config->connections[wi->slot].username, + strerror(errno), wi->client_fd, status); + pgagroal_log_message(msg); + errno = 0; + + client_inactive(wi->slot); + + exit_code = WORKER_CLIENT_FAILURE; + running = 0; + ev_break(loop, EVBREAK_ALL); + return; + +server_done: + config = (struct main_configuration*)shmem; + pgagroal_log_debug("[S] Server done (slot %d database %s user %s): %s (socket %d status %d)", + wi->slot, config->connections[wi->slot].database, config->connections[wi->slot].username, + strerror(errno), wi->server_fd, status); + errno = 0; + + client_inactive(wi->slot); + + running = 0; + ev_break(loop, EVBREAK_ALL); + return; + +server_error: + config = (struct main_configuration*)shmem; + pgagroal_log_warn("[S] Server error (slot %d database %s user %s): %s (socket %d status %d)", + wi->slot, config->connections[wi->slot].database, config->connections[wi->slot].username, + strerror(errno), wi->server_fd, status); + pgagroal_log_message(msg); + errno = 0; + + client_inactive(wi->slot); + + exit_code = WORKER_SERVER_FAILURE; + running = 0; + ev_break(loop, EVBREAK_ALL); + return; +} + +static void +client_active(int slot) +{ + struct client_session* client; + + if (pipeline_shmem != NULL) + { + client = pipeline_shmem + (slot * sizeof(struct client_session)); + atomic_store(&client->state, CLIENT_ACTIVE); + client->timestamp = time(NULL); + } +} + +static void +client_inactive(int slot) +{ + struct client_session* client; + + if (pipeline_shmem != NULL) + { + client = pipeline_shmem + (slot * sizeof(struct client_session)); + atomic_store(&client->state, CLIENT_IDLE); + client->timestamp = time(NULL); + } +} diff --git a/src/libpgagroal/pipeline_transaction.c b/src/libpgagroal/pipeline_transaction.c new file mode 100644 index 00000000..f874a84e --- /dev/null +++ b/src/libpgagroal/pipeline_transaction.c @@ -0,0 +1,677 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +//#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include + +static int transaction_initialize(void*, void**, size_t*); +static void transaction_start(struct ev_loop* loop, struct worker_io*); +static void transaction_client(struct ev_loop* loop, struct ev_io* watcher, int revents); +static void transaction_server(struct ev_loop* loop, struct ev_io* watcher, int revents); +static void transaction_stop(struct ev_loop* loop, struct worker_io*); +static void transaction_destroy(void*, size_t); +static void transaction_periodic(void); + +static void start_mgt(struct ev_loop* loop); +static void shutdown_mgt(struct ev_loop* loop); +static void accept_cb(struct ev_loop* loop, struct ev_io* watcher, int revents); + +static int slot; +static char username[MAX_USERNAME_LENGTH]; +static char database[MAX_DATABASE_LENGTH]; +static char appname[MAX_APPLICATION_NAME]; +static bool in_tx; +static int next_client_message; +static int next_server_message; +static int unix_socket = -1; +static int deallocate; +static bool fatal; +static int fds[MAX_NUMBER_OF_CONNECTIONS]; +static bool saw_x = false; +static struct ev_io io_mgt; +static struct worker_io server_io; + +struct pipeline +transaction_pipeline(void) +{ + struct pipeline pipeline; + + pipeline.initialize = &transaction_initialize; + pipeline.start = &transaction_start; + pipeline.client = &transaction_client; + pipeline.server = &transaction_server; + pipeline.stop = &transaction_stop; + pipeline.destroy = &transaction_destroy; + pipeline.periodic = &transaction_periodic; + + return pipeline; +} + +static int +transaction_initialize(void* shmem, void** pipeline_shmem, size_t* pipeline_shmem_size) +{ + return 0; +} + +static void +transaction_start(struct ev_loop* loop, struct worker_io* w) +{ + char p[MISC_LENGTH]; + bool is_new; + struct main_configuration* config = NULL; + + config = (struct main_configuration*)shmem; + + slot = -1; + memcpy(&username[0], config->connections[w->slot].username, MAX_USERNAME_LENGTH); + memcpy(&database[0], config->connections[w->slot].database, MAX_DATABASE_LENGTH); + memcpy(&appname[0], config->connections[w->slot].appname, MAX_APPLICATION_NAME); + in_tx = false; + next_client_message = 0; + next_server_message = 0; + deallocate = false; + + memset(&p, 0, sizeof(p)); + snprintf(&p[0], sizeof(p), ".s.pgagroal.%d", getpid()); + + if (pgagroal_bind_unix_socket(config->unix_socket_dir, &p[0], &unix_socket)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s/%s", config->unix_socket_dir, &p[0]); + goto error; + } + + for (int i = 0; i < config->max_connections; i++) + { + fds[i] = config->connections[i].fd; + } + + start_mgt(loop); + + pgagroal_tracking_event_slot(TRACKER_TX_RETURN_CONNECTION_START, w->slot); + + is_new = config->connections[w->slot].new; + pgagroal_return_connection(w->slot, w->server_ssl, true); + + w->server_fd = -1; + w->slot = -1; + + if (is_new) + { + /* Sleep for 5ms */ + SLEEP(5000000L) + } + + return; + +error: + + exit_code = WORKER_FAILURE; + running = 0; + ev_break(loop, EVBREAK_ALL); + return; +} + +static void +transaction_stop(struct ev_loop* loop, struct worker_io* w) +{ + if (slot != -1) + { + struct main_configuration* config = NULL; + + config = (struct main_configuration*)shmem; + + /* We are either in 'X' or the client terminated (consider cancel query) */ + if (in_tx) + { + /* ROLLBACK */ + pgagroal_write_rollback(NULL, config->connections[slot].fd); + } + + ev_io_stop(loop, (struct ev_io*)&server_io); + pgagroal_tracking_event_slot(TRACKER_TX_RETURN_CONNECTION_STOP, w->slot); + pgagroal_return_connection(slot, w->server_ssl, true); + slot = -1; + } + + shutdown_mgt(loop); +} + +static void +transaction_destroy(void* pipeline_shmem, size_t pipeline_shmem_size) +{ +} + +static void +transaction_periodic(void) +{ +} + +static void +transaction_client(struct ev_loop* loop, struct ev_io* watcher, int revents) +{ + int status = MESSAGE_STATUS_ERROR; + SSL* s_ssl = NULL; + struct worker_io* wi = NULL; + struct message* msg = NULL; + struct main_configuration* config = NULL; + + wi = (struct worker_io*)watcher; + config = (struct main_configuration*)shmem; + + /* We can't use the information from wi except from client_fd/client_ssl */ + if (slot == -1) + { + pgagroal_tracking_event_basic(TRACKER_TX_GET_CONNECTION, &username[0], &database[0]); + if (pgagroal_get_connection(&username[0], &database[0], true, true, &slot, &s_ssl)) + { + pgagroal_write_pool_full(wi->client_ssl, wi->client_fd); + goto get_error; + } + + wi->server_fd = config->connections[slot].fd; + wi->server_ssl = s_ssl; + wi->slot = slot; + + memcpy(&config->connections[slot].appname[0], &appname[0], MAX_APPLICATION_NAME); + + ev_io_init((struct ev_io*)&server_io, transaction_server, config->connections[slot].fd, EV_READ); + server_io.client_fd = wi->client_fd; + server_io.server_fd = config->connections[slot].fd; + server_io.slot = slot; + server_io.client_ssl = wi->client_ssl; + server_io.server_ssl = wi->server_ssl; + + fatal = false; + + ev_io_start(loop, (struct ev_io*)&server_io); + } + + if (wi->client_ssl == NULL) + { + status = pgagroal_read_socket_message(wi->client_fd, &msg); + } + else + { + status = pgagroal_read_ssl_message(wi->client_ssl, &msg); + } + if (likely(status == MESSAGE_STATUS_OK)) + { + pgagroal_prometheus_network_sent_add(msg->length); + + if (likely(msg->kind != 'X')) + { + int offset = 0; + + while (offset < msg->length) + { + if (next_client_message == 0) + { + char kind = pgagroal_read_byte(msg->data + offset); + int length = pgagroal_read_int32(msg->data + offset + 1); + + if (config->track_prepared_statements) + { + /* The P message tell us the prepared statement */ + if (kind == 'P') + { + char* ps = pgagroal_read_string(msg->data + offset + 5); + if (strcmp(ps, "")) + { + deallocate = true; + } + } + } + + /* The Q and E message tell us the execute of the simple query and the prepared statement */ + if (kind == 'Q' || kind == 'E') + { + pgagroal_prometheus_query_count_add(); + pgagroal_prometheus_query_count_specified_add(wi->slot); + } + + /* Calculate the offset to the next message */ + if (offset + length + 1 <= msg->length) + { + next_client_message = 0; + offset += length + 1; + } + else + { + next_client_message = length + 1 - (msg->length - offset); + offset = msg->length; + } + } + else + { + offset = MIN(next_client_message, msg->length); + next_client_message -= offset; + } + } + + if (wi->server_ssl == NULL) + { + status = pgagroal_write_socket_message(wi->server_fd, msg); + } + else + { + status = pgagroal_write_ssl_message(wi->server_ssl, msg); + } + if (unlikely(status == MESSAGE_STATUS_ERROR)) + { + if (config->failover) + { + pgagroal_server_failover(slot); + pgagroal_write_client_failover(wi->client_ssl, wi->client_fd); + pgagroal_prometheus_failed_servers(); + + goto failover; + } + else + { + goto server_error; + } + } + } + else if (msg->kind == 'X') + { + saw_x = true; + running = 0; + } + } + else if (status == MESSAGE_STATUS_ZERO) + { + goto client_done; + } + else + { + goto client_error; + } + + ev_break(loop, EVBREAK_ONE); + return; + +client_done: + pgagroal_log_debug("[C] Client done (slot %d database %s user %s): %s (socket %d status %d)", + wi->slot, config->connections[wi->slot].database, config->connections[wi->slot].username, + strerror(errno), wi->client_fd, status); + errno = 0; + + if (saw_x) + { + exit_code = WORKER_SUCCESS; + } + else + { + exit_code = WORKER_SERVER_FAILURE; + } + + running = 0; + ev_break(loop, EVBREAK_ALL); + return; + +client_error: + pgagroal_log_warn("[C] Client error (slot %d database %s user %s): %s (socket %d status %d)", + wi->slot, config->connections[wi->slot].database, config->connections[wi->slot].username, + strerror(errno), wi->client_fd, status); + pgagroal_log_message(msg); + errno = 0; + + exit_code = WORKER_CLIENT_FAILURE; + running = 0; + ev_break(loop, EVBREAK_ALL); + return; + +server_error: + pgagroal_log_warn("[C] Server error (slot %d database %s user %s): %s (socket %d status %d)", + wi->slot, config->connections[wi->slot].database, config->connections[wi->slot].username, + strerror(errno), wi->server_fd, status); + pgagroal_log_message(msg); + errno = 0; + + exit_code = WORKER_SERVER_FAILURE; + running = 0; + ev_break(loop, EVBREAK_ALL); + return; + +failover: + + exit_code = WORKER_FAILOVER; + running = 0; + ev_break(loop, EVBREAK_ALL); + return; + +get_error: + pgagroal_log_warn("Failure during obtaining connection"); + + exit_code = WORKER_SERVER_FAILURE; + running = 0; + ev_break(loop, EVBREAK_ALL); + return; +} + +static void +transaction_server(struct ev_loop* loop, struct ev_io* watcher, int revents) +{ + int status = MESSAGE_STATUS_ERROR; + bool has_z = false; + struct worker_io* wi = NULL; + struct message* msg = NULL; + struct main_configuration* config = NULL; + + wi = (struct worker_io*)watcher; + config = (struct main_configuration*)shmem; + + /* We can't use the information from wi except from client_fd/client_ssl */ + wi->server_fd = config->connections[slot].fd; + wi->slot = slot; + + if (!pgagroal_socket_isvalid(wi->client_fd)) + { + goto client_error; + } + + if (wi->server_ssl == NULL) + { + status = pgagroal_read_socket_message(wi->server_fd, &msg); + } + else + { + status = pgagroal_read_ssl_message(wi->server_ssl, &msg); + } + if (likely(status == MESSAGE_STATUS_OK)) + { + pgagroal_prometheus_network_received_add(msg->length); + + int offset = 0; + + while (offset < msg->length) + { + if (next_server_message == 0) + { + char kind = pgagroal_read_byte(msg->data + offset); + int length = pgagroal_read_int32(msg->data + offset + 1); + + /* The Z message tell us the transaction state */ + if (kind == 'Z') + { + char tx_state = pgagroal_read_byte(msg->data + offset + 5); + + has_z = true; + + if (tx_state != 'I' && !in_tx) + { + pgagroal_prometheus_tx_count_add(); + } + + in_tx = tx_state != 'I'; + } + + /* Calculate the offset to the next message */ + if (offset + length + 1 <= msg->length) + { + next_server_message = 0; + offset += length + 1; + } + else + { + next_server_message = length + 1 - (msg->length - offset); + offset = msg->length; + } + } + else + { + offset = MIN(next_server_message, msg->length); + next_server_message -= offset; + } + } + + if (wi->client_ssl == NULL) + { + status = pgagroal_write_socket_message(wi->client_fd, msg); + } + else + { + status = pgagroal_write_ssl_message(wi->client_ssl, msg); + } + if (unlikely(status != MESSAGE_STATUS_OK)) + { + goto client_error; + } + + if (unlikely(msg->kind == 'E')) + { + if (!strncmp(msg->data + 6, "FATAL", 5) || !strncmp(msg->data + 6, "PANIC", 5)) + { + fatal = true; + } + } + + if (!fatal) + { + if (has_z && !in_tx && slot != -1) + { + ev_io_stop(loop, (struct ev_io*)&server_io); + + if (deallocate) + { + pgagroal_write_deallocate_all(wi->server_ssl, wi->server_fd); + deallocate = false; + } + + pgagroal_tracking_event_slot(TRACKER_TX_RETURN_CONNECTION, slot); + if (pgagroal_return_connection(slot, wi->server_ssl, true)) + { + goto return_error; + } + + slot = -1; + } + } + else + { + if (has_z && !in_tx && slot != -1) + { + ev_io_stop(loop, (struct ev_io*)&server_io); + + exit_code = WORKER_SERVER_FATAL; + running = 0; + } + } + } + else if (status == MESSAGE_STATUS_ZERO) + { + goto server_done; + } + else + { + goto server_error; + } + + ev_break(loop, EVBREAK_ONE); + return; + +client_error: + pgagroal_log_warn("[S] Client error (slot %d database %s user %s): %s (socket %d status %d)", + wi->slot, config->connections[wi->slot].database, config->connections[wi->slot].username, + strerror(errno), wi->client_fd, status); + pgagroal_log_message(msg); + errno = 0; + + exit_code = WORKER_CLIENT_FAILURE; + running = 0; + ev_break(loop, EVBREAK_ALL); + return; + +server_done: + pgagroal_log_debug("[S] Server done (slot %d database %s user %s): %s (socket %d status %d)", + wi->slot, config->connections[wi->slot].database, config->connections[wi->slot].username, + strerror(errno), wi->server_fd, status); + errno = 0; + + running = 0; + ev_break(loop, EVBREAK_ALL); + return; + +server_error: + pgagroal_log_warn("[S] Server error (slot %d database %s user %s): %s (socket %d status %d)", + wi->slot, config->connections[wi->slot].database, config->connections[wi->slot].username, + strerror(errno), wi->server_fd, status); + pgagroal_log_message(msg); + errno = 0; + + exit_code = WORKER_SERVER_FAILURE; + running = 0; + ev_break(loop, EVBREAK_ALL); + return; + +return_error: + pgagroal_log_warn("Failure during connection return"); + + exit_code = WORKER_SERVER_FAILURE; + running = 0; + ev_break(loop, EVBREAK_ALL); + return; +} + +static void +start_mgt(struct ev_loop* loop) +{ + memset(&io_mgt, 0, sizeof(struct ev_io)); + ev_io_init(&io_mgt, accept_cb, unix_socket, EV_READ); + ev_io_start(loop, &io_mgt); +} + +static void +shutdown_mgt(struct ev_loop* loop) +{ + char p[MISC_LENGTH]; + struct main_configuration* config = NULL; + + config = (struct main_configuration*)shmem; + + memset(&p, 0, sizeof(p)); + snprintf(&p[0], sizeof(p), ".s.%d", getpid()); + + ev_io_stop(loop, &io_mgt); + pgagroal_disconnect(unix_socket); + errno = 0; + pgagroal_remove_unix_socket(config->unix_socket_dir, &p[0]); + errno = 0; +} + +static void +accept_cb(struct ev_loop* loop, struct ev_io* watcher, int revents) +{ + struct sockaddr_in client_addr; + socklen_t client_addr_length; + int client_fd = -1; + int id = -1; + int32_t slot = -1; + int fd = -1; + struct main_configuration* config = NULL; + + config = (struct main_configuration*)shmem; + + if (EV_ERROR & revents) + { + pgagroal_log_debug("accept_cb: invalid event: %s", strerror(errno)); + errno = 0; + return; + } + + client_addr_length = sizeof(client_addr); + client_fd = accept(watcher->fd, (struct sockaddr*)&client_addr, &client_addr_length); + if (client_fd == -1) + { + pgagroal_log_debug("accept: %s (%d)", strerror(errno), watcher->fd); + errno = 0; + return; + } + + /* Process management request */ + if (pgagroal_connection_id_read(client_fd, &id)) + { + pgagroal_log_error("pgagroal: Management client: ID: %d", id); + goto done; + } + + if (id == CONNECTION_CLIENT_FD) + { + if (pgagroal_connection_transfer_read(client_fd, &slot, &fd)) + { + pgagroal_log_error("pgagroal: Management client_fd: ID: %d Slot %d FD %d", id, slot, fd); + goto done; + } + + fds[slot] = fd; + } + else if (id == CONNECTION_REMOVE_FD) + { + if (pgagroal_connection_transfer_read(client_fd, &slot, &fd)) + { + pgagroal_log_error("pgagroal: Management remove_fd: ID: %d Slot %d FD %d", id, slot, fd); + goto done; + } + + if (fds[slot] == fd && !config->connections[slot].new && config->connections[slot].fd > 0) + { + pgagroal_disconnect(fd); + fds[slot] = 0; + } + } + else + { + pgagroal_log_debug("pgagroal: Unsupported management id: %d", id); + } + +done: + + pgagroal_disconnect(client_fd); +} diff --git a/src/libpgagroal/pool.c b/src/libpgagroal/pool.c new file mode 100644 index 00000000..c4492245 --- /dev/null +++ b/src/libpgagroal/pool.c @@ -0,0 +1,1597 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int find_best_rule(char* username, char* database); +static bool remove_connection(char* username, char* database); +static void connection_details(int slot); +static bool do_prefill(char* username, char* database, int size); + +int +pgagroal_get_connection(char* username, char* database, bool reuse, bool transaction_mode, int* slot, SSL** ssl) +{ + bool do_init; + bool has_lock; + int connections; + signed char not_init; + signed char free; + int server; + int fd; + time_t start_time; + int best_rule; + int retries; + int ret; + + struct main_configuration* config; + struct main_prometheus* prometheus; + + config = (struct main_configuration*)shmem; + prometheus = (struct main_prometheus*)prometheus_shmem; + + pgagroal_prometheus_connection_get(); + + best_rule = find_best_rule(username, database); + retries = 0; + start_time = time(NULL); + pgagroal_prometheus_connection_awaiting(best_rule); + +start: + + *slot = -1; + *ssl = NULL; + do_init = false; + has_lock = false; + + if (best_rule >= 0) + { + connections = atomic_fetch_add(&config->limits[best_rule].active_connections, 1); + if (connections >= config->limits[best_rule].max_size) + { + goto retry; + } + } + + connections = atomic_fetch_add(&config->active_connections, 1); + has_lock = true; + if (connections >= config->max_connections) + { + goto retry; + } + + /* Try and find an existing free connection */ + if (reuse) + { + for (int i = 0; *slot == -1 && i < config->max_connections; i++) + { + free = STATE_FREE; + + if (atomic_compare_exchange_strong(&config->states[i], &free, STATE_IN_USE)) + { + if (best_rule == config->connections[i].limit_rule && + !strcmp((const char*)(&config->connections[i].username), username) && + !strcmp((const char*)(&config->connections[i].database), database)) + { + *slot = i; + } + else + { + atomic_store(&config->states[i], STATE_FREE); + } + } + } + } + + if (*slot == -1 && !transaction_mode) + { + /* Ok, try and create a new connection */ + for (int i = 0; *slot == -1 && i < config->max_connections; i++) + { + not_init = STATE_NOTINIT; + + if (atomic_compare_exchange_strong(&config->states[i], ¬_init, STATE_INIT)) + { + *slot = i; + do_init = true; + } + } + } + + if (*slot != -1) + { + config->connections[*slot].limit_rule = best_rule; + config->connections[*slot].pid = getpid(); + + if (do_init) + { + /* We need to find the server for the connection */ + if (pgagroal_get_primary(&server)) + { + config->connections[*slot].limit_rule = -1; + config->connections[*slot].pid = -1; + atomic_store(&config->states[*slot], STATE_NOTINIT); + + if (!fork()) + { + pgagroal_flush(FLUSH_GRACEFULLY, "*"); + exit(0); + } + + goto error; + } + + pgagroal_log_debug("connect: server %d", server); + + if (config->servers[server].host[0] == '/') + { + char pgsql[MISC_LENGTH]; + + memset(&pgsql, 0, sizeof(pgsql)); + snprintf(&pgsql[0], sizeof(pgsql), ".s.PGSQL.%d", config->servers[server].port); + ret = pgagroal_connect_unix_socket(config->servers[server].host, &pgsql[0], &fd); + } + else + { + ret = pgagroal_connect(config->servers[server].host, config->servers[server].port, &fd, config->keep_alive, config->non_blocking, config->nodelay); + } + + if (ret) + { + pgagroal_log_error("pgagroal: No connection to %s:%d", config->servers[server].host, config->servers[server].port); + config->connections[*slot].limit_rule = -1; + config->connections[*slot].pid = -1; + atomic_store(&config->states[*slot], STATE_NOTINIT); + + pgagroal_prometheus_server_error(server); + + if (!fork()) + { + pgagroal_flush_server(server); + } + + if (config->failover) + { + pgagroal_server_force_failover(server); + pgagroal_prometheus_failed_servers(); + goto retry; + } + + goto error; + } + + pgagroal_log_debug("connect: %s:%d using slot %d fd %d", config->servers[server].host, config->servers[server].port, *slot, fd); + + config->connections[*slot].server = server; + + memset(&config->connections[*slot].username, 0, MAX_USERNAME_LENGTH); + memcpy(&config->connections[*slot].username, username, MIN(strlen(username), MAX_USERNAME_LENGTH - 1)); + + memset(&config->connections[*slot].database, 0, MAX_DATABASE_LENGTH); + memcpy(&config->connections[*slot].database, database, MIN(strlen(database), MAX_DATABASE_LENGTH - 1)); + + config->connections[*slot].has_security = SECURITY_INVALID; + config->connections[*slot].fd = fd; + + atomic_store(&config->states[*slot], STATE_IN_USE); + } + else + { + bool kill = false; + + /* Verify the socket for the slot */ + if (!pgagroal_socket_isvalid(config->connections[*slot].fd)) + { + if (!transaction_mode) + { + kill = true; + } + else + { + atomic_store(&config->states[*slot], STATE_FREE); + goto retry; + } + } + + if (!kill && config->validation == VALIDATION_FOREGROUND) + { + kill = !pgagroal_connection_isvalid(config->connections[*slot].fd); + } + + if (kill) + { + int status; + + pgagroal_log_debug("pgagroal_get_connection: Slot %d FD %d - Error", *slot, config->connections[*slot].fd); + pgagroal_tracking_event_slot(TRACKER_BAD_CONNECTION, *slot); + status = pgagroal_kill_connection(*slot, *ssl); + + pgagroal_prefill_if_can(true, false); + + if (status == 0) + { + goto retry2; + } + else + { + goto timeout; + } + } + } + + if (config->connections[*slot].start_time == -1) + { + config->connections[*slot].start_time = time(NULL); + } + + config->connections[*slot].timestamp = time(NULL); + + if (config->common.metrics > 0) + { + atomic_store(&prometheus->client_wait_time, difftime(time(NULL), start_time)); + } + pgagroal_prometheus_connection_success(); + pgagroal_tracking_event_slot(TRACKER_GET_CONNECTION_SUCCESS, *slot); + pgagroal_prometheus_connection_unawaiting(best_rule); + return 0; + } + else + { +retry: + if (best_rule >= 0) + { + atomic_fetch_sub(&config->limits[best_rule].active_connections, 1); + } + if (has_lock) + { + atomic_fetch_sub(&config->active_connections, 1); + } +retry2: + if (config->blocking_timeout > 0) + { + /* Sleep for 500ms */ + SLEEP(500000000L) + + double diff = difftime(time(NULL), start_time); + if (diff >= (double)config->blocking_timeout) + { + goto timeout; + } + + if (best_rule == -1) + { + remove_connection(username, database); + } + + goto start; + } + else + { + if (!transaction_mode) + { + if (best_rule == -1) + { + if (remove_connection(username, database)) + { + if (retries < config->max_retries) + { + retries++; + goto start; + } + } + } + else + { + if (retries < config->max_retries) + { + retries++; + goto start; + } + } + } + else + /* Sleep for 1000 nanos */ + { + SLEEP_AND_GOTO(1000L, start) + } + + } + } + +timeout: + if (config->common.metrics > 0) + { + atomic_store(&prometheus->client_wait_time, difftime(time(NULL), start_time)); + } + pgagroal_prometheus_connection_timeout(); + pgagroal_tracking_event_basic(TRACKER_GET_CONNECTION_TIMEOUT, username, database); + pgagroal_prometheus_connection_unawaiting(best_rule); + return 1; + +error: + if (best_rule >= 0) + { + atomic_fetch_sub(&config->limits[best_rule].active_connections, 1); + } + atomic_fetch_sub(&config->active_connections, 1); + if (config->common.metrics > 0) + { + atomic_store(&prometheus->client_wait_time, difftime(time(NULL), start_time)); + } + pgagroal_prometheus_connection_error(); + pgagroal_prometheus_connection_unawaiting(best_rule); + pgagroal_tracking_event_basic(TRACKER_GET_CONNECTION_ERROR, username, database); + + return 2; +} + +int +pgagroal_return_connection(int slot, SSL* ssl, bool transaction_mode) +{ + int state; + struct main_configuration* config; + time_t now; + signed char in_use; + signed char age_check; + int transfer_fd = -1; + + config = (struct main_configuration*)shmem; + + /* Kill the connection, if it lives longer than max_connection_age */ + if (config->max_connection_age > 0) + { + now = time(NULL); + in_use = STATE_IN_USE; + age_check = STATE_MAX_CONNECTION_AGE; + if (atomic_compare_exchange_strong(&config->states[slot], &in_use, age_check)) + { + double age = difftime(now, config->connections[slot].start_time); + if ((age >= (double) config->max_connection_age && !config->connections[slot].tx_mode) || + !atomic_compare_exchange_strong(&config->states[slot], &age_check, STATE_IN_USE)) + { + pgagroal_prometheus_connection_max_connection_age(); + pgagroal_tracking_event_slot(TRACKER_MAX_CONNECTION_AGE, slot); + goto kill_connection; + } + } + } + + /* Verify the socket for the slot */ + if (!transaction_mode && !pgagroal_socket_isvalid(config->connections[slot].fd)) + { + pgagroal_log_debug("pgagroal_return_connection: Slot %d FD %d - Error", slot, config->connections[slot].fd); + config->connections[slot].has_security = SECURITY_INVALID; + } + + /* Can we cache this connection ? */ + if (config->connections[slot].has_security != SECURITY_INVALID && + (config->connections[slot].has_security != SECURITY_SCRAM256 || + (config->connections[slot].has_security == SECURITY_SCRAM256 && + (config->authquery || pgagroal_user_known(config->connections[slot].username)))) && + ssl == NULL) + { + state = atomic_load(&config->states[slot]); + + /* Return the connection, if not GRACEFULLY */ + if (state == STATE_IN_USE) + { + pgagroal_log_debug("pgagroal_return_connection: Slot %d FD %d", slot, config->connections[slot].fd); + + if (!transaction_mode) + { + if (pgagroal_write_discard_all(ssl, config->connections[slot].fd)) + { + goto kill_connection; + } + } + + pgagroal_tracking_event_slot(TRACKER_RETURN_CONNECTION_SUCCESS, slot); + + config->connections[slot].timestamp = time(NULL); + + if (config->connections[slot].new) + { + if (pgagroal_connection_get(&transfer_fd)) + { + goto kill_connection; + } + + if (pgagroal_connection_id_write(transfer_fd, CONNECTION_TRANSFER)) + { + goto kill_connection; + } + + if (pgagroal_connection_transfer_write(transfer_fd, slot)) + { + goto kill_connection; + } + + pgagroal_disconnect(transfer_fd); + transfer_fd = -1; + } + + if (pgagroal_connection_get(&transfer_fd)) + { + goto kill_connection; + } + + if (pgagroal_connection_id_write(transfer_fd, CONNECTION_RETURN)) + { + goto kill_connection; + } + + if (pgagroal_connection_slot_write(transfer_fd, slot)) + { + goto kill_connection; + } + + pgagroal_disconnect(transfer_fd); + transfer_fd = -1; + + if (config->connections[slot].limit_rule >= 0) + { + atomic_fetch_sub(&config->limits[config->connections[slot].limit_rule].active_connections, 1); + } + + config->connections[slot].new = false; + config->connections[slot].pid = -1; + config->connections[slot].tx_mode = transaction_mode; + memset(&config->connections[slot].appname, 0, sizeof(config->connections[slot].appname)); + atomic_store(&config->states[slot], STATE_FREE); + atomic_fetch_sub(&config->active_connections, 1); + + pgagroal_prometheus_connection_return(); + + return 0; + } + else if (state == STATE_GRACEFULLY) + { + pgagroal_write_terminate(ssl, config->connections[slot].fd); + } + } + +kill_connection: + + pgagroal_disconnect(transfer_fd); + + pgagroal_tracking_event_slot(TRACKER_RETURN_CONNECTION_KILL, slot); + + return pgagroal_kill_connection(slot, ssl); +} + +int +pgagroal_kill_connection(int slot, SSL* ssl) +{ + SSL_CTX* ctx; + int ssl_shutdown; + int result = 0; + int fd; + int transfer_fd; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + pgagroal_log_debug("pgagroal_kill_connection: Slot %d FD %d State %d PID %d", + slot, config->connections[slot].fd, atomic_load(&config->states[slot]), + config->connections[slot].pid); + + pgagroal_tracking_event_slot(TRACKER_KILL_CONNECTION, slot); + + fd = config->connections[slot].fd; + if (fd != -1) + { + if (pgagroal_connection_get(&transfer_fd)) + { + result = 1; + } + + if (pgagroal_connection_id_write(transfer_fd, CONNECTION_KILL)) + { + result = 1; + } + + if (pgagroal_connection_slot_write(transfer_fd, slot)) + { + result = 1; + } + + if (pgagroal_connection_socket_write(transfer_fd, fd)) + { + result = 1; + } + + pgagroal_disconnect(transfer_fd); + + if (ssl != NULL) + { + ctx = SSL_get_SSL_CTX(ssl); + ssl_shutdown = SSL_shutdown(ssl); + if (ssl_shutdown == 0) + { + SSL_shutdown(ssl); + } + SSL_free(ssl); + SSL_CTX_free(ctx); + } + + if (!pgagroal_socket_has_error(fd)) + { + pgagroal_disconnect(fd); + } + } + else + { + result = 1; + } + + if (config->connections[slot].pid != -1) + { + if (config->connections[slot].limit_rule >= 0) + { + atomic_fetch_sub(&config->limits[config->connections[slot].limit_rule].active_connections, 1); + } + + atomic_fetch_sub(&config->active_connections, 1); + } + + memset(&config->connections[slot].username, 0, sizeof(config->connections[slot].username)); + memset(&config->connections[slot].database, 0, sizeof(config->connections[slot].database)); + memset(&config->connections[slot].appname, 0, sizeof(config->connections[slot].appname)); + + config->connections[slot].new = true; + config->connections[slot].server = -1; + config->connections[slot].tx_mode = false; + + config->connections[slot].has_security = SECURITY_INVALID; + for (int i = 0; i < NUMBER_OF_SECURITY_MESSAGES; i++) + { + config->connections[slot].security_lengths[i] = 0; + memset(&config->connections[slot].security_messages[i], 0, SECURITY_BUFFER_SIZE); + } + + config->connections[slot].backend_pid = 0; + config->connections[slot].backend_secret = 0; + + config->connections[slot].limit_rule = -1; + config->connections[slot].start_time = -1; + config->connections[slot].timestamp = -1; + config->connections[slot].fd = -1; + config->connections[slot].pid = -1; + + atomic_store(&config->states[slot], STATE_NOTINIT); + + pgagroal_prometheus_connection_kill(); + + return result; +} + +void +pgagroal_idle_timeout(void) +{ + bool prefill; + time_t now; + signed char free; + signed char idle_check; + struct main_configuration* config; + + pgagroal_start_logging(); + pgagroal_memory_init(); + + config = (struct main_configuration*)shmem; + now = time(NULL); + prefill = false; + + pgagroal_log_debug("pgagroal_idle_timeout"); + + /* Here we run backwards in order to keep hot connections in the beginning */ + for (int i = config->max_connections - 1; i >= 0; i--) + { + free = STATE_FREE; + idle_check = STATE_IDLE_CHECK; + + if (atomic_compare_exchange_strong(&config->states[i], &free, idle_check)) + { + double diff = difftime(now, config->connections[i].timestamp); + if (diff >= (double)config->idle_timeout && !config->connections[i].tx_mode) + { + pgagroal_prometheus_connection_idletimeout(); + pgagroal_tracking_event_slot(TRACKER_IDLE_TIMEOUT, i); + pgagroal_kill_connection(i, NULL); + prefill = true; + } + else + { + if (!atomic_compare_exchange_strong(&config->states[i], &idle_check, STATE_FREE)) + { + pgagroal_prometheus_connection_idletimeout(); + pgagroal_tracking_event_slot(TRACKER_IDLE_TIMEOUT, i); + pgagroal_kill_connection(i, NULL); + prefill = true; + } + } + } + } + + if (prefill) + { + pgagroal_prefill_if_can(true, false); + } + + pgagroal_pool_status(); + pgagroal_memory_destroy(); + pgagroal_stop_logging(); + + exit(0); +} + +void +pgagroal_max_connection_age(void) +{ + bool prefill; + time_t now; + signed char free; + signed char age_check; + struct main_configuration* config; + + pgagroal_start_logging(); + pgagroal_memory_init(); + + config = (struct main_configuration*)shmem; + now = time(NULL); + prefill = false; + + pgagroal_log_debug("pgagroal_max_connection_age"); + + /* Here we run backwards in order to keep hot connections in the beginning */ + for (int i = config->max_connections - 1; i >= 0; i--) + { + free = STATE_FREE; + age_check = STATE_MAX_CONNECTION_AGE; + + if (atomic_compare_exchange_strong(&config->states[i], &free, age_check)) + { + double age = difftime(now, config->connections[i].start_time); + if (age >= (double)config->max_connection_age && !config->connections[i].tx_mode) + { + pgagroal_prometheus_connection_max_connection_age(); + pgagroal_tracking_event_slot(TRACKER_MAX_CONNECTION_AGE, i); + pgagroal_kill_connection(i, NULL); + prefill = true; + } + else + { + if (!atomic_compare_exchange_strong(&config->states[i], &age_check, STATE_FREE)) + { + pgagroal_prometheus_connection_max_connection_age(); + pgagroal_tracking_event_slot(TRACKER_MAX_CONNECTION_AGE, i); + pgagroal_kill_connection(i, NULL); + prefill = true; + } + } + } + } + + if (prefill) + { + pgagroal_prefill_if_can(true, false); + } + + pgagroal_pool_status(); + pgagroal_memory_destroy(); + pgagroal_stop_logging(); + + exit(0); +} + +void +pgagroal_validation(void) +{ + bool prefill = true; + time_t now; + signed char free; + signed char validation; + struct main_configuration* config; + + pgagroal_start_logging(); + pgagroal_memory_init(); + + config = (struct main_configuration*)shmem; + now = time(NULL); + + pgagroal_log_debug("pgagroal_validation"); + + /* We run backwards */ + for (int i = config->max_connections - 1; i >= 0; i--) + { + free = STATE_FREE; + validation = STATE_VALIDATION; + + if (atomic_compare_exchange_strong(&config->states[i], &free, validation)) + { + bool kill = false; + double diff, age; + + /* Verify the socket for the slot */ + if (!pgagroal_socket_isvalid(config->connections[i].fd)) + { + kill = true; + } + + /* While we have the connection in validation may as well check for idle_timeout */ + if (!kill && config->idle_timeout > 0) + { + diff = difftime(now, config->connections[i].timestamp); + if (diff >= (double)config->idle_timeout) + { + kill = true; + } + } + + /* Also check for max_connection_age */ + if (!kill && config->max_connection_age > 0) + { + age = difftime(now, config->connections[i].start_time); + if (age >= (double)config->max_connection_age) + { + kill = true; + } + } + + /* Ok, send SELECT 1 */ + if (!kill) + { + kill = !pgagroal_connection_isvalid(config->connections[i].fd); + } + + if (kill) + { + pgagroal_prometheus_connection_invalid(); + pgagroal_tracking_event_slot(TRACKER_INVALID_CONNECTION, i); + pgagroal_kill_connection(i, NULL); + prefill = true; + } + else + { + if (!atomic_compare_exchange_strong(&config->states[i], &validation, STATE_FREE)) + { + pgagroal_prometheus_connection_invalid(); + pgagroal_tracking_event_slot(TRACKER_INVALID_CONNECTION, i); + pgagroal_kill_connection(i, NULL); + prefill = true; + } + } + } + } + + if (prefill) + { + pgagroal_prefill_if_can(true, false); + } + + pgagroal_pool_status(); + pgagroal_memory_destroy(); + pgagroal_stop_logging(); + + exit(0); +} + +void +pgagroal_flush(int mode, char* database) +{ + bool prefill; + signed char free; + signed char in_use; + bool do_kill; + signed char server_state; + struct main_configuration* config; + + pgagroal_start_logging(); + pgagroal_memory_init(); + + config = (struct main_configuration*)shmem; + + prefill = false; + + pgagroal_log_debug("pgagroal_flush"); + for (int i = config->max_connections - 1; i >= 0; i--) + { + free = STATE_FREE; + in_use = STATE_IN_USE; + do_kill = false; + + if (config->connections[i].server != -1) + { + server_state = atomic_load(&config->servers[config->connections[i].server].state); + if (server_state == SERVER_FAILED) + { + do_kill = true; + } + } + + if (!do_kill) + { + bool consider = false; + + if (!strcmp(database, "*") || !strcmp(config->connections[i].database, database)) + { + consider = true; + } + + if (consider) + { + if (atomic_compare_exchange_strong(&config->states[i], &free, STATE_FLUSH)) + { + if (pgagroal_socket_isvalid(config->connections[i].fd)) + { + pgagroal_write_terminate(NULL, config->connections[i].fd); + } + pgagroal_prometheus_connection_flush(); + pgagroal_tracking_event_slot(TRACKER_FLUSH, i); + pgagroal_kill_connection(i, NULL); + prefill = true; + } + else if (mode == FLUSH_ALL || mode == FLUSH_GRACEFULLY) + { + if (atomic_compare_exchange_strong(&config->states[i], &in_use, STATE_FLUSH)) + { + if (mode == FLUSH_ALL) + { + kill(config->connections[i].pid, SIGQUIT); + pgagroal_prometheus_connection_flush(); + pgagroal_tracking_event_slot(TRACKER_FLUSH, i); + pgagroal_kill_connection(i, NULL); + prefill = true; + } + else if (mode == FLUSH_GRACEFULLY) + { + atomic_store(&config->states[i], STATE_GRACEFULLY); + } + } + } + } + } + else + { + switch (atomic_load(&config->states[i])) + { + case STATE_NOTINIT: + case STATE_INIT: + /* Do nothing */ + break; + case STATE_FREE: + atomic_store(&config->states[i], STATE_GRACEFULLY); + pgagroal_prometheus_connection_flush(); + pgagroal_tracking_event_slot(TRACKER_FLUSH, i); + pgagroal_kill_connection(i, NULL); + prefill = true; + break; + case STATE_IN_USE: + case STATE_GRACEFULLY: + case STATE_FLUSH: + atomic_store(&config->states[i], STATE_GRACEFULLY); + break; + case STATE_IDLE_CHECK: + case STATE_MAX_CONNECTION_AGE: + case STATE_VALIDATION: + case STATE_REMOVE: + atomic_store(&config->states[i], STATE_GRACEFULLY); + break; + default: + break; + } + } + } + + if (prefill) + { + pgagroal_prefill_if_can(true, false); + } + + pgagroal_pool_status(); + pgagroal_memory_destroy(); + pgagroal_stop_logging(); +} + +void +pgagroal_flush_server(signed char server) +{ + struct main_configuration* config; + int primary = -1; + + pgagroal_start_logging(); + pgagroal_memory_init(); + + config = (struct main_configuration*)shmem; + + pgagroal_log_debug("pgagroal_flush_server %s", config->servers[server].name); + for (int i = 0; i < config->max_connections; i++) + { + if (config->connections[i].server == server) + { + switch (atomic_load(&config->states[i])) + { + case STATE_NOTINIT: + case STATE_INIT: + /* Do nothing */ + break; + case STATE_FREE: + atomic_store(&config->states[i], STATE_GRACEFULLY); + if (pgagroal_socket_isvalid(config->connections[i].fd)) + { + pgagroal_write_terminate(NULL, config->connections[i].fd); + } + pgagroal_prometheus_connection_flush(); + pgagroal_tracking_event_slot(TRACKER_FLUSH, i); + pgagroal_kill_connection(i, NULL); + break; + case STATE_IN_USE: + case STATE_GRACEFULLY: + case STATE_FLUSH: + atomic_store(&config->states[i], STATE_GRACEFULLY); + break; + case STATE_IDLE_CHECK: + case STATE_MAX_CONNECTION_AGE: + case STATE_VALIDATION: + case STATE_REMOVE: + atomic_store(&config->states[i], STATE_GRACEFULLY); + break; + default: + break; + } + } + } + + if (pgagroal_get_primary(&primary)) + { + pgagroal_log_debug("No primary defined"); + } + else + { + if (server != (unsigned char)primary && primary != -1) + { + pgagroal_prefill_if_can(true, true); + } + } + + pgagroal_pool_status(); + pgagroal_memory_destroy(); + pgagroal_stop_logging(); + + exit(0); +} + +void +pgagroal_request_flush(SSL* ssl, int client_fd, uint8_t compression, uint8_t encryption, struct json* payload) +{ + time_t start_time; + time_t end_time; + struct json* req = NULL; + int mode = 0; + char* database = NULL; + + start_time = time(NULL); + + req = (struct json*)pgagroal_json_get(payload, MANAGEMENT_CATEGORY_REQUEST); + mode = (int)pgagroal_json_get(req, MANAGEMENT_ARGUMENT_MODE); + database = (char*)pgagroal_json_get(req, MANAGEMENT_ARGUMENT_DATABASE); + + if (database != NULL) + { + pgagroal_flush(mode, database); + } + else + { + pgagroal_flush(mode, "*"); + } + + end_time = time(NULL); + + pgagroal_management_response_ok(NULL, client_fd, start_time, end_time, compression, encryption, payload); + + pgagroal_json_destroy(payload); + + exit(0); +} + +void +pgagroal_prefill(bool initial) +{ + struct main_configuration* config; + + pgagroal_start_logging(); + pgagroal_memory_init(); + + config = (struct main_configuration*)shmem; + + pgagroal_log_debug("pgagroal_prefill"); + + for (int i = 0; i < config->number_of_limits; i++) + { + int size; + + if (initial) + { + size = config->limits[i].initial_size; + } + else + { + size = config->limits[i].min_size; + } + + if (size > 0) + { + if (strcmp("all", config->limits[i].database) && strcmp("all", config->limits[i].username)) + { + int user = -1; + + for (int j = 0; j < config->number_of_users && user == -1; j++) + { + if (!strcmp(config->limits[i].username, config->users[j].username)) + { + user = j; + } + } + + if (user != -1) + { + while (do_prefill(config->users[user].username, config->limits[i].database, size)) + { + int32_t slot = -1; + SSL* ssl = NULL; + + if (pgagroal_prefill_auth(config->users[user].username, config->users[user].password, + config->limits[i].database, &slot, &ssl) != AUTH_SUCCESS) + { + pgagroal_log_warn("Invalid data for user '%s' using limit entry (%d)", config->limits[i].username, i + 1); + + if (slot != -1) + { + if (config->connections[slot].fd != -1) + { + if (pgagroal_socket_isvalid(config->connections[slot].fd)) + { + pgagroal_write_terminate(NULL, config->connections[slot].fd); + } + } + pgagroal_tracking_event_slot(TRACKER_PREFILL_KILL, slot); + pgagroal_kill_connection(slot, ssl); + } + + break; + } + + if (slot != -1) + { + if (config->connections[slot].has_security != SECURITY_INVALID) + { + pgagroal_tracking_event_slot(TRACKER_PREFILL_RETURN, slot); + pgagroal_return_connection(slot, ssl, false); + } + else + { + pgagroal_log_warn("Unsupported security model during prefill for user '%s' using limit entry (%d)", config->limits[i].username, i + 1); + if (config->connections[slot].fd != -1) + { + if (pgagroal_socket_isvalid(config->connections[slot].fd)) + { + pgagroal_write_terminate(NULL, config->connections[slot].fd); + } + } + pgagroal_tracking_event_slot(TRACKER_PREFILL_KILL, slot); + pgagroal_kill_connection(slot, ssl); + break; + } + } + } + } + else + { + pgagroal_log_warn("Unknown user '%s' for limit entry (%d)", config->limits[i].username, i + 1); + } + } + else + { + pgagroal_log_warn("Limit entry (%d) with invalid definition", i + 1); + } + } + } + + pgagroal_pool_status(); + pgagroal_memory_destroy(); + pgagroal_stop_logging(); + + exit(0); +} + +int +pgagroal_pool_init(void) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + /* States */ + for (int i = 0; i < MAX_NUMBER_OF_CONNECTIONS; i++) + { + atomic_init(&config->states[i], STATE_NOTINIT); + } + + /* Connections */ + for (int i = 0; i < config->max_connections; i++) + { + config->connections[i].new = true; + config->connections[i].tx_mode = false; + config->connections[i].server = -1; + config->connections[i].has_security = SECURITY_INVALID; + config->connections[i].limit_rule = -1; + config->connections[i].start_time = -1; + config->connections[i].timestamp = -1; + config->connections[i].fd = -1; + config->connections[i].pid = -1; + } + + return 0; +} + +int +pgagroal_pool_shutdown(void) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + for (int i = 0; i < config->max_connections; i++) + { + int state = atomic_load(&config->states[i]); + + if (state != STATE_NOTINIT) + { + if (state == STATE_FREE) + { + if (pgagroal_socket_isvalid(config->connections[i].fd)) + { + pgagroal_write_terminate(NULL, config->connections[i].fd); + } + } + pgagroal_disconnect(config->connections[i].fd); + + if (config->connections[i].pid != -1) + { + kill(config->connections[i].pid, SIGQUIT); + } + + atomic_store(&config->states[i], STATE_NOTINIT); + } + } + + return 0; +} + +int +pgagroal_pool_status(void) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + pgagroal_log_debug("pgagroal_pool_status: %d/%d", atomic_load(&config->active_connections), config->max_connections); + + for (int i = 0; i < config->max_connections; i++) + { + connection_details(i); + } + +#ifdef DEBUG + assert(atomic_load(&config->active_connections) <= config->max_connections); +#endif + + return 0; +} + +static int +find_best_rule(char* username, char* database) +{ + int best_rule; + struct main_configuration* config; + + best_rule = -1; + config = (struct main_configuration*)shmem; + + if (config->number_of_limits > 0) + { + for (int i = 0; i < config->number_of_limits; i++) + { + /* There is a match */ + if ((!strcmp("all", config->limits[i].username) || !strcmp(username, config->limits[i].username)) && + (!strcmp("all", config->limits[i].database) || !strcmp(database, config->limits[i].database))) + { + if (best_rule == -1) + { + best_rule = i; + } + else + { + if (!strcmp(username, config->limits[best_rule].username) && + !strcmp(database, config->limits[best_rule].database)) + { + /* We have a precise rule already */ + } + else if (!strcmp("all", config->limits[best_rule].username)) + { + /* User is better */ + if (strcmp("all", config->limits[i].username)) + { + best_rule = i; + } + } + else if (!strcmp("all", config->limits[best_rule].database)) + { + /* Database is better */ + if (strcmp("all", config->limits[i].database)) + { + best_rule = i; + } + } + } + } + } + } + + return best_rule; +} + +static bool +remove_connection(char* username, char* database) +{ + signed char free; + signed char remove; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + pgagroal_log_trace("remove_connection"); + for (int i = config->max_connections - 1; i >= 0; i--) + { + free = STATE_FREE; + remove = STATE_REMOVE; + + if (atomic_compare_exchange_strong(&config->states[i], &free, remove)) + { + if (!strcmp(username, config->connections[i].username) && !strcmp(database, config->connections[i].database)) + { + if (!atomic_compare_exchange_strong(&config->states[i], &remove, STATE_FREE)) + { + pgagroal_prometheus_connection_remove(); + pgagroal_tracking_event_slot(TRACKER_REMOVE_CONNECTION, i); + pgagroal_kill_connection(i, NULL); + } + } + else + { + pgagroal_prometheus_connection_remove(); + pgagroal_tracking_event_slot(TRACKER_REMOVE_CONNECTION, i); + pgagroal_kill_connection(i, NULL); + } + + return true; + } + } + + return false; +} + +static void +connection_details(int slot) +{ + int state; + char time_buf[32]; + char start_buf[32]; + struct main_configuration* config; + struct connection connection; + + config = (struct main_configuration*)shmem; + + connection = config->connections[slot]; + state = atomic_load(&config->states[slot]); + + memset(&time_buf, 0, sizeof(time_buf)); + ctime_r(&(connection.timestamp), &time_buf[0]); + time_buf[strlen(time_buf) - 1] = 0; + + memset(&start_buf, 0, sizeof(start_buf)); + ctime_r(&(connection.start_time), &start_buf[0]); + start_buf[strlen(start_buf) - 1] = 0; + + switch (state) + { + case STATE_NOTINIT: + pgagroal_log_debug("pgagroal_pool_status: State: NOTINIT"); + pgagroal_log_debug(" Slot: %d", slot); + pgagroal_log_debug(" FD: %d", connection.fd); + break; + case STATE_INIT: + pgagroal_log_debug("pgagroal_pool_status: State: INIT"); + pgagroal_log_debug(" Slot: %d", slot); + pgagroal_log_debug(" FD: %d", connection.fd); + break; + case STATE_FREE: + pgagroal_log_debug("pgagroal_pool_status: State: FREE"); + pgagroal_log_debug(" Slot: %d", slot); + pgagroal_log_debug(" Server: %d", connection.server); + pgagroal_log_debug(" User: %s", connection.username); + pgagroal_log_debug(" Database: %s", connection.database); + pgagroal_log_debug(" AppName: %s", connection.appname); + pgagroal_log_debug(" Rule: %d", connection.limit_rule); + pgagroal_log_debug(" Start: %s", &start_buf[0]); + pgagroal_log_debug(" Time: %s", &time_buf[0]); + pgagroal_log_debug(" FD: %d", connection.fd); + pgagroal_log_trace(" PID: %d", connection.pid); + pgagroal_log_trace(" Auth: %d", connection.has_security); + for (int i = 0; i < NUMBER_OF_SECURITY_MESSAGES; i++) + { + pgagroal_log_trace(" Size: %zd", connection.security_lengths[i]); + pgagroal_log_mem(&connection.security_messages[i], connection.security_lengths[i]); + } + pgagroal_log_trace(" Backend PID: %d", connection.backend_pid); + pgagroal_log_trace(" Backend Secret: %d", connection.backend_secret); + break; + case STATE_IN_USE: + pgagroal_log_debug("pgagroal_pool_status: State: IN_USE"); + pgagroal_log_debug(" Slot: %d", slot); + pgagroal_log_debug(" Server: %d", connection.server); + pgagroal_log_debug(" User: %s", connection.username); + pgagroal_log_debug(" Database: %s", connection.database); + pgagroal_log_debug(" AppName: %s", connection.appname); + pgagroal_log_debug(" Rule: %d", connection.limit_rule); + pgagroal_log_debug(" Start: %s", &start_buf[0]); + pgagroal_log_debug(" Time: %s", &time_buf[0]); + pgagroal_log_debug(" FD: %d", connection.fd); + pgagroal_log_trace(" PID: %d", connection.pid); + pgagroal_log_trace(" Auth: %d", connection.has_security); + for (int i = 0; i < NUMBER_OF_SECURITY_MESSAGES; i++) + { + pgagroal_log_trace(" Size: %zd", connection.security_lengths[i]); + pgagroal_log_mem(&connection.security_messages[i], connection.security_lengths[i]); + } + pgagroal_log_trace(" Backend PID: %d", connection.backend_pid); + pgagroal_log_trace(" Backend Secret: %d", connection.backend_secret); + break; + case STATE_GRACEFULLY: + pgagroal_log_debug("pgagroal_pool_status: State: GRACEFULLY"); + pgagroal_log_debug(" Slot: %d", slot); + pgagroal_log_debug(" Server: %d", connection.server); + pgagroal_log_debug(" User: %s", connection.username); + pgagroal_log_debug(" Database: %s", connection.database); + pgagroal_log_debug(" AppName: %s", connection.appname); + pgagroal_log_debug(" Rule: %d", connection.limit_rule); + pgagroal_log_debug(" Start: %s", &start_buf[0]); + pgagroal_log_debug(" Time: %s", &time_buf[0]); + pgagroal_log_debug(" FD: %d", connection.fd); + pgagroal_log_trace(" PID: %d", connection.pid); + pgagroal_log_trace(" Auth: %d", connection.has_security); + for (int i = 0; i < NUMBER_OF_SECURITY_MESSAGES; i++) + { + pgagroal_log_trace(" Size: %zd", connection.security_lengths[i]); + pgagroal_log_mem(&connection.security_messages[i], connection.security_lengths[i]); + } + pgagroal_log_trace(" Backend PID: %d", connection.backend_pid); + pgagroal_log_trace(" Backend Secret: %d", connection.backend_secret); + break; + case STATE_FLUSH: + pgagroal_log_debug("pgagroal_pool_status: State: FLUSH"); + pgagroal_log_debug(" Slot: %d", slot); + pgagroal_log_debug(" Server: %d", connection.server); + pgagroal_log_debug(" User: %s", connection.username); + pgagroal_log_debug(" Database: %s", connection.database); + pgagroal_log_debug(" AppName: %s", connection.appname); + pgagroal_log_debug(" Rule: %d", connection.limit_rule); + pgagroal_log_debug(" Start: %s", &start_buf[0]); + pgagroal_log_debug(" Time: %s", &time_buf[0]); + pgagroal_log_debug(" FD: %d", connection.fd); + pgagroal_log_trace(" PID: %d", connection.pid); + pgagroal_log_trace(" Auth: %d", connection.has_security); + for (int i = 0; i < NUMBER_OF_SECURITY_MESSAGES; i++) + { + pgagroal_log_trace(" Size: %zd", connection.security_lengths[i]); + pgagroal_log_mem(&connection.security_messages[i], connection.security_lengths[i]); + } + pgagroal_log_trace(" Backend PID: %d", connection.backend_pid); + pgagroal_log_trace(" Backend Secret: %d", connection.backend_secret); + break; + case STATE_IDLE_CHECK: + pgagroal_log_debug("pgagroal_pool_status: State: IDLE CHECK"); + pgagroal_log_debug(" Slot: %d", slot); + pgagroal_log_debug(" Server: %d", connection.server); + pgagroal_log_debug(" User: %s", connection.username); + pgagroal_log_debug(" Database: %s", connection.database); + pgagroal_log_debug(" AppName: %s", connection.appname); + pgagroal_log_debug(" Rule: %d", connection.limit_rule); + pgagroal_log_debug(" Start: %s", &start_buf[0]); + pgagroal_log_debug(" Time: %s", &time_buf[0]); + pgagroal_log_debug(" FD: %d", connection.fd); + pgagroal_log_trace(" PID: %d", connection.pid); + pgagroal_log_trace(" Auth: %d", connection.has_security); + for (int i = 0; i < NUMBER_OF_SECURITY_MESSAGES; i++) + { + pgagroal_log_trace(" Size: %zd", connection.security_lengths[i]); + pgagroal_log_mem(&connection.security_messages[i], connection.security_lengths[i]); + } + pgagroal_log_trace(" Backend PID: %d", connection.backend_pid); + pgagroal_log_trace(" Backend Secret: %d", connection.backend_secret); + break; + case STATE_MAX_CONNECTION_AGE: + pgagroal_log_debug("pgagroal_pool_status: State: MAX CONNECTION AGE"); + pgagroal_log_debug(" Slot: %d", slot); + pgagroal_log_debug(" Server: %d", connection.server); + pgagroal_log_debug(" User: %s", connection.username); + pgagroal_log_debug(" Database: %s", connection.database); + pgagroal_log_debug(" AppName: %s", connection.appname); + pgagroal_log_debug(" Rule: %d", connection.limit_rule); + pgagroal_log_debug(" Start: %s", &start_buf[0]); + pgagroal_log_debug(" Time: %s", &time_buf[0]); + pgagroal_log_debug(" FD: %d", connection.fd); + pgagroal_log_trace(" PID: %d", connection.pid); + pgagroal_log_trace(" Auth: %d", connection.has_security); + for (int i = 0; i < NUMBER_OF_SECURITY_MESSAGES; i++) + { + pgagroal_log_trace(" Size: %zd", connection.security_lengths[i]); + pgagroal_log_mem(&connection.security_messages[i], connection.security_lengths[i]); + } + pgagroal_log_trace(" Backend PID: %d", connection.backend_pid); + pgagroal_log_trace(" Backend Secret: %d", connection.backend_secret); + break; + case STATE_VALIDATION: + pgagroal_log_debug("pgagroal_pool_status: State: VALIDATION"); + pgagroal_log_debug(" Slot: %d", slot); + pgagroal_log_debug(" Server: %d", connection.server); + pgagroal_log_debug(" User: %s", connection.username); + pgagroal_log_debug(" Database: %s", connection.database); + pgagroal_log_debug(" AppName: %s", connection.appname); + pgagroal_log_debug(" Rule: %d", connection.limit_rule); + pgagroal_log_debug(" Start: %s", &start_buf[0]); + pgagroal_log_debug(" Time: %s", &time_buf[0]); + pgagroal_log_debug(" FD: %d", connection.fd); + pgagroal_log_trace(" PID: %d", connection.pid); + pgagroal_log_trace(" Auth: %d", connection.has_security); + for (int i = 0; i < NUMBER_OF_SECURITY_MESSAGES; i++) + { + pgagroal_log_trace(" Size: %zd", connection.security_lengths[i]); + pgagroal_log_mem(&connection.security_messages[i], connection.security_lengths[i]); + } + pgagroal_log_trace(" Backend PID: %d", connection.backend_pid); + pgagroal_log_trace(" Backend Secret: %d", connection.backend_secret); + break; + case STATE_REMOVE: + pgagroal_log_debug("pgagroal_pool_status: State: REMOVE"); + pgagroal_log_debug(" Slot: %d", slot); + pgagroal_log_debug(" Server: %d", connection.server); + pgagroal_log_debug(" User: %s", connection.username); + pgagroal_log_debug(" Database: %s", connection.database); + pgagroal_log_debug(" AppName: %s", connection.appname); + pgagroal_log_debug(" Rule: %d", connection.limit_rule); + pgagroal_log_debug(" Start: %s", &start_buf[0]); + pgagroal_log_debug(" Time: %s", &time_buf[0]); + pgagroal_log_debug(" FD: %d", connection.fd); + pgagroal_log_trace(" PID: %d", connection.pid); + pgagroal_log_trace(" Auth: %d", connection.has_security); + for (int i = 0; i < NUMBER_OF_SECURITY_MESSAGES; i++) + { + pgagroal_log_trace(" Size: %zd", connection.security_lengths[i]); + pgagroal_log_mem(&connection.security_messages[i], connection.security_lengths[i]); + } + pgagroal_log_trace(" Backend PID: %d", connection.backend_pid); + pgagroal_log_trace(" Backend Secret: %d", connection.backend_secret); + break; + default: + pgagroal_log_debug("pgagroal_pool_status: State %d Slot %d FD %d", state, slot, connection.fd); + break; + } +} + +static bool +do_prefill(char* username, char* database, int size) +{ + signed char state; + int free = 0; + int connections = 0; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + for (int i = 0; i < config->max_connections; i++) + { + if (!strcmp((const char*)(&config->connections[i].username), username) && + !strcmp((const char*)(&config->connections[i].database), database)) + { + connections++; + } + else + { + state = atomic_load(&config->states[i]); + + if (state == STATE_NOTINIT) + { + free++; + } + } + } + + return connections < size && free > 0; +} + +void +pgagroal_prefill_if_can(bool do_fork, bool initial) +{ + int primary; + + if (pgagroal_can_prefill()) + { + if (pgagroal_get_primary(&primary)) + { + pgagroal_log_warn("No primary detected, cannot try to prefill!"); + return; + } + + if (do_fork) + { + if (!fork()) + { + pgagroal_prefill(initial); + } + } + else + { + pgagroal_prefill(initial); + } + } +} diff --git a/src/libpgagroal/prometheus.c b/src/libpgagroal/prometheus.c new file mode 100644 index 00000000..790eaf4e --- /dev/null +++ b/src/libpgagroal/prometheus.c @@ -0,0 +1,3009 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include + +#define CHUNK_SIZE 32768 + +#define PAGE_UNKNOWN 0 +#define PAGE_HOME 1 +#define PAGE_METRICS 2 +#define BAD_REQUEST 3 + +#define FIVE_SECONDS 5 +#define TEN_SECONDS 10 +#define TWENTY_SECONDS 20 +#define THIRTY_SECONDS 30 +#define FOURTYFIVE_SECONDS 45 +#define ONE_MINUTE 60 +#define FIVE_MINUTES 300 +#define TEN_MINUTES 600 +#define TWENTY_MINUTES 1200 +#define THIRTY_MINUTES 1800 +#define FOURTYFIVE_MINUTES 2700 +#define ONE_HOUR 3600 +#define TWO_HOURS 7200 +#define FOUR_HOURS 14400 +#define SIX_HOURS 21600 +#define TWELVE_HOURS 43200 +#define TWENTYFOUR_HOURS 86400 + +static int resolve_page(struct message* msg); +static int badrequest_page(int client_fd); +static int unknown_page(int client_fd); +static int home_page(int client_fd); +static int home_vault_page(int client_fd); +static int metrics_page(int client_fd); +static int metrics_vault_page(int client_fd); +static int bad_request(int client_fd); + +static void general_information(int client_fd); +static void general_vault_information(int client_fd); +static void connection_information(int client_fd); +static void limit_information(int client_fd); +static void session_information(int client_fd); +static void pool_information(int client_fd); +static void auth_information(int client_fd); +static void client_information(int client_fd); +static void internal_information(int client_fd); +static void internal_vault_information(int client_fd); +static void connection_awaiting_information(int client_fd); + +static int send_chunk(int client_fd, char* data); + +static bool is_metrics_cache_configured(void); +static bool is_metrics_cache_valid(void); +static bool metrics_cache_append(char* data); +static bool metrics_cache_finalize(void); +static size_t metrics_cache_size_to_alloc(void); +static void metrics_cache_invalidate(void); +static bool is_prometheus_enabled(void); + +void +pgagroal_prometheus(int client_fd) +{ + int status; + int page; + struct message* msg = NULL; + struct main_configuration* config; + + if (!is_prometheus_enabled()) + { + exit(1); + } + + pgagroal_start_logging(); + pgagroal_memory_init(); + + config = (struct main_configuration*)shmem; + + status = pgagroal_read_timeout_message(NULL, client_fd, config->common.authentication_timeout, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + page = resolve_page(msg); + + if (page == PAGE_HOME) + { + home_page(client_fd); + } + else if (page == PAGE_METRICS) + { + metrics_page(client_fd); + } + else if (page == PAGE_UNKNOWN) + { + unknown_page(client_fd); + } + else + { + bad_request(client_fd); + } + + pgagroal_disconnect(client_fd); + + pgagroal_memory_destroy(); + pgagroal_stop_logging(); + + exit(0); + +error: + + badrequest_page(client_fd); + + pgagroal_log_debug("pgagroal_prometheus: disconnect %d", client_fd); + pgagroal_disconnect(client_fd); + + pgagroal_memory_destroy(); + pgagroal_stop_logging(); + + exit(1); +} + +void +pgagroal_vault_prometheus(int client_fd) +{ + int status; + int page; + struct message* msg = NULL; + struct vault_configuration* config; + + if (!is_prometheus_enabled()) + { + exit(1); + } + + pgagroal_start_logging(); + pgagroal_memory_init(); + + config = (struct vault_configuration*)shmem; + + status = pgagroal_read_timeout_message(NULL, client_fd, config->common.authentication_timeout, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + page = resolve_page(msg); + + if (page == PAGE_HOME) + { + home_vault_page(client_fd); + } + else if (page == PAGE_METRICS) + { + metrics_vault_page(client_fd); + } + else if (page == PAGE_UNKNOWN) + { + unknown_page(client_fd); + } + else + { + bad_request(client_fd); + } + + pgagroal_disconnect(client_fd); + + pgagroal_memory_destroy(); + pgagroal_stop_logging(); + + exit(0); + +error: + + pgagroal_log_debug("pgagroal_prometheus: disconnect %d", client_fd); + pgagroal_disconnect(client_fd); + + pgagroal_memory_destroy(); + pgagroal_stop_logging(); + + exit(1); +} + +int +pgagroal_init_prometheus(size_t* p_size, void** p_shmem) +{ + size_t tmp_p_size = 0; + void* tmp_p_shmem = NULL; + struct main_prometheus* prometheus; + struct main_configuration* config; + + config = (struct main_configuration*) shmem; + + *p_size = 0; + *p_shmem = NULL; + + tmp_p_size = sizeof(struct main_prometheus) + (config->max_connections * sizeof(struct prometheus_connection)); + if (pgagroal_create_shared_memory(tmp_p_size, config->common.hugepage, &tmp_p_shmem)) + { + goto error; + } + + prometheus = (struct main_prometheus*)tmp_p_shmem; + + for (int i = 0; i < HISTOGRAM_BUCKETS; i++) + { + atomic_init(&prometheus->session_time[i], 0); + } + atomic_init(&prometheus->session_time_sum, 0); + + atomic_init(&prometheus->connection_error, 0); + atomic_init(&prometheus->connection_kill, 0); + atomic_init(&prometheus->connection_remove, 0); + atomic_init(&prometheus->connection_timeout, 0); + atomic_init(&prometheus->connection_return, 0); + atomic_init(&prometheus->connection_invalid, 0); + atomic_init(&prometheus->connection_get, 0); + atomic_init(&prometheus->connection_idletimeout, 0); + atomic_init(&prometheus->connection_max_connection_age, 0); + atomic_init(&prometheus->connection_flush, 0); + atomic_init(&prometheus->connection_success, 0); + + atomic_init(&prometheus->prometheus_base.logging_info, 0); + atomic_init(&prometheus->prometheus_base.logging_warn, 0); + atomic_init(&prometheus->prometheus_base.logging_error, 0); + atomic_init(&prometheus->prometheus_base.logging_fatal, 0); + + // awating connections are those on hold due to + // the `blocking_timeout` setting + atomic_init(&prometheus->connections_awaiting_total, 0); + + for (int i = 0; i < NUMBER_OF_LIMITS; i++) + { + atomic_init(&prometheus->connections_awaiting[i], 0); + } + + atomic_init(&prometheus->auth_user_success, 0); + atomic_init(&prometheus->auth_user_bad_password, 0); + atomic_init(&prometheus->auth_user_error, 0); + + atomic_init(&prometheus->client_wait, 0); + atomic_init(&prometheus->client_active, 0); + atomic_init(&prometheus->client_wait_time, 0); + + atomic_init(&prometheus->query_count, 0); + atomic_init(&prometheus->tx_count, 0); + + atomic_init(&prometheus->network_sent, 0); + atomic_init(&prometheus->network_received, 0); + + atomic_init(&prometheus->prometheus_base.client_sockets, 0); + atomic_init(&prometheus->prometheus_base.self_sockets, 0); + + for (int i = 0; i < NUMBER_OF_SERVERS; i++) + { + atomic_init(&prometheus->server_error[i], 0); + } + atomic_init(&prometheus->failed_servers, 0); + + for (int i = 0; i < config->max_connections; i++) + { + memset(&prometheus->prometheus_connections[i], 0, sizeof(struct prometheus_connection)); + atomic_init(&prometheus->prometheus_connections[i].query_count, 0); + } + + *p_size = tmp_p_size; + *p_shmem = tmp_p_shmem; + + return 0; + +error: + + return 1; +} + +int +pgagroal_vault_init_prometheus(size_t* p_size, void** p_shmem) +{ + size_t tmp_p_size = 0; + void* tmp_p_shmem = NULL; + struct vault_prometheus* prometheus; + struct vault_configuration* config; + + config = (struct vault_configuration*) shmem; + + *p_size = 0; + *p_shmem = NULL; + + tmp_p_size = sizeof(struct vault_prometheus); + if (pgagroal_create_shared_memory(tmp_p_size, config->common.hugepage, &tmp_p_shmem)) + { + goto error; + } + + prometheus = (struct vault_prometheus*)tmp_p_shmem; + + atomic_init(&prometheus->prometheus_base.logging_info, 0); + atomic_init(&prometheus->prometheus_base.logging_warn, 0); + atomic_init(&prometheus->prometheus_base.logging_error, 0); + atomic_init(&prometheus->prometheus_base.logging_fatal, 0); + + atomic_init(&prometheus->prometheus_base.client_sockets, 0); + atomic_init(&prometheus->prometheus_base.self_sockets, 0); + + *p_size = tmp_p_size; + *p_shmem = tmp_p_shmem; + + return 0; + +error: + + return 1; +} + +void +pgagroal_prometheus_session_time(double time) +{ + unsigned long t; + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + t = (unsigned long)time; + + atomic_fetch_add(&prometheus->session_time_sum, t); + + if (t <= FIVE_SECONDS) + { + atomic_fetch_add(&prometheus->session_time[0], 1); + } + else if (t > FIVE_SECONDS && t <= TEN_SECONDS) + { + atomic_fetch_add(&prometheus->session_time[1], 1); + } + else if (t > TEN_SECONDS && t <= TWENTY_SECONDS) + { + atomic_fetch_add(&prometheus->session_time[2], 1); + } + else if (t > TWENTY_SECONDS && t <= THIRTY_SECONDS) + { + atomic_fetch_add(&prometheus->session_time[3], 1); + } + else if (t > THIRTY_SECONDS && t <= FOURTYFIVE_SECONDS) + { + atomic_fetch_add(&prometheus->session_time[4], 1); + } + else if (t > FOURTYFIVE_SECONDS && t <= ONE_MINUTE) + { + atomic_fetch_add(&prometheus->session_time[5], 1); + } + else if (t > ONE_MINUTE && t <= FIVE_MINUTES) + { + atomic_fetch_add(&prometheus->session_time[6], 1); + } + else if (t > FIVE_MINUTES && t <= TEN_MINUTES) + { + atomic_fetch_add(&prometheus->session_time[7], 1); + } + else if (t > TEN_MINUTES && t <= TWENTY_MINUTES) + { + atomic_fetch_add(&prometheus->session_time[8], 1); + } + else if (t > TWENTY_MINUTES && t <= THIRTY_MINUTES) + { + atomic_fetch_add(&prometheus->session_time[9], 1); + } + else if (t > THIRTY_MINUTES && t <= FOURTYFIVE_MINUTES) + { + atomic_fetch_add(&prometheus->session_time[10], 1); + } + else if (t > FOURTYFIVE_MINUTES && t <= ONE_HOUR) + { + atomic_fetch_add(&prometheus->session_time[11], 1); + } + else if (t > ONE_HOUR && t <= TWO_HOURS) + { + atomic_fetch_add(&prometheus->session_time[12], 1); + } + else if (t > TWO_HOURS && t <= FOUR_HOURS) + { + atomic_fetch_add(&prometheus->session_time[13], 1); + } + else if (t > FOUR_HOURS && t <= SIX_HOURS) + { + atomic_fetch_add(&prometheus->session_time[14], 1); + } + else if (t > SIX_HOURS && t <= TWELVE_HOURS) + { + atomic_fetch_add(&prometheus->session_time[15], 1); + } + else if (t > TWELVE_HOURS && t <= TWENTYFOUR_HOURS) + { + atomic_fetch_add(&prometheus->session_time[16], 1); + } + else + { + atomic_fetch_add(&prometheus->session_time[17], 1); + } +} + +void +pgagroal_prometheus_connection_error(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->connection_error, 1); +} + +void +pgagroal_prometheus_connection_kill(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->connection_kill, 1); +} + +void +pgagroal_prometheus_connection_remove(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->connection_remove, 1); +} + +void +pgagroal_prometheus_connection_timeout(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->connection_timeout, 1); +} + +void +pgagroal_prometheus_connection_return(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->connection_return, 1); +} + +void +pgagroal_prometheus_connection_invalid(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->connection_invalid, 1); +} + +void +pgagroal_prometheus_connection_get(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->connection_get, 1); +} + +void +pgagroal_prometheus_connection_idletimeout(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->connection_idletimeout, 1); +} + +void +pgagroal_prometheus_connection_max_connection_age(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->connection_max_connection_age, 1); +} + +void +pgagroal_prometheus_connection_awaiting(int limit_index) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + if (limit_index >= 0) + { + atomic_fetch_add(&prometheus->connections_awaiting[limit_index], 1); + } + + atomic_fetch_add(&prometheus->connections_awaiting_total, 1); +} + +void +pgagroal_prometheus_connection_unawaiting(int limit_index) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + if (limit_index >= 0 && atomic_load(&prometheus->connections_awaiting[limit_index]) > 0) + { + atomic_fetch_sub(&prometheus->connections_awaiting[limit_index], 1); + } + + if (atomic_load(&prometheus->connections_awaiting_total) > 0) + { + atomic_fetch_sub(&prometheus->connections_awaiting_total, 1); + } +} + +void +pgagroal_prometheus_connection_flush(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->connection_flush, 1); +} + +void +pgagroal_prometheus_connection_success(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->connection_success, 1); +} + +void +pgagroal_prometheus_auth_user_success(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->auth_user_success, 1); +} + +void +pgagroal_prometheus_auth_user_bad_password(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->auth_user_bad_password, 1); +} + +void +pgagroal_prometheus_auth_user_error(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->auth_user_error, 1); +} + +void +pgagroal_prometheus_client_wait_add(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->client_wait, 1); +} + +void +pgagroal_prometheus_client_wait_sub(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_sub(&prometheus->client_wait, 1); +} + +void +pgagroal_prometheus_client_active_add(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->client_active, 1); +} + +void +pgagroal_prometheus_client_active_sub(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_sub(&prometheus->client_active, 1); +} + +void +pgagroal_prometheus_query_count_add(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->query_count, 1); +} + +void +pgagroal_prometheus_query_count_specified_add(int slot) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->prometheus_connections[slot].query_count, 1); +} + +void +pgagroal_prometheus_query_count_specified_reset(int slot) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_store(&prometheus->prometheus_connections[slot].query_count, 0); +} + +void +pgagroal_prometheus_tx_count_add(void) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->tx_count, 1); +} + +void +pgagroal_prometheus_network_sent_add(ssize_t s) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->network_sent, s); +} + +void +pgagroal_prometheus_network_received_add(ssize_t s) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->network_received, s); +} + +void +pgagroal_prometheus_client_sockets_add(void) +{ + struct prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->client_sockets, 1); +} + +void +pgagroal_prometheus_client_sockets_sub(void) +{ + struct prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct prometheus*)prometheus_shmem; + + atomic_fetch_sub(&prometheus->client_sockets, 1); +} + +void +pgagroal_prometheus_self_sockets_add(void) +{ + struct prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->self_sockets, 1); +} + +void +pgagroal_prometheus_self_sockets_sub(void) +{ + struct prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct prometheus*)prometheus_shmem; + + atomic_fetch_sub(&prometheus->self_sockets, 1); +} + +void +pgagroal_prometheus_clear(void) +{ + signed char cache_is_free; + struct main_configuration* config; + struct main_prometheus* prometheus; + struct prometheus_cache* cache; + + if (!is_prometheus_enabled()) + { + return; + } + + config = (struct main_configuration*) shmem; + prometheus = (struct main_prometheus*)prometheus_shmem; + cache = (struct prometheus_cache*)prometheus_cache_shmem; + + for (int i = 0; i < HISTOGRAM_BUCKETS; i++) + { + atomic_store(&prometheus->session_time[i], 0); + } + atomic_store(&prometheus->session_time_sum, 0); + + atomic_store(&prometheus->connection_error, 0); + atomic_store(&prometheus->connection_kill, 0); + atomic_store(&prometheus->connection_remove, 0); + atomic_store(&prometheus->connection_timeout, 0); + atomic_store(&prometheus->connection_return, 0); + atomic_store(&prometheus->connection_invalid, 0); + atomic_store(&prometheus->connection_get, 0); + atomic_store(&prometheus->connection_idletimeout, 0); + atomic_store(&prometheus->connection_max_connection_age, 0); + atomic_store(&prometheus->connection_flush, 0); + atomic_store(&prometheus->connection_success, 0); + + atomic_store(&prometheus->prometheus_base.logging_info, 0); + atomic_store(&prometheus->prometheus_base.logging_warn, 0); + atomic_store(&prometheus->prometheus_base.logging_error, 0); + atomic_store(&prometheus->prometheus_base.logging_fatal, 0); + + // awaiting connections are on hold due to `blocking_timeout` + atomic_store(&prometheus->connections_awaiting_total, 0); + for (int i = 0; i < NUMBER_OF_LIMITS; i++) + { + atomic_store(&prometheus->connections_awaiting[i], 0); + } + + atomic_store(&prometheus->auth_user_success, 0); + atomic_store(&prometheus->auth_user_bad_password, 0); + atomic_store(&prometheus->auth_user_error, 0); + + atomic_store(&prometheus->client_active, 0); + atomic_store(&prometheus->client_wait, 0); + atomic_store(&prometheus->client_wait_time, 0); + + atomic_store(&prometheus->query_count, 0); + atomic_store(&prometheus->tx_count, 0); + + atomic_store(&prometheus->network_sent, 0); + atomic_store(&prometheus->network_received, 0); + + atomic_store(&prometheus->prometheus_base.client_sockets, 0); + atomic_store(&prometheus->prometheus_base.self_sockets, 0); + + for (int i = 0; i < NUMBER_OF_SERVERS; i++) + { + atomic_store(&prometheus->server_error[i], 0); + } + + for (int i = 0; i < config->max_connections; i++) + { + atomic_store(&prometheus->prometheus_connections[i].query_count, 0); + } + +retry_cache_locking: + cache_is_free = STATE_FREE; + if (atomic_compare_exchange_strong(&cache->lock, &cache_is_free, STATE_IN_USE)) + { + metrics_cache_invalidate(); + + atomic_store(&cache->lock, STATE_FREE); + } + else + { + /* Sleep for 1ms */ + SLEEP_AND_GOTO(1000000L, retry_cache_locking); + } +} + +void +pgagroal_prometheus_server_error(int server) +{ + struct main_prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + + atomic_fetch_add(&prometheus->server_error[server], 1); +} + +void +pgagroal_prometheus_failed_servers(void) +{ + int count; + struct main_prometheus* prometheus; + struct main_configuration* config; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct main_prometheus*)prometheus_shmem; + config = (struct main_configuration*) shmem; + + count = 0; + + for (int i = 0; i < config->number_of_servers; i++) + { + signed char state = atomic_load(&config->servers[i].state); + if (state == SERVER_FAILED) + { + count++; + } + } + + atomic_store(&prometheus->failed_servers, count); +} + +void +pgagroal_prometheus_logging(int type) +{ + struct prometheus* prometheus; + + if (!is_prometheus_enabled()) + { + return; + } + + prometheus = (struct prometheus*)prometheus_shmem; + + switch (type) + { + case PGAGROAL_LOGGING_LEVEL_INFO: + atomic_fetch_add(&prometheus->logging_info, 1); + break; + case PGAGROAL_LOGGING_LEVEL_WARN: + atomic_fetch_add(&prometheus->logging_warn, 1); + break; + case PGAGROAL_LOGGING_LEVEL_ERROR: + atomic_fetch_add(&prometheus->logging_error, 1); + break; + case PGAGROAL_LOGGING_LEVEL_FATAL: + atomic_fetch_add(&prometheus->logging_fatal, 1); + break; + default: + break; + } +} + +static int +resolve_page(struct message* msg) +{ + char* from = NULL; + int index; + + if (msg->length < 3 || strncmp((char*)msg->data, "GET", 3) != 0) + { + pgagroal_log_debug("Promethus: Not a GET request"); + return BAD_REQUEST; + } + + index = 4; + from = (char*)msg->data + index; + + while (pgagroal_read_byte(msg->data + index) != ' ') + { + index++; + } + + pgagroal_write_byte(msg->data + index, '\0'); + + if (strcmp(from, "/") == 0 || strcmp(from, "/index.html") == 0) + { + return PAGE_HOME; + } + else if (strcmp(from, "/metrics") == 0) + { + return PAGE_METRICS; + } + + return PAGE_UNKNOWN; +} + +static int +badrequest_page(int client_fd) +{ + char* data = NULL; + time_t now; + char time_buf[32]; + int status; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(&data, 0, sizeof(data)); + + now = time(NULL); + + memset(&time_buf, 0, sizeof(time_buf)); + ctime_r(&now, &time_buf[0]); + time_buf[strlen(time_buf) - 1] = 0; + + data = pgagroal_append(data, "HTTP/1.1 400 Bad Request\r\n"); + data = pgagroal_append(data, "Date: "); + data = pgagroal_append(data, &time_buf[0]); + data = pgagroal_append(data, "\r\n"); + + msg.kind = 0; + msg.length = strlen(data); + msg.data = data; + + status = pgagroal_write_message(NULL, client_fd, &msg); + + free(data); + + return status; +} + +static int +unknown_page(int client_fd) +{ + char* data = NULL; + time_t now; + char time_buf[32]; + int status; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(&data, 0, sizeof(data)); + + now = time(NULL); + + memset(&time_buf, 0, sizeof(time_buf)); + ctime_r(&now, &time_buf[0]); + time_buf[strlen(time_buf) - 1] = 0; + + data = pgagroal_append(data, "HTTP/1.1 403 Forbidden\r\n"); + data = pgagroal_append(data, "Date: "); + data = pgagroal_append(data, &time_buf[0]); + data = pgagroal_append(data, "\r\n"); + + msg.kind = 0; + msg.length = strlen(data); + msg.data = data; + + status = pgagroal_write_message(NULL, client_fd, &msg); + + free(data); + + return status; +} + +static int +home_page(int client_fd) +{ + char* data = NULL; + time_t now; + char time_buf[32]; + int status; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(&data, 0, sizeof(data)); + + now = time(NULL); + + memset(&time_buf, 0, sizeof(time_buf)); + ctime_r(&now, &time_buf[0]); + time_buf[strlen(time_buf) - 1] = 0; + + data = pgagroal_append(data, "HTTP/1.1 200 OK\r\n"); + data = pgagroal_append(data, "Content-Type: text/html; charset=utf-8\r\n"); + data = pgagroal_append(data, "Date: "); + data = pgagroal_append(data, &time_buf[0]); + data = pgagroal_append(data, "\r\n"); + data = pgagroal_append(data, "Transfer-Encoding: chunked\r\n"); + data = pgagroal_append(data, "\r\n"); + + msg.kind = 0; + msg.length = strlen(data); + msg.data = data; + + status = pgagroal_write_message(NULL, client_fd, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto done; + } + + free(data); + data = NULL; + + data = pgagroal_append(data, "\n"); + data = pgagroal_append(data, "\n"); + data = pgagroal_append(data, "\n"); + data = pgagroal_append(data, " pgagroal exporter\n"); + data = pgagroal_append(data, " "); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, "\n"); + data = pgagroal_append(data, "\n"); + data = pgagroal_append(data, "

pgagroal exporter

\n"); + data = pgagroal_append(data, "
\n"); + data = pgagroal_append(data, "

pgagroal_state

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " The state of pgagroal\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, "
valueState\n"); + data = pgagroal_append(data, "
    \n"); + data = pgagroal_append(data, "
  1. Running
  2. \n"); + data = pgagroal_append(data, "
  3. Graceful shutdown
  4. \n"); + data = pgagroal_append(data, "
\n"); + data = pgagroal_append(data, "
\n"); + data = pgagroal_append(data, "

pgagroal_pipeline_mode

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " The mode of pipeline\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, "
valueMode\n"); + data = pgagroal_append(data, "
    \n"); + data = pgagroal_append(data, "
  1. Performance
  2. \n"); + data = pgagroal_append(data, "
  3. Session
  4. \n"); + data = pgagroal_append(data, "
  5. Transaction
  6. \n"); + data = pgagroal_append(data, "
\n"); + data = pgagroal_append(data, "
\n"); + data = pgagroal_append(data, "

pgagroal_logging_info

\n"); + data = pgagroal_append(data, " The number of INFO logging statements\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_logging_warn

\n"); + data = pgagroal_append(data, " The number of WARN logging statements\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_logging_error

\n"); + data = pgagroal_append(data, " The number of ERROR logging statements\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_logging_fatal

\n"); + data = pgagroal_append(data, " The number of FATAL logging statements\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_server_error

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Errors for servers\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, "
nameThe name of the server
stateThe server state\n"); + data = pgagroal_append(data, "
    \n"); + data = pgagroal_append(data, "
  • not_init
  • \n"); + data = pgagroal_append(data, "
  • primary
  • \n"); + data = pgagroal_append(data, "
  • replica
  • \n"); + data = pgagroal_append(data, "
  • failover
  • \n"); + data = pgagroal_append(data, "
  • failed
  • \n"); + data = pgagroal_append(data, "
\n"); + data = pgagroal_append(data, "
\n"); + data = pgagroal_append(data, "

pgagroal_failed_servers

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " The number of failed servers. Only set if failover is enabled\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_wait_time

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " The waiting time of clients\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_query_count

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " The number of queries. Only session and transaction modes are supported\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_connection_query_count

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " The number of queries per connection. Only session and transaction modes are supported\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, "
idThe connection identifier
userThe user name
databaseThe database
application_nameThe application name
\n"); + data = pgagroal_append(data, "

pgagroal_tx_count

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " The number of transactions. Only session and transaction modes are supported\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_active_connections

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " The number of active connections\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_total_connections

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " The number of total connections\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_max_connections

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " The maximum number of connections\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_connection

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Connection information\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, "
idThe connection identifier
userThe user name
databaseThe database
application_nameThe application name
stateThe connection state\n"); + data = pgagroal_append(data, "
    \n"); + data = pgagroal_append(data, "
  • not_init
  • \n"); + data = pgagroal_append(data, "
  • init
  • \n"); + data = pgagroal_append(data, "
  • free
  • \n"); + data = pgagroal_append(data, "
  • in_use
  • \n"); + data = pgagroal_append(data, "
  • gracefully
  • \n"); + data = pgagroal_append(data, "
  • flush
  • \n"); + data = pgagroal_append(data, "
  • idle_check
  • \n"); + data = pgagroal_append(data, "
  • max_connection_age
  • \n"); + data = pgagroal_append(data, "
  • validation
  • \n"); + data = pgagroal_append(data, "
  • remove
  • \n"); + data = pgagroal_append(data, "
\n"); + data = pgagroal_append(data, "
\n"); + data = pgagroal_append(data, "

pgagroal_limit

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Limit information\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, "
userThe user name
databaseThe database
typeThe information type\n"); + data = pgagroal_append(data, "
    \n"); + data = pgagroal_append(data, "
  • min
  • \n"); + data = pgagroal_append(data, "
  • initial
  • \n"); + data = pgagroal_append(data, "
  • max
  • \n"); + data = pgagroal_append(data, "
  • active
  • \n"); + data = pgagroal_append(data, "
\n"); + data = pgagroal_append(data, "
\n"); + data = pgagroal_append(data, "

pgagroal_limit_awaiting

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Connections awaiting on hold reported by limit entries\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, " \n"); + data = pgagroal_append(data, "
userThe user name
databaseThe database
\n"); + data = pgagroal_append(data, "

pgagroal_session_time

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Histogram of session times\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_connection_error

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of connection errors\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_connection_kill

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of connection kills\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_connection_remove

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of connection removes\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_connection_timeout

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of connection time outs\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_connection_return

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of connection returns\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_connection_invalid

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of connection invalids\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_connection_get

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of connection gets\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_connection_idletimeout

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of connection idle timeouts\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_connection_max_connection_age

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of connection max age timeouts\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_connection_flush

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of connection flushes\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_connection_success

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of connection successes\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_connection_awaiting

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of connection suspended due to blocking_timeout\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_auth_user_success

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of successful user authentications\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_auth_user_bad_password

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of bad passwords during user authentication\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_auth_user_error

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of errors during user authentication\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_client_wait

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of waiting clients\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_client_active

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of active clients\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_network_sent

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Bytes sent by clients. Only session and transaction modes are supported\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_network_received

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Bytes received from servers. Only session and transaction modes are supported\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_client_sockets

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of sockets the client used\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_self_sockets

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of sockets used by pgagroal itself\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " agroal.github.io/pgagroal/\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "\n"); + data = pgagroal_append(data, "\n"); + + send_chunk(client_fd, data); + free(data); + data = NULL; + + /* Footer */ + data = pgagroal_append(data, "0\r\n\r\n"); + + msg.kind = 0; + msg.length = strlen(data); + msg.data = data; + + status = pgagroal_write_message(NULL, client_fd, &msg); + +done: + if (data != NULL) + { + free(data); + } + + return status; +} + +static int +home_vault_page(int client_fd) +{ + char* data = NULL; + time_t now; + char time_buf[32]; + int status; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(&data, 0, sizeof(data)); + + now = time(NULL); + + memset(&time_buf, 0, sizeof(time_buf)); + ctime_r(&now, &time_buf[0]); + time_buf[strlen(time_buf) - 1] = 0; + + data = pgagroal_append(data, "HTTP/1.1 200 OK\r\n"); + data = pgagroal_append(data, "Content-Type: text/html; charset=utf-8\r\n"); + data = pgagroal_append(data, "Date: "); + data = pgagroal_append(data, &time_buf[0]); + data = pgagroal_append(data, "\r\n"); + data = pgagroal_append(data, "Transfer-Encoding: chunked\r\n"); + data = pgagroal_append(data, "\r\n"); + + msg.kind = 0; + msg.length = strlen(data); + msg.data = data; + + status = pgagroal_write_message(NULL, client_fd, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto done; + } + + free(data); + data = NULL; + + data = pgagroal_append(data, "\n"); + data = pgagroal_append(data, "\n"); + data = pgagroal_append(data, "\n"); + data = pgagroal_append(data, " pgagroal-vault exporter\n"); + data = pgagroal_append(data, " "); + data = pgagroal_append(data, "\n"); + data = pgagroal_append(data, "\n"); + data = pgagroal_append(data, "

pgagroal-vault exporter

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Metrics\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_vault_logging_info

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " The number of INFO logging statements\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_vault_logging_warn

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " The number of WARN logging statements\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_vault_logging_error

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " The number of ERROR logging statements\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_vault_logging_fatal

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " The number of FATAL logging statements\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_vault_client_sockets

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of sockets the client used\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

pgagroal_vault_self_sockets

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " Number of sockets used by pgagroal-vault itself\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, " agroal.github.io/pgagroal/\n"); + data = pgagroal_append(data, "

\n"); + data = pgagroal_append(data, "\n"); + data = pgagroal_append(data, "\n"); + + send_chunk(client_fd, data); + free(data); + data = NULL; + + /* Footer */ + data = pgagroal_append(data, "0\r\n\r\n"); + + msg.kind = 0; + msg.length = strlen(data); + msg.data = data; + + status = pgagroal_write_message(NULL, client_fd, &msg); + +done: + if (data != NULL) + { + free(data); + } + + return status; +} + +static int +metrics_page(int client_fd) +{ + char* data = NULL; + time_t now; + char time_buf[32]; + int status; + struct message msg; + struct prometheus_cache* cache; + signed char cache_is_free; + + cache = (struct prometheus_cache*)prometheus_cache_shmem; + + memset(&msg, 0, sizeof(struct message)); + +retry_cache_locking: + cache_is_free = STATE_FREE; + if (atomic_compare_exchange_strong(&cache->lock, &cache_is_free, STATE_IN_USE)) + { + // can serve the message out of cache? + if (is_metrics_cache_configured() && is_metrics_cache_valid()) + { + // serve the message directly out of the cache + pgagroal_log_debug("Serving metrics out of cache (%d/%d bytes valid until %lld)", + strlen(cache->data), + cache->size, + cache->valid_until); + + msg.kind = 0; + msg.length = strlen(cache->data); + msg.data = cache->data; + } + else + { + // build the message without the cache + metrics_cache_invalidate(); + + now = time(NULL); + + memset(&time_buf, 0, sizeof(time_buf)); + ctime_r(&now, &time_buf[0]); + time_buf[strlen(time_buf) - 1] = 0; + + data = pgagroal_append(data, "HTTP/1.1 200 OK\r\n"); + data = pgagroal_append(data, "Content-Type: text/plain; version=0.0.3; charset=utf-8\r\n"); + data = pgagroal_append(data, "Date: "); + data = pgagroal_append(data, &time_buf[0]); + data = pgagroal_append(data, "\r\n"); + metrics_cache_append(data); // cache here to avoid the chunking for the cache + data = pgagroal_append(data, "Transfer-Encoding: chunked\r\n"); + data = pgagroal_append(data, "\r\n"); + + msg.kind = 0; + msg.length = strlen(data); + msg.data = data; + + status = pgagroal_write_message(NULL, client_fd, &msg); + if (status != MESSAGE_STATUS_OK) + { + metrics_cache_invalidate(); + atomic_store(&cache->lock, STATE_FREE); + + goto error; + } + + free(data); + data = NULL; + + general_information(client_fd); + connection_information(client_fd); + limit_information(client_fd); + session_information(client_fd); + pool_information(client_fd); + auth_information(client_fd); + client_information(client_fd); + internal_information(client_fd); + connection_awaiting_information(client_fd); + + /* Footer */ + data = pgagroal_append(data, "0\r\n\r\n"); + + msg.kind = 0; + msg.length = strlen(data); + msg.data = data; + + metrics_cache_finalize(); + + } + + // free the cache + atomic_store(&cache->lock, STATE_FREE); + + } // end of cache locking + else + { + /* Sleep for 1ms */ + SLEEP_AND_GOTO(1000000L, retry_cache_locking) + } + + status = pgagroal_write_message(NULL, client_fd, &msg); + + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + free(data); + + return 0; + +error: + + free(data); + + return 1; +} + +static int +metrics_vault_page(int client_fd) +{ + char* data = NULL; + time_t now; + char time_buf[32]; + int status; + struct message msg; + struct prometheus_cache* cache; + signed char cache_is_free; + + cache = (struct prometheus_cache*)prometheus_cache_shmem; + + memset(&msg, 0, sizeof(struct message)); + +retry_cache_locking: + cache_is_free = STATE_FREE; + if (atomic_compare_exchange_strong(&cache->lock, &cache_is_free, STATE_IN_USE)) + { + // can serve the message out of cache? + if (is_metrics_cache_configured() && is_metrics_cache_valid()) + { + // serve the message directly out of the cache + pgagroal_log_debug("Serving metrics out of cache (%d/%d bytes valid until %lld)", + strlen(cache->data), + cache->size, + cache->valid_until); + + msg.kind = 0; + msg.length = strlen(cache->data); + msg.data = cache->data; + } + else + { + // build the message without the cache + metrics_cache_invalidate(); + + now = time(NULL); + + memset(&time_buf, 0, sizeof(time_buf)); + ctime_r(&now, &time_buf[0]); + time_buf[strlen(time_buf) - 1] = 0; + + data = pgagroal_append(data, "HTTP/1.1 200 OK\r\n"); + data = pgagroal_append(data, "Content-Type: text/plain; version=0.0.3; charset=utf-8\r\n"); + data = pgagroal_append(data, "Date: "); + data = pgagroal_append(data, &time_buf[0]); + data = pgagroal_append(data, "\r\n"); + metrics_cache_append(data); // cache here to avoid the chunking for the cache + data = pgagroal_append(data, "Transfer-Encoding: chunked\r\n"); + data = pgagroal_append(data, "\r\n"); + + msg.kind = 0; + msg.length = strlen(data); + msg.data = data; + + status = pgagroal_write_message(NULL, client_fd, &msg); + if (status != MESSAGE_STATUS_OK) + { + metrics_cache_invalidate(); + atomic_store(&cache->lock, STATE_FREE); + + goto error; + } + + free(data); + data = NULL; + + general_vault_information(client_fd); + internal_vault_information(client_fd); + + /* Footer */ + data = pgagroal_append(data, "0\r\n\r\n"); + + msg.kind = 0; + msg.length = strlen(data); + msg.data = data; + + metrics_cache_finalize(); + + } + + // free the cache + atomic_store(&cache->lock, STATE_FREE); + + } // end of cache locking + else + { + /* Sleep for 1ms */ + SLEEP_AND_GOTO(1000000L, retry_cache_locking) + } + + status = pgagroal_write_message(NULL, client_fd, &msg); + + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + free(data); + + return 0; + +error: + + free(data); + + return 1; +} + +static int +bad_request(int client_fd) +{ + char* data = NULL; + time_t now; + char time_buf[32]; + int status; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + memset(&data, 0, sizeof(data)); + + now = time(NULL); + + memset(&time_buf, 0, sizeof(time_buf)); + ctime_r(&now, &time_buf[0]); + time_buf[strlen(time_buf) - 1] = 0; + + data = pgagroal_append(data, "HTTP/1.1 400 Bad Request\r\n"); + data = pgagroal_append(data, "Date: "); + data = pgagroal_append(data, &time_buf[0]); + data = pgagroal_append(data, "\r\n"); + + msg.kind = 0; + msg.length = strlen(data); + msg.data = data; + + status = pgagroal_write_message(NULL, client_fd, &msg); + + free(data); + + return status; +} + +static void +general_information(int client_fd) +{ + char* data = NULL; + struct main_configuration* config; + struct main_prometheus* prometheus; + + config = (struct main_configuration*)shmem; + + prometheus = (struct main_prometheus*)prometheus_shmem; + + data = pgagroal_append(data, "#HELP pgagroal_state The state of pgagroal\n"); + data = pgagroal_append(data, "#TYPE pgagroal_state gauge\n"); + data = pgagroal_append(data, "pgagroal_state "); + if (config->gracefully) + { + data = pgagroal_append(data, "2"); + } + else + { + data = pgagroal_append(data, "1"); + } + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_pipeline_mode The mode of pipeline\n"); + data = pgagroal_append(data, "#TYPE pgagroal_pipeline_mode gauge\n"); + data = pgagroal_append(data, "pgagroal_pipeline_mode "); + data = pgagroal_append_int(data, config->pipeline); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_server_error The number of errors for servers\n"); + data = pgagroal_append(data, "#TYPE pgagroal_server_error counter\n"); + for (int i = 0; i < config->number_of_servers; i++) + { + int state = atomic_load(&config->servers[i].state); + + data = pgagroal_append(data, "pgagroal_server_error{"); + + data = pgagroal_append(data, "name=\""); + data = pgagroal_append(data, config->servers[i].name); + data = pgagroal_append(data, "\","); + + data = pgagroal_append(data, "state=\""); + + switch (state) + { + case SERVER_NOTINIT: + case SERVER_NOTINIT_PRIMARY: + data = pgagroal_append(data, "not_init"); + break; + case SERVER_PRIMARY: + data = pgagroal_append(data, "primary"); + break; + case SERVER_REPLICA: + data = pgagroal_append(data, "replica"); + break; + case SERVER_FAILOVER: + data = pgagroal_append(data, "failover"); + break; + case SERVER_FAILED: + data = pgagroal_append(data, "failed"); + break; + default: + break; + } + + data = pgagroal_append(data, "\"} "); + + data = pgagroal_append_ulong(data, atomic_load(&prometheus->server_error[i])); + data = pgagroal_append(data, "\n"); + } + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "#HELP pgagroal_logging_info The number of INFO logging statements\n"); + data = pgagroal_append(data, "#TYPE pgagroal_logging_info gauge\n"); + data = pgagroal_append(data, "pgagroal_logging_info "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->prometheus_base.logging_info)); + data = pgagroal_append(data, "\n\n"); + data = pgagroal_append(data, "#HELP pgagroal_logging_warn The number of WARN logging statements\n"); + data = pgagroal_append(data, "#TYPE pgagroal_logging_warn gauge\n"); + data = pgagroal_append(data, "pgagroal_logging_warn "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->prometheus_base.logging_warn)); + data = pgagroal_append(data, "\n\n"); + data = pgagroal_append(data, "#HELP pgagroal_logging_error The number of ERROR logging statements\n"); + data = pgagroal_append(data, "#TYPE pgagroal_logging_error gauge\n"); + data = pgagroal_append(data, "pgagroal_logging_error "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->prometheus_base.logging_error)); + data = pgagroal_append(data, "\n\n"); + data = pgagroal_append(data, "#HELP pgagroal_logging_fatal The number of FATAL logging statements\n"); + data = pgagroal_append(data, "#TYPE pgagroal_logging_fatal gauge\n"); + data = pgagroal_append(data, "pgagroal_logging_fatal "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->prometheus_base.logging_fatal)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_failed_servers The number of failed servers\n"); + data = pgagroal_append(data, "#TYPE pgagroal_failed_servers gauge\n"); + data = pgagroal_append(data, "pgagroal_failed_servers "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->failed_servers)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_wait_time The waiting time of clients\n"); + data = pgagroal_append(data, "#TYPE pgagroal_wait_time gauge\n"); + data = pgagroal_append(data, "pgagroal_wait_time "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->client_wait_time)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_query_count The number of queries\n"); + data = pgagroal_append(data, "#TYPE pgagroal_query_count counter\n"); + data = pgagroal_append(data, "pgagroal_query_count "); + data = pgagroal_append_ullong(data, atomic_load(&prometheus->query_count)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_connection_query_count The number of queries per connection\n"); + data = pgagroal_append(data, "#TYPE pgagroal_connection_query_count counter\n"); + for (int i = 0; i < config->max_connections; i++) + { + data = pgagroal_append(data, "pgagroal_connection_query_count{"); + + data = pgagroal_append(data, "id=\""); + data = pgagroal_append_int(data, i); + data = pgagroal_append(data, "\","); + + data = pgagroal_append(data, "user=\""); + data = pgagroal_append(data, config->connections[i].username); + data = pgagroal_append(data, "\","); + + data = pgagroal_append(data, "database=\""); + data = pgagroal_append(data, config->connections[i].database); + data = pgagroal_append(data, "\","); + + data = pgagroal_append(data, "application_name=\""); + data = pgagroal_append(data, config->connections[i].appname); + data = pgagroal_append(data, "\"} "); + + data = pgagroal_append_ullong(data, atomic_load(&prometheus->prometheus_connections[i].query_count)); + data = pgagroal_append(data, "\n"); + } + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "#HELP pgagroal_tx_count The number of transactions\n"); + data = pgagroal_append(data, "#TYPE pgagroal_tx_count counter\n"); + data = pgagroal_append(data, "pgagroal_tx_count "); + data = pgagroal_append_ullong(data, atomic_load(&prometheus->tx_count)); + data = pgagroal_append(data, "\n\n"); + + if (data != NULL) + { + send_chunk(client_fd, data); + metrics_cache_append(data); + free(data); + data = NULL; + } +} + +static void +general_vault_information(int client_fd) +{ + char* data = NULL; + struct vault_prometheus* prometheus; + + prometheus = (struct vault_prometheus*)prometheus_shmem; + + data = pgagroal_append(data, "#HELP pgagroal_vault_logging_info The number of INFO logging statements\n"); + data = pgagroal_append(data, "#TYPE pgagroal_vault_logging_info gauge\n"); + data = pgagroal_append(data, "pgagroal_vault_logging_info "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->prometheus_base.logging_info)); + data = pgagroal_append(data, "\n\n"); + data = pgagroal_append(data, "#HELP pgagroal_vault_logging_warn The number of WARN logging statements\n"); + data = pgagroal_append(data, "#TYPE pgagroal_vault_logging_warn gauge\n"); + data = pgagroal_append(data, "pgagroal_vault_logging_warn "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->prometheus_base.logging_warn)); + data = pgagroal_append(data, "\n\n"); + data = pgagroal_append(data, "#HELP pgagroal_vault_logging_error The number of ERROR logging statements\n"); + data = pgagroal_append(data, "#TYPE pgagroal_vault_logging_error gauge\n"); + data = pgagroal_append(data, "pgagroal_vault_logging_error "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->prometheus_base.logging_error)); + data = pgagroal_append(data, "\n\n"); + data = pgagroal_append(data, "#HELP pgagroal_vault_logging_fatal The number of FATAL logging statements\n"); + data = pgagroal_append(data, "#TYPE pgagroal_vault_logging_fatal gauge\n"); + data = pgagroal_append(data, "pgagroal_vault_logging_fatal "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->prometheus_base.logging_fatal)); + data = pgagroal_append(data, "\n\n"); + + if (data != NULL) + { + send_chunk(client_fd, data); + metrics_cache_append(data); + free(data); + data = NULL; + } +} + +static void +connection_information(int client_fd) +{ + char* data = NULL; + int active; + int total; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + active = 0; + total = 0; + + for (int i = 0; i < config->max_connections; i++) + { + int state = atomic_load(&config->states[i]); + switch (state) + { + case STATE_IN_USE: + case STATE_GRACEFULLY: + active++; + case STATE_INIT: + case STATE_FREE: + case STATE_FLUSH: + case STATE_IDLE_CHECK: + case STATE_MAX_CONNECTION_AGE: + case STATE_VALIDATION: + case STATE_REMOVE: + total++; + break; + default: + break; + } + } + + data = pgagroal_append(data, "#HELP pgagroal_active_connections The number of active connections\n"); + data = pgagroal_append(data, "#TYPE pgagroal_active_connections gauge\n"); + data = pgagroal_append(data, "pgagroal_active_connections "); + data = pgagroal_append_int(data, active); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_total_connections The total number of connections\n"); + data = pgagroal_append(data, "#TYPE pgagroal_total_connections gauge\n"); + data = pgagroal_append(data, "pgagroal_total_connections "); + data = pgagroal_append_int(data, total); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_max_connections The maximum number of connections\n"); + data = pgagroal_append(data, "#TYPE pgagroal_max_connections counter\n"); + data = pgagroal_append(data, "pgagroal_max_connections "); + data = pgagroal_append_int(data, config->max_connections); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_connection The connection information\n"); + data = pgagroal_append(data, "#TYPE pgagroal_connection gauge\n"); + for (int i = 0; i < config->max_connections; i++) + { + int state = atomic_load(&config->states[i]); + + data = pgagroal_append(data, "pgagroal_connection{"); + + data = pgagroal_append(data, "id=\""); + data = pgagroal_append_int(data, i); + data = pgagroal_append(data, "\","); + + data = pgagroal_append(data, "user=\""); + data = pgagroal_append(data, config->connections[i].username); + data = pgagroal_append(data, "\","); + + data = pgagroal_append(data, "database=\""); + data = pgagroal_append(data, config->connections[i].database); + data = pgagroal_append(data, "\","); + + data = pgagroal_append(data, "application_name=\""); + data = pgagroal_append(data, config->connections[i].appname); + data = pgagroal_append(data, "\","); + + data = pgagroal_append(data, "state=\""); + + switch (state) + { + case STATE_NOTINIT: + data = pgagroal_append(data, "not_init"); + break; + case STATE_INIT: + data = pgagroal_append(data, "init"); + break; + case STATE_FREE: + data = pgagroal_append(data, "free"); + break; + case STATE_IN_USE: + data = pgagroal_append(data, "in_use"); + break; + case STATE_GRACEFULLY: + data = pgagroal_append(data, "gracefully"); + break; + case STATE_FLUSH: + data = pgagroal_append(data, "flush"); + break; + case STATE_IDLE_CHECK: + data = pgagroal_append(data, "idle_check"); + break; + case STATE_MAX_CONNECTION_AGE: + data = pgagroal_append(data, "max_connection_age"); + break; + case STATE_VALIDATION: + data = pgagroal_append(data, "validation"); + break; + case STATE_REMOVE: + data = pgagroal_append(data, "remove"); + break; + default: + break; + } + + data = pgagroal_append(data, "\"} "); + + switch (state) + { + case STATE_NOTINIT: + data = pgagroal_append(data, "0"); + break; + case STATE_INIT: + case STATE_FREE: + case STATE_IN_USE: + case STATE_GRACEFULLY: + case STATE_FLUSH: + case STATE_IDLE_CHECK: + case STATE_MAX_CONNECTION_AGE: + case STATE_VALIDATION: + case STATE_REMOVE: + data = pgagroal_append(data, "1"); + break; + default: + break; + } + + data = pgagroal_append(data, "\n"); + + if (strlen(data) > CHUNK_SIZE) + { + send_chunk(client_fd, data); + metrics_cache_append(data); + free(data); + data = NULL; + } + } + + data = pgagroal_append(data, "\n"); + + if (data != NULL) + { + send_chunk(client_fd, data); + metrics_cache_append(data); + free(data); + data = NULL; + } +} + +static void +limit_information(int client_fd) +{ + char* data = NULL; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + if (config->number_of_limits > 0) + { + data = pgagroal_append(data, "#HELP pgagroal_limit The limit information\n"); + data = pgagroal_append(data, "#TYPE pgagroal_limit gauge\n"); + for (int i = 0; i < config->number_of_limits; i++) + { + data = pgagroal_append(data, "pgagroal_limit{"); + + data = pgagroal_append(data, "user=\""); + data = pgagroal_append(data, config->limits[i].username); + data = pgagroal_append(data, "\","); + + data = pgagroal_append(data, "database=\""); + data = pgagroal_append(data, config->limits[i].database); + data = pgagroal_append(data, "\","); + + data = pgagroal_append(data, "type=\"min\"} "); + data = pgagroal_append_int(data, config->limits[i].min_size); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_limit{"); + + data = pgagroal_append(data, "user=\""); + data = pgagroal_append(data, config->limits[i].username); + data = pgagroal_append(data, "\","); + + data = pgagroal_append(data, "database=\""); + data = pgagroal_append(data, config->limits[i].database); + data = pgagroal_append(data, "\","); + + data = pgagroal_append(data, "type=\"initial\"} "); + data = pgagroal_append_int(data, config->limits[i].initial_size); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_limit{"); + + data = pgagroal_append(data, "user=\""); + data = pgagroal_append(data, config->limits[i].username); + data = pgagroal_append(data, "\","); + + data = pgagroal_append(data, "database=\""); + data = pgagroal_append(data, config->limits[i].database); + data = pgagroal_append(data, "\","); + + data = pgagroal_append(data, "type=\"max\"} "); + data = pgagroal_append_int(data, config->limits[i].max_size); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_limit{"); + + data = pgagroal_append(data, "user=\""); + data = pgagroal_append(data, config->limits[i].username); + data = pgagroal_append(data, "\","); + + data = pgagroal_append(data, "database=\""); + data = pgagroal_append(data, config->limits[i].database); + data = pgagroal_append(data, "\","); + + data = pgagroal_append(data, "type=\"active\"} "); + data = pgagroal_append_int(data, config->limits[i].active_connections); + data = pgagroal_append(data, "\n"); + + if (strlen(data) > CHUNK_SIZE) + { + send_chunk(client_fd, data); + metrics_cache_append(data); + free(data); + data = NULL; + } + } + + data = pgagroal_append(data, "\n"); + + if (data != NULL) + { + send_chunk(client_fd, data); + metrics_cache_append(data); + free(data); + data = NULL; + } + } +} + +static void +session_information(int client_fd) +{ + char* data = NULL; + unsigned long counter; + struct main_prometheus* prometheus; + + prometheus = (struct main_prometheus*)prometheus_shmem; + + counter = 0; + + data = pgagroal_append(data, "#HELP pgagroal_session_time_seconds The session times\n"); + data = pgagroal_append(data, "#TYPE pgagroal_session_time_seconds histogram\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_bucket{le=\"5\"} "); + counter += atomic_load(&prometheus->session_time[0]); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_bucket{le=\"10\"} "); + counter += atomic_load(&prometheus->session_time[1]); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_bucket{le=\"20\"} "); + counter += atomic_load(&prometheus->session_time[2]); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_bucket{le=\"30\"} "); + counter += atomic_load(&prometheus->session_time[3]); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_bucket{le=\"45\"} "); + counter += atomic_load(&prometheus->session_time[4]); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_bucket{le=\"60\"} "); + counter += atomic_load(&prometheus->session_time[5]); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_bucket{le=\"300\"} "); + counter += atomic_load(&prometheus->session_time[6]); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_bucket{le=\"600\"} "); + counter += atomic_load(&prometheus->session_time[7]); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_bucket{le=\"1200\"} "); + counter += atomic_load(&prometheus->session_time[8]); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_bucket{le=\"1800\"} "); + counter += atomic_load(&prometheus->session_time[9]); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_bucket{le=\"2700\"} "); + counter += atomic_load(&prometheus->session_time[10]); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_bucket{le=\"3600\"} "); + counter += atomic_load(&prometheus->session_time[11]); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_bucket{le=\"7200\"} "); + counter += atomic_load(&prometheus->session_time[12]); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_bucket{le=\"14400\"} "); + counter += atomic_load(&prometheus->session_time[13]); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_bucket{le=\"21600\"} "); + counter += atomic_load(&prometheus->session_time[14]); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_bucket{le=\"43200\"} "); + counter += atomic_load(&prometheus->session_time[15]); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_bucket{le=\"86400\"} "); + counter += atomic_load(&prometheus->session_time[16]); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_bucket{le=\"+Inf\"} "); + counter += atomic_load(&prometheus->session_time[17]); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_sum "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->session_time_sum)); + data = pgagroal_append(data, "\n"); + + data = pgagroal_append(data, "pgagroal_session_time_seconds_count "); + data = pgagroal_append_ulong(data, counter); + data = pgagroal_append(data, "\n\n"); + + send_chunk(client_fd, data); + metrics_cache_append(data); + free(data); + data = NULL; +} + +static void +pool_information(int client_fd) +{ + char* data = NULL; + struct main_prometheus* prometheus; + + prometheus = (struct main_prometheus*)prometheus_shmem; + + data = pgagroal_append(data, "#HELP pgagroal_connection_error Number of connection errors\n"); + data = pgagroal_append(data, "#TYPE pgagroal_connection_error counter\n"); + data = pgagroal_append(data, "pgagroal_connection_error "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->connection_error)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_connection_kill Number of connection kills\n"); + data = pgagroal_append(data, "#TYPE pgagroal_connection_kill counter\n"); + data = pgagroal_append(data, "pgagroal_connection_kill "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->connection_kill)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_connection_remove Number of connection removes\n"); + data = pgagroal_append(data, "#TYPE pgagroal_connection_remove counter\n"); + data = pgagroal_append(data, "pgagroal_connection_remove "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->connection_remove)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_connection_timeout Number of connection time outs\n"); + data = pgagroal_append(data, "#TYPE pgagroal_connection_timeout counter\n"); + data = pgagroal_append(data, "pgagroal_connection_timeout "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->connection_timeout)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_connection_return Number of connection returns\n"); + data = pgagroal_append(data, "#TYPE pgagroal_connection_return counter\n"); + data = pgagroal_append(data, "pgagroal_connection_return "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->connection_return)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_connection_invalid Number of connection invalids\n"); + data = pgagroal_append(data, "#TYPE pgagroal_connection_invalid counter\n"); + data = pgagroal_append(data, "pgagroal_connection_invalid "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->connection_invalid)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_connection_get Number of connection gets\n"); + data = pgagroal_append(data, "#TYPE pgagroal_connection_get counter\n"); + data = pgagroal_append(data, "pgagroal_connection_get "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->connection_get)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_connection_idletimeout Number of connection idle timeouts\n"); + data = pgagroal_append(data, "#TYPE pgagroal_connection_idletimeout counter\n"); + data = pgagroal_append(data, "pgagroal_connection_idletimeout "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->connection_idletimeout)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_connection_max_connection_age Number of connection max age timeouts\n"); + data = pgagroal_append(data, "#TYPE pgagroal_connection_max_connection_age counter\n"); + data = pgagroal_append(data, "pgagroal_connection_max_connection_age "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->connection_max_connection_age)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_connection_flush Number of connection flushes\n"); + data = pgagroal_append(data, "#TYPE pgagroal_connection_flush counter\n"); + data = pgagroal_append(data, "pgagroal_connection_flush "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->connection_flush)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_connection_success Number of connection successes\n"); + data = pgagroal_append(data, "#TYPE pgagroal_connection_success counter\n"); + data = pgagroal_append(data, "pgagroal_connection_success "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->connection_success)); + data = pgagroal_append(data, "\n\n"); + + send_chunk(client_fd, data); + metrics_cache_append(data); + free(data); + data = NULL; +} + +static void +auth_information(int client_fd) +{ + char* data = NULL; + struct main_prometheus* prometheus; + + prometheus = (struct main_prometheus*)prometheus_shmem; + + data = pgagroal_append(data, "#HELP pgagroal_auth_user_success Number of successful user authentications\n"); + data = pgagroal_append(data, "#TYPE pgagroal_auth_user_success counter\n"); + data = pgagroal_append(data, "pgagroal_auth_user_success "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->auth_user_success)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_auth_user_bad_password Number of bad passwords during user authentication\n"); + data = pgagroal_append(data, "#TYPE pgagroal_auth_user_bad_password counter\n"); + data = pgagroal_append(data, "pgagroal_auth_user_bad_password "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->auth_user_bad_password)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_auth_user_error Number of errors during user authentication\n"); + data = pgagroal_append(data, "#TYPE pgagroal_auth_user_error counter\n"); + data = pgagroal_append(data, "pgagroal_auth_user_error "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->auth_user_error)); + data = pgagroal_append(data, "\n\n"); + + send_chunk(client_fd, data); + metrics_cache_append(data); + free(data); + data = NULL; +} + +static void +client_information(int client_fd) +{ + char* data = NULL; + struct main_prometheus* prometheus; + + prometheus = (struct main_prometheus*)prometheus_shmem; + + data = pgagroal_append(data, "#HELP pgagroal_client_wait Number of waiting clients\n"); + data = pgagroal_append(data, "#TYPE pgagroal_client_wait gauge\n"); + data = pgagroal_append(data, "pgagroal_client_wait "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->client_wait)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_client_active Number of active clients\n"); + data = pgagroal_append(data, "#TYPE pgagroal_client_active gauge\n"); + data = pgagroal_append(data, "pgagroal_client_active "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->client_active)); + data = pgagroal_append(data, "\n\n"); + + send_chunk(client_fd, data); + metrics_cache_append(data); + free(data); + data = NULL; +} + +static void +internal_information(int client_fd) +{ + char* data = NULL; + struct main_prometheus* prometheus; + + prometheus = (struct main_prometheus*)prometheus_shmem; + + data = pgagroal_append(data, "#HELP pgagroal_network_sent Bytes sent by clients\n"); + data = pgagroal_append(data, "#TYPE pgagroal_network_sent gauge\n"); + data = pgagroal_append(data, "pgagroal_network_sent "); + data = pgagroal_append_ullong(data, atomic_load(&prometheus->network_sent)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_network_received Bytes received from servers\n"); + data = pgagroal_append(data, "#TYPE pgagroal_network_received gauge\n"); + data = pgagroal_append(data, "pgagroal_network_received "); + data = pgagroal_append_ullong(data, atomic_load(&prometheus->network_received)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_client_sockets Number of sockets the client used\n"); + data = pgagroal_append(data, "#TYPE pgagroal_client_sockets gauge\n"); + data = pgagroal_append(data, "pgagroal_client_sockets "); + data = pgagroal_append_int(data, atomic_load(&prometheus->prometheus_base.client_sockets)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_self_sockets Number of sockets used by pgagroal itself\n"); + data = pgagroal_append(data, "#TYPE pgagroal_self_sockets gauge\n"); + data = pgagroal_append(data, "pgagroal_self_sockets "); + data = pgagroal_append_int(data, atomic_load(&prometheus->prometheus_base.self_sockets)); + data = pgagroal_append(data, "\n\n"); + + send_chunk(client_fd, data); + metrics_cache_append(data); + free(data); + data = NULL; +} + +static void +internal_vault_information(int client_fd) +{ + char* data = NULL; + struct vault_prometheus* prometheus; + + prometheus = (struct vault_prometheus*)prometheus_shmem; + + data = pgagroal_append(data, "#HELP pgagroal_vault_client_sockets Number of sockets the client used\n"); + data = pgagroal_append(data, "#TYPE pgagroal_vault_client_sockets gauge\n"); + data = pgagroal_append(data, "pgagroal_client_sockets "); + data = pgagroal_append_int(data, atomic_load(&prometheus->prometheus_base.client_sockets)); + data = pgagroal_append(data, "\n\n"); + + data = pgagroal_append(data, "#HELP pgagroal_vault_self_sockets Number of sockets used by pgagroal-vault itself\n"); + data = pgagroal_append(data, "#TYPE pgagroal_vault_self_sockets gauge\n"); + data = pgagroal_append(data, "pgagroal_vault_self_sockets "); + data = pgagroal_append_int(data, atomic_load(&prometheus->prometheus_base.self_sockets)); + data = pgagroal_append(data, "\n\n"); + + send_chunk(client_fd, data); + metrics_cache_append(data); + free(data); + data = NULL; +} +/** + * Provides information about the connection awaiting. + * + * Prints the total connection awaiting counter + * and also one line per limit if there are limits. + */ +static void +connection_awaiting_information(int client_fd) +{ + char* data = NULL; + struct main_configuration* config; + struct main_prometheus* prometheus; + + config = (struct main_configuration*)shmem; + + prometheus = (struct main_prometheus*)prometheus_shmem; + + data = pgagroal_append(data, "#HELP pgagroal_connection_awaiting Number of connection on-hold (awaiting)\n"); + data = pgagroal_append(data, "#TYPE pgagroal_connection_awaiting gauge\n"); + data = pgagroal_append(data, "pgagroal_connection_awaiting "); + data = pgagroal_append_ulong(data, atomic_load(&prometheus->connections_awaiting_total)); + data = pgagroal_append(data, "\n\n"); + + if (config->number_of_limits > 0) + { + data = pgagroal_append(data, "#HELP pgagroal_limit_awaiting The connections on-hold (awaiting) information\n"); + data = pgagroal_append(data, "#TYPE pgagroal_limit_awaiting gauge\n"); + for (int i = 0; i < config->number_of_limits; i++) + { + + data = pgagroal_append(data, "pgagroal_limit_awaiting{"); + + data = pgagroal_append(data, "user=\""); + data = pgagroal_append(data, config->limits[i].username); + data = pgagroal_append(data, "\","); + + data = pgagroal_append(data, "database=\""); + data = pgagroal_append(data, config->limits[i].database); + data = pgagroal_append(data, "\"} "); + + data = pgagroal_append_ulong(data, atomic_load(&prometheus->connections_awaiting[i])); + data = pgagroal_append(data, "\n"); + + if (strlen(data) > CHUNK_SIZE) + { + send_chunk(client_fd, data); + metrics_cache_append(data); + free(data); + data = NULL; + } + + } + } + + if (data != NULL) + { + data = pgagroal_append(data, "\n"); + send_chunk(client_fd, data); + metrics_cache_append(data); + free(data); + data = NULL; + } +} + +static int +send_chunk(int client_fd, char* data) +{ + int status; + char* m = NULL; + struct message msg; + + memset(&msg, 0, sizeof(struct message)); + + m = calloc(1, 20); + if (m == NULL) + { + pgagroal_log_fatal("Couldn't allocate memory while binding host"); + return MESSAGE_STATUS_ERROR; + } + + sprintf(m, "%zX\r\n", strlen(data)); + + m = pgagroal_append(m, data); + m = pgagroal_append(m, "\r\n"); + + msg.kind = 0; + msg.length = strlen(m); + msg.data = m; + + status = pgagroal_write_message(NULL, client_fd, &msg); + + free(m); + + return status; +} + +/** + * Checks if the Prometheus cache configuration setting + * (`metrics_cache`) has a non-zero value, that means there + * are seconds to cache the response. + * + * @return true if there is a cache configuration, + * false if no cache is active + */ +static bool +is_metrics_cache_configured(void) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + // cannot have caching if not set metrics! + if (config->common.metrics == 0) + { + return false; + } + + return config->common.metrics_cache_max_age != PGAGROAL_PROMETHEUS_CACHE_DISABLED; +} + +/** + * Checks if the cache is still valid, and therefore can be + * used to serve as a response. + * A cache is considred valid if it has non-empty payload and + * a timestamp in the future. + * + * @return true if the cache is still valid + */ +static bool +is_metrics_cache_valid(void) +{ + time_t now; + + struct prometheus_cache* cache; + + cache = (struct prometheus_cache*)prometheus_cache_shmem; + + if (cache->valid_until == 0 || strlen(cache->data) == 0) + { + return false; + } + + now = time(NULL); + return now <= cache->valid_until; +} + +int +pgagroal_init_prometheus_cache(size_t* p_size, void** p_shmem) +{ + struct prometheus_cache* cache; + struct configuration* config = (struct configuration*) shmem; + size_t cache_size = 0; + size_t struct_size = 0; + + // first of all, allocate the overall cache structure + cache_size = metrics_cache_size_to_alloc(); + struct_size = sizeof(struct prometheus_cache); + + if (pgagroal_create_shared_memory(struct_size + cache_size, config->hugepage, (void*) &cache)) + { + goto error; + } + + memset(cache, 0, struct_size + cache_size); + cache->valid_until = 0; + cache->size = cache_size; + atomic_init(&cache->lock, STATE_FREE); + + // success! do the memory swap + *p_shmem = cache; + *p_size = cache_size + struct_size; + return 0; + +error: + // disable caching + config->metrics_cache_max_age = config->metrics_cache_max_size = PGAGROAL_PROMETHEUS_CACHE_DISABLED; + pgagroal_log_error("Cannot allocate shared memory for the Prometheus cache!"); + *p_size = 0; + *p_shmem = NULL; + + return 1; +} + +/** + * Provides the size of the cache to allocate. + * + * It checks if the metrics cache is configured, and + * computers the right minimum value between the + * user configured requested size and the default + * cache size. + * + * @return the cache size to allocate + */ +static size_t +metrics_cache_size_to_alloc(void) +{ + struct main_configuration* config; + size_t cache_size = 0; + + config = (struct main_configuration*)shmem; + + // which size to use ? + // either the configured (i.e., requested by user) if lower than the max size + // or the default value + if (is_metrics_cache_configured()) + { + cache_size = config->common.metrics_cache_max_size > 0 + ? MIN(config->common.metrics_cache_max_size, PROMETHEUS_MAX_CACHE_SIZE) + : PROMETHEUS_DEFAULT_CACHE_SIZE; + } + + return cache_size; +} + +/** + * Invalidates the cache. + * + * Requires the caller to hold the lock on the cache! + * + * Invalidating the cache means that the payload is zero-filled + * and that the valid_until field is set to zero too. + */ +static void +metrics_cache_invalidate(void) +{ + struct prometheus_cache* cache; + + cache = (struct prometheus_cache*)prometheus_cache_shmem; + + memset(cache->data, 0, cache->size); + cache->valid_until = 0; +} + +/** + * Appends data to the cache. + * + * Requires the caller to hold the lock on the cache! + * + * If the input data is empty, nothing happens. + * The data is appended only if the cache does not overflows, that + * means the current size of the cache plus the size of the data + * to append does not exceed the current cache size. + * If the cache overflows, the cache is flushed and marked + * as invalid. + * This makes safe to call this method along the workflow of + * building the Prometheus response. + * + * @param data the string to append to the cache + * @return true on success + */ +static bool +metrics_cache_append(char* data) +{ + int origin_length = 0; + int append_length = 0; + struct prometheus_cache* cache; + + cache = (struct prometheus_cache*)prometheus_cache_shmem; + + if (!is_metrics_cache_configured()) + { + return false; + } + + origin_length = strlen(cache->data); + append_length = strlen(data); + // need to append the data to the cache + if (origin_length + append_length >= cache->size) + { + // cannot append new data, so invalidate cache + pgagroal_log_debug("Cannot append %d bytes to the Prometheus cache because it will overflow the size of %d bytes (currently at %d bytes). HINT: try adjusting `metrics_cache_max_size`", + append_length, + cache->size, + origin_length); + metrics_cache_invalidate(); + return false; + } + + // append the data to the data field + memcpy(cache->data + origin_length, data, append_length); + cache->data[origin_length + append_length + 1] = '\0'; + return true; +} + +/** + * Finalizes the cache. + * + * Requires the caller to hold the lock on the cache! + * + * This method should be invoked when the cache is complete + * and therefore can be served. + * + * @return true if the cache has a validity + */ +static bool +metrics_cache_finalize(void) +{ + struct main_configuration* config; + struct prometheus_cache* cache; + time_t now; + + cache = (struct prometheus_cache*)prometheus_cache_shmem; + config = (struct main_configuration*)shmem; + + if (!is_metrics_cache_configured()) + { + return false; + } + + now = time(NULL); + cache->valid_until = now + config->common.metrics_cache_max_age; + return cache->valid_until > now; +} + +static bool +is_prometheus_enabled(void) +{ + struct prometheus* prometheus = (struct prometheus*) prometheus_shmem; + struct configuration* config = (struct configuration*)shmem; + return (config->metrics > 0 && prometheus != NULL); +} diff --git a/src/libpgagroal/remote.c b/src/libpgagroal/remote.c new file mode 100644 index 00000000..c9c2d463 --- /dev/null +++ b/src/libpgagroal/remote.c @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include + +void +pgagroal_remote_management(int client_fd, char* address) +{ + int server_fd = -1; + int exit_code; + int auth_status; + uint8_t compression; + uint8_t encryption; + SSL* client_ssl = NULL; + struct json* payload = NULL; + struct main_configuration* config; + + pgagroal_start_logging(); + pgagroal_memory_init(); + + exit_code = 0; + + config = (struct main_configuration*)shmem; + + pgagroal_log_debug("pgagroal_remote_management: connect %d", client_fd); + + auth_status = pgagroal_remote_management_auth(client_fd, address, &client_ssl); + if (auth_status == AUTH_SUCCESS) + { + if (pgagroal_connect_unix_socket(config->unix_socket_dir, MAIN_UDS, &server_fd)) + { + goto done; + } + + if (pgagroal_management_read_json(client_ssl, client_fd, &compression, &encryption, &payload)) + { + goto done; + } + + if (pgagroal_management_write_json(NULL, server_fd, compression, encryption, payload)) + { + goto done; + } + + pgagroal_json_destroy(payload); + payload = NULL; + + if (pgagroal_management_read_json(NULL, server_fd, &compression, &encryption, &payload)) + { + goto done; + } + + if (pgagroal_management_write_json(client_ssl, client_fd, compression, encryption, payload)) + { + goto done; + } + } + else + { + exit_code = 1; + } + +done: + + pgagroal_json_destroy(payload); + payload = NULL; + + if (client_ssl != NULL) + { + int res; + SSL_CTX* ctx = SSL_get_SSL_CTX(client_ssl); + res = SSL_shutdown(client_ssl); + if (res == 0) + { + SSL_shutdown(client_ssl); + } + SSL_free(client_ssl); + SSL_CTX_free(ctx); + } + + pgagroal_log_debug("pgagroal_remote_management: disconnect %d", client_fd); + pgagroal_disconnect(client_fd); + pgagroal_disconnect(server_fd); + + free(address); + + pgagroal_memory_destroy(); + pgagroal_stop_logging(); + + exit(exit_code); +} diff --git a/src/libpgagroal/security.c b/src/libpgagroal/security.c new file mode 100644 index 00000000..7ffca53d --- /dev/null +++ b/src/libpgagroal/security.c @@ -0,0 +1,5631 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int get_auth_type(struct message* msg, int* auth_type); +static int compare_auth_response(struct message* orig, struct message* response, int auth_type); + +static int use_pooled_connection(SSL* c_ssl, int client_fd, int slot, char* username, char* database, int hba_method, SSL** server_ssl); +static int use_unpooled_connection(struct message* msg, SSL* c_ssl, int client_fd, int slot, + char* username, int hba_method, SSL** server_ssl); +static int client_trust(SSL* c_ssl, int client_fd, char* username, char* password, int slot); +static int client_password(SSL* c_ssl, int client_fd, char* username, char* password, int slot); +static int client_md5(SSL* c_ssl, int client_fd, char* username, char* password, int slot); +static int client_scram256(SSL* c_ssl, int client_fd, char* username, char* password, int slot); +static int client_ok(SSL* c_ssl, int client_fd, int slot); +static int server_passthrough(struct message* msg, int auth_type, SSL* c_ssl, int client_fd, int slot); +static int server_authenticate(struct message* msg, int auth_type, char* username, char* password, + int slot, SSL* server_ssl); +static int server_trust(int slot, SSL* server_ssl); +static int server_password(char* username, char* password, int slot, SSL* server_ssl); +static int server_md5(char* username, char* password, int slot, SSL* server_ssl); +static int server_scram256(char* username, char* password, int slot, SSL* server_ssl); + +static bool is_allowed(char* username, char* database, char* address, int* hba_method); +static bool is_allowed_username(char* username, char* entry); +static bool is_allowed_database(char* database, char* entry); +static bool is_allowed_address(char* address, char* entry); +static bool is_disabled(char* database); + +static int get_hba_method(int index); +static char* get_password(char* username); +static char* get_frontend_password(char* username); +static char* get_admin_password(char* username); +static int get_salt(void* data, char** salt); + +static int sasl_prep(char* password, char** password_prep); +static int generate_nounce(char** nounce); +static int get_scram_attribute(char attribute, char* input, size_t size, char** value); +static int client_proof(char* password, char* salt, int salt_length, int iterations, + char* client_first_message_bare, size_t client_first_message_bare_length, + char* server_first_message, size_t server_first_message_length, + char* client_final_message_wo_proof, size_t client_final_message_wo_proof_length, + unsigned char** result, int* result_length); +static int verify_client_proof(char* stored_key, int stored_key_length, + char* client_proof, int client_proof_length, + char* salt, int salt_length, int iterations, + char* client_first_message_bare, size_t client_first_message_bare_length, + char* server_first_message, size_t server_first_message_length, + char* client_final_message_wo_proof, size_t client_final_message_wo_proof_length); +static int salted_password(char* password, char* salt, int salt_length, int iterations, unsigned char** result, int* result_length); +static int salted_password_key(unsigned char* salted_password, int salted_password_length, char* key, + unsigned char** result, int* result_length); +static int stored_key(unsigned char* client_key, int client_key_length, unsigned char** result, int* result_length); +static int generate_salt(char** salt, int* size); +static int server_signature(char* password, char* salt, int salt_length, int iterations, + char* server_key, int server_key_length, + char* client_first_message_bare, size_t client_first_message_bare_length, + char* server_first_message, size_t server_first_message_length, + char* client_final_message_wo_proof, size_t client_final_message_wo_proof_length, + unsigned char** result, size_t* result_length); + +static bool is_tls_user(char* username, char* database); +static int create_ssl_ctx(bool client, SSL_CTX** ctx); +static int create_ssl_client(SSL_CTX* ctx, char* key, char* cert, char* root, int socket, SSL** ssl); +static int create_ssl_server(SSL_CTX* ctx, int socket, SSL** ssl); +static int establish_client_tls_connection(int server, int fd, SSL** ssl); +static int create_client_tls_connection(int fd, SSL** ssl, char* tls_key_file, char* tls_cert_file, char* tls_ca_file); + +static int auth_query(SSL* c_ssl, int client_fd, int slot, char* username, char* database, int hba_method); +static int auth_query_get_connection(char* username, char* password, char* database, int* server_fd, SSL** server_ssl); +static int auth_query_server_md5(struct message* startup_response_msg, char* username, char* password, int socket, SSL* server_ssl); +static int auth_query_server_scram256(char* username, char* password, int socket, SSL* server_ssl); +static int auth_query_get_password(int socket, SSL* server_ssl, char* username, char* database, char** password); +static int auth_query_client_md5(SSL* c_ssl, int client_fd, char* username, char* hash, int slot); +static int auth_query_client_scram256(SSL* c_ssl, int client_fd, char* username, char* shadow, int slot); + +int +pgagroal_authenticate(int client_fd, char* address, int* slot, SSL** client_ssl, SSL** server_ssl) +{ + int status = MESSAGE_STATUS_ERROR; + int ret; + int server = 0; + int server_fd = -1; + int hba_method; + struct main_configuration* config; + struct message* msg = NULL; + struct message* request_msg = NULL; + int32_t request; + char* username = NULL; + char* database = NULL; + char* appname = NULL; + SSL* c_ssl = NULL; + + config = (struct main_configuration*)shmem; + + *slot = -1; + *client_ssl = NULL; + *server_ssl = NULL; + + /* Receive client calls - at any point if client exits return AUTH_ERROR */ + status = pgagroal_read_timeout_message(NULL, client_fd, config->common.authentication_timeout, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + request = pgagroal_get_request(msg); + + /* Cancel request: 80877102 */ + if (request == 80877102) + { + pgagroal_log_debug("Cancel request from client: %d", client_fd); + + /* We need to find the server for the connection */ + if (pgagroal_get_primary(&server)) + { + pgagroal_log_error("pgagroal: No valid server available"); + pgagroal_write_connection_refused(NULL, client_fd); + pgagroal_write_empty(NULL, client_fd); + goto error; + } + + if (config->servers[server].host[0] == '/') + { + char pgsql[MISC_LENGTH]; + + memset(&pgsql, 0, sizeof(pgsql)); + snprintf(&pgsql[0], sizeof(pgsql), ".s.PGSQL.%d", config->servers[server].port); + ret = pgagroal_connect_unix_socket(config->servers[server].host, &pgsql[0], &server_fd); + } + else + { + ret = pgagroal_connect(config->servers[server].host, config->servers[server].port, &server_fd, config->keep_alive, config->non_blocking, config->nodelay); + } + + if (ret) + { + pgagroal_log_error("pgagroal: No connection to %s:%d", config->servers[server].host, config->servers[server].port); + goto error; + } + + status = pgagroal_write_message(NULL, server_fd, msg); + if (status != MESSAGE_STATUS_OK) + { + pgagroal_disconnect(server_fd); + + goto error; + } + pgagroal_free_message(msg); + + pgagroal_disconnect(server_fd); + + return AUTH_BAD_PASSWORD; + } + + /* GSS request: 80877104 */ + if (request == 80877104) + { + pgagroal_log_debug("GSS request from client: %d", client_fd); + status = pgagroal_write_notice(NULL, client_fd); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + pgagroal_free_message(msg); + + status = pgagroal_read_timeout_message(NULL, client_fd, config->common.authentication_timeout, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + request = pgagroal_get_request(msg); + } + + /* SSL request: 80877103 */ + if (request == 80877103) + { + pgagroal_log_debug("SSL request from client: %d", client_fd); + + if (config->common.tls) + { + SSL_CTX* ctx = NULL; + + /* We are acting as a server against the client */ + if (create_ssl_ctx(false, &ctx)) + { + goto error; + } + + if (create_ssl_server(ctx, client_fd, &c_ssl)) + { + pgagroal_log_debug("authenticate: connection error"); + pgagroal_write_connection_refused(NULL, client_fd); + pgagroal_write_empty(NULL, client_fd); + goto error; + } + + *client_ssl = c_ssl; + + /* Switch to TLS mode */ + status = pgagroal_write_tls(NULL, client_fd); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + pgagroal_free_message(msg); + + status = SSL_accept(c_ssl); + if (status != 1) + { + unsigned long err; + + err = ERR_get_error(); + pgagroal_log_error("SSL failed: %s", ERR_reason_error_string(err)); + goto error; + } + + status = pgagroal_read_timeout_message(c_ssl, client_fd, config->common.authentication_timeout, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + request = pgagroal_get_request(msg); + } + else + { + status = pgagroal_write_notice(NULL, client_fd); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + pgagroal_free_message(msg); + + status = pgagroal_read_timeout_message(NULL, client_fd, config->common.authentication_timeout, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + request = pgagroal_get_request(msg); + } + } + + /* 196608 -> Ok */ + if (request == 196608) + { + request_msg = pgagroal_copy_message(msg); + + /* Extract parameters: username / database */ + pgagroal_log_trace("authenticate: username/database (%d)", client_fd); + pgagroal_extract_username_database(request_msg, &username, &database, &appname); + + /* TLS scenario */ + if (is_tls_user(username, database) && c_ssl == NULL) + { + pgagroal_log_debug("authenticate: tls: %s / %s / %s", username, database, address); + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + goto bad_password; + } + + /* Verify client against pgagroal_hba.conf */ + if (!is_allowed(username, database, address, &hba_method)) + { + /* User not allowed */ + pgagroal_log_debug("authenticate: not allowed: %s / %s / %s", username, database, address); + pgagroal_write_no_hba_entry(c_ssl, client_fd, username, database, address); + pgagroal_write_empty(c_ssl, client_fd); + goto bad_password; + } + + /* Reject scenario */ + if (hba_method == SECURITY_REJECT) + { + pgagroal_log_debug("authenticate: reject: %s / %s / %s", username, database, address); + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + goto bad_password; + } + + /* Gracefully scenario */ + if (config->gracefully) + { + pgagroal_log_debug("authenticate: gracefully: %s / %s / %s", username, database, address); + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + goto bad_password; + } + + /* Disabled scenario */ + if (is_disabled(database)) + { + pgagroal_log_debug("authenticate: disabled: %s / %s / %s", username, database, address); + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + goto bad_password; + } + + /* Get connection */ + pgagroal_tracking_event_basic(TRACKER_AUTHENTICATE, username, database); + ret = pgagroal_get_connection(username, database, true, false, slot, server_ssl); + if (ret != 0) + { + if (ret == 1) + { + /* Pool full */ + pgagroal_log_debug("authenticate: pool is full"); + pgagroal_write_pool_full(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + goto bad_password; + } + else + { + /* Other error */ + pgagroal_log_debug("authenticate: connection error"); + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + goto error; + } + } + + /* Set the application_name on the connection */ + if (appname != NULL) + { + memset(&config->connections[*slot].appname, 0, MAX_APPLICATION_NAME); + memcpy(&config->connections[*slot].appname, appname, strlen(appname)); + } + + if (config->connections[*slot].has_security != SECURITY_INVALID) + { + pgagroal_log_debug("authenticate: getting pooled connection"); + pgagroal_free_message(msg); + + ret = use_pooled_connection(c_ssl, client_fd, *slot, username, database, hba_method, server_ssl); + if (ret == AUTH_BAD_PASSWORD) + { + goto bad_password; + } + else if (ret == AUTH_ERROR) + { + goto error; + } + + pgagroal_log_debug("authenticate: got pooled connection (%d)", *slot); + } + else + { + pgagroal_log_debug("authenticate: creating pooled connection"); + + ret = use_unpooled_connection(request_msg, c_ssl, client_fd, *slot, username, hba_method, server_ssl); + if (ret == AUTH_BAD_PASSWORD) + { + goto bad_password; + } + else if (ret == AUTH_ERROR) + { + goto error; + } + + pgagroal_log_debug("authenticate: created pooled connection (%d)", *slot); + } + + pgagroal_free_copy_message(request_msg); + free(username); + free(database); + free(appname); + + pgagroal_prometheus_auth_user_success(); + + pgagroal_log_debug("authenticate: SUCCESS"); + return AUTH_SUCCESS; + } + else if (request == -1) + { + goto error; + } + else + { + pgagroal_log_debug("authenticate: old version: %d (%s)", request, address); + pgagroal_write_connection_refused_old(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + goto bad_password; + } + +bad_password: + pgagroal_free_message(msg); + pgagroal_free_copy_message(request_msg); + + free(username); + free(database); + free(appname); + + pgagroal_prometheus_auth_user_bad_password(); + + pgagroal_log_debug("authenticate: BAD_PASSWORD"); + return AUTH_BAD_PASSWORD; + +error: + pgagroal_free_message(msg); + pgagroal_free_copy_message(request_msg); + + free(username); + free(database); + free(appname); + + pgagroal_prometheus_auth_user_error(); + + pgagroal_log_debug("authenticate: ERROR"); + return AUTH_ERROR; +} + +int +accept_ssl_vault(struct vault_configuration* config, int client_fd, SSL** client_ssl) +{ + + pgagroal_log_debug("SSL request from client: %d", client_fd); + int status = MESSAGE_STATUS_ERROR; + SSL_CTX* ctx = NULL; + SSL* c_ssl = NULL; + + /* We are acting as a server against the client */ + if (create_ssl_ctx(false, &ctx)) + { + goto error; + } + + if (create_ssl_server(ctx, client_fd, &c_ssl)) + { + goto error; + } + + *client_ssl = c_ssl; + + /* Switch to TLS mode */ + status = SSL_accept(c_ssl); + if (status != 1) + { + unsigned long err; + + err = ERR_get_error(); + pgagroal_log_error("SSL failed: %s", ERR_reason_error_string(err)); + goto error; + } + + return 0; +error: + + if (ctx != NULL) + { + SSL_CTX_free(ctx); + } + + if (c_ssl != NULL) + { + SSL_free(c_ssl); + } + + pgagroal_log_debug("accept_ssl_vault: ERROR"); + return 1; +} + +int +pgagroal_prefill_auth(char* username, char* password, char* database, int* slot, SSL** server_ssl) +{ + int server_fd = -1; + int auth_type = -1; + signed char server_state; + struct main_configuration* config = NULL; + struct message* startup_msg = NULL; + struct message* msg = NULL; + int ret = -1; + int status = -1; + + config = (struct main_configuration*)shmem; + + *slot = -1; + *server_ssl = NULL; + + /* Get connection */ + pgagroal_tracking_event_basic(TRACKER_PREFILL, username, database); + ret = pgagroal_get_connection(username, database, false, false, slot, server_ssl); + if (ret != 0) + { + goto error; + } + server_fd = config->connections[*slot].fd; + + status = pgagroal_create_startup_message(username, database, &startup_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_write_message(*server_ssl, server_fd, startup_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_read_block_message(*server_ssl, server_fd, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + get_auth_type(msg, &auth_type); + pgagroal_log_trace("prefill_auth: auth type %d", auth_type); + + /* Supported security models: */ + /* trust (0) */ + /* password (3) */ + /* md5 (5) */ + /* scram256 (10) */ + if (auth_type == -1) + { + goto error; + } + else if (auth_type != SECURITY_TRUST && auth_type != SECURITY_PASSWORD && auth_type != SECURITY_MD5 && auth_type != SECURITY_SCRAM256) + { + goto error; + } + + if (server_authenticate(msg, auth_type, username, password, *slot, *server_ssl)) + { + goto error; + } + + server_state = atomic_load(&config->servers[config->connections[*slot].server].state); + if (server_state == SERVER_NOTINIT || server_state == SERVER_NOTINIT_PRIMARY) + { + pgagroal_log_debug("Verify server mode: %d", config->connections[*slot].server); + pgagroal_update_server_state(*slot, server_fd, *server_ssl); + pgagroal_server_status(); + } + + pgagroal_log_trace("prefill_auth: has_security %d", config->connections[*slot].has_security); + pgagroal_log_debug("prefill_auth: SUCCESS"); + + pgagroal_free_copy_message(startup_msg); + pgagroal_free_message(msg); + + return AUTH_SUCCESS; + +error: + + pgagroal_log_debug("prefill_auth: ERROR"); + + if (*slot != -1) + { + pgagroal_tracking_event_slot(TRACKER_PREFILL_KILL, *slot); + pgagroal_kill_connection(*slot, *server_ssl); + } + + *slot = -1; + *server_ssl = NULL; + + pgagroal_free_copy_message(startup_msg); + pgagroal_free_message(msg); + + return AUTH_ERROR; +} + +int +pgagroal_remote_management_auth(int client_fd, char* address, SSL** client_ssl) +{ + int status = MESSAGE_STATUS_ERROR; + int hba_method; + struct main_configuration* config; + struct message* msg = NULL; + struct message* request_msg = NULL; + int32_t request; + char* username = NULL; + char* database = NULL; + char* appname = NULL; + char* password = NULL; + SSL* c_ssl = NULL; + + config = (struct main_configuration*)shmem; + + *client_ssl = NULL; + + pgagroal_memory_init(); + + /* Receive client calls - at any point if client exits return AUTH_ERROR */ + status = pgagroal_read_timeout_message(NULL, client_fd, config->common.authentication_timeout, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + request = pgagroal_get_request(msg); + + /* SSL request: 80877103 */ + if (request == 80877103) + { + pgagroal_log_debug("SSL request from client: %d", client_fd); + + if (config->common.tls) + { + SSL_CTX* ctx = NULL; + + /* We are acting as a server against the client */ + if (create_ssl_ctx(false, &ctx)) + { + goto error; + } + + if (create_ssl_server(ctx, client_fd, &c_ssl)) + { + goto error; + } + + *client_ssl = c_ssl; + + /* Switch to TLS mode */ + status = pgagroal_write_tls(NULL, client_fd); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + pgagroal_free_message(msg); + + status = SSL_accept(c_ssl); + if (status != 1) + { + unsigned long err; + + err = ERR_get_error(); + pgagroal_log_error("SSL failed: %s", ERR_reason_error_string(err)); + goto error; + } + + status = pgagroal_read_timeout_message(c_ssl, client_fd, config->common.authentication_timeout, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + request = pgagroal_get_request(msg); + } + else + { + status = pgagroal_write_notice(NULL, client_fd); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + pgagroal_free_message(msg); + + status = pgagroal_read_timeout_message(NULL, client_fd, config->common.authentication_timeout, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + request = pgagroal_get_request(msg); + } + } + + /* 196608 -> Ok */ + if (request == 196608) + { + request_msg = pgagroal_copy_message(msg); + + /* Extract parameters: username / database */ + pgagroal_log_trace("remote_management_auth: username/database (%d)", client_fd); + pgagroal_extract_username_database(request_msg, &username, &database, &appname); + + /* Must be admin database */ + if (strcmp("admin", database) != 0) + { + pgagroal_log_debug("remote_management_auth: admin: %s / %s", username, address); + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + goto bad_password; + } + + /* TLS scenario */ + if (is_tls_user(username, "admin") && c_ssl == NULL) + { + pgagroal_log_debug("remote_management_auth: tls: %s / admin / %s", username, address); + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + goto bad_password; + } + + /* Verify client against pgagroal_hba.conf */ + if (!is_allowed(username, "admin", address, &hba_method)) + { + /* User not allowed */ + pgagroal_log_debug("remote_management_auth: not allowed: %s / admin / %s", username, address); + pgagroal_write_no_hba_entry(c_ssl, client_fd, username, "admin", address); + pgagroal_write_empty(c_ssl, client_fd); + goto bad_password; + } + + /* Reject scenario */ + if (hba_method == SECURITY_REJECT) + { + pgagroal_log_debug("remote_management_auth: reject: %s / admin / %s", username, address); + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + goto bad_password; + } + + password = get_admin_password(username); + if (password == NULL) + { + pgagroal_log_debug("remote_management_auth: password: %s / admin / %s", username, address); + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + goto bad_password; + } + + status = client_scram256(c_ssl, client_fd, username, password, -1); + if (status == AUTH_BAD_PASSWORD) + { + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + goto bad_password; + } + else if (status == AUTH_ERROR) + { + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + goto error; + } + + status = pgagroal_write_auth_success(c_ssl, client_fd); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + pgagroal_free_copy_message(request_msg); + free(username); + free(database); + free(appname); + + pgagroal_log_debug("remote_management_auth: SUCCESS"); + return AUTH_SUCCESS; + } + else if (request == -1) + { + goto error; + } + else + { + pgagroal_log_debug("remote_management_auth: old version: %d (%s)", request, address); + pgagroal_write_connection_refused_old(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + goto bad_password; + } + +bad_password: + pgagroal_free_message(msg); + pgagroal_free_copy_message(request_msg); + + free(username); + free(database); + free(appname); + + pgagroal_log_debug("remote_management_auth: BAD_PASSWORD"); + return AUTH_BAD_PASSWORD; + +error: + pgagroal_free_message(msg); + pgagroal_free_copy_message(request_msg); + + free(username); + free(database); + free(appname); + + pgagroal_log_debug("remote_management_auth: ERROR"); + return AUTH_ERROR; +} + +int +pgagroal_remote_management_scram_sha256(char* username, char* password, int server_fd, SSL** s_ssl) +{ + int status = MESSAGE_STATUS_ERROR; + SSL* ssl = NULL; + char key_file[MISC_LENGTH]; + char cert_file[MISC_LENGTH]; + char root_file[MISC_LENGTH]; + struct stat st = {0}; + char* salt = NULL; + size_t salt_length = 0; + char* password_prep = NULL; + char* client_nounce = NULL; + char* combined_nounce = NULL; + char* base64_salt = NULL; + char* iteration_string = NULL; + char* err = NULL; + int iteration; + char* client_first_message_bare = NULL; + char* server_first_message = NULL; + char wo_proof[58]; + unsigned char* proof = NULL; + int proof_length; + char* proof_base = NULL; + size_t proof_base_length; + char* base64_server_signature = NULL; + char* server_signature_received = NULL; + size_t server_signature_received_length; + unsigned char* server_signature_calc = NULL; + size_t server_signature_calc_length; + struct message* sslrequest_msg = NULL; + struct message* startup_msg = NULL; + struct message* sasl_response = NULL; + struct message* sasl_continue = NULL; + struct message* sasl_continue_response = NULL; + struct message* sasl_final = NULL; + struct message* msg = NULL; + + if (pgagroal_get_home_directory() == NULL) + { + goto error; + } + + memset(&key_file, 0, sizeof(key_file)); + snprintf(&key_file[0], sizeof(key_file), "%s/.pgagroal/pgagroal.key", pgagroal_get_home_directory()); + + memset(&cert_file, 0, sizeof(cert_file)); + snprintf(&cert_file[0], sizeof(cert_file), "%s/.pgagroal/pgagroal.crt", pgagroal_get_home_directory()); + + memset(&root_file, 0, sizeof(root_file)); + snprintf(&root_file[0], sizeof(root_file), "%s/.pgagroal/root.crt", pgagroal_get_home_directory()); + + if (stat(&key_file[0], &st) == 0) + { + if (S_ISREG(st.st_mode) && st.st_mode & (S_IRUSR | S_IWUSR) && !(st.st_mode & S_IRWXG) && !(st.st_mode & S_IRWXO)) + { + if (stat(&cert_file[0], &st) == 0) + { + if (S_ISREG(st.st_mode)) + { + SSL_CTX* ctx = NULL; + + status = pgagroal_create_ssl_message(&sslrequest_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_write_message(NULL, server_fd, sslrequest_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_read_block_message(NULL, server_fd, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (msg->kind == 'S') + { + if (create_ssl_ctx(true, &ctx)) + { + goto error; + } + + if (stat(&root_file[0], &st) == -1) + { + memset(&root_file, 0, sizeof(root_file)); + } + + if (create_ssl_client(ctx, &key_file[0], &cert_file[0], &root_file[0], server_fd, &ssl)) + { + goto error; + } + + *s_ssl = ssl; + + do + { + status = SSL_connect(ssl); + + if (status != 1) + { + int err = SSL_get_error(ssl, status); + switch (err) + { + case SSL_ERROR_ZERO_RETURN: + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_CONNECT: + case SSL_ERROR_WANT_ACCEPT: + case SSL_ERROR_WANT_X509_LOOKUP: +#ifndef HAVE_OPENBSD + case SSL_ERROR_WANT_ASYNC: + case SSL_ERROR_WANT_ASYNC_JOB: + case SSL_ERROR_WANT_CLIENT_HELLO_CB: +#endif + break; + case SSL_ERROR_SYSCALL: + pgagroal_log_error("SSL_ERROR_SYSCALL: %s (%d)", strerror(errno), server_fd); + errno = 0; + goto error; + break; + case SSL_ERROR_SSL: + pgagroal_log_error("SSL_ERROR_SSL: %s (%d)", strerror(errno), server_fd); + pgagroal_log_error("%s", ERR_error_string(err, NULL)); + pgagroal_log_error("%s", ERR_lib_error_string(err)); + pgagroal_log_error("%s", ERR_reason_error_string(err)); + errno = 0; + goto error; + break; + } + ERR_clear_error(); + } + } + while (status != 1); + } + } + } + } + } + + status = pgagroal_create_startup_message(username, "admin", &startup_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_write_message(ssl, server_fd, startup_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_read_block_message(ssl, server_fd, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (msg->kind != 'R') + { + goto error; + } + + status = sasl_prep(password, &password_prep); + if (status) + { + goto error; + } + + generate_nounce(&client_nounce); + + status = pgagroal_create_auth_scram256_response(client_nounce, &sasl_response); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_write_message(ssl, server_fd, sasl_response); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_read_block_message(ssl, server_fd, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + sasl_continue = pgagroal_copy_message(msg); + + get_scram_attribute('r', (char*)(sasl_continue->data + 9), sasl_continue->length - 9, &combined_nounce); + get_scram_attribute('s', (char*)(sasl_continue->data + 9), sasl_continue->length - 9, &base64_salt); + get_scram_attribute('i', (char*)(sasl_continue->data + 9), sasl_continue->length - 9, &iteration_string); + get_scram_attribute('e', (char*)(sasl_continue->data + 9), sasl_continue->length - 9, &err); + + if (err != NULL) + { + goto error; + } + + pgagroal_base64_decode(base64_salt, strlen(base64_salt), (void**)&salt, &salt_length); + + iteration = atoi(iteration_string); + + memset(&wo_proof[0], 0, sizeof(wo_proof)); + snprintf(&wo_proof[0], sizeof(wo_proof), "c=biws,r=%s", combined_nounce); + + /* n=,r=... */ + client_first_message_bare = sasl_response->data + 26; + + /* r=...,s=...,i=4096 */ + server_first_message = sasl_continue->data + 9; + + if (client_proof(password_prep, salt, salt_length, iteration, + client_first_message_bare, sasl_response->length - 26, + server_first_message, sasl_continue->length - 9, + &wo_proof[0], strlen(wo_proof), + &proof, &proof_length)) + { + goto error; + } + + pgagroal_base64_encode((char*)proof, proof_length, &proof_base, &proof_base_length); + + status = pgagroal_create_auth_scram256_continue_response(&wo_proof[0], (char*)proof_base, &sasl_continue_response); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_write_message(ssl, server_fd, sasl_continue_response); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_read_block_message(ssl, server_fd, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (pgagroal_extract_message('R', msg, &sasl_final)) + { + goto error; + } + + /* Get 'v' attribute */ + base64_server_signature = sasl_final->data + 11; + pgagroal_base64_decode(base64_server_signature, sasl_final->length - 11, (void**)&server_signature_received, &server_signature_received_length); + + if (server_signature(password_prep, salt, salt_length, iteration, + NULL, 0, + client_first_message_bare, sasl_response->length - 26, + server_first_message, sasl_continue->length - 9, + &wo_proof[0], strlen(wo_proof), + &server_signature_calc, &server_signature_calc_length)) + { + goto error; + } + + if (server_signature_calc_length != server_signature_received_length || + memcmp(server_signature_received, server_signature_calc, server_signature_calc_length) != 0) + { + goto bad_password; + } + + if (msg->length == 55) + { + status = pgagroal_read_block_message(ssl, server_fd, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + } + + free(salt); + free(err); + free(password_prep); + free(client_nounce); + free(combined_nounce); + free(base64_salt); + free(iteration_string); + free(proof); + free(proof_base); + free(server_signature_received); + free(server_signature_calc); + + pgagroal_free_message(msg); + pgagroal_free_copy_message(sslrequest_msg); + pgagroal_free_copy_message(startup_msg); + pgagroal_free_copy_message(sasl_response); + pgagroal_free_copy_message(sasl_continue); + pgagroal_free_copy_message(sasl_continue_response); + pgagroal_free_copy_message(sasl_final); + + pgagroal_memory_destroy(); + + return AUTH_SUCCESS; + +bad_password: + + free(salt); + free(err); + free(password_prep); + free(client_nounce); + free(combined_nounce); + free(base64_salt); + free(iteration_string); + free(proof); + free(proof_base); + free(server_signature_received); + free(server_signature_calc); + + pgagroal_free_message(msg); + pgagroal_free_copy_message(sslrequest_msg); + pgagroal_free_copy_message(startup_msg); + pgagroal_free_copy_message(sasl_response); + pgagroal_free_copy_message(sasl_continue); + pgagroal_free_copy_message(sasl_continue_response); + pgagroal_free_copy_message(sasl_final); + + pgagroal_memory_destroy(); + + return AUTH_BAD_PASSWORD; + +error: + + free(salt); + free(err); + free(password_prep); + free(client_nounce); + free(combined_nounce); + free(base64_salt); + free(iteration_string); + free(proof); + free(proof_base); + free(server_signature_received); + free(server_signature_calc); + + pgagroal_free_message(msg); + pgagroal_free_copy_message(sslrequest_msg); + pgagroal_free_copy_message(startup_msg); + pgagroal_free_copy_message(sasl_response); + pgagroal_free_copy_message(sasl_continue); + pgagroal_free_copy_message(sasl_continue_response); + pgagroal_free_copy_message(sasl_final); + + pgagroal_memory_destroy(); + + return AUTH_ERROR; +} + +static int +get_auth_type(struct message* msg, int* auth_type) +{ + int32_t length; + int32_t type = -1; + int offset; + + *auth_type = -1; + + if (msg->kind != 'R') + { + return 1; + } + + length = pgagroal_read_int32(msg->data + 1); + type = pgagroal_read_int32(msg->data + 5); + offset = 9; + + if (type == 0 && msg->length > 8) + { + if ('E' == pgagroal_read_byte(msg->data + 9)) + { + return 0; + } + } + + switch (type) + { + case 0: + pgagroal_log_trace("Backend: R - Success"); + break; + case 2: + pgagroal_log_trace("Backend: R - KerberosV5"); + break; + case 3: + pgagroal_log_trace("Backend: R - CleartextPassword"); + break; + case 5: + pgagroal_log_trace("Backend: R - MD5Password"); + pgagroal_log_trace(" Salt %02hhx%02hhx%02hhx%02hhx", + (signed char)(pgagroal_read_byte(msg->data + 9) & 0xFF), + (signed char)(pgagroal_read_byte(msg->data + 10) & 0xFF), + (signed char)(pgagroal_read_byte(msg->data + 11) & 0xFF), + (signed char)(pgagroal_read_byte(msg->data + 12) & 0xFF)); + break; + case 6: + pgagroal_log_trace("Backend: R - SCMCredential"); + break; + case 7: + pgagroal_log_trace("Backend: R - GSS"); + break; + case 8: + pgagroal_log_trace("Backend: R - GSSContinue"); + break; + case 9: + pgagroal_log_trace("Backend: R - SSPI"); + break; + case 10: + pgagroal_log_trace("Backend: R - SASL"); + while (offset < length - 8) + { + char* mechanism = pgagroal_read_string(msg->data + offset); + pgagroal_log_trace(" %s", mechanism); + offset += strlen(mechanism) + 1; + } + break; + case 11: + pgagroal_log_trace("Backend: R - SASLContinue"); + break; + case 12: + pgagroal_log_trace("Backend: R - SASLFinal"); + offset += length - 8; + + if (offset < msg->length) + { + signed char peek = pgagroal_read_byte(msg->data + offset); + switch (peek) + { + case 'R': + type = pgagroal_read_int32(msg->data + offset + 5); + break; + default: + break; + } + } + + break; + default: + break; + } + + *auth_type = type; + + return 0; +} + +static int +compare_auth_response(struct message* orig, struct message* response, int auth_type) +{ + switch (auth_type) + { + case 3: + case 5: + return strcmp(pgagroal_read_string(orig->data + 5), pgagroal_read_string(response->data + 5)); + break; + case 10: + return memcmp(orig->data, response->data, orig->length); + break; + default: + break; + } + + return 1; +} + +static int +use_pooled_connection(SSL* c_ssl, int client_fd, int slot, char* username, char* database, int hba_method, SSL** server_ssl) +{ + int status = MESSAGE_STATUS_ERROR; + struct main_configuration* config = NULL; + struct message* auth_msg = NULL; + struct message* msg = NULL; + char* password = NULL; + + config = (struct main_configuration*)shmem; + + password = get_frontend_password(username); + if (password == NULL) + { + password = get_password(username); + } + + if (hba_method == SECURITY_ALL) + { + hba_method = config->connections[slot].has_security; + } + + if (config->authquery) + { + status = auth_query(c_ssl, client_fd, slot, username, database, hba_method); + if (status == AUTH_BAD_PASSWORD) + { + goto bad_password; + } + else if (status == AUTH_ERROR) + { + goto error; + } + } + else if (password == NULL) + { + /* We can only deal with SECURITY_TRUST, SECURITY_PASSWORD and SECURITY_MD5 */ + pgagroal_create_message(&config->connections[slot].security_messages[0], + config->connections[slot].security_lengths[0], + &auth_msg); + + status = pgagroal_write_message(c_ssl, client_fd, auth_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + pgagroal_free_copy_message(auth_msg); + auth_msg = NULL; + + /* Password or MD5 */ + if (config->connections[slot].has_security != SECURITY_TRUST) + { + status = pgagroal_read_timeout_message(c_ssl, client_fd, config->common.authentication_timeout, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + pgagroal_create_message(&config->connections[slot].security_messages[1], + config->connections[slot].security_lengths[1], + &auth_msg); + + if (compare_auth_response(auth_msg, msg, config->connections[slot].has_security)) + { + pgagroal_write_bad_password(c_ssl, client_fd, username); + pgagroal_write_empty(c_ssl, client_fd); + + goto error; + } + + pgagroal_free_copy_message(auth_msg); + auth_msg = NULL; + + pgagroal_create_message(&config->connections[slot].security_messages[2], + config->connections[slot].security_lengths[2], + &auth_msg); + + status = pgagroal_write_message(c_ssl, client_fd, auth_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + pgagroal_free_copy_message(auth_msg); + auth_msg = NULL; + } + } + else + { + /* We have a password */ + + if (hba_method == SECURITY_TRUST) + { + /* R/0 */ + client_trust(c_ssl, client_fd, username, password, slot); + } + else if (hba_method == SECURITY_PASSWORD) + { + /* R/3 */ + status = client_password(c_ssl, client_fd, username, password, slot); + if (status == AUTH_BAD_PASSWORD) + { + goto bad_password; + } + else if (status == AUTH_ERROR) + { + goto error; + } + } + else if (hba_method == SECURITY_MD5) + { + /* R/5 */ + status = client_md5(c_ssl, client_fd, username, password, slot); + if (status == AUTH_BAD_PASSWORD) + { + goto bad_password; + } + else if (status == AUTH_ERROR) + { + goto error; + } + } + else if (hba_method == SECURITY_SCRAM256) + { + /* R/10 */ + status = client_scram256(c_ssl, client_fd, username, password, slot); + if (status == AUTH_BAD_PASSWORD) + { + goto bad_password; + } + else if (status == AUTH_ERROR) + { + goto error; + } + } + else + { + goto error; + } + + if (client_ok(c_ssl, client_fd, slot)) + { + goto error; + } + } + + return AUTH_SUCCESS; + +bad_password: + + pgagroal_log_trace("use_pooled_connection: bad password for slot %d", slot); + + return AUTH_BAD_PASSWORD; + +error: + + pgagroal_log_trace("use_pooled_connection: failed for slot %d", slot); + + return AUTH_ERROR; +} + +static int +use_unpooled_connection(struct message* request_msg, SSL* c_ssl, int client_fd, int slot, + char* username, int hba_method, SSL** server_ssl) +{ + int status = MESSAGE_STATUS_ERROR; + int server_fd; + int auth_type = -1; + char* password; + signed char server_state; + struct message* msg = NULL; + struct message* auth_msg = NULL; + struct main_configuration* config = NULL; + + config = (struct main_configuration*)shmem; + server_fd = config->connections[slot].fd; + + password = get_frontend_password(username); + if (password == NULL) + { + password = get_password(username); + } + + /* Disallow unknown users */ + if (password == NULL && !config->allow_unknown_users) + { + pgagroal_log_debug("reject: %s", username); + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + goto error; + } + + /* TLS support */ + establish_client_tls_connection(config->connections[slot].server, server_fd, server_ssl); + + /* Send auth request to PostgreSQL */ + pgagroal_log_trace("authenticate: client auth request (%d)", client_fd); + status = pgagroal_write_message(*server_ssl, server_fd, request_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + pgagroal_free_message(msg); + + /* Keep response, and send response to client */ + pgagroal_log_trace("authenticate: server auth request (%d)", server_fd); + status = pgagroal_read_block_message(*server_ssl, server_fd, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + get_auth_type(msg, &auth_type); + pgagroal_log_trace("authenticate: auth type %d", auth_type); + + /* Supported security models: */ + /* trust (0) */ + /* password (3) */ + /* md5 (5) */ + /* scram-sha-256 (10) */ + if (auth_type == -1) + { + pgagroal_write_message(c_ssl, client_fd, msg); + pgagroal_write_empty(c_ssl, client_fd); + goto error; + } + else if (auth_type != SECURITY_TRUST && auth_type != SECURITY_PASSWORD && auth_type != SECURITY_MD5 && auth_type != SECURITY_SCRAM256) + { + pgagroal_log_info("Unsupported security model: %d", auth_type); + pgagroal_write_unsupported_security_model(c_ssl, client_fd, username); + pgagroal_write_empty(c_ssl, client_fd); + goto error; + } + + if (password == NULL) + { + if (server_passthrough(msg, auth_type, c_ssl, client_fd, slot)) + { + goto error; + } + } + else + { + if (hba_method == SECURITY_ALL) + { + hba_method = auth_type; + } + + auth_msg = pgagroal_copy_message(msg); + + if (hba_method == SECURITY_TRUST) + { + /* R/0 */ + client_trust(c_ssl, client_fd, username, password, slot); + } + else if (hba_method == SECURITY_PASSWORD) + { + /* R/3 */ + status = client_password(c_ssl, client_fd, username, password, slot); + if (status == AUTH_BAD_PASSWORD) + { + goto bad_password; + } + else if (status == AUTH_ERROR) + { + goto error; + } + } + else if (hba_method == SECURITY_MD5) + { + /* R/5 */ + status = client_md5(c_ssl, client_fd, username, password, slot); + if (status == AUTH_BAD_PASSWORD) + { + goto bad_password; + } + else if (status == AUTH_ERROR) + { + goto error; + } + } + else if (hba_method == SECURITY_SCRAM256) + { + /* R/10 */ + status = client_scram256(c_ssl, client_fd, username, password, slot); + if (status == AUTH_BAD_PASSWORD) + { + goto bad_password; + } + else if (status == AUTH_ERROR) + { + goto error; + } + } + else + { + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + + goto error; + } + + if (server_authenticate(auth_msg, auth_type, username, get_password(username), slot, *server_ssl)) + { + if (pgagroal_socket_isvalid(client_fd)) + { + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + } + + goto error; + } + + if (client_ok(c_ssl, client_fd, slot)) + { + goto error; + } + } + + server_state = atomic_load(&config->servers[config->connections[slot].server].state); + if (server_state == SERVER_NOTINIT || server_state == SERVER_NOTINIT_PRIMARY) + { + pgagroal_log_debug("Verify server mode: %d", config->connections[slot].server); + pgagroal_update_server_state(slot, server_fd, *server_ssl); + pgagroal_server_status(); + } + + pgagroal_log_trace("authenticate: has_security %d", config->connections[slot].has_security); + + pgagroal_free_copy_message(auth_msg); + + return AUTH_SUCCESS; + +bad_password: + + pgagroal_free_copy_message(auth_msg); + + if (pgagroal_socket_isvalid(client_fd)) + { + pgagroal_write_bad_password(c_ssl, client_fd, username); + if (hba_method == SECURITY_SCRAM256) + { + pgagroal_write_empty(c_ssl, client_fd); + } + } + + return AUTH_BAD_PASSWORD; + +error: + + pgagroal_free_copy_message(auth_msg); + + pgagroal_log_trace("use_unpooled_connection: failed for slot %d", slot); + + return AUTH_ERROR; +} + +static int +client_trust(SSL* c_ssl, int client_fd, char* username, char* password, int slot) +{ + pgagroal_log_debug("client_trust %d %d", client_fd, slot); + + return AUTH_SUCCESS; +} + +static int +client_password(SSL* c_ssl, int client_fd, char* username, char* password, int slot) +{ + int status; + time_t start_time; + bool non_blocking; + struct main_configuration* config; + struct message* msg = NULL; + + pgagroal_log_debug("client_password %d %d", client_fd, slot); + + config = (struct main_configuration*)shmem; + + status = pgagroal_write_auth_password(c_ssl, client_fd); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + start_time = time(NULL); + + non_blocking = pgagroal_socket_is_nonblocking(client_fd); + pgagroal_socket_nonblocking(client_fd, true); + + /* psql may just close the connection without word, so loop */ +retry: + status = pgagroal_read_timeout_message(c_ssl, client_fd, 1, &msg); + if (status != MESSAGE_STATUS_OK) + { + if (difftime(time(NULL), start_time) < config->common.authentication_timeout) + { + if (pgagroal_socket_isvalid(client_fd)) + /* Sleep for 100ms */ + { + SLEEP_AND_GOTO(100000000L, retry) + } + } + } + + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (!non_blocking) + { + pgagroal_socket_nonblocking(client_fd, false); + } + + if (strcmp(pgagroal_read_string(msg->data + 5), password)) + { + pgagroal_write_bad_password(c_ssl, client_fd, username); + + goto bad_password; + } + + pgagroal_free_message(msg); + + return AUTH_SUCCESS; + +bad_password: + + pgagroal_free_message(msg); + + return AUTH_BAD_PASSWORD; + +error: + + pgagroal_free_message(msg); + + return AUTH_ERROR; +} + +static int +client_md5(SSL* c_ssl, int client_fd, char* username, char* password, int slot) +{ + int status; + char salt[4]; + time_t start_time; + bool non_blocking; + size_t size; + char* pwdusr = NULL; + char* shadow = NULL; + char* md5_req = NULL; + char* md5 = NULL; + struct main_configuration* config; + struct message* msg = NULL; + + pgagroal_log_debug("client_md5 %d %d", client_fd, slot); + + config = (struct main_configuration*)shmem; + + salt[0] = (char)(random() & 0xFF); + salt[1] = (char)(random() & 0xFF); + salt[2] = (char)(random() & 0xFF); + salt[3] = (char)(random() & 0xFF); + + status = pgagroal_write_auth_md5(c_ssl, client_fd, salt); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + start_time = time(NULL); + + non_blocking = pgagroal_socket_is_nonblocking(client_fd); + pgagroal_socket_nonblocking(client_fd, true); + + /* psql may just close the connection without word, so loop */ +retry: + status = pgagroal_read_timeout_message(c_ssl, client_fd, 1, &msg); + if (status != MESSAGE_STATUS_OK) + { + if (difftime(time(NULL), start_time) < config->common.authentication_timeout) + { + if (pgagroal_socket_isvalid(client_fd)) + /* Sleep for 100ms */ + { + SLEEP_AND_GOTO(100000000L, retry) + } + + } + } + + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (!non_blocking) + { + pgagroal_socket_nonblocking(client_fd, false); + } + + size = strlen(username) + strlen(password) + 1; + pwdusr = calloc(1, size); + + snprintf(pwdusr, size, "%s%s", password, username); + + if (pgagroal_md5(pwdusr, strlen(pwdusr), &shadow)) + { + goto error; + } + + md5_req = calloc(1, 36); + memcpy(md5_req, shadow, 32); + memcpy(md5_req + 32, &salt[0], 4); + + if (pgagroal_md5(md5_req, 36, &md5)) + { + goto error; + } + + if (strcmp(pgagroal_read_string(msg->data + 8), md5)) + { + pgagroal_write_bad_password(c_ssl, client_fd, username); + + goto bad_password; + } + + pgagroal_free_message(msg); + + free(pwdusr); + free(shadow); + free(md5_req); + free(md5); + + return AUTH_SUCCESS; + +bad_password: + + pgagroal_free_message(msg); + + free(pwdusr); + free(shadow); + free(md5_req); + free(md5); + + return AUTH_BAD_PASSWORD; + +error: + + pgagroal_free_message(msg); + + free(pwdusr); + free(shadow); + free(md5_req); + free(md5); + + return AUTH_ERROR; +} + +static int +client_scram256(SSL* c_ssl, int client_fd, char* username, char* password, int slot) +{ + int status; + time_t start_time; + bool non_blocking; + char* password_prep = NULL; + char* client_first_message_bare = NULL; + char* server_first_message = NULL; + char* client_final_message_without_proof = NULL; + char* client_nounce = NULL; + char* server_nounce = NULL; + char* salt = NULL; + int salt_length = 0; + char* base64_salt = NULL; + size_t base64_salt_length; + char* base64_client_proof = NULL; + char* client_proof_received = NULL; + size_t client_proof_received_length = 0; + unsigned char* client_proof_calc = NULL; + int client_proof_calc_length = 0; + unsigned char* server_signature_calc = NULL; + size_t server_signature_calc_length = 0; + char* base64_server_signature_calc = NULL; + size_t base64_server_signature_calc_length; + struct main_configuration* config; + struct message* msg = NULL; + struct message* sasl_continue = NULL; + struct message* sasl_final = NULL; + + pgagroal_log_debug("client_scram256 %d %d", client_fd, slot); + + config = (struct main_configuration*)shmem; + + status = pgagroal_write_auth_scram256(c_ssl, client_fd); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + start_time = time(NULL); + + non_blocking = pgagroal_socket_is_nonblocking(client_fd); + pgagroal_socket_nonblocking(client_fd, true); + + /* psql may just close the connection without word, so loop */ +retry: + status = pgagroal_read_timeout_message(c_ssl, client_fd, 1, &msg); + if (status != MESSAGE_STATUS_OK) + { + if (difftime(time(NULL), start_time) < config->common.authentication_timeout) + { + if (pgagroal_socket_isvalid(client_fd)) + /* Sleep for 100ms */ + { + SLEEP_AND_GOTO(100000000L, retry) + } + + } + } + + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (!non_blocking) + { + pgagroal_socket_nonblocking(client_fd, false); + } + + client_first_message_bare = calloc(1, msg->length - 25); + + memcpy(client_first_message_bare, msg->data + 26, msg->length - 26); + + get_scram_attribute('r', (char*)msg->data + 26, msg->length - 26, &client_nounce); + generate_nounce(&server_nounce); + generate_salt(&salt, &salt_length); + pgagroal_base64_encode(salt, salt_length, &base64_salt, &base64_salt_length); + + server_first_message = calloc(1, 89); + + snprintf(server_first_message, 89, "r=%s%s,s=%s,i=4096", client_nounce, server_nounce, base64_salt); + + status = pgagroal_create_auth_scram256_continue(client_nounce, server_nounce, base64_salt, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + sasl_continue = pgagroal_copy_message(msg); + + pgagroal_free_copy_message(msg); + msg = NULL; + + status = pgagroal_write_message(c_ssl, client_fd, sasl_continue); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_read_timeout_message(c_ssl, client_fd, config->common.authentication_timeout, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + get_scram_attribute('p', (char*)msg->data + 5, msg->length - 5, &base64_client_proof); + pgagroal_base64_decode(base64_client_proof, strlen(base64_client_proof), (void**)&client_proof_received, &client_proof_received_length); + + client_final_message_without_proof = calloc(1, 58); + + memcpy(client_final_message_without_proof, msg->data + 5, 57); + + sasl_prep(password, &password_prep); + + if (client_proof(password_prep, salt, salt_length, 4096, + client_first_message_bare, strlen(client_first_message_bare), + server_first_message, strlen(server_first_message), + client_final_message_without_proof, strlen(client_final_message_without_proof), + &client_proof_calc, &client_proof_calc_length)) + { + goto error; + } + + if (client_proof_received_length != client_proof_calc_length || + memcmp(client_proof_received, client_proof_calc, client_proof_calc_length) != 0) + { + goto bad_password; + } + + if (server_signature(password_prep, salt, salt_length, 4096, + NULL, 0, + client_first_message_bare, strlen(client_first_message_bare), + server_first_message, strlen(server_first_message), + client_final_message_without_proof, strlen(client_final_message_without_proof), + &server_signature_calc, &server_signature_calc_length)) + { + goto error; + } + + pgagroal_base64_encode((char*)server_signature_calc, server_signature_calc_length, &base64_server_signature_calc, &base64_server_signature_calc_length); + + status = pgagroal_create_auth_scram256_final(base64_server_signature_calc, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + sasl_final = pgagroal_copy_message(msg); + + pgagroal_free_copy_message(msg); + msg = NULL; + + status = pgagroal_write_message(c_ssl, client_fd, sasl_final); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + pgagroal_log_debug("client_scram256 done"); + + free(password_prep); + free(client_first_message_bare); + free(server_first_message); + free(client_final_message_without_proof); + free(client_nounce); + free(server_nounce); + free(salt); + free(base64_salt); + free(base64_client_proof); + free(client_proof_received); + free(client_proof_calc); + free(server_signature_calc); + free(base64_server_signature_calc); + + pgagroal_free_copy_message(sasl_continue); + pgagroal_free_copy_message(sasl_final); + + return AUTH_SUCCESS; + +bad_password: + free(password_prep); + free(client_first_message_bare); + free(server_first_message); + free(client_final_message_without_proof); + free(client_nounce); + free(server_nounce); + free(salt); + free(base64_salt); + free(base64_client_proof); + free(client_proof_received); + free(client_proof_calc); + free(server_signature_calc); + free(base64_server_signature_calc); + + pgagroal_free_copy_message(sasl_continue); + pgagroal_free_copy_message(sasl_final); + + return AUTH_BAD_PASSWORD; + +error: + free(password_prep); + free(client_first_message_bare); + free(server_first_message); + free(client_final_message_without_proof); + free(client_nounce); + free(server_nounce); + free(salt); + free(base64_salt); + free(base64_client_proof); + free(client_proof_received); + free(client_proof_calc); + free(server_signature_calc); + free(base64_server_signature_calc); + + pgagroal_free_copy_message(sasl_continue); + pgagroal_free_copy_message(sasl_final); + + return AUTH_ERROR; +} + +static int +client_ok(SSL* c_ssl, int client_fd, int slot) +{ + int status; + size_t size; + char* data; + struct message msg; + struct main_configuration* config; + + data = NULL; + memset(&msg, 0, sizeof(msg)); + + config = (struct main_configuration*)shmem; + + if (config->connections[slot].has_security == SECURITY_TRUST) + { + size = config->connections[slot].security_lengths[0]; + data = malloc(size); + if (data == NULL) + { + goto error; + } + memcpy(data, config->connections[slot].security_messages[0], size); + } + else if (config->connections[slot].has_security == SECURITY_PASSWORD || config->connections[slot].has_security == SECURITY_MD5) + { + size = config->connections[slot].security_lengths[2]; + data = malloc(size); + if (data == NULL) + { + goto error; + } + memcpy(data, config->connections[slot].security_messages[2], size); + } + else if (config->connections[slot].has_security == SECURITY_SCRAM256) + { + size = config->connections[slot].security_lengths[4] - 55; + data = malloc(size); + if (data == NULL) + { + goto error; + } + memcpy(data, config->connections[slot].security_messages[4] + 55, size); + } + else + { + goto error; + } + + msg.kind = 'R'; + msg.length = size; + msg.data = data; + + status = pgagroal_write_message(c_ssl, client_fd, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + free(data); + + return 0; + +error: + + free(data); + + return 1; +} + +static int +server_passthrough(struct message* msg, int auth_type, SSL* c_ssl, int client_fd, int slot) +{ + int status = MESSAGE_STATUS_ERROR; + int server_fd; + int auth_index = 0; + int auth_response = -1; + struct message* smsg = NULL; + struct message* kmsg = NULL; + struct main_configuration* config = NULL; + + config = (struct main_configuration*)shmem; + server_fd = config->connections[slot].fd; + + pgagroal_log_trace("server_passthrough %d %d", auth_type, slot); + + for (int i = 0; i < NUMBER_OF_SECURITY_MESSAGES; i++) + { + memset(&config->connections[slot].security_messages[i], 0, SECURITY_BUFFER_SIZE); + } + + if (msg->length > SECURITY_BUFFER_SIZE) + { + pgagroal_log_message(msg); + pgagroal_log_error("Security message too large: %ld", msg->length); + goto error; + } + + config->connections[slot].security_lengths[auth_index] = msg->length; + memcpy(&config->connections[slot].security_messages[auth_index], msg->data, msg->length); + auth_index++; + + status = pgagroal_write_message(c_ssl, client_fd, msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + pgagroal_free_message(msg); + + /* Non-trust clients */ + if (auth_type != SECURITY_TRUST) + { + /* Receive client response, keep it, and send it to PostgreSQL */ + status = pgagroal_read_timeout_message(c_ssl, client_fd, config->common.authentication_timeout, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (msg->length > SECURITY_BUFFER_SIZE) + { + pgagroal_log_message(msg); + pgagroal_log_error("Security message too large: %ld", msg->length); + goto error; + } + + config->connections[slot].security_lengths[auth_index] = msg->length; + memcpy(&config->connections[slot].security_messages[auth_index], msg->data, msg->length); + auth_index++; + + status = pgagroal_write_message(NULL, server_fd, msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + pgagroal_free_message(msg); + + status = pgagroal_read_block_message(NULL, server_fd, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (auth_type == SECURITY_SCRAM256) + { + if (msg->length > SECURITY_BUFFER_SIZE) + { + pgagroal_log_message(msg); + pgagroal_log_error("Security message too large: %ld", msg->length); + goto error; + } + + config->connections[slot].security_lengths[auth_index] = msg->length; + memcpy(&config->connections[slot].security_messages[auth_index], msg->data, msg->length); + auth_index++; + + status = pgagroal_write_message(c_ssl, client_fd, msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + pgagroal_free_message(msg); + + status = pgagroal_read_timeout_message(c_ssl, client_fd, config->common.authentication_timeout, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (msg->length > SECURITY_BUFFER_SIZE) + { + pgagroal_log_message(msg); + pgagroal_log_error("Security message too large: %ld", msg->length); + goto error; + } + + config->connections[slot].security_lengths[auth_index] = msg->length; + memcpy(&config->connections[slot].security_messages[auth_index], msg->data, msg->length); + auth_index++; + + status = pgagroal_write_message(NULL, server_fd, msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + pgagroal_free_message(msg); + + status = pgagroal_read_block_message(NULL, server_fd, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + } + + /* Ok: Keep the response, send it to the client, and exit authenticate() */ + get_auth_type(msg, &auth_response); + pgagroal_log_trace("authenticate: auth response %d", auth_response); + + if (auth_response == 0) + { + if (msg->length > SECURITY_BUFFER_SIZE) + { + pgagroal_log_message(msg); + pgagroal_log_error("Security message too large: %ld", msg->length); + goto error; + } + + config->connections[slot].security_lengths[auth_index] = msg->length; + memcpy(&config->connections[slot].security_messages[auth_index], msg->data, msg->length); + + config->connections[slot].has_security = auth_type; + } + + status = pgagroal_write_message(c_ssl, client_fd, msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + pgagroal_free_message(msg); + + if (auth_response != 0) + { + goto error; + } + } + else + { + /* Trust */ + config->connections[slot].has_security = SECURITY_TRUST; + } + + if (config->connections[slot].has_security == SECURITY_TRUST) + { + pgagroal_create_message(&config->connections[slot].security_messages[0], + config->connections[slot].security_lengths[0], + &smsg); + } + else if (config->connections[slot].has_security == SECURITY_PASSWORD || config->connections[slot].has_security == SECURITY_MD5) + { + pgagroal_create_message(&config->connections[slot].security_messages[2], + config->connections[slot].security_lengths[2], + &smsg); + } + else if (config->connections[slot].has_security == SECURITY_SCRAM256) + { + pgagroal_create_message(&config->connections[slot].security_messages[4], + config->connections[slot].security_lengths[4], + &smsg); + } + + if (smsg != NULL) + { + pgagroal_extract_message('K', smsg, &kmsg); + + if (kmsg != NULL) + { + config->connections[slot].backend_pid = pgagroal_read_int32(kmsg->data + 5); + config->connections[slot].backend_secret = pgagroal_read_int32(kmsg->data + 9); + } + } + + pgagroal_free_copy_message(smsg); + pgagroal_free_copy_message(kmsg); + + return 0; + +error: + + pgagroal_free_copy_message(smsg); + pgagroal_free_copy_message(kmsg); + + return 1; +} + +static int +server_authenticate(struct message* msg, int auth_type, char* username, char* password, int slot, SSL* server_ssl) +{ + int ret = AUTH_ERROR; + struct message* smsg = NULL; + struct message* kmsg = NULL; + struct main_configuration* config = NULL; + + config = (struct main_configuration*)shmem; + + for (int i = 0; i < NUMBER_OF_SECURITY_MESSAGES; i++) + { + memset(&config->connections[slot].security_messages[i], 0, SECURITY_BUFFER_SIZE); + } + + if (msg->length > SECURITY_BUFFER_SIZE) + { + pgagroal_log_message(msg); + pgagroal_log_error("Security message too large: %ld", msg->length); + goto error; + } + + config->connections[slot].security_lengths[0] = msg->length; + memcpy(&config->connections[slot].security_messages[0], msg->data, msg->length); + + if (auth_type == SECURITY_TRUST) + { + ret = server_trust(slot, server_ssl); + } + else if (auth_type == SECURITY_PASSWORD) + { + ret = server_password(username, password, slot, server_ssl); + } + else if (auth_type == SECURITY_MD5) + { + ret = server_md5(username, password, slot, server_ssl); + } + else if (auth_type == SECURITY_SCRAM256) + { + ret = server_scram256(username, password, slot, server_ssl); + } + + if (config->connections[slot].has_security == SECURITY_TRUST) + { + pgagroal_create_message(&config->connections[slot].security_messages[0], + config->connections[slot].security_lengths[0], + &smsg); + } + else if (config->connections[slot].has_security == SECURITY_PASSWORD || config->connections[slot].has_security == SECURITY_MD5) + { + pgagroal_create_message(&config->connections[slot].security_messages[2], + config->connections[slot].security_lengths[2], + &smsg); + } + else if (config->connections[slot].has_security == SECURITY_SCRAM256) + { + pgagroal_create_message(&config->connections[slot].security_messages[4], + config->connections[slot].security_lengths[4], + &smsg); + } + + if (smsg != NULL) + { + pgagroal_extract_message('K', smsg, &kmsg); + + if (kmsg != NULL) + { + config->connections[slot].backend_pid = pgagroal_read_int32(kmsg->data + 5); + config->connections[slot].backend_secret = pgagroal_read_int32(kmsg->data + 9); + } + } + + pgagroal_free_copy_message(smsg); + pgagroal_free_copy_message(kmsg); + + return ret; + +error: + + pgagroal_log_error("server_authenticate: %d", auth_type); + + pgagroal_free_copy_message(smsg); + pgagroal_free_copy_message(kmsg); + + return AUTH_ERROR; +} + +static int +server_trust(int slot, SSL* server_ssl) +{ + struct main_configuration* config = NULL; + + config = (struct main_configuration*)shmem; + + pgagroal_log_trace("server_trust"); + + config->connections[slot].has_security = SECURITY_TRUST; + + return AUTH_SUCCESS; +} + +static int +server_password(char* username, char* password, int slot, SSL* server_ssl) +{ + int status = MESSAGE_STATUS_ERROR; + int auth_index = 1; + int auth_response = -1; + int server_fd; + struct message* auth_msg = NULL; + struct message* password_msg = NULL; + struct main_configuration* config = NULL; + + config = (struct main_configuration*)shmem; + server_fd = config->connections[slot].fd; + + pgagroal_log_trace("server_password"); + + status = pgagroal_create_auth_password_response(password, &password_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_write_message(server_ssl, server_fd, password_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + config->connections[slot].security_lengths[auth_index] = password_msg->length; + memcpy(&config->connections[slot].security_messages[auth_index], password_msg->data, password_msg->length); + auth_index++; + + status = pgagroal_read_block_message(server_ssl, server_fd, &auth_msg); + if (auth_msg->length > SECURITY_BUFFER_SIZE) + { + pgagroal_log_message(auth_msg); + pgagroal_log_error("Security message too large: %ld", auth_msg->length); + goto error; + } + + get_auth_type(auth_msg, &auth_response); + pgagroal_log_trace("authenticate: auth response %d", auth_response); + + if (auth_response == 0) + { + if (auth_msg->length > SECURITY_BUFFER_SIZE) + { + pgagroal_log_message(auth_msg); + pgagroal_log_error("Security message too large: %ld", auth_msg->length); + goto error; + } + + config->connections[slot].security_lengths[auth_index] = auth_msg->length; + memcpy(&config->connections[slot].security_messages[auth_index], auth_msg->data, auth_msg->length); + + config->connections[slot].has_security = SECURITY_PASSWORD; + } + else + { + goto bad_password; + } + + pgagroal_free_copy_message(password_msg); + pgagroal_free_message(auth_msg); + + return AUTH_SUCCESS; + +bad_password: + + pgagroal_log_warn("Wrong password for user: %s", username); + + pgagroal_free_copy_message(password_msg); + pgagroal_free_message(auth_msg); + + return AUTH_BAD_PASSWORD; + +error: + + pgagroal_free_copy_message(password_msg); + pgagroal_free_message(auth_msg); + + return AUTH_ERROR; +} + +static int +server_md5(char* username, char* password, int slot, SSL* server_ssl) +{ + int status = MESSAGE_STATUS_ERROR; + int auth_index = 1; + int auth_response = -1; + int server_fd; + size_t size; + char* pwdusr = NULL; + char* shadow = NULL; + char* md5_req = NULL; + char* md5 = NULL; + char md5str[36]; + char* salt = NULL; + struct message* auth_msg = NULL; + struct message* md5_msg = NULL; + struct main_configuration* config = NULL; + + config = (struct main_configuration*)shmem; + server_fd = config->connections[slot].fd; + + pgagroal_log_trace("server_md5"); + + if (get_salt(config->connections[slot].security_messages[0], &salt)) + { + goto error; + } + + size = strlen(username) + strlen(password) + 1; + pwdusr = calloc(1, size); + + snprintf(pwdusr, size, "%s%s", password, username); + + if (pgagroal_md5(pwdusr, strlen(pwdusr), &shadow)) + { + goto error; + } + + md5_req = calloc(1, 36); + + memcpy(md5_req, shadow, 32); + memcpy(md5_req + 32, salt, 4); + + if (pgagroal_md5(md5_req, 36, &md5)) + { + goto error; + } + + memset(&md5str, 0, sizeof(md5str)); + snprintf(&md5str[0], 36, "md5%s", md5); + + status = pgagroal_create_auth_md5_response(md5str, &md5_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_write_message(server_ssl, server_fd, md5_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + config->connections[slot].security_lengths[auth_index] = md5_msg->length; + memcpy(&config->connections[slot].security_messages[auth_index], md5_msg->data, md5_msg->length); + auth_index++; + + status = pgagroal_read_block_message(server_ssl, server_fd, &auth_msg); + if (auth_msg->length > SECURITY_BUFFER_SIZE) + { + pgagroal_log_message(auth_msg); + pgagroal_log_error("Security message too large: %ld", auth_msg->length); + goto error; + } + + get_auth_type(auth_msg, &auth_response); + pgagroal_log_trace("authenticate: auth response %d", auth_response); + + if (auth_response == 0) + { + if (auth_msg->length > SECURITY_BUFFER_SIZE) + { + pgagroal_log_message(auth_msg); + pgagroal_log_error("Security message too large: %ld", auth_msg->length); + goto error; + } + + config->connections[slot].security_lengths[auth_index] = auth_msg->length; + memcpy(&config->connections[slot].security_messages[auth_index], auth_msg->data, auth_msg->length); + + config->connections[slot].has_security = SECURITY_MD5; + } + else + { + goto bad_password; + } + + free(pwdusr); + free(shadow); + free(md5_req); + free(md5); + free(salt); + + pgagroal_free_copy_message(md5_msg); + pgagroal_free_message(auth_msg); + + return AUTH_SUCCESS; + +bad_password: + + pgagroal_log_warn("Wrong password for user: %s", username); + + free(pwdusr); + free(shadow); + free(md5_req); + free(md5); + free(salt); + + pgagroal_free_copy_message(md5_msg); + pgagroal_free_message(auth_msg); + + return AUTH_BAD_PASSWORD; + +error: + + free(pwdusr); + free(shadow); + free(md5_req); + free(md5); + free(salt); + + pgagroal_free_copy_message(md5_msg); + pgagroal_free_message(auth_msg); + + return AUTH_ERROR; +} + +static int +server_scram256(char* username, char* password, int slot, SSL* server_ssl) +{ + int status = MESSAGE_STATUS_ERROR; + int auth_index = 1; + int server_fd; + char* salt = NULL; + size_t salt_length = 0; + char* password_prep = NULL; + char* client_nounce = NULL; + char* combined_nounce = NULL; + char* base64_salt = NULL; + char* iteration_string = NULL; + char* err = NULL; + int iteration; + char* client_first_message_bare = NULL; + char* server_first_message = NULL; + char wo_proof[58]; + unsigned char* proof = NULL; + int proof_length; + char* proof_base = NULL; + size_t proof_base_length; + char* base64_server_signature = NULL; + char* server_signature_received = NULL; + size_t server_signature_received_length; + unsigned char* server_signature_calc = NULL; + size_t server_signature_calc_length; + struct message* sasl_response = NULL; + struct message* sasl_continue = NULL; + struct message* sasl_continue_response = NULL; + struct message* sasl_final = NULL; + struct message* msg = NULL; + struct main_configuration* config = NULL; + + config = (struct main_configuration*)shmem; + server_fd = config->connections[slot].fd; + + pgagroal_log_trace("server_scram256"); + + status = sasl_prep(password, &password_prep); + if (status) + { + goto error; + } + + generate_nounce(&client_nounce); + + status = pgagroal_create_auth_scram256_response(client_nounce, &sasl_response); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + config->connections[slot].security_lengths[auth_index] = sasl_response->length; + memcpy(&config->connections[slot].security_messages[auth_index], sasl_response->data, sasl_response->length); + auth_index++; + + status = pgagroal_write_message(server_ssl, server_fd, sasl_response); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_read_block_message(server_ssl, server_fd, &msg); + if (msg->length > SECURITY_BUFFER_SIZE) + { + pgagroal_log_message(msg); + pgagroal_log_error("Security message too large: %ld", msg->length); + goto error; + } + + sasl_continue = pgagroal_copy_message(msg); + + config->connections[slot].security_lengths[auth_index] = sasl_continue->length; + memcpy(&config->connections[slot].security_messages[auth_index], sasl_continue->data, sasl_continue->length); + auth_index++; + + get_scram_attribute('r', (char*)(sasl_continue->data + 9), sasl_continue->length - 9, &combined_nounce); + get_scram_attribute('s', (char*)(sasl_continue->data + 9), sasl_continue->length - 9, &base64_salt); + get_scram_attribute('i', (char*)(sasl_continue->data + 9), sasl_continue->length - 9, &iteration_string); + get_scram_attribute('e', (char*)(sasl_continue->data + 9), sasl_continue->length - 9, &err); + + if (err != NULL) + { + pgagroal_log_error("SCRAM-SHA-256: %s", err); + goto error; + } + + pgagroal_base64_decode(base64_salt, strlen(base64_salt), (void**)&salt, &salt_length); + + iteration = atoi(iteration_string); + + memset(&wo_proof[0], 0, sizeof(wo_proof)); + snprintf(&wo_proof[0], sizeof(wo_proof), "c=biws,r=%s", combined_nounce); + + /* n=,r=... */ + client_first_message_bare = config->connections[slot].security_messages[1] + 26; + + /* r=...,s=...,i=4096 */ + server_first_message = config->connections[slot].security_messages[2] + 9; + + if (client_proof(password_prep, salt, salt_length, iteration, + client_first_message_bare, config->connections[slot].security_lengths[1] - 26, + server_first_message, config->connections[slot].security_lengths[2] - 9, + &wo_proof[0], strlen(wo_proof), + &proof, &proof_length)) + { + goto error; + } + + pgagroal_base64_encode((char*)proof, proof_length, &proof_base, &proof_base_length); + + status = pgagroal_create_auth_scram256_continue_response(&wo_proof[0], (char*)proof_base, &sasl_continue_response); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + config->connections[slot].security_lengths[auth_index] = sasl_continue_response->length; + memcpy(&config->connections[slot].security_messages[auth_index], sasl_continue_response->data, sasl_continue_response->length); + auth_index++; + + status = pgagroal_write_message(server_ssl, server_fd, sasl_continue_response); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_read_block_message(server_ssl, server_fd, &msg); + if (msg->length > SECURITY_BUFFER_SIZE) + { + pgagroal_log_message(msg); + pgagroal_log_error("Security message too large: %ld", msg->length); + goto error; + } + + config->connections[slot].security_lengths[auth_index] = msg->length; + memcpy(&config->connections[slot].security_messages[auth_index], msg->data, msg->length); + auth_index++; + + if (pgagroal_extract_message('R', msg, &sasl_final)) + { + goto error; + } + + /* Get 'v' attribute */ + base64_server_signature = sasl_final->data + 11; + pgagroal_base64_decode(base64_server_signature, sasl_final->length - 11, + (void**)&server_signature_received, &server_signature_received_length); + + if (server_signature(password_prep, salt, salt_length, iteration, + NULL, 0, + client_first_message_bare, config->connections[slot].security_lengths[1] - 26, + server_first_message, config->connections[slot].security_lengths[2] - 9, + &wo_proof[0], strlen(wo_proof), + &server_signature_calc, &server_signature_calc_length)) + { + goto error; + } + + if (server_signature_calc_length != server_signature_received_length || + memcmp(server_signature_received, server_signature_calc, server_signature_calc_length) != 0) + { + goto bad_password; + } + + config->connections[slot].has_security = SECURITY_SCRAM256; + + free(salt); + free(err); + free(password_prep); + free(client_nounce); + free(combined_nounce); + free(base64_salt); + free(iteration_string); + free(proof); + free(proof_base); + free(server_signature_received); + free(server_signature_calc); + + pgagroal_free_copy_message(sasl_response); + pgagroal_free_copy_message(sasl_continue); + pgagroal_free_copy_message(sasl_continue_response); + pgagroal_free_copy_message(sasl_final); + + return AUTH_SUCCESS; + +bad_password: + + pgagroal_log_warn("Wrong password for user: %s", username); + + free(salt); + free(err); + free(password_prep); + free(client_nounce); + free(combined_nounce); + free(base64_salt); + free(iteration_string); + free(proof); + free(proof_base); + free(server_signature_received); + free(server_signature_calc); + + pgagroal_free_copy_message(sasl_response); + pgagroal_free_copy_message(sasl_continue); + pgagroal_free_copy_message(sasl_continue_response); + pgagroal_free_copy_message(sasl_final); + + return AUTH_BAD_PASSWORD; + +error: + + free(salt); + free(err); + free(password_prep); + free(client_nounce); + free(combined_nounce); + free(base64_salt); + free(iteration_string); + free(proof); + free(proof_base); + free(server_signature_received); + free(server_signature_calc); + + pgagroal_free_copy_message(sasl_response); + pgagroal_free_copy_message(sasl_continue); + pgagroal_free_copy_message(sasl_continue_response); + pgagroal_free_copy_message(sasl_final); + + return AUTH_ERROR; +} + +static bool +is_allowed(char* username, char* database, char* address, int* hba_method) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + for (int i = 0; i < config->number_of_hbas; i++) + { + if (is_allowed_address(address, config->hbas[i].address) && + is_allowed_database(database, config->hbas[i].database) && + is_allowed_username(username, config->hbas[i].username)) + { + *hba_method = get_hba_method(i); + + return true; + } + } + + return false; +} + +static bool +is_allowed_username(char* username, char* entry) +{ + if (!strcasecmp(entry, "all") || !strcmp(username, entry)) + { + return true; + } + + return false; +} + +static bool +is_allowed_database(char* database, char* entry) +{ + if (!strcasecmp(entry, "all") || !strcmp(database, entry)) + { + return true; + } + + return false; +} + +static bool +is_allowed_address(char* address, char* entry) +{ + struct sockaddr_in address_sa4; + struct sockaddr_in6 address_sa6; + struct sockaddr_in entry_sa4; + struct sockaddr_in6 entry_sa6; + char addr[INET6_ADDRSTRLEN]; + char s_mask[4]; + int mask; + char* marker; + bool ipv4 = true; + + memset(&addr, 0, sizeof(addr)); + memset(&s_mask, 0, sizeof(s_mask)); + + if (!strcasecmp(entry, "all")) + { + return true; + } + + marker = strchr(entry, '/'); + if (!marker) + { + pgagroal_log_warn("Invalid HBA entry: %s", entry); + return false; + } + + memcpy(&addr, entry, marker - entry); + marker += sizeof(char); + memcpy(&s_mask, marker, strlen(marker)); + mask = atoi(s_mask); + + if (strchr(addr, ':') == NULL) + { + inet_pton(AF_INET, addr, &(entry_sa4.sin_addr)); + + if (strchr(address, ':') == NULL) + { + inet_pton(AF_INET, address, &(address_sa4.sin_addr)); + } + else + { + return false; + } + } + else + { + ipv4 = false; + + inet_pton(AF_INET6, addr, &(entry_sa6.sin6_addr)); + + if (strchr(address, ':') != NULL) + { + inet_pton(AF_INET6, address, &(address_sa6.sin6_addr)); + } + else + { + return false; + } + } + + if (ipv4) + { + if (!strcmp(entry, "0.0.0.0/0")) + { + return true; + } + + if (mask < 0 || mask > 32) + { + pgagroal_log_warn("Invalid HBA entry: %s", entry); + return false; + } + + unsigned char a1 = (ntohl(address_sa4.sin_addr.s_addr) >> 24) & 0xff; + unsigned char a2 = (ntohl(address_sa4.sin_addr.s_addr) >> 16) & 0xff; + unsigned char a3 = (ntohl(address_sa4.sin_addr.s_addr) >> 8) & 0xff; + unsigned char a4 = (ntohl(address_sa4.sin_addr.s_addr)) & 0xff; + + unsigned char e1 = (ntohl(entry_sa4.sin_addr.s_addr) >> 24) & 0xff; + unsigned char e2 = (ntohl(entry_sa4.sin_addr.s_addr) >> 16) & 0xff; + unsigned char e3 = (ntohl(entry_sa4.sin_addr.s_addr) >> 8) & 0xff; + unsigned char e4 = (ntohl(entry_sa4.sin_addr.s_addr)) & 0xff; + + if (mask <= 8) + { + return a1 == e1; + } + else if (mask <= 16) + { + return a1 == e1 && a2 == e2; + } + else if (mask <= 24) + { + return a1 == e1 && a2 == e2 && a3 == e3; + } + else + { + return a1 == e1 && a2 == e2 && a3 == e3 && a4 == e4; + } + } + else + { + if (!strcmp(entry, "::0/0")) + { + return true; + } + + if (mask < 0 || mask > 128) + { + pgagroal_log_warn("Invalid HBA entry: %s", entry); + return false; + } + + struct sockaddr_in6 netmask; + bool result = false; + + memset(&netmask, 0, sizeof(struct sockaddr_in6)); + + for (long i = mask, j = 0; i > 0; i -= 8, ++j) + { + netmask.sin6_addr.s6_addr[j] = i >= 8 ? 0xff : (unsigned long int)((0xffU << (8 - i)) & 0xffU); + } + + for (unsigned i = 0; i < 16; i++) + { + result |= (0 != (address_sa6.sin6_addr.s6_addr[i] & !netmask.sin6_addr.s6_addr[i])); + } + + return result; + } + + return false; +} + +static bool +is_disabled(char* database) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + if (config->all_disabled) + { + return true; + } + + for (int i = 0; i < NUMBER_OF_DISABLED; i++) + { + if (!strcmp(config->disabled[i], database)) + { + return true; + } + } + + return false; +} + +static int +get_hba_method(int index) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + if (!strcasecmp(config->hbas[index].method, "reject")) + { + return SECURITY_REJECT; + } + + if (!strcasecmp(config->hbas[index].method, "trust")) + { + return SECURITY_TRUST; + } + + if (!strcasecmp(config->hbas[index].method, "password")) + { + return SECURITY_PASSWORD; + } + + if (!strcasecmp(config->hbas[index].method, "md5")) + { + return SECURITY_MD5; + } + + if (!strcasecmp(config->hbas[index].method, "scram-sha-256")) + { + return SECURITY_SCRAM256; + } + + if (!strcasecmp(config->hbas[index].method, "all")) + { + return SECURITY_ALL; + } + + return SECURITY_REJECT; +} + +static char* +get_password(char* username) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + for (int i = 0; i < config->number_of_users; i++) + { + if (!strcmp(&config->users[i].username[0], username)) + { + return &config->users[i].password[0]; + } + } + + return NULL; +} + +static char* +get_frontend_password(char* username) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + for (int i = 0; i < config->number_of_frontend_users; i++) + { + if (!strcmp(&config->frontend_users[i].username[0], username)) + { + return &config->frontend_users[i].password[0]; + } + } + + return NULL; +} + +static char* +get_admin_password(char* username) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + for (int i = 0; i < config->number_of_admins; i++) + { + if (!strcmp(&config->admins[i].username[0], username)) + { + return &config->admins[i].password[0]; + } + } + + return NULL; +} + +static int +get_salt(void* data, char** salt) +{ + char* result; + + result = calloc(1, 4); + + memcpy(result, data + 9, 4); + + *salt = result; + + return 0; +} + +int +pgagroal_get_master_key(char** masterkey) +{ + FILE* master_key_file = NULL; + char buf[MISC_LENGTH]; + char line[MISC_LENGTH]; + char* mk = NULL; + size_t mk_length = 0; + struct stat st = {0}; + + if (pgagroal_get_home_directory() == NULL) + { + goto error; + } + + memset(&buf, 0, sizeof(buf)); + snprintf(&buf[0], sizeof(buf), "%s/.pgagroal", pgagroal_get_home_directory()); + + if (stat(&buf[0], &st) == -1) + { + goto error; + } + else + { + if (S_ISDIR(st.st_mode) && st.st_mode & S_IRWXU && !(st.st_mode & S_IRWXG) && !(st.st_mode & S_IRWXO)) + { + /* Ok */ + } + else + { + goto error; + } + } + + memset(&buf, 0, sizeof(buf)); + snprintf(&buf[0], sizeof(buf), "%s/.pgagroal/master.key", pgagroal_get_home_directory()); + + if (stat(&buf[0], &st) == -1) + { + goto error; + } + else + { + if (S_ISREG(st.st_mode) && st.st_mode & (S_IRUSR | S_IWUSR) && !(st.st_mode & S_IRWXG) && !(st.st_mode & S_IRWXO)) + { + /* Ok */ + } + else + { + goto error; + } + } + + master_key_file = fopen(&buf[0], "r"); + if (master_key_file == NULL) + { + goto error; + } + + memset(&line, 0, sizeof(line)); + if (fgets(line, sizeof(line), master_key_file) == NULL) + { + goto error; + } + + pgagroal_base64_decode(&line[0], strlen(&line[0]), (void**)&mk, &mk_length); + + *masterkey = mk; + + fclose(master_key_file); + + return 0; + +error: + + free(mk); + + if (master_key_file) + { + fclose(master_key_file); + } + + return 1; +} + +int +pgagroal_md5(char* str, int length, char** md5) +{ + int n; + MD5_CTX c; + unsigned char digest[16]; + char* out; + + out = calloc(1, 33); + MD5_Init(&c); + MD5_Update(&c, str, length); + MD5_Final(digest, &c); + + for (n = 0; n < 16; ++n) + { + snprintf(&(out[n * 2]), 32, "%02x", (unsigned int)digest[n]); + } + + *md5 = out; + + return 0; +} + +bool +pgagroal_user_known(char* user) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + for (int i = 0; i < config->number_of_users; i++) + { + if (!strcmp(user, config->users[i].username)) + { + return true; + } + } + + return false; +} + +int +pgagroal_tls_valid(void) +{ + struct main_configuration* config; + struct stat st = {0}; + + config = (struct main_configuration*)shmem; + + if (config->common.tls) + { + if (strlen(config->common.tls_cert_file) == 0) + { + pgagroal_log_error("No TLS certificate defined"); + goto error; + } + + if (strlen(config->common.tls_key_file) == 0) + { + pgagroal_log_error("No TLS private key defined"); + goto error; + } + + if (stat(config->common.tls_cert_file, &st) == -1) + { + pgagroal_log_error("Can't locate TLS certificate file: %s", config->common.tls_cert_file); + goto error; + } + + if (!S_ISREG(st.st_mode)) + { + pgagroal_log_error("TLS certificate file is not a regular file: %s", config->common.tls_cert_file); + goto error; + } + + if (st.st_uid && st.st_uid != geteuid()) + { + pgagroal_log_error("TLS certificate file not owned by user or root: %s", config->common.tls_cert_file); + goto error; + } + + memset(&st, 0, sizeof(struct stat)); + + if (stat(config->common.tls_key_file, &st) == -1) + { + pgagroal_log_error("Can't locate TLS private key file: %s", config->common.tls_key_file); + goto error; + } + + if (!S_ISREG(st.st_mode)) + { + pgagroal_log_error("TLS private key file is not a regular file: %s", config->common.tls_key_file); + goto error; + } + + if (st.st_uid == geteuid()) + { + if (st.st_mode & (S_IRWXG | S_IRWXO)) + { + pgagroal_log_error("TLS private key file must have 0600 permissions when owned by a non-root user: %s", config->common.tls_key_file); + goto error; + } + } + else if (st.st_uid == 0) + { + if (st.st_mode & (S_IWGRP | S_IXGRP | S_IRWXO)) + { + pgagroal_log_error("TLS private key file must have at least 0640 permissions when owned by root: %s", config->common.tls_key_file); + goto error; + } + + } + else + { + pgagroal_log_error("TLS private key file not owned by user or root: %s", config->common.tls_key_file); + goto error; + } + + if (strlen(config->common.tls_ca_file) > 0) + { + memset(&st, 0, sizeof(struct stat)); + + if (stat(config->common.tls_ca_file, &st) == -1) + { + pgagroal_log_error("Can't locate TLS CA file: %s", config->common.tls_ca_file); + goto error; + } + + if (!S_ISREG(st.st_mode)) + { + pgagroal_log_error("TLS CA file is not a regular file: %s", config->common.tls_ca_file); + goto error; + } + + if (st.st_uid && st.st_uid != geteuid()) + { + pgagroal_log_error("TLS CA file not owned by user or root: %s", config->common.tls_ca_file); + goto error; + } + } + else + { + pgagroal_log_debug("No TLS CA file"); + } + } + + return 0; + +error: + + return 1; +} + +static int +sasl_prep(char* password, char** password_prep) +{ + char* p = NULL; + + /* Only support ASCII for now */ + for (int i = 0; i < strlen(password); i++) + { + if ((unsigned char)(*(password + i)) & 0x80) + { + goto error; + } + } + + p = strdup(password); + + *password_prep = p; + + return 0; + +error: + + *password_prep = NULL; + + return 1; +} + +static int +generate_nounce(char** nounce) +{ + size_t s = 18; + unsigned char r[s + 1]; + char* base = NULL; + size_t base_length; + int result; + + memset(&r[0], 0, sizeof(r)); + + result = RAND_bytes(r, sizeof(r)); + if (result != 1) + { + goto error; + } + + r[s] = '\0'; + + pgagroal_base64_encode((char*)&r[0], s, &base, &base_length); + + *nounce = base; + + return 0; + +error: + + return 1; +} + +static int +get_scram_attribute(char attribute, char* input, size_t size, char** value) +{ + char* dup = NULL; + char* result = NULL; + char* ptr = NULL; + size_t token_size; + char match[2]; + + match[0] = attribute; + match[1] = '='; + + dup = (char*)calloc(1, size + 1); + memcpy(dup, input, size); + + ptr = strtok(dup, ","); + while (ptr != NULL) + { + if (!strncmp(ptr, &match[0], 2)) + { + token_size = strlen(ptr) - 1; + result = calloc(1, token_size); + memcpy(result, ptr + 2, token_size); + goto done; + } + + ptr = strtok(NULL, ","); + } + + if (result == NULL) + { + goto error; + } + +done: + + *value = result; + + free(dup); + + return 0; + +error: + + *value = NULL; + + free(dup); + + return 1; +} + +static int +client_proof(char* password, char* salt, int salt_length, int iterations, + char* client_first_message_bare, size_t client_first_message_bare_length, + char* server_first_message, size_t server_first_message_length, + char* client_final_message_wo_proof, size_t client_final_message_wo_proof_length, + unsigned char** result, int* result_length) +{ + size_t size = 32; + unsigned char* s_p = NULL; + int s_p_length; + unsigned char* c_k = NULL; + int c_k_length; + unsigned char* s_k = NULL; + int s_k_length; + unsigned char* c_s = NULL; + unsigned int length; + unsigned char* r = NULL; + HMAC_CTX* ctx = HMAC_CTX_new(); + + if (salted_password(password, salt, salt_length, iterations, &s_p, &s_p_length)) + { + goto error; + } + + if (salted_password_key(s_p, s_p_length, "Client Key", &c_k, &c_k_length)) + { + goto error; + } + + if (stored_key(c_k, c_k_length, &s_k, &s_k_length)) + { + goto error; + } + + c_s = calloc(1, size); + + r = calloc(1, size); + + /* Client signature: HMAC(StoredKey, AuthMessage) */ + if (HMAC_Init_ex(ctx, s_k, s_k_length, EVP_sha256(), NULL) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, (unsigned char*)client_first_message_bare, client_first_message_bare_length) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, (unsigned char*)",", 1) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, (unsigned char*)server_first_message, server_first_message_length) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, (unsigned char*)",", 1) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, (unsigned char*)client_final_message_wo_proof, client_final_message_wo_proof_length) != 1) + { + goto error; + } + + if (HMAC_Final(ctx, c_s, &length) != 1) + { + goto error; + } + + /* ClientProof: ClientKey XOR ClientSignature */ + for (int i = 0; i < size; i++) + { + *(r + i) = *(c_k + i) ^ *(c_s + i); + } + + *result = r; + *result_length = size; + + HMAC_CTX_free(ctx); + + free(s_p); + free(c_k); + free(s_k); + free(c_s); + + return 0; + +error: + + *result = NULL; + *result_length = 0; + + if (ctx != NULL) + { + HMAC_CTX_free(ctx); + } + + free(s_p); + free(c_k); + free(s_k); + free(c_s); + + return 1; +} + +static int +verify_client_proof(char* s_key, int s_key_length, + char* client_proof, int client_proof_length, + char* salt, int salt_length, int iterations, + char* client_first_message_bare, size_t client_first_message_bare_length, + char* server_first_message, size_t server_first_message_length, + char* client_final_message_wo_proof, size_t client_final_message_wo_proof_length) +{ + size_t size = 32; + unsigned char* c_k = NULL; + int c_k_length; + unsigned char* s_k = NULL; + int s_k_length; + unsigned char* c_s = NULL; + unsigned int length; + HMAC_CTX* ctx = HMAC_CTX_new(); + + c_k = calloc(1, size); + c_k_length = size; + + c_s = calloc(1, size); + + /* Client signature: HMAC(StoredKey, AuthMessage) */ + if (HMAC_Init_ex(ctx, s_key, s_key_length, EVP_sha256(), NULL) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, (unsigned char*)client_first_message_bare, client_first_message_bare_length) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, (unsigned char*)",", 1) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, (unsigned char*)server_first_message, server_first_message_length) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, (unsigned char*)",", 1) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, (unsigned char*)client_final_message_wo_proof, client_final_message_wo_proof_length) != 1) + { + goto error; + } + + if (HMAC_Final(ctx, c_s, &length) != 1) + { + goto error; + } + + /* ClientKey: ClientProof XOR ClientSignature */ + for (int i = 0; i < size; i++) + { + *(c_k + i) = *(client_proof + i) ^ *(c_s + i); + } + + if (stored_key(c_k, c_k_length, &s_k, &s_k_length)) + { + goto error; + } + + if (memcmp(s_key, s_k, size) != 0) + { + goto error; + } + + HMAC_CTX_free(ctx); + + free(c_k); + free(s_k); + free(c_s); + + return 0; + +error: + + if (ctx != NULL) + { + HMAC_CTX_free(ctx); + } + + free(c_k); + free(s_k); + free(c_s); + + return 1; +} + +static int +salted_password(char* password, char* salt, int salt_length, int iterations, unsigned char** result, int* result_length) +{ + size_t size = 32; + int password_length; + unsigned int one; + unsigned char Ui[size]; + unsigned char Ui_prev[size]; + unsigned int Ui_length; + unsigned char* r = NULL; + HMAC_CTX* ctx = HMAC_CTX_new(); + + if (ctx == NULL) + { + goto error; + } + + password_length = strlen(password); + + if (!pgagroal_bigendian()) + { + one = pgagroal_swap(1); + } + else + { + one = 1; + } + + r = calloc(1, size); + + /* SaltedPassword: Hi(Normalize(password), salt, iterations) */ + if (HMAC_Init_ex(ctx, password, password_length, EVP_sha256(), NULL) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, (unsigned char*)salt, salt_length) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, (unsigned char*)&one, sizeof(one)) != 1) + { + goto error; + } + + if (HMAC_Final(ctx, &Ui_prev[0], &Ui_length) != 1) + { + goto error; + } + memcpy(r, &Ui_prev[0], size); + + for (int i = 2; i <= iterations; i++) + { + if (HMAC_CTX_reset(ctx) != 1) + { + goto error; + } + + if (HMAC_Init_ex(ctx, password, password_length, EVP_sha256(), NULL) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, &Ui_prev[0], size) != 1) + { + goto error; + } + + if (HMAC_Final(ctx, &Ui[0], &Ui_length) != 1) + { + goto error; + } + + for (int j = 0; j < size; j++) + { + *(r + j) ^= *(Ui + j); + } + memcpy(&Ui_prev[0], &Ui[0], size); + } + + *result = r; + *result_length = size; + + HMAC_CTX_free(ctx); + + return 0; + +error: + + if (ctx != NULL) + { + HMAC_CTX_free(ctx); + } + + *result = NULL; + *result_length = 0; + + return 1; +} + +static int +salted_password_key(unsigned char* salted_password, int salted_password_length, char* key, unsigned char** result, int* result_length) +{ + size_t size = 32; + unsigned char* r = NULL; + unsigned int length; + HMAC_CTX* ctx = HMAC_CTX_new(); + + if (ctx == NULL) + { + goto error; + } + + r = calloc(1, size); + + /* HMAC(SaltedPassword, Key) */ + if (HMAC_Init_ex(ctx, salted_password, salted_password_length, EVP_sha256(), NULL) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, (unsigned char*)key, strlen(key)) != 1) + { + goto error; + } + + if (HMAC_Final(ctx, r, &length) != 1) + { + goto error; + } + + *result = r; + *result_length = size; + + HMAC_CTX_free(ctx); + + return 0; + +error: + + if (ctx != NULL) + { + HMAC_CTX_free(ctx); + } + + *result = NULL; + *result_length = 0; + + return 1; +} + +static int +stored_key(unsigned char* client_key, int client_key_length, unsigned char** result, int* result_length) +{ + size_t size = 32; + unsigned char* r = NULL; + unsigned int length; + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + + if (ctx == NULL) + { + goto error; + } + + r = calloc(1, size); + + /* StoredKey: H(ClientKey) */ + if (EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1) + { + goto error; + } + + if (EVP_DigestUpdate(ctx, client_key, client_key_length) != 1) + { + goto error; + } + + if (EVP_DigestFinal_ex(ctx, r, &length) != 1) + { + goto error; + } + + *result = r; + *result_length = size; + + EVP_MD_CTX_free(ctx); + + return 0; + +error: + + if (ctx != NULL) + { + EVP_MD_CTX_free(ctx); + } + + *result = NULL; + *result_length = 0; + + return 1; +} + +static int +generate_salt(char** salt, int* size) +{ + size_t s = 16; + unsigned char* r = NULL; + int result; + + r = calloc(1, s); + + result = RAND_bytes(r, s); + if (result != 1) + { + goto error; + } + + *salt = (char*)r; + *size = s; + + return 0; + +error: + + free(r); + + *salt = NULL; + *size = 0; + + return 1; +} + +static int +server_signature(char* password, char* salt, int salt_length, int iterations, + char* s_key, int s_key_length, + char* client_first_message_bare, size_t client_first_message_bare_length, + char* server_first_message, size_t server_first_message_length, + char* client_final_message_wo_proof, size_t client_final_message_wo_proof_length, + unsigned char** result, size_t* result_length) +{ + size_t size = 32; + unsigned char* r = NULL; + unsigned char* s_p = NULL; + int s_p_length; + unsigned char* s_k = NULL; + int s_k_length; + unsigned int length; + bool do_free = true; + HMAC_CTX* ctx = HMAC_CTX_new(); + + if (ctx == NULL) + { + goto error; + } + + r = calloc(1, size); + + if (password != NULL) + { + if (salted_password(password, salt, salt_length, iterations, &s_p, &s_p_length)) + { + goto error; + } + + if (salted_password_key(s_p, s_p_length, "Server Key", &s_k, &s_k_length)) + { + goto error; + } + } + else + { + do_free = false; + s_k = (unsigned char*)s_key; + s_k_length = s_key_length; + } + + /* Server signature: HMAC(ServerKey, AuthMessage) */ + if (HMAC_Init_ex(ctx, s_k, s_k_length, EVP_sha256(), NULL) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, (unsigned char*)client_first_message_bare, client_first_message_bare_length) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, (unsigned char*)",", 1) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, (unsigned char*)server_first_message, server_first_message_length) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, (unsigned char*)",", 1) != 1) + { + goto error; + } + + if (HMAC_Update(ctx, (unsigned char*)client_final_message_wo_proof, client_final_message_wo_proof_length) != 1) + { + goto error; + } + + if (HMAC_Final(ctx, r, &length) != 1) + { + goto error; + } + + *result = r; + *result_length = length; + + HMAC_CTX_free(ctx); + + free(s_p); + if (do_free) + { + free(s_k); + } + + return 0; + +error: + + *result = NULL; + *result_length = 0; + + if (ctx != NULL) + { + HMAC_CTX_free(ctx); + } + + free(s_p); + if (do_free) + { + free(s_k); + } + + return 1; +} + +static bool +is_tls_user(char* username, char* database) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + for (int i = 0; i < config->number_of_hbas; i++) + { + if ((!strcmp(database, config->hbas[i].database) || !strcmp("all", config->hbas[i].database)) && + (!strcmp(username, config->hbas[i].username) || !strcmp("all", config->hbas[i].username))) + { + if (!strcmp("hostssl", config->hbas[i].type)) + { + return true; + } + } + } + + return false; +} + +static int +create_ssl_ctx(bool client, SSL_CTX** ctx) +{ + SSL_CTX* c = NULL; + + if (client) + { + c = SSL_CTX_new(TLS_client_method()); + } + else + { + c = SSL_CTX_new(TLS_server_method()); + } + + if (c == NULL) + { + goto error; + } + + if (SSL_CTX_set_min_proto_version(c, TLS1_2_VERSION) == 0) + { + goto error; + } + + SSL_CTX_set_mode(c, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); + SSL_CTX_set_options(c, SSL_OP_NO_TICKET); + SSL_CTX_set_session_cache_mode(c, SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL_STORE); + + *ctx = c; + + return 0; + +error: + + if (c != NULL) + { + SSL_CTX_free(c); + } + + return 1; +} + +static int +create_ssl_client(SSL_CTX* ctx, char* key, char* cert, char* root, int socket, SSL** ssl) +{ + SSL* s = NULL; + bool have_cert = false; + bool have_rootcert = false; + + if (root != NULL && strlen(root) > 0) + { + if (SSL_CTX_load_verify_locations(ctx, root, NULL) != 1) + { + unsigned long err; + + err = ERR_get_error(); + pgagroal_log_error("Couldn't load TLS CA: %s", root); + pgagroal_log_error("Reason: %s", ERR_reason_error_string(err)); + goto error; + } + + have_rootcert = true; + } + + if (cert != NULL && strlen(cert) > 0) + { + if (SSL_CTX_use_certificate_chain_file(ctx, cert) != 1) + { + unsigned long err; + + err = ERR_get_error(); + pgagroal_log_error("Couldn't load TLS certificate: %s", cert); + pgagroal_log_error("Reason: %s", ERR_reason_error_string(err)); + goto error; + } + + have_cert = true; + } + + s = SSL_new(ctx); + + if (s == NULL) + { + goto error; + } + + if (SSL_set_fd(s, socket) == 0) + { + goto error; + } + + if (have_cert && key != NULL && strlen(key) > 0) + { + if (SSL_use_PrivateKey_file(s, key, SSL_FILETYPE_PEM) != 1) + { + unsigned long err; + + err = ERR_get_error(); + pgagroal_log_error("Couldn't load TLS private key: %s", key); + pgagroal_log_error("Reason: %s", ERR_reason_error_string(err)); + goto error; + } + + if (SSL_check_private_key(s) != 1) + { + unsigned long err; + + err = ERR_get_error(); + pgagroal_log_error("TLS private key check failed: %s", key); + pgagroal_log_error("Reason: %s", ERR_reason_error_string(err)); + goto error; + } + } + + if (have_rootcert) + { + SSL_set_verify(s, SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, NULL); + } + + *ssl = s; + + return 0; + +error: + + if (s != NULL) + { + SSL_shutdown(s); + SSL_free(s); + } + SSL_CTX_free(ctx); + + return 1; +} + +static int +create_ssl_server(SSL_CTX* ctx, int socket, SSL** ssl) +{ + SSL* s = NULL; + STACK_OF(X509_NAME) * root_cert_list = NULL; + struct configuration* config; + + config = (struct configuration*)shmem; + + if (strlen(config->tls_cert_file) == 0) + { + pgagroal_log_error("No TLS certificate defined"); + goto error; + } + + if (strlen(config->tls_key_file) == 0) + { + pgagroal_log_error("No TLS private key defined"); + goto error; + } + + if (SSL_CTX_use_certificate_chain_file(ctx, config->tls_cert_file) != 1) + { + unsigned long err; + + err = ERR_get_error(); + pgagroal_log_error("Couldn't load TLS certificate: %s", config->tls_cert_file); + pgagroal_log_error("Reason: %s", ERR_reason_error_string(err)); + goto error; + } + + if (SSL_CTX_use_PrivateKey_file(ctx, config->tls_key_file, SSL_FILETYPE_PEM) != 1) + { + unsigned long err; + + err = ERR_get_error(); + pgagroal_log_error("Couldn't load TLS private key: %s", config->tls_key_file); + pgagroal_log_error("Reason: %s", ERR_reason_error_string(err)); + goto error; + } + + if (SSL_CTX_check_private_key(ctx) != 1) + { + unsigned long err; + + err = ERR_get_error(); + pgagroal_log_error("TLS private key check failed: %s", config->tls_key_file); + pgagroal_log_error("Reason: %s", ERR_reason_error_string(err)); + goto error; + } + + if (strlen(config->tls_ca_file) > 0) + { + if (SSL_CTX_load_verify_locations(ctx, config->tls_ca_file, NULL) != 1) + { + unsigned long err; + + err = ERR_get_error(); + pgagroal_log_error("Couldn't load TLS CA: %s", config->tls_ca_file); + pgagroal_log_error("Reason: %s", ERR_reason_error_string(err)); + goto error; + } + + root_cert_list = SSL_load_client_CA_file(config->tls_ca_file); + if (root_cert_list == NULL) + { + unsigned long err; + + err = ERR_get_error(); + pgagroal_log_error("Couldn't load TLS CA: %s", config->tls_ca_file); + pgagroal_log_error("Reason: %s", ERR_reason_error_string(err)); + goto error; + } + + SSL_CTX_set_verify(ctx, (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT | SSL_VERIFY_CLIENT_ONCE), NULL); + SSL_CTX_set_client_CA_list(ctx, root_cert_list); + } + + s = SSL_new(ctx); + + if (s == NULL) + { + goto error; + } + + if (SSL_set_fd(s, socket) == 0) + { + goto error; + } + + *ssl = s; + + return 0; + +error: + + if (s != NULL) + { + SSL_shutdown(s); + SSL_free(s); + } + SSL_CTX_free(ctx); + + return 1; +} + +static int +auth_query(SSL* c_ssl, int client_fd, int slot, char* username, char* database, int hba_method) +{ + int su_socket; + SSL* su_ssl = NULL; + char* shadow = NULL; + int ret; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + /* Get connection to server using the superuser */ + ret = auth_query_get_connection(config->superuser.username, config->superuser.password, database, &su_socket, &su_ssl); + if (ret == AUTH_BAD_PASSWORD) + { + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + atomic_store(&config->su_connection, STATE_FREE); + goto bad_password; + } + else if (ret == AUTH_ERROR) + { + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + atomic_store(&config->su_connection, STATE_FREE); + goto error; + } + else if (ret == AUTH_TIMEOUT) + { + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + atomic_store(&config->su_connection, STATE_FREE); + goto error; + } + + /* Call pgagroal_get_password */ + if (auth_query_get_password(su_socket, su_ssl, username, database, &shadow)) + { + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + atomic_store(&config->su_connection, STATE_FREE); + goto error; + } + + /* Close connection */ + pgagroal_disconnect(su_socket); + atomic_store(&config->su_connection, STATE_FREE); + + /* Client security */ + if (config->connections[slot].has_security == SECURITY_MD5) + { + ret = auth_query_client_md5(c_ssl, client_fd, username, shadow, slot); + if (ret == AUTH_BAD_PASSWORD) + { + pgagroal_write_bad_password(c_ssl, client_fd, username); + pgagroal_write_empty(c_ssl, client_fd); + goto bad_password; + } + else if (ret == AUTH_ERROR) + { + goto error; + } + } + else if (config->connections[slot].has_security == SECURITY_SCRAM256) + { + ret = auth_query_client_scram256(c_ssl, client_fd, username, shadow, slot); + if (ret == AUTH_BAD_PASSWORD) + { + pgagroal_write_bad_password(c_ssl, client_fd, username); + pgagroal_write_empty(c_ssl, client_fd); + goto bad_password; + } + else if (ret == AUTH_ERROR) + { + goto error; + } + } + else + { + pgagroal_log_error("Authentication query not supported: %d", config->connections[slot].has_security); + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + goto error; + } + + /* Client ok */ + if (client_ok(c_ssl, client_fd, slot)) + { + pgagroal_write_connection_refused(c_ssl, client_fd); + pgagroal_write_empty(c_ssl, client_fd); + goto error; + } + + free(shadow); + + return AUTH_SUCCESS; + +bad_password: + + free(shadow); + + return AUTH_BAD_PASSWORD; + +error: + + free(shadow); + + return AUTH_ERROR; +} + +static int +auth_query_get_connection(char* username, char* password, char* database, int* server_fd, SSL** server_ssl) +{ + int auth_type = -1; + int server; + signed char isfree; + time_t start_time; + char* error = NULL; + struct main_configuration* config = NULL; + struct message* startup_msg = NULL; + struct message* startup_response_msg = NULL; + struct message* msg = NULL; + int ret = -1; + int status = -1; + + config = (struct main_configuration*)shmem; + + *server_fd = -1; + + pgagroal_prometheus_connection_awaiting(-1); + + /* We need to find the server for the connection */ + if (pgagroal_get_primary(&server)) + { + pgagroal_log_error("pgagroal: No valid server available"); + goto error; + } + pgagroal_log_debug("connect: server %d", server); + + start_time = time(NULL); + +retry: + + isfree = STATE_FREE; + + if (atomic_compare_exchange_strong(&config->su_connection, &isfree, STATE_IN_USE)) + { + if (config->servers[server].host[0] == '/') + { + char pgsql[MISC_LENGTH]; + + memset(&pgsql, 0, sizeof(pgsql)); + snprintf(&pgsql[0], sizeof(pgsql), ".s.PGSQL.%d", config->servers[server].port); + ret = pgagroal_connect_unix_socket(config->servers[server].host, &pgsql[0], server_fd); + } + else + { + ret = pgagroal_connect(config->servers[server].host, config->servers[server].port, server_fd, config->keep_alive, config->non_blocking, config->nodelay); + } + + if (ret) + { + pgagroal_log_error("pgagroal: No connection to %s:%d", config->servers[server].host, config->servers[server].port); + atomic_store(&config->su_connection, STATE_FREE); + goto error; + } + } + else + { + if (config->blocking_timeout > 0) + { + + /* Sleep for 100ms */ + SLEEP(100000000L) + + double diff = difftime(time(NULL), start_time); + if (diff >= (double)config->blocking_timeout) + { + goto timeout; + } + + goto retry; + } + else + { + goto timeout; + } + } + + pgagroal_log_debug("connect: %s:%d using fd %d", config->servers[server].host, config->servers[server].port, *server_fd); + + /* TLS support */ + establish_client_tls_connection(server, *server_fd, server_ssl); + + /* Startup message */ + status = pgagroal_create_startup_message(username, database, &startup_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_write_message(*server_ssl, *server_fd, startup_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_read_block_message(*server_ssl, *server_fd, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + startup_response_msg = pgagroal_copy_message(msg); + + get_auth_type(msg, &auth_type); + pgagroal_log_trace("auth_query_get_connection: auth type %d", auth_type); + + /* Supported security models: */ + /* md5 (5) */ + /* scram256 (10) */ + if (auth_type == SECURITY_MD5) + { + ret = auth_query_server_md5(startup_response_msg, username, password, *server_fd, *server_ssl); + if (ret == AUTH_BAD_PASSWORD) + { + goto bad_password; + } + else if (ret == AUTH_ERROR) + { + goto error; + } + } + else if (auth_type == SECURITY_SCRAM256) + { + ret = auth_query_server_scram256(username, password, *server_fd, *server_ssl); + if (ret == AUTH_BAD_PASSWORD) + { + goto bad_password; + } + else if (ret == AUTH_ERROR) + { + goto error; + } + } + else + { + if (msg->kind == 'E') + { + if (pgagroal_extract_error_message(msg, &error)) + { + goto error; + } + pgagroal_log_error("%s", error); + } + + goto error; + } + + free(error); + + pgagroal_prometheus_connection_unawaiting(-1); + + pgagroal_free_copy_message(startup_msg); + pgagroal_free_copy_message(startup_response_msg); + pgagroal_free_message(msg); + + return AUTH_SUCCESS; + +bad_password: + pgagroal_prometheus_connection_unawaiting(-1); + pgagroal_log_debug("auth_query_get_connection: BAD_PASSWORD"); + + if (*server_fd != -1) + { + pgagroal_disconnect(*server_fd); + } + + *server_fd = -1; + + free(error); + + pgagroal_free_copy_message(startup_msg); + pgagroal_free_copy_message(startup_response_msg); + pgagroal_free_message(msg); + + return AUTH_BAD_PASSWORD; + +error: + pgagroal_prometheus_connection_unawaiting(-1); + pgagroal_log_debug("auth_query_get_connection: ERROR (%d)", auth_type); + + if (*server_fd != -1) + { + pgagroal_disconnect(*server_fd); + } + + *server_fd = -1; + + free(error); + + pgagroal_free_copy_message(startup_msg); + pgagroal_free_copy_message(startup_response_msg); + pgagroal_free_message(msg); + + return AUTH_ERROR; + +timeout: + pgagroal_prometheus_connection_unawaiting(-1); + + pgagroal_log_debug("auth_query_get_connection: TIMEOUT"); + + *server_fd = -1; + + free(error); + + pgagroal_free_copy_message(startup_msg); + pgagroal_free_copy_message(startup_response_msg); + pgagroal_free_message(msg); + + return AUTH_TIMEOUT; +} + +static int +auth_query_server_md5(struct message* startup_response_msg, char* username, char* password, int socket, SSL* server_ssl) +{ + int status = MESSAGE_STATUS_ERROR; + int auth_response = -1; + size_t size; + char* pwdusr = NULL; + char* shadow = NULL; + char* md5_req = NULL; + char* md5 = NULL; + char md5str[36]; + char* salt = NULL; + struct message* auth_msg = NULL; + struct message* md5_msg = NULL; + + pgagroal_log_trace("auth_query_server_md5"); + + if (get_salt(startup_response_msg->data, &salt)) + { + goto error; + } + + size = strlen(username) + strlen(password) + 1; + pwdusr = calloc(1, size); + + snprintf(pwdusr, size, "%s%s", password, username); + + if (pgagroal_md5(pwdusr, strlen(pwdusr), &shadow)) + { + goto error; + } + + md5_req = calloc(1, 36); + memcpy(md5_req, shadow, 32); + memcpy(md5_req + 32, salt, 4); + + if (pgagroal_md5(md5_req, 36, &md5)) + { + goto error; + } + + memset(&md5str, 0, sizeof(md5str)); + snprintf(&md5str[0], 36, "md5%s", md5); + + status = pgagroal_create_auth_md5_response(md5str, &md5_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_write_message(server_ssl, socket, md5_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_read_block_message(server_ssl, socket, &auth_msg); + if (auth_msg->length > SECURITY_BUFFER_SIZE) + { + pgagroal_log_message(auth_msg); + pgagroal_log_error("Security message too large: %ld", auth_msg->length); + goto error; + } + + get_auth_type(auth_msg, &auth_response); + pgagroal_log_trace("authenticate: auth response %d", auth_response); + + if (auth_response == 0) + { + if (auth_msg->length > SECURITY_BUFFER_SIZE) + { + pgagroal_log_message(auth_msg); + pgagroal_log_error("Security message too large: %ld", auth_msg->length); + goto error; + } + } + else + { + goto bad_password; + } + + free(pwdusr); + free(shadow); + free(md5_req); + free(md5); + free(salt); + + pgagroal_free_copy_message(md5_msg); + pgagroal_free_message(auth_msg); + + return AUTH_SUCCESS; + +bad_password: + + pgagroal_log_warn("Wrong password for user: %s", username); + + free(pwdusr); + free(shadow); + free(md5_req); + free(md5); + free(salt); + + pgagroal_free_copy_message(md5_msg); + pgagroal_free_message(auth_msg); + + return AUTH_BAD_PASSWORD; + +error: + + free(pwdusr); + free(shadow); + free(md5_req); + free(md5); + free(salt); + + pgagroal_free_copy_message(md5_msg); + pgagroal_free_message(auth_msg); + + return AUTH_ERROR; +} + +static int +auth_query_server_scram256(char* username, char* password, int socket, SSL* server_ssl) +{ + int status = MESSAGE_STATUS_ERROR; + char* salt = NULL; + size_t salt_length = 0; + char* password_prep = NULL; + char* client_nounce = NULL; + char* combined_nounce = NULL; + char* base64_salt = NULL; + char* iteration_string = NULL; + char* err = NULL; + int iteration; + char* client_first_message_bare = NULL; + char* server_first_message = NULL; + char wo_proof[58]; + unsigned char* proof = NULL; + int proof_length; + char* proof_base = NULL; + size_t proof_base_length; + char* base64_server_signature = NULL; + char* server_signature_received = NULL; + size_t server_signature_received_length; + unsigned char* server_signature_calc = NULL; + size_t server_signature_calc_length; + char* error = NULL; + struct message* sasl_response = NULL; + struct message* sasl_continue = NULL; + struct message* sasl_continue_response = NULL; + struct message* sasl_final = NULL; + struct message* msg = NULL; + + pgagroal_log_trace("auth_query_server_scram256"); + + status = sasl_prep(password, &password_prep); + if (status) + { + goto error; + } + + generate_nounce(&client_nounce); + + status = pgagroal_create_auth_scram256_response(client_nounce, &sasl_response); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_write_message(server_ssl, socket, sasl_response); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_read_block_message(server_ssl, socket, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + sasl_continue = pgagroal_copy_message(msg); + + get_scram_attribute('r', (char*)(sasl_continue->data + 9), sasl_continue->length - 9, &combined_nounce); + get_scram_attribute('s', (char*)(sasl_continue->data + 9), sasl_continue->length - 9, &base64_salt); + get_scram_attribute('i', (char*)(sasl_continue->data + 9), sasl_continue->length - 9, &iteration_string); + get_scram_attribute('e', (char*)(sasl_continue->data + 9), sasl_continue->length - 9, &err); + + if (err != NULL) + { + pgagroal_log_error("SCRAM-SHA-256: %s", err); + goto error; + } + + pgagroal_base64_decode(base64_salt, strlen(base64_salt), (void**)&salt, &salt_length); + + iteration = atoi(iteration_string); + + memset(&wo_proof[0], 0, sizeof(wo_proof)); + snprintf(&wo_proof[0], sizeof(wo_proof), "c=biws,r=%s", combined_nounce); + + /* n=,r=... */ + client_first_message_bare = sasl_response->data + 26; + + /* r=...,s=...,i=4096 */ + server_first_message = sasl_continue->data + 9; + + if (client_proof(password_prep, salt, salt_length, iteration, + client_first_message_bare, sasl_response->length - 26, + server_first_message, sasl_continue->length - 9, + &wo_proof[0], strlen(wo_proof), + &proof, &proof_length)) + { + goto error; + } + + pgagroal_base64_encode((char*)proof, proof_length, &proof_base, &proof_base_length); + + status = pgagroal_create_auth_scram256_continue_response(&wo_proof[0], (char*)proof_base, &sasl_continue_response); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_write_message(server_ssl, socket, sasl_continue_response); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_read_block_message(server_ssl, socket, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (msg->kind == 'E') + { + pgagroal_extract_error_message(msg, &error); + if (error != NULL) + { + pgagroal_log_error("%s", error); + } + goto bad_password; + } + + if (pgagroal_extract_message('R', msg, &sasl_final)) + { + goto error; + } + + /* Get 'v' attribute */ + base64_server_signature = sasl_final->data + 11; + pgagroal_base64_decode(base64_server_signature, sasl_final->length - 11, + (void**)&server_signature_received, &server_signature_received_length); + + if (server_signature(password_prep, salt, salt_length, iteration, + NULL, 0, + client_first_message_bare, sasl_response->length - 26, + server_first_message, sasl_continue_response->length - 9, + &wo_proof[0], strlen(wo_proof), + &server_signature_calc, &server_signature_calc_length)) + { + goto error; + } + + if (server_signature_calc_length != server_signature_received_length || + memcmp(server_signature_received, server_signature_calc, server_signature_calc_length) != 0) + { + goto bad_password; + } + + free(error); + free(salt); + free(err); + free(password_prep); + free(client_nounce); + free(combined_nounce); + free(base64_salt); + free(iteration_string); + free(proof); + free(proof_base); + free(server_signature_received); + free(server_signature_calc); + + pgagroal_free_copy_message(sasl_response); + pgagroal_free_copy_message(sasl_continue); + pgagroal_free_copy_message(sasl_continue_response); + pgagroal_free_copy_message(sasl_final); + + return AUTH_SUCCESS; + +bad_password: + + pgagroal_log_warn("Wrong password for user: %s", username); + + free(error); + free(salt); + free(err); + free(password_prep); + free(client_nounce); + free(combined_nounce); + free(base64_salt); + free(iteration_string); + free(proof); + free(proof_base); + free(server_signature_received); + free(server_signature_calc); + + pgagroal_free_copy_message(sasl_response); + pgagroal_free_copy_message(sasl_continue); + pgagroal_free_copy_message(sasl_continue_response); + pgagroal_free_copy_message(sasl_final); + + return AUTH_BAD_PASSWORD; + +error: + + free(error); + free(salt); + free(err); + free(password_prep); + free(client_nounce); + free(combined_nounce); + free(base64_salt); + free(iteration_string); + free(proof); + free(proof_base); + free(server_signature_received); + free(server_signature_calc); + + pgagroal_free_copy_message(sasl_response); + pgagroal_free_copy_message(sasl_continue); + pgagroal_free_copy_message(sasl_continue_response); + pgagroal_free_copy_message(sasl_final); + + return AUTH_ERROR; +} + +static int +auth_query_get_password(int socket, SSL* server_ssl, char* username, char* database, char** password) +{ + int status; + size_t size; + char* aq = NULL; + size_t result_size; + char* result = NULL; + struct message qmsg; + struct message* tmsg = NULL; + struct message* dmsg = NULL; + + *password = NULL; + + size = 53 + strlen(username); + aq = calloc(1, size); + + memset(&qmsg, 0, sizeof(struct message)); + + pgagroal_write_byte(aq, 'Q'); + pgagroal_write_int32(aq + 1, size - 1); + pgagroal_write_string(aq + 5, "SELECT * FROM public.pgagroal_get_password(\'"); + pgagroal_write_string(aq + 49, username); + pgagroal_write_string(aq + 49 + strlen(username), "\');"); + + qmsg.kind = 'Q'; + qmsg.length = size; + qmsg.data = aq; + + status = pgagroal_write_message(server_ssl, socket, &qmsg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_read_block_message(server_ssl, socket, &tmsg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (pgagroal_extract_message('D', tmsg, &dmsg)) + { + goto error; + } + + result_size = dmsg->length - 11 + 1; + result = (char*)calloc(1, result_size); + memcpy(result, dmsg->data + 11, dmsg->length - 11); + + *password = result; + + free(aq); + pgagroal_free_message(tmsg); + pgagroal_free_copy_message(dmsg); + + return 0; + +error: + pgagroal_log_trace("auth_query_get_password: socket (%d) status (%d)", socket, status); + + if (tmsg->kind == 'E') + { + char* error = NULL; + + if (pgagroal_extract_error_message(tmsg, &error)) + { + goto error; + } + + pgagroal_log_error("%s in %s", error, database); + free(error); + } + + free(aq); + pgagroal_free_message(tmsg); + pgagroal_free_copy_message(dmsg); + + return 1; +} + +static int +auth_query_client_md5(SSL* c_ssl, int client_fd, char* username, char* hash, int slot) +{ + int status; + char salt[4]; + time_t start_time; + bool non_blocking; + char* md5_req = NULL; + char* md5 = NULL; + struct main_configuration* config; + struct message* msg = NULL; + + config = (struct main_configuration*)shmem; + + salt[0] = (char)(random() & 0xFF); + salt[1] = (char)(random() & 0xFF); + salt[2] = (char)(random() & 0xFF); + salt[3] = (char)(random() & 0xFF); + + status = pgagroal_write_auth_md5(c_ssl, client_fd, salt); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + start_time = time(NULL); + + non_blocking = pgagroal_socket_is_nonblocking(client_fd); + pgagroal_socket_nonblocking(client_fd, true); + + /* psql may just close the connection without word, so loop */ +retry: + status = pgagroal_read_timeout_message(c_ssl, client_fd, 1, &msg); + if (status != MESSAGE_STATUS_OK) + { + if (difftime(time(NULL), start_time) < config->common.authentication_timeout) + { + if (pgagroal_socket_isvalid(client_fd)) + /* Sleep for 100ms */ + { + SLEEP_AND_GOTO(100000000L, retry) + } + + } + } + + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (!non_blocking) + { + pgagroal_socket_nonblocking(client_fd, false); + } + + md5_req = calloc(1, 36); + memcpy(md5_req, hash + 3, 32); + memcpy(md5_req + 32, &salt[0], 4); + + if (pgagroal_md5(md5_req, 36, &md5)) + { + goto error; + } + + if (strcmp(pgagroal_read_string(msg->data + 8), md5)) + { + pgagroal_write_bad_password(c_ssl, client_fd, username); + + goto bad_password; + } + + pgagroal_free_message(msg); + + free(md5_req); + free(md5); + + return AUTH_SUCCESS; + +bad_password: + + pgagroal_free_message(msg); + + free(md5_req); + free(md5); + + return AUTH_BAD_PASSWORD; + +error: + + pgagroal_free_message(msg); + + free(md5_req); + free(md5); + + return AUTH_ERROR; +} + +static int +auth_query_client_scram256(SSL* c_ssl, int client_fd, char* username, char* shadow, int slot) +{ + int status; + time_t start_time; + bool non_blocking; + char* scram256 = NULL; + char* s1 = NULL; + char* s2 = NULL; + char* s_iterations = NULL; + char* base64_stored_key = NULL; + char* base64_server_key = NULL; + int iterations = 4096; + char* stored_key = NULL; + size_t stored_key_length = 0; + char* server_key = NULL; + size_t server_key_length = 0; + char* client_first_message_bare = NULL; + char* server_first_message = NULL; + char* client_final_message_without_proof = NULL; + char* client_nounce = NULL; + char* server_nounce = NULL; + char* salt = NULL; + size_t salt_length = 0; + char* base64_salt = NULL; + char* base64_client_proof = NULL; + char* client_proof_received = NULL; + size_t client_proof_received_length = 0; + unsigned char* server_signature_calc = NULL; + size_t server_signature_calc_length = 0; + char* base64_server_signature_calc = NULL; + size_t base64_server_signature_calc_length; + struct main_configuration* config; + struct message* msg = NULL; + struct message* sasl_continue = NULL; + struct message* sasl_final = NULL; + + pgagroal_log_debug("auth_query_client_scram256 %d %d", client_fd, slot); + + config = (struct main_configuration*)shmem; + + status = pgagroal_write_auth_scram256(c_ssl, client_fd); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + start_time = time(NULL); + + non_blocking = pgagroal_socket_is_nonblocking(client_fd); + pgagroal_socket_nonblocking(client_fd, true); + + /* psql may just close the connection without word, so loop */ +retry: + status = pgagroal_read_timeout_message(c_ssl, client_fd, 1, &msg); + if (status != MESSAGE_STATUS_OK) + { + if (difftime(time(NULL), start_time) < config->common.authentication_timeout) + { + if (pgagroal_socket_isvalid(client_fd)) + /* Sleep for 100ms */ + { + SLEEP_AND_GOTO(100000000L, retry) + } + + } + } + + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (!non_blocking) + { + pgagroal_socket_nonblocking(client_fd, false); + } + + /* Split shadow */ + scram256 = strtok(shadow, "$"); + s1 = strtok(NULL, "$"); + s2 = strtok(NULL, "$"); + + s_iterations = strtok(s1, ":"); + base64_salt = strtok(NULL, ":"); + + base64_stored_key = strtok(s2, ":"); + base64_server_key = strtok(NULL, ":"); + + if (strcmp("SCRAM-SHA-256", scram256) != 0) + { + goto error; + } + + /* Process shadow information */ + iterations = atoi(s_iterations); + if (pgagroal_base64_decode(base64_salt, strlen(base64_salt), (void**)&salt, &salt_length)) + { + goto error; + } + if (pgagroal_base64_decode(base64_stored_key, strlen(base64_stored_key), (void**)&stored_key, &stored_key_length)) + { + goto error; + } + if (pgagroal_base64_decode(base64_server_key, strlen(base64_server_key), (void**)&server_key, &server_key_length)) + { + goto error; + } + + /* Start the flow */ + client_first_message_bare = calloc(1, msg->length - 25); + memcpy(client_first_message_bare, msg->data + 26, msg->length - 26); + + get_scram_attribute('r', (char*)msg->data + 26, msg->length - 26, &client_nounce); + generate_nounce(&server_nounce); + + server_first_message = calloc(1, 89); + snprintf(server_first_message, 89, "r=%s%s,s=%s,i=%d", client_nounce, server_nounce, base64_salt, iterations); + + status = pgagroal_create_auth_scram256_continue(client_nounce, server_nounce, base64_salt, &sasl_continue); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_write_message(c_ssl, client_fd, sasl_continue); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_read_timeout_message(c_ssl, client_fd, config->common.authentication_timeout, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + get_scram_attribute('p', (char*)msg->data + 5, msg->length - 5, &base64_client_proof); + pgagroal_base64_decode(base64_client_proof, strlen(base64_client_proof), (void**)&client_proof_received, &client_proof_received_length); + + client_final_message_without_proof = calloc(1, 58); + memcpy(client_final_message_without_proof, msg->data + 5, 57); + + if (verify_client_proof(stored_key, stored_key_length, + client_proof_received, client_proof_received_length, + salt, salt_length, iterations, + client_first_message_bare, strlen(client_first_message_bare), + server_first_message, strlen(server_first_message), + client_final_message_without_proof, strlen(client_final_message_without_proof))) + { + goto bad_password; + } + + if (server_signature(NULL, salt, salt_length, iterations, + server_key, server_key_length, + client_first_message_bare, strlen(client_first_message_bare), + server_first_message, strlen(server_first_message), + client_final_message_without_proof, strlen(client_final_message_without_proof), + &server_signature_calc, &server_signature_calc_length)) + { + goto error; + } + + pgagroal_base64_encode((char*)server_signature_calc, server_signature_calc_length, &base64_server_signature_calc, &base64_server_signature_calc_length); + + status = pgagroal_create_auth_scram256_final(base64_server_signature_calc, &sasl_final); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_write_message(c_ssl, client_fd, sasl_final); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + pgagroal_log_debug("auth_query_client_scram256 success (%d)", slot); + + free(salt); + free(stored_key); + free(server_key); + free(client_first_message_bare); + free(server_first_message); + free(client_final_message_without_proof); + free(client_nounce); + free(server_nounce); + free(base64_client_proof); + free(client_proof_received); + free(server_signature_calc); + free(base64_server_signature_calc); + + pgagroal_free_copy_message(sasl_continue); + pgagroal_free_copy_message(sasl_final); + + return AUTH_SUCCESS; + +bad_password: + pgagroal_log_debug("auth_query_client_scram256 bad_password (%d)", slot); + + free(salt); + free(stored_key); + free(server_key); + free(client_first_message_bare); + free(server_first_message); + free(client_final_message_without_proof); + free(client_nounce); + free(server_nounce); + free(base64_client_proof); + free(client_proof_received); + free(server_signature_calc); + free(base64_server_signature_calc); + + pgagroal_free_copy_message(sasl_continue); + pgagroal_free_copy_message(sasl_final); + + return AUTH_BAD_PASSWORD; + +error: + pgagroal_log_debug("auth_query_client_scram256 error (%d)", slot); + + free(salt); + free(stored_key); + free(server_key); + free(client_first_message_bare); + free(server_first_message); + free(client_final_message_without_proof); + free(client_nounce); + free(server_nounce); + free(base64_client_proof); + free(client_proof_received); + free(server_signature_calc); + free(base64_server_signature_calc); + + pgagroal_free_copy_message(sasl_continue); + pgagroal_free_copy_message(sasl_final); + + return AUTH_ERROR; +} + +static int +establish_client_tls_connection(int server, int fd, SSL** ssl) +{ + struct main_configuration* config = NULL; + struct message* ssl_msg = NULL; + struct message* msg = NULL; + int status = -1; + + config = (struct main_configuration*)shmem; + + if (config->servers[server].tls) + { + status = pgagroal_create_ssl_message(&ssl_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_write_message(NULL, fd, ssl_msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_read_block_message(NULL, fd, &msg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + if (msg->kind == 'S') + { + create_client_tls_connection(fd, ssl, config->servers[server].tls_key_file, config->servers[server].tls_cert_file, config->servers[server].tls_ca_file); + } + } + + pgagroal_free_copy_message(ssl_msg); + pgagroal_free_message(msg); + + return AUTH_SUCCESS; + +error: + + pgagroal_free_copy_message(ssl_msg); + pgagroal_free_message(msg); + + return AUTH_ERROR; +} + +static int +create_client_tls_connection(int fd, SSL** ssl, char* tls_key_file, char* tls_cert_file, char* tls_ca_file) +{ + SSL_CTX* ctx = NULL; + SSL* s = NULL; + int status = -1; + + /* We are acting as a client against the server */ + if (create_ssl_ctx(true, &ctx)) + { + pgagroal_log_error("CTX failed"); + goto error; + } + + /* Create SSL structure */ + if (create_ssl_client(ctx, tls_key_file, tls_cert_file, tls_ca_file, fd, &s)) + { + pgagroal_log_error("Client failed"); + goto error; + } + + do + { + status = SSL_connect(s); + + if (status != 1) + { + int err = SSL_get_error(s, status); + switch (err) + { + case SSL_ERROR_ZERO_RETURN: + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_CONNECT: + case SSL_ERROR_WANT_ACCEPT: + case SSL_ERROR_WANT_X509_LOOKUP: +#ifndef HAVE_OPENBSD + case SSL_ERROR_WANT_ASYNC: + case SSL_ERROR_WANT_ASYNC_JOB: + case SSL_ERROR_WANT_CLIENT_HELLO_CB: +#endif + break; + case SSL_ERROR_SYSCALL: + pgagroal_log_error("SSL_ERROR_SYSCALL: FD %d", fd); + pgagroal_log_error("%s", ERR_error_string(err, NULL)); + pgagroal_log_error("%s", ERR_lib_error_string(err)); + pgagroal_log_error("%s", ERR_reason_error_string(err)); + errno = 0; + goto error; + break; + case SSL_ERROR_SSL: + pgagroal_log_error("SSL_ERROR_SSL: FD %d", fd); + pgagroal_log_error("%s", ERR_error_string(err, NULL)); + pgagroal_log_error("%s", ERR_lib_error_string(err)); + pgagroal_log_error("%s", ERR_reason_error_string(err)); + errno = 0; + goto error; + break; + } + ERR_clear_error(); + } + } + while (status != 1); + + *ssl = s; + + return AUTH_SUCCESS; + +error: + + *ssl = s; + + return AUTH_ERROR; +} + +void +pgagroal_initialize_random() +{ + time_t t; + srand((unsigned)time(&t)); +} + +int +pgagroal_generate_password(int pwd_length, char** password) +{ + char* pwd; + + pwd = (char*) malloc((pwd_length + 1) * sizeof(char)); + if (!pwd) + { + pgagroal_log_fatal("Couldn't allocate memory while generating password"); + return 1; + } + + for (int i = 0; i < pwd_length; i++) + { + pwd[i] = (char) (32 + rand() % (126 - 32 + 1)); + } + pwd[pwd_length] = '\0'; + + // avoid leading/trailing/consecutive spaces. + if (pwd[0] == ' ') + { + pwd[0] = (char) (33 + rand() % (126 - 33 + 1)); + } + if (pwd[pwd_length - 1] == ' ') + { + pwd[pwd_length - 1] = (char) (33 + rand() % (126 - 33 + 1)); + } + for (int i = 2; i < pwd_length - 1; i++) + { + if (pwd[i] == ' ' && pwd[i - 1] == ' ') + { + pwd[i] = (char) (33 + rand() % (126 - 33 + 1)); + } + } + *password = pwd; + return 0; +} diff --git a/src/libpgagroal/server.c b/src/libpgagroal/server.c new file mode 100644 index 00000000..bca41c21 --- /dev/null +++ b/src/libpgagroal/server.c @@ -0,0 +1,421 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include + +static int failover(int old_primary); + +int +pgagroal_get_primary(int* server) +{ + int primary; + signed char server_state; + struct main_configuration* config; + + primary = -1; + config = (struct main_configuration*)shmem; + + /* Find PRIMARY */ + for (int i = 0; primary == -1 && i < config->number_of_servers; i++) + { + server_state = atomic_load(&config->servers[i].state); + if (server_state == SERVER_PRIMARY) + { + pgagroal_log_trace("pgagroal_get_primary: server (%d) name (%s) primary", i, config->servers[i].name); + primary = i; + } + } + + /* Find NOTINIT_PRIMARY */ + for (int i = 0; primary == -1 && i < config->number_of_servers; i++) + { + server_state = atomic_load(&config->servers[i].state); + if (server_state == SERVER_NOTINIT_PRIMARY) + { + pgagroal_log_trace("pgagroal_get_primary: server (%d) name (%s) noninit_primary", i, config->servers[i].name); + primary = i; + } + } + + /* Find the first valid server */ + for (int i = 0; primary == -1 && i < config->number_of_servers; i++) + { + server_state = atomic_load(&config->servers[i].state); + if (server_state != SERVER_FAILOVER && server_state != SERVER_FAILED) + { + pgagroal_log_trace("pgagroal_get_primary: server (%d) name (%s) any (%d)", i, config->servers[i].name, server_state); + primary = i; + } + } + + if (primary == -1) + { + goto error; + } + + *server = primary; + + return 0; + +error: + + *server = -1; + + return 1; +} + +int +pgagroal_update_server_state(int slot, int socket, SSL* ssl) +{ + int status; + int server; + size_t size = 40; + signed char state; + char is_recovery[size]; + struct message qmsg; + struct message* tmsg = NULL; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + server = config->connections[slot].server; + + memset(&qmsg, 0, sizeof(struct message)); + memset(&is_recovery, 0, size); + + pgagroal_write_byte(&is_recovery, 'Q'); + pgagroal_write_int32(&(is_recovery[1]), size - 1); + pgagroal_write_string(&(is_recovery[5]), "SELECT * FROM pg_is_in_recovery();"); + + qmsg.kind = 'Q'; + qmsg.length = size; + qmsg.data = &is_recovery; + + status = pgagroal_write_message(ssl, socket, &qmsg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + status = pgagroal_read_block_message(ssl, socket, &tmsg); + if (status != MESSAGE_STATUS_OK) + { + goto error; + } + + /* Read directly from the D message fragment */ + state = pgagroal_read_byte(tmsg->data + 54); + + pgagroal_free_message(tmsg); + + if (state == 'f') + { + atomic_store(&config->servers[server].state, SERVER_PRIMARY); + } + else + { + atomic_store(&config->servers[server].state, SERVER_REPLICA); + } + + pgagroal_free_message(tmsg); + + return 0; + +error: + pgagroal_log_trace("pgagroal_update_server_state: slot (%d) status (%d)", slot, status); + + pgagroal_free_message(tmsg); + + return 1; +} + +int +pgagroal_server_status(void) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + for (int i = 0; i < NUMBER_OF_SERVERS; i++) + { + if (strlen(config->servers[i].name) > 0) + { + pgagroal_log_debug("pgagroal_server_status: #: %d", i); + pgagroal_log_debug(" Name: %s", config->servers[i].name); + pgagroal_log_debug(" Host: %s", config->servers[i].host); + pgagroal_log_debug(" Port: %d", config->servers[i].port); + switch (atomic_load(&config->servers[i].state)) + { + case SERVER_NOTINIT: + pgagroal_log_debug(" State: NOTINIT"); + break; + case SERVER_NOTINIT_PRIMARY: + pgagroal_log_debug(" State: NOTINIT_PRIMARY"); + break; + case SERVER_PRIMARY: + pgagroal_log_debug(" State: PRIMARY"); + break; + case SERVER_REPLICA: + pgagroal_log_debug(" State: REPLICA"); + break; + case SERVER_FAILOVER: + pgagroal_log_debug(" State: FAILOVER"); + break; + case SERVER_FAILED: + pgagroal_log_debug(" State: FAILED"); + break; + default: + pgagroal_log_debug(" State: %d", atomic_load(&config->servers[i].state)); + break; + } + } + } + + return 0; +} + +int +pgagroal_server_failover(int slot) +{ + signed char primary; + signed char old_primary; + int ret = 1; + struct main_configuration* config = NULL; + + config = (struct main_configuration*)shmem; + + primary = SERVER_PRIMARY; + + old_primary = config->connections[slot].server; + + if (atomic_compare_exchange_strong(&config->servers[old_primary].state, &primary, SERVER_FAILOVER)) + { + ret = failover(old_primary); + + if (!fork()) + { + pgagroal_flush_server(old_primary); + } + } + + return ret; +} + +int +pgagroal_server_force_failover(int server) +{ + signed char cur_state; + signed char prev_state; + struct main_configuration* config = NULL; + + config = (struct main_configuration*)shmem; + + cur_state = atomic_load(&config->servers[server].state); + + if (cur_state != SERVER_FAILOVER && cur_state != SERVER_FAILED) + { + prev_state = atomic_exchange(&config->servers[server].state, SERVER_FAILOVER); + + if (prev_state == SERVER_NOTINIT || prev_state == SERVER_NOTINIT_PRIMARY || prev_state == SERVER_PRIMARY || prev_state == SERVER_REPLICA) + { + return failover(server); + } + else if (prev_state == SERVER_FAILED) + { + atomic_store(&config->servers[server].state, SERVER_FAILED); + } + } + + return 1; +} + +int +pgagroal_server_clear(char* server) +{ + signed char state; + struct main_configuration* config = NULL; + + config = (struct main_configuration*)shmem; + + for (int i = 0; i < config->number_of_servers; i++) + { + if (!strcmp(config->servers[i].name, server)) + { + state = atomic_load(&config->servers[i].state); + + if (state == SERVER_FAILED) + { + atomic_store(&config->servers[i].state, SERVER_NOTINIT); + } + + return 0; + } + } + + return 1; +} + +int +pgagroal_server_switch(char* server) +{ + int old_primary; + int new_primary; + signed char state; + struct main_configuration* config = NULL; + + config = (struct main_configuration*)shmem; + + old_primary = -1; + new_primary = -1; + + for (int i = 0; i < config->number_of_servers; i++) + { + state = atomic_load(&config->servers[i].state); + + if (state == SERVER_PRIMARY) + { + old_primary = i; + } + else if (!strcmp(config->servers[i].name, server)) + { + new_primary = i; + } + } + + if (old_primary != -1 && new_primary != -1) + { + atomic_store(&config->servers[old_primary].state, SERVER_FAILED); + atomic_store(&config->servers[new_primary].state, SERVER_PRIMARY); + return 0; + } + else if (old_primary == -1 && new_primary != -1) + { + atomic_store(&config->servers[new_primary].state, SERVER_PRIMARY); + return 0; + } + + return 1; +} + +static int +failover(int old_primary) +{ + signed char state; + char old_primary_port[6]; + int new_primary; + char new_primary_port[6]; + int status; + pid_t pid; + struct main_configuration* config = NULL; + + config = (struct main_configuration*)shmem; + + new_primary = -1; + + for (int i = 0; new_primary == -1 && i < config->number_of_servers; i++) + { + state = atomic_load(&config->servers[i].state); + if (state == SERVER_NOTINIT || state == SERVER_NOTINIT_PRIMARY || state == SERVER_REPLICA) + { + new_primary = i; + } + } + + if (new_primary == -1) + { + pgagroal_log_error("Failover: New primary could not be found"); + atomic_store(&config->servers[old_primary].state, SERVER_FAILED); + goto error; + } + + pid = fork(); + if (pid == -1) + { + pgagroal_log_error("Failover: Unable to execute failover script"); + atomic_store(&config->servers[old_primary].state, SERVER_FAILED); + goto error; + } + else if (pid > 0) + { + waitpid(pid, &status, 0); + + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) + { + pgagroal_log_info("Failover: New primary is %s (%s:%d)", config->servers[new_primary].name, config->servers[new_primary].host, config->servers[new_primary].port); + atomic_store(&config->servers[old_primary].state, SERVER_FAILED); + atomic_store(&config->servers[new_primary].state, SERVER_PRIMARY); + } + else + { + if (WIFEXITED(status)) + { + pgagroal_log_error("Failover: Error from failover script (exit %d)", WEXITSTATUS(status)); + } + else + { + pgagroal_log_error("Failover: Error from failover script (status %d)", status); + } + + atomic_store(&config->servers[old_primary].state, SERVER_FAILED); + atomic_store(&config->servers[new_primary].state, SERVER_FAILED); + } + } + else + { + memset(&old_primary_port, 0, sizeof(old_primary_port)); + memset(&new_primary_port, 0, sizeof(new_primary_port)); + + sprintf(&old_primary_port[0], "%d", config->servers[old_primary].port); + sprintf(&new_primary_port[0], "%d", config->servers[new_primary].port); + + execl(config->failover_script, "pgagroal_failover", + config->servers[old_primary].host, old_primary_port, + config->servers[new_primary].host, new_primary_port, + (char*)NULL); + } + + return 0; + +error: + + return 1; +} diff --git a/src/libpgagroal/shmem.c b/src/libpgagroal/shmem.c new file mode 100644 index 00000000..1de66218 --- /dev/null +++ b/src/libpgagroal/shmem.c @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include + +/* system */ +#include +#include +#include + +void* shmem = NULL; +void* pipeline_shmem = NULL; +void* prometheus_shmem = NULL; +void* prometheus_cache_shmem = NULL; + +int +pgagroal_create_shared_memory(size_t size, unsigned char hp, void** shmem) +{ + void* s = NULL; + int protection = PROT_READ | PROT_WRITE; + int visibility = MAP_ANONYMOUS | MAP_SHARED; + + *shmem = NULL; + +#ifdef HAVE_LINUX + if (hp == HUGEPAGE_TRY || hp == HUGEPAGE_ON) + { + visibility = visibility | MAP_HUGETLB; + } + +#endif + + s = mmap(NULL, size, protection, visibility, -1, 0); + if (s == (void*)-1) + { + errno = 0; + s = NULL; + + if (hp == HUGEPAGE_OFF || hp == HUGEPAGE_ON) + { + return 1; + } + } + + if (s == NULL) + { + visibility = MAP_ANONYMOUS | MAP_SHARED; + s = mmap(NULL, size, protection, visibility, 0, 0); + + if (s == (void*)-1) + { + errno = 0; + return 1; + } + } + + memset(s, 0, size); + + *shmem = s; + + return 0; +} + +int +pgagroal_resize_shared_memory(size_t size, void* shmem, size_t* new_size, void** new_shmem) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + *new_size = size + (config->max_connections * sizeof(struct connection)); + if (pgagroal_create_shared_memory(*new_size, config->common.hugepage, new_shmem)) + { + return 1; + } + + memset(*new_shmem, 0, *new_size); + memcpy(*new_shmem, shmem, size); + + return 0; +} + +int +pgagroal_destroy_shared_memory(void* shmem, size_t size) +{ + return munmap(shmem, size); +} diff --git a/src/libpgagroal/status.c b/src/libpgagroal/status.c new file mode 100644 index 00000000..381f8c75 --- /dev/null +++ b/src/libpgagroal/status.c @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#include +#include +#include +#include + +static void status_details(bool details, struct json* response); + +void +pgagroal_status(SSL* ssl, int client_fd, uint8_t compression, uint8_t encryption, struct json* payload) +{ + char* elapsed = NULL; + time_t start_time; + time_t end_time; + int total_seconds; + struct json* response = NULL; + + pgagroal_memory_init(); + pgagroal_start_logging(); + + start_time = time(NULL); + + if (pgagroal_management_create_response(payload, -1, &response)) + { + goto error; + } + + status_details(false, response); + + end_time = time(NULL); + + if (pgagroal_management_response_ok(NULL, client_fd, start_time, end_time, compression, encryption, payload)) + { + pgagroal_management_response_error(NULL, client_fd, NULL, MANAGEMENT_ERROR_STATUS_NETWORK, compression, encryption, payload); + pgagroal_log_error("Status: Error sending response"); + + goto error; + } + + elapsed = pgagroal_get_timestamp_string(start_time, end_time, &total_seconds); + + pgagroal_log_info("Status (Elapsed: %s)", elapsed); + + pgagroal_json_destroy(payload); + + pgagroal_disconnect(client_fd); + + pgagroal_stop_logging(); + pgagroal_memory_destroy(); + + exit(0); + +error: + + pgagroal_json_destroy(payload); + + pgagroal_disconnect(client_fd); + + pgagroal_stop_logging(); + pgagroal_memory_destroy(); + + exit(1); +} + +void +pgagroal_status_details(SSL* ssl, int client_fd, uint8_t compression, uint8_t encryption, struct json* payload) +{ + char* elapsed = NULL; + time_t start_time; + time_t end_time; + int total_seconds; + struct json* response = NULL; + + pgagroal_memory_init(); + pgagroal_start_logging(); + + start_time = time(NULL); + + if (pgagroal_management_create_response(payload, -1, &response)) + { + goto error; + } + + status_details(true, response); + + end_time = time(NULL); + + if (pgagroal_management_response_ok(NULL, client_fd, start_time, end_time, compression, encryption, payload)) + { + pgagroal_management_response_error(NULL, client_fd, NULL, MANAGEMENT_ERROR_STATUS_DETAILS_NETWORK, compression, encryption, payload); + pgagroal_log_error("Status details: Error sending response"); + + goto error; + } + + elapsed = pgagroal_get_timestamp_string(start_time, end_time, &total_seconds); + + pgagroal_log_info("Status details (Elapsed: %s)", elapsed); + + pgagroal_json_destroy(payload); + + pgagroal_disconnect(client_fd); + + pgagroal_stop_logging(); + pgagroal_memory_destroy(); + + exit(0); + +error: + + pgagroal_json_destroy(payload); + + pgagroal_disconnect(client_fd); + + pgagroal_stop_logging(); + pgagroal_memory_destroy(); + + exit(1); +} + +static void +status_details(bool details, struct json* response) +{ + int active = 0; + int total = 0; + struct json* servers = NULL; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + pgagroal_json_put(response, MANAGEMENT_ARGUMENT_STATUS, (uintptr_t)(config->gracefully ? "Graceful shutdown" : "Running"), ValueString); + + for (int i = 0; i < config->max_connections; i++) + { + int state = atomic_load(&config->states[i]); + switch (state) + { + case STATE_IN_USE: + case STATE_GRACEFULLY: + active++; + case STATE_INIT: + case STATE_FREE: + case STATE_FLUSH: + case STATE_IDLE_CHECK: + case STATE_MAX_CONNECTION_AGE: + case STATE_VALIDATION: + case STATE_REMOVE: + total++; + break; + default: + break; + } + } + + pgagroal_json_put(response, MANAGEMENT_ARGUMENT_ACTIVE_CONNECTIONS, (uintptr_t)active, ValueUInt32); + pgagroal_json_put(response, MANAGEMENT_ARGUMENT_TOTAL_CONNECTIONS, (uintptr_t)total, ValueUInt32); + pgagroal_json_put(response, MANAGEMENT_ARGUMENT_MAX_CONNECTIONS, (uintptr_t)config->max_connections, ValueUInt32); + + pgagroal_json_put(response, MANAGEMENT_ARGUMENT_NUMBER_OF_SERVERS, (uintptr_t)config->number_of_servers, ValueInt32); + + pgagroal_json_create(&servers); + + for (int i = 0; i < config->number_of_servers; i++) + { + struct json* js = NULL; + + pgagroal_json_create(&js); + + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_SERVER, (uintptr_t)config->servers[i].name, ValueString); + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_HOST, (uintptr_t)config->servers[i].host, ValueString); + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_PORT, (uintptr_t)config->servers[i].port, ValueInt32); + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_STATE, (uintptr_t)pgagroal_server_state_as_string(config->servers[i].state), ValueString); + + pgagroal_json_append(servers, (uintptr_t)js, ValueJSON); + } + + pgagroal_json_put(response, MANAGEMENT_ARGUMENT_SERVERS, (uintptr_t)servers, ValueJSON); + + if (details) + { + int number_of_disabled = 0; + struct json* limits = NULL; + struct json* databases = NULL; + struct json* connections = NULL; + + pgagroal_json_create(&limits); + pgagroal_json_create(&connections); + + for (int i = 0; i < config->number_of_limits; i++) + { + struct json* js = NULL; + + pgagroal_json_create(&js); + + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_DATABASE, (uintptr_t)config->limits[i].database, ValueString); + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_USERNAME, (uintptr_t)config->limits[i].username, ValueString); + + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_ACTIVE_CONNECTIONS, (uintptr_t)atomic_load(&config->limits[i].active_connections), ValueUInt32); + + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_MAX_CONNECTIONS, (uintptr_t)config->limits[i].max_size, ValueUInt32); + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_INITIAL_CONNECTIONS, (uintptr_t)config->limits[i].initial_size, ValueUInt32); + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_MIN_CONNECTIONS, (uintptr_t)config->limits[i].min_size, ValueUInt32); + + pgagroal_json_append(limits, (uintptr_t)js, ValueJSON); + } + + pgagroal_json_put(response, MANAGEMENT_ARGUMENT_LIMITS, (uintptr_t)limits, ValueJSON); + + pgagroal_json_create(&databases); + + for (int i = 0; i < NUMBER_OF_DISABLED; i++) + { + struct json* js = NULL; + + if (strlen(config->disabled[i]) > 0) + { + pgagroal_json_create(&js); + + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_DATABASE, (uintptr_t)config->disabled[i], ValueString); + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_ENABLED, (uintptr_t)false, ValueBool); + + pgagroal_json_append(databases, (uintptr_t)js, ValueJSON); + + number_of_disabled++; + } + } + + if (number_of_disabled == 0) + { + struct json* js = NULL; + + pgagroal_json_create(&js); + + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_DATABASE, (uintptr_t)"*", ValueString); + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_ENABLED, (uintptr_t) !config->all_disabled, ValueBool); + + pgagroal_json_append(databases, (uintptr_t)js, ValueJSON); + } + + pgagroal_json_put(response, MANAGEMENT_ARGUMENT_DATABASES, (uintptr_t)databases, ValueJSON); + + for (int i = 0; i < config->max_connections; i++) + { + struct json* js = NULL; + + pgagroal_json_create(&js); + + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_START_TIME, (uintptr_t)config->connections[i].start_time, ValueInt64); + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_TIMESTAMP, (uintptr_t)config->connections[i].timestamp, ValueInt64); + + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_PID, (uintptr_t)config->connections[i].pid, ValueInt32); + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_FD, (uintptr_t)config->connections[i].fd, ValueInt32); + + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_DATABASE, (uintptr_t)config->connections[i].database, ValueString); + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_USERNAME, (uintptr_t)config->connections[i].username, ValueString); + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_APPNAME, (uintptr_t)config->connections[i].appname, ValueString); + + pgagroal_json_append(connections, (uintptr_t)js, ValueJSON); + } + + pgagroal_json_put(response, MANAGEMENT_ARGUMENT_CONNECTIONS, (uintptr_t)connections, ValueJSON); + } +} diff --git a/src/libpgagroal/tracker.c b/src/libpgagroal/tracker.c new file mode 100644 index 00000000..e0699d05 --- /dev/null +++ b/src/libpgagroal/tracker.c @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include + +static int count_connections(void); + +void +pgagroal_tracking_event_basic(int id, char* username, char* database) +{ + int primary; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + if (config->tracker) + { + struct timeval t; + long long milliseconds; + + gettimeofday(&t, NULL); + milliseconds = t.tv_sec * 1000 + t.tv_usec / 1000; + + if (username == NULL) + { + username = ""; + } + + if (database == NULL) + { + database = ""; + } + + pgagroal_get_primary(&primary); + + pgagroal_log_info("PGAGROAL|%d|%d|%d|%lld|%d|%s|%s|%s|%d|%d|%d|%d|%d|%d|%d|%d|", + id, + -1, + -3, + milliseconds, + getpid(), + username, + database, + "", + -1, + primary, + -1, + -3, + -1, + -1, + atomic_load(&config->active_connections), + count_connections()); + } +} + +void +pgagroal_tracking_event_slot(int id, int slot) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + if (config->tracker) + { + char* username = NULL; + char* database = NULL; + char* appname = NULL; + struct timeval t; + long long milliseconds; + + gettimeofday(&t, NULL); + milliseconds = t.tv_sec * 1000 + t.tv_usec / 1000; + + if (slot != -1) + { + username = &config->connections[slot].username[0]; + database = &config->connections[slot].database[0]; + appname = &config->connections[slot].appname[0]; + } + else + { + username = ""; + database = ""; + appname = ""; + } + + pgagroal_log_info("PGAGROAL|%d|%d|%d|%lld|%d|%s|%s|%s|%d|%d|%d|%d|%d|%d|%d|%d|", + id, + slot, + atomic_load(&config->states[slot]), + milliseconds, + getpid(), + username, + database, + appname, + config->connections[slot].new, + config->connections[slot].server, + config->connections[slot].tx_mode, + config->connections[slot].has_security, + config->connections[slot].limit_rule, + config->connections[slot].fd, + atomic_load(&config->active_connections), + count_connections()); + } +} + +void +pgagroal_tracking_event_socket(int id, int socket) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + if (config->tracker) + { + struct timeval t; + long long milliseconds; + + gettimeofday(&t, NULL); + milliseconds = t.tv_sec * 1000 + t.tv_usec / 1000; + + pgagroal_log_info("PGAGROAL|%d|%lld|%d|%d|", + id, + milliseconds, + getpid(), + socket); + } +} + +static int +count_connections(void) +{ + int active = 0; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + for (int i = 0; i < config->max_connections; i++) + { + int state = atomic_load(&config->states[i]); + switch (state) + { + case STATE_IN_USE: + case STATE_GRACEFULLY: + active++; + default: + break; + } + } + + return active; +} diff --git a/src/libpgagroal/utils.c b/src/libpgagroal/utils.c new file mode 100644 index 00000000..eb688537 --- /dev/null +++ b/src/libpgagroal/utils.c @@ -0,0 +1,1304 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef EVBACKEND_LINUXAIO +#define EVBACKEND_LINUXAIO 0x00000040U +#endif + +#ifndef EVBACKEND_IOURING +#define EVBACKEND_IOURING 0x00000080U +#endif + +extern char** environ; +#if defined(HAVE_LINUX) || defined(HAVE_OSX) +static bool env_changed = false; +static int max_process_title_size = 0; +#endif + +int32_t +pgagroal_get_request(struct message* msg) +{ + if (msg == NULL || msg->data == NULL || msg->length < 8) + { + return -1; + } + + return pgagroal_read_int32(msg->data + 4); +} + +int +pgagroal_extract_username_database(struct message* msg, char** username, char** database, char** appname) +{ + int start, end; + int counter = 0; + signed char c; + char** array = NULL; + size_t size; + char* un = NULL; + char* db = NULL; + char* an = NULL; + + *username = NULL; + *database = NULL; + *appname = NULL; + + /* We know where the parameters start, and we know that the message is zero terminated */ + for (int i = 8; i < msg->length - 1; i++) + { + c = pgagroal_read_byte(msg->data + i); + if (c == 0) + { + counter++; + } + } + + array = (char**)malloc(sizeof(char*) * counter); + + counter = 0; + start = 8; + end = 8; + + for (int i = 8; i < msg->length - 1; i++) + { + c = pgagroal_read_byte(msg->data + i); + end++; + if (c == 0) + { + array[counter] = (char*)calloc(1, end - start); + memcpy(array[counter], msg->data + start, end - start); + + start = end; + counter++; + } + } + + for (int i = 0; i < counter; i++) + { + if (!strcmp(array[i], "user")) + { + size = strlen(array[i + 1]) + 1; + un = calloc(1, size); + memcpy(un, array[i + 1], size); + + *username = un; + } + else if (!strcmp(array[i], "database")) + { + size = strlen(array[i + 1]) + 1; + db = calloc(1, size); + memcpy(db, array[i + 1], size); + + *database = db; + } + else if (!strcmp(array[i], "application_name")) + { + size = strlen(array[i + 1]) + 1; + an = calloc(1, size); + memcpy(an, array[i + 1], size); + + *appname = an; + } + } + + if (*database == NULL) + { + *database = *username; + } + + pgagroal_log_trace("Username: %s", *username); + pgagroal_log_trace("Database: %s", *database); + + for (int i = 0; i < counter; i++) + { + free(array[i]); + } + free(array); + + return 0; +} + +int +pgagroal_extract_message(char type, struct message* msg, struct message** extracted) +{ + int offset; + int m_length; + void* data = NULL; + struct message* result = NULL; + + offset = 0; + *extracted = NULL; + + while (result == NULL && offset < msg->length) + { + char t = (char)pgagroal_read_byte(msg->data + offset); + + if (type == t) + { + m_length = pgagroal_read_int32(msg->data + offset + 1); + + result = (struct message*)malloc(sizeof(struct message)); + data = (void*)malloc(1 + m_length); + + memcpy(data, msg->data + offset, 1 + m_length); + + result->kind = pgagroal_read_byte(data); + result->length = 1 + m_length; + result->data = data; + + *extracted = result; + + return 0; + } + else + { + offset += 1; + offset += pgagroal_read_int32(msg->data + offset); + } + } + + return 1; +} + +int +pgagroal_extract_error_message(struct message* msg, char** error) +{ + int max = 0; + int offset = 5; + signed char type; + char* s = NULL; + char* result = NULL; + + *error = NULL; + + if (msg->kind == 'E') + { + max = pgagroal_read_int32(msg->data + 1); + + while (result == NULL && offset < max) + { + type = pgagroal_read_byte(msg->data + offset); + s = pgagroal_read_string(msg->data + offset + 1); + + if (type == 'M') + { + result = (char*)calloc(1, strlen(s) + 1); + memcpy(result, s, strlen(s)); + + *error = result; + } + + offset += 1 + strlen(s) + 1; + } + } + else + { + goto error; + } + + return 0; + +error: + + return 1; +} + +char* +pgagroal_connection_state_as_string(signed char state) +{ + char* buf; + int buf_size = strlen("Unknown") + 1 + 4 + 1; // 'unknown' + + + \0 + + switch (state) + { + case STATE_NOTINIT: + return "Not initialized"; + case STATE_INIT: + return "Initializing"; + case STATE_FREE: + return "Free"; + case STATE_IN_USE: + return "Active"; + case STATE_GRACEFULLY: + return "Graceful"; + case STATE_FLUSH: + return "Flush"; + case STATE_IDLE_CHECK: + return "Idle check"; + case STATE_MAX_CONNECTION_AGE: + return "Max connection age check"; + case STATE_VALIDATION: + return "Validating"; + case STATE_REMOVE: + return "Removing"; + default: + buf = malloc(buf_size); + memset(buf, 0, buf_size); + snprintf(buf, buf_size, "Unknown %02d", state); + return buf; + } +} + +signed char +pgagroal_read_byte(void* data) +{ + return (signed char) *((char*)data); +} + +uint8_t +pgagroal_read_uint8(void* data) +{ + return (uint8_t) *((char*)data); +} + +int16_t +pgagroal_read_int16(void* data) +{ + unsigned char bytes[] = {*((unsigned char*)data), + *((unsigned char*)(data + 1))}; + + int16_t res = (int16_t)((bytes[0] << 8)) | + ((bytes[1])); + + return res; +} + +int32_t +pgagroal_read_int32(void* data) +{ + unsigned char bytes[] = {*((unsigned char*)data), + *((unsigned char*)(data + 1)), + *((unsigned char*)(data + 2)), + *((unsigned char*)(data + 3))}; + + int32_t res = (int32_t)((bytes[0] << 24)) | + ((bytes[1] << 16)) | + ((bytes[2] << 8)) | + ((bytes[3])); + + return res; +} + +uint32_t +pgagroal_read_uint32(void* data) +{ + uint8_t bytes[] = {*((uint8_t*)data), + *((uint8_t*)(data + 1)), + *((uint8_t*)(data + 2)), + *((uint8_t*)(data + 3))}; + + uint32_t res = (uint32_t)(((uint32_t)bytes[0] << 24)) | + (((uint32_t)bytes[1] << 16)) | + (((uint32_t)bytes[2] << 8)) | + (((uint32_t)bytes[3])); + + return res; +} + +long +pgagroal_read_long(void* data) +{ + unsigned char bytes[] = {*((unsigned char*)data), + *((unsigned char*)(data + 1)), + *((unsigned char*)(data + 2)), + *((unsigned char*)(data + 3)), + *((unsigned char*)(data + 4)), + *((unsigned char*)(data + 5)), + *((unsigned char*)(data + 6)), + *((unsigned char*)(data + 7))}; + + long res = (long)(((long)bytes[0]) << 56) | + (((long)bytes[1]) << 48) | + (((long)bytes[2]) << 40) | + (((long)bytes[3]) << 32) | + (((long)bytes[4]) << 24) | + (((long)bytes[5]) << 16) | + (((long)bytes[6]) << 8) | + (((long)bytes[7])); + + return res; +} + +char* +pgagroal_read_string(void* data) +{ + return (char*)data; +} + +void +pgagroal_write_byte(void* data, signed char b) +{ + *((char*)(data)) = b; +} + +void +pgagroal_write_uint8(void* data, uint8_t b) +{ + *((uint8_t*)(data)) = b; +} + +void +pgagroal_write_int32(void* data, int32_t i) +{ + char* ptr = (char*)&i; + + *((char*)(data + 3)) = *ptr; + ptr++; + *((char*)(data + 2)) = *ptr; + ptr++; + *((char*)(data + 1)) = *ptr; + ptr++; + *((char*)(data)) = *ptr; +} + +void +pgagroal_write_uint32(void* data, uint32_t i) +{ + uint8_t* ptr = (uint8_t*)&i; + + *((uint8_t*)(data + 3)) = *ptr; + ptr++; + *((uint8_t*)(data + 2)) = *ptr; + ptr++; + *((uint8_t*)(data + 1)) = *ptr; + ptr++; + *((uint8_t*)(data)) = *ptr; +} + +void +pgagroal_write_long(void* data, long l) +{ + char* ptr = (char*)&l; + + *((char*)(data + 7)) = *ptr; + ptr++; + *((char*)(data + 6)) = *ptr; + ptr++; + *((char*)(data + 5)) = *ptr; + ptr++; + *((char*)(data + 4)) = *ptr; + ptr++; + *((char*)(data + 3)) = *ptr; + ptr++; + *((char*)(data + 2)) = *ptr; + ptr++; + *((char*)(data + 1)) = *ptr; + ptr++; + *((char*)(data)) = *ptr; +} + +void +pgagroal_write_string(void* data, char* s) +{ + memcpy(data, s, strlen(s)); +} + +bool +pgagroal_bigendian(void) +{ + short int word = 0x0001; + char* b = (char*)&word; + return (b[0] ? false : true); +} + +unsigned int +pgagroal_swap(unsigned int i) +{ + return ((i << 24) & 0xff000000) | + ((i << 8) & 0x00ff0000) | + ((i >> 8) & 0x0000ff00) | + ((i >> 24) & 0x000000ff); +} + +void +pgagroal_libev_engines(void) +{ + unsigned int engines = ev_supported_backends(); + + if (engines & EVBACKEND_SELECT) + { + pgagroal_log_debug("libev available: select"); + } + if (engines & EVBACKEND_POLL) + { + pgagroal_log_debug("libev available: poll"); + } + if (engines & EVBACKEND_EPOLL) + { + pgagroal_log_debug("libev available: epoll"); + } + if (engines & EVBACKEND_LINUXAIO) + { + pgagroal_log_debug("libev available: linuxaio"); + } + if (engines & EVBACKEND_IOURING) + { + pgagroal_log_debug("libev available: iouring"); + } + if (engines & EVBACKEND_KQUEUE) + { + pgagroal_log_debug("libev available: kqueue"); + } + if (engines & EVBACKEND_DEVPOLL) + { + pgagroal_log_debug("libev available: devpoll"); + } + if (engines & EVBACKEND_PORT) + { + pgagroal_log_debug("libev available: port"); + } +} + +unsigned int +pgagroal_libev(char* engine) +{ + unsigned int engines = ev_supported_backends(); + + if (engine) + { + if (!strcmp("select", engine)) + { + if (engines & EVBACKEND_SELECT) + { + return EVBACKEND_SELECT; + } + else + { + pgagroal_log_warn("libev not available: select"); + } + } + else if (!strcmp("poll", engine)) + { + if (engines & EVBACKEND_POLL) + { + return EVBACKEND_POLL; + } + else + { + pgagroal_log_warn("libev not available: poll"); + } + } + else if (!strcmp("epoll", engine)) + { + if (engines & EVBACKEND_EPOLL) + { + return EVBACKEND_EPOLL; + } + else + { + pgagroal_log_warn("libev not available: epoll"); + } + } + else if (!strcmp("linuxaio", engine)) + { + return EVFLAG_AUTO; + } + else if (!strcmp("iouring", engine)) + { + if (engines & EVBACKEND_IOURING) + { + return EVBACKEND_IOURING; + } + else + { + pgagroal_log_warn("libev not available: iouring"); + } + } + else if (!strcmp("devpoll", engine)) + { + if (engines & EVBACKEND_DEVPOLL) + { + return EVBACKEND_DEVPOLL; + } + else + { + pgagroal_log_warn("libev not available: devpoll"); + } + } + else if (!strcmp("port", engine)) + { + if (engines & EVBACKEND_PORT) + { + return EVBACKEND_PORT; + } + else + { + pgagroal_log_warn("libev not available: port"); + } + } + else if (!strcmp("auto", engine) || !strcmp("", engine)) + { + return EVFLAG_AUTO; + } + else + { + pgagroal_log_warn("libev unknown option: %s", engine); + } + } + + return EVFLAG_AUTO; +} + +char* +pgagroal_libev_engine(unsigned int val) +{ + switch (val) + { + case EVBACKEND_SELECT: + return "select"; + case EVBACKEND_POLL: + return "poll"; + case EVBACKEND_EPOLL: + return "epoll"; + case EVBACKEND_LINUXAIO: + return "linuxaio"; + case EVBACKEND_IOURING: + return "iouring"; + case EVBACKEND_KQUEUE: + return "kqueue"; + case EVBACKEND_DEVPOLL: + return "devpoll"; + case EVBACKEND_PORT: + return "port"; + } + + return "Unknown"; +} + +char* +pgagroal_get_timestamp_string(time_t start_time, time_t end_time, int32_t* seconds) +{ + int32_t total_seconds; + int hours; + int minutes; + int sec; + char elapsed[128]; + char* result = NULL; + + *seconds = 0; + + total_seconds = (int32_t)difftime(end_time, start_time); + + *seconds = total_seconds; + + hours = total_seconds / 3600; + minutes = (total_seconds % 3600) / 60; + sec = total_seconds % 60; + + memset(&elapsed[0], 0, sizeof(elapsed)); + sprintf(&elapsed[0], "%02i:%02i:%02i", hours, minutes, sec); + + result = pgagroal_append(result, &elapsed[0]); + + return result; +} + +char* +pgagroal_get_home_directory(void) +{ + struct passwd* pw = getpwuid(getuid()); + + if (pw == NULL) + { + return NULL; + } + + return pw->pw_dir; +} + +char* +pgagroal_get_user_name(void) +{ + struct passwd* pw = getpwuid(getuid()); + + if (pw == NULL) + { + return NULL; + } + + return pw->pw_name; +} + +char* +pgagroal_get_password(void) +{ + char p[MAX_PASSWORD_LENGTH]; + struct termios oldt, newt; + int i = 0; + int c; + char* result = NULL; + + memset(&p, 0, sizeof(p)); + + tcgetattr(STDIN_FILENO, &oldt); + newt = oldt; + + newt.c_lflag &= ~(ECHO); + + tcsetattr(STDIN_FILENO, TCSANOW, &newt); + + while ((c = getchar()) != '\n' && c != EOF && i < MAX_PASSWORD_LENGTH) + { + p[i++] = c; + } + p[i] = '\0'; + + tcsetattr(STDIN_FILENO, TCSANOW, &oldt); + + result = calloc(1, strlen(p) + 1); + + memcpy(result, &p, strlen(p)); + + return result; +} + +bool +pgagroal_exists(char* f) +{ + if (access(f, F_OK) == 0) + { + return true; + } + + return false; +} + +int +pgagroal_base64_encode(void* raw, size_t raw_length, char** encoded, size_t* encoded_length) +{ + BIO* b64_bio; + BIO* mem_bio; + BUF_MEM* mem_bio_mem_ptr; + char* r = NULL; + + *encoded = NULL; + *encoded_length = 0; + + if (raw == NULL) + { + goto error; + } + + b64_bio = BIO_new(BIO_f_base64()); + mem_bio = BIO_new(BIO_s_mem()); + + BIO_push(b64_bio, mem_bio); + BIO_set_flags(b64_bio, BIO_FLAGS_BASE64_NO_NL); + BIO_write(b64_bio, raw, raw_length); + BIO_flush(b64_bio); + + BIO_get_mem_ptr(mem_bio, &mem_bio_mem_ptr); + + BIO_set_close(mem_bio, BIO_NOCLOSE); + BIO_free_all(b64_bio); + + BUF_MEM_grow(mem_bio_mem_ptr, (*mem_bio_mem_ptr).length + 1); + (*mem_bio_mem_ptr).data[(*mem_bio_mem_ptr).length] = '\0'; + + r = calloc(1, strlen((*mem_bio_mem_ptr).data) + 1); + memcpy(r, (*mem_bio_mem_ptr).data, strlen((*mem_bio_mem_ptr).data)); + + BUF_MEM_free(mem_bio_mem_ptr); + + *encoded = r; + *encoded_length = strlen(r); + + return 0; + +error: + + *encoded = NULL; + + return 1; +} + +int +pgagroal_base64_decode(char* encoded, size_t encoded_length, void** raw, size_t* raw_length) +{ + BIO* b64_bio; + BIO* mem_bio; + size_t size; + char* decoded; + int index; + + *raw = NULL; + *raw_length = 0; + + if (encoded == NULL) + { + goto error; + } + + size = (encoded_length * 3) / 4 + 1; + decoded = calloc(1, size); + + b64_bio = BIO_new(BIO_f_base64()); + mem_bio = BIO_new(BIO_s_mem()); + + BIO_write(mem_bio, encoded, encoded_length); + BIO_push(b64_bio, mem_bio); + BIO_set_flags(b64_bio, BIO_FLAGS_BASE64_NO_NL); + + index = 0; + while (0 < BIO_read(b64_bio, decoded + index, 1)) + { + index++; + } + + BIO_free_all(b64_bio); + + *raw = decoded; + *raw_length = index; + + return 0; + +error: + + *raw = NULL; + *raw_length = 0; + + return 1; +} + +void +pgagroal_set_proc_title(int argc, char** argv, char* s1, char* s2) +{ +#if defined(HAVE_LINUX) || defined(HAVE_OSX) + char title[MAX_PROCESS_TITLE_LENGTH]; + size_t size; + char** env = environ; + int es = 0; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + // sanity check: if the user does not want to + // update the process title, do nothing + if (config->update_process_title == UPDATE_PROCESS_TITLE_NEVER) + { + return; + } + + if (!env_changed) + { + for (int i = 0; env[i] != NULL; i++) + { + es++; + } + + environ = (char**)malloc(sizeof(char*) * (es + 1)); + if (environ == NULL) + { + return; + } + + for (int i = 0; env[i] != NULL; i++) + { + size = strlen(env[i]); + environ[i] = (char*)calloc(1, size + 1); + + if (environ[i] == NULL) + { + return; + } + memcpy(environ[i], env[i], size); + } + environ[es] = NULL; + env_changed = true; + } + + // compute how long was the command line + // when the application was started + if (max_process_title_size == 0) + { + for (int i = 0; i < argc; i++) + { + max_process_title_size += strlen(argv[i]) + 1; + } + } + + // compose the new title + memset(&title, 0, sizeof(title)); + snprintf(title, sizeof(title) - 1, "pgagroal: %s%s%s", + s1 != NULL ? s1 : "", + s1 != NULL && s2 != NULL ? "/" : "", + s2 != NULL ? s2 : ""); + + // nuke the command line info + memset(*argv, 0, max_process_title_size); + + // copy the new title over argv checking + // the update_process_title policy + if (config->update_process_title == UPDATE_PROCESS_TITLE_STRICT) + { + size = max_process_title_size; + } + else + { + // here we can set the title to a full description + size = strlen(title) + 1; + } + + memcpy(*argv, title, size); + memset(*argv + size, 0, 1); + + // keep track of how long is now the title + max_process_title_size = size; + +#else + setproctitle("-pgagroal: %s%s%s", + s1 != NULL ? s1 : "", + s1 != NULL && s2 != NULL ? "/" : "", + s2 != NULL ? s2 : ""); + +#endif +} + +void +pgagroal_set_connection_proc_title(int argc, char** argv, struct connection* connection) +{ + struct main_configuration* config; + int primary; + char* info = NULL; + + config = (struct main_configuration*)shmem; + + if (pgagroal_get_primary(&primary)) + { + // cannot find the primary, this is a problem! + pgagroal_set_proc_title(argc, argv, connection->username, connection->database); + return; + } + + info = pgagroal_append(info, connection->username); + info = pgagroal_append(info, "@"); + info = pgagroal_append(info, config->servers[primary].host); + info = pgagroal_append(info, ":"); + info = pgagroal_append_int(info, config->servers[primary].port); + + pgagroal_set_proc_title(argc, argv, info, connection->database); + free(info); +} + +unsigned int +pgagroal_version_as_number(unsigned int major, unsigned int minor, unsigned int patch) +{ + return (patch % 100) + + (minor % 100) * 100 + + (major % 100) * 10000; +} + +unsigned int +pgagroal_version_number(void) +{ + return pgagroal_version_as_number(PGAGROAL_MAJOR_VERSION, + PGAGROAL_MINOR_VERSION, + PGAGROAL_PATCH_VERSION); +} + +bool +pgagroal_version_ge(unsigned int major, unsigned int minor, unsigned int patch) +{ + if (pgagroal_version_number() >= pgagroal_version_as_number(major, minor, patch)) + { + return true; + } + else + { + return false; + } +} + +bool +pgagroal_ends_with(char* str, char* suffix) +{ + int str_len = strlen(str); + int suffix_len = strlen(suffix); + + return (str_len >= suffix_len) && (strcmp(str + (str_len - suffix_len), suffix) == 0); +} + +char* +pgagroal_append(char* orig, char* s) +{ + size_t orig_length; + size_t s_length; + char* n = NULL; + + if (s == NULL) + { + return orig; + } + + if (orig != NULL) + { + orig_length = strlen(orig); + } + else + { + orig_length = 0; + } + + s_length = strlen(s); + + n = (char*)realloc(orig, orig_length + s_length + 1); + + memcpy(n + orig_length, s, s_length); + + n[orig_length + s_length] = '\0'; + + return n; +} + +char* +pgagroal_append_int(char* orig, int i) +{ + char number[12]; + + memset(&number[0], 0, sizeof(number)); + snprintf(&number[0], 11, "%d", i); + orig = pgagroal_append(orig, number); + + return orig; +} + +char* +pgagroal_append_ulong(char* orig, unsigned long l) +{ + char number[21]; + + memset(&number[0], 0, sizeof(number)); + snprintf(&number[0], 20, "%lu", l); + orig = pgagroal_append(orig, number); + + return orig; +} + +char* +pgagroal_append_ullong(char* orig, unsigned long long l) +{ + char number[21]; + + memset(&number[0], 0, sizeof(number)); + snprintf(&number[0], 20, "%llu", l); + orig = pgagroal_append(orig, number); + + return orig; +} + +#ifdef DEBUG + +int +pgagroal_backtrace(void) +{ +#ifdef HAVE_LINUX + void* array[100]; + size_t size; + char** strings; + + size = backtrace(array, 100); + strings = backtrace_symbols(array, size); + + for (size_t i = 0; i < size; i++) + { + printf("%s\n", strings[i]); + } + + free(strings); +#endif + + return 0; +} + +#endif + +/* Parser for pgagroal-cli commands */ +bool +parse_command(int argc, + char** argv, + int offset, + struct pgagroal_parsed_command* parsed, + const struct pgagroal_command command_table[], + size_t command_count) +{ +#define EMPTY_STR(_s) (_s[0] == 0) + + char* command = NULL; + char* subcommand = NULL; + bool command_match = false; + int default_command_match = -1; + int arg_count = -1; + int command_index = -1; + + /* Parse command, and exit if there is no match */ + if (offset < argc) + { + command = argv[offset++]; + } + else + { + warnx("A command is required\n"); + return false; + } + + if (offset < argc) + { + subcommand = argv[offset]; + } + + for (int i = 0; i < command_count; i++) + { + if (strncmp(command, command_table[i].command, MISC_LENGTH) == 0) + { + command_match = true; + if (subcommand && strncmp(subcommand, command_table[i].subcommand, MISC_LENGTH) == 0) + { + offset++; + command_index = i; + break; + } + else if (EMPTY_STR(command_table[i].subcommand)) + { + /* Default command does not require a subcommand, might be followed by an argument */ + default_command_match = i; + } + } + } + + if (command_match == false) + { + warnx("Unknown command '%s'\n", command); + return false; + } + + if (command_index == -1 && default_command_match >= 0) + { + command_index = default_command_match; + subcommand = ""; + } + else if (command_index == -1) /* Command was matched, but subcommand was not */ + { + if (subcommand) + { + warnx("Unknown subcommand '%s' for command '%s'\n", subcommand, command); + } + else /* User did not type a subcommand */ + { + warnx("Command '%s' requires a subcommand\n", command); + } + return false; + } + + parsed->cmd = &command_table[command_index]; + + /* Iterate until find an accepted_arg_count that is equal or greater than the typed command arg_count */ + arg_count = argc - offset; + int j; + for (j = 0; j < MISC_LENGTH; j++) + { + if (parsed->cmd->accepted_argument_count[j] >= arg_count) + { + break; + } + } + if (arg_count < parsed->cmd->accepted_argument_count[0]) + { + warnx("Too few arguments provided for command '%s%s%s'\n", command, + (command && !EMPTY_STR(subcommand)) ? " " : "", subcommand); + return false; + } + if (j == MISC_LENGTH || arg_count > parsed->cmd->accepted_argument_count[j]) + { + warnx("Too many arguments provided for command '%s%s%s'\n", command, + (command && !EMPTY_STR(subcommand)) ? " " : "", subcommand); + return false; + } + + /* Copy argv + offset pointers into parsed->args */ + for (int i = 0; i < arg_count; i++) + { + parsed->args[i] = argv[i + offset]; + } + parsed->args[0] = parsed->args[0] ? parsed->args[0] : (char*) parsed->cmd->default_argument; + + /* Warn the user if there is enough information about deprecation */ + if (parsed->cmd->deprecated + && pgagroal_version_ge(parsed->cmd->deprecated_since_major, + parsed->cmd->deprecated_since_minor, 0)) + { + warnx("command <%s> has been deprecated by <%s> since version %d.%d", + parsed->cmd->command, + parsed->cmd->deprecated_by, + parsed->cmd->deprecated_since_major, + parsed->cmd->deprecated_since_minor); + } + + return true; + +#undef EMPTY_STR +} + +char* +pgagroal_server_state_as_string(signed char state) +{ + char* buf; + + switch (state) + { + case SERVER_NOTINIT: return "Not init"; + case SERVER_NOTINIT_PRIMARY: return "Not init (primary)"; + case SERVER_PRIMARY: return "Primary"; + case SERVER_REPLICA: return "Replica"; + case SERVER_FAILOVER: return "Failover"; + case SERVER_FAILED: return "Failed"; + default: + buf = malloc(5); + memset(buf, 0, 5); + snprintf(buf, 5, "%d", state); + return buf; + } +} + +char* +pgagroal_append_char(char* orig, char c) +{ + char str[2]; + + memset(&str[0], 0, sizeof(str)); + snprintf(&str[0], 2, "%c", c); + orig = pgagroal_append(orig, str); + + return orig; +} + +char* +pgagroal_indent(char* str, char* tag, int indent) +{ + for (int i = 0; i < indent; i++) + { + str = pgagroal_append(str, " "); + } + if (tag != NULL) + { + str = pgagroal_append(str, tag); + } + return str; +} + +bool +pgagroal_compare_string(const char* str1, const char* str2) +{ + if (str1 == NULL && str2 == NULL) + { + return true; + } + if ((str1 == NULL && str2 != NULL) || (str1 != NULL && str2 == NULL)) + { + return false; + } + return strcmp(str1, str2) == 0; +} + +char* +pgagroal_escape_string(char* str) +{ + if (str == NULL) + { + return NULL; + } + + char* translated_ec_string = NULL; + int len = 0; + int idx = 0; + size_t translated_len = 0; + + len = strlen(str); + for (int i = 0; i < len; i++) + { + if (str[i] == '\"' || str[i] == '\\' || str[i] == '\n' || str[i] == '\t' || str[i] == '\r') + { + translated_len++; + } + translated_len++; + } + translated_ec_string = (char*)malloc(translated_len + 1); + + for (int i = 0; i < len; i++, idx++) + { + switch (str[i]) + { + case '\\': + case '\"': + translated_ec_string[idx] = '\\'; + idx++; + translated_ec_string[idx] = str[i]; + break; + case '\n': + translated_ec_string[idx] = '\\'; + idx++; + translated_ec_string[idx] = 'n'; + break; + case '\t': + translated_ec_string[idx] = '\\'; + idx++; + translated_ec_string[idx] = 't'; + break; + case '\r': + translated_ec_string[idx] = '\\'; + idx++; + translated_ec_string[idx] = 'r'; + break; + default: + translated_ec_string[idx] = str[i]; + break; + } + } + translated_ec_string[idx] = '\0'; // terminator + + return translated_ec_string; +} diff --git a/src/libpgagroal/value.c b/src/libpgagroal/value.c new file mode 100644 index 00000000..7c1dcc0e --- /dev/null +++ b/src/libpgagroal/value.c @@ -0,0 +1,549 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* pgagroal */ +#include +#include +#include +#include +#include + +/* System */ +#include +#include +#include +#include + +static void noop_destroy_cb(uintptr_t data); +static void free_destroy_cb(uintptr_t data); +static void art_destroy_cb(uintptr_t data); +static void deque_destroy_cb(uintptr_t data); +static void json_destroy_cb(uintptr_t data); +static char* noop_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent); +static char* int8_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent); +static char* uint8_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent); +static char* int16_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent); +static char* uint16_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent); +static char* int32_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent); +static char* uint32_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent); +static char* int64_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent); +static char* uint64_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent); +static char* float_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent); +static char* double_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent); +static char* string_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent); +static char* char_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent); +static char* bool_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent); +static char* deque_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent); +static char* art_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent); +static char* json_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent); +static char* mem_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent); + +int +pgagroal_value_create(enum value_type type, uintptr_t data, struct value** value) +{ + struct value* val = NULL; + val = (struct value*) malloc(sizeof(struct value)); + if (val == NULL) + { + goto error; + } + val->data = 0; + val->type = type; + switch (type) + { + case ValueInt8: + val->to_string = int8_to_string_cb; + break; + case ValueUInt8: + val->to_string = uint8_to_string_cb; + break; + case ValueInt16: + val->to_string = int16_to_string_cb; + break; + case ValueUInt16: + val->to_string = uint16_to_string_cb; + break; + case ValueInt32: + val->to_string = int32_to_string_cb; + break; + case ValueUInt32: + val->to_string = uint32_to_string_cb; + break; + case ValueInt64: + val->to_string = int64_to_string_cb; + break; + case ValueUInt64: + val->to_string = uint64_to_string_cb; + break; + case ValueFloat: + val->to_string = float_to_string_cb; + break; + case ValueDouble: + val->to_string = double_to_string_cb; + break; + case ValueBool: + val->to_string = bool_to_string_cb; + break; + case ValueChar: + val->to_string = char_to_string_cb; + break; + case ValueString: + val->to_string = string_to_string_cb; + break; + case ValueBASE64: + val->to_string = string_to_string_cb; + break; + case ValueJSON: + val->to_string = json_to_string_cb; + break; + case ValueDeque: + val->to_string = deque_to_string_cb; + break; + case ValueART: + val->to_string = art_to_string_cb; + break; + case ValueMem: + case ValueRef: + val->to_string = mem_to_string_cb; + break; + default: + val->to_string = noop_to_string_cb; + break; + } + switch (type) + { + case ValueString: + { + val->data = (uintptr_t)pgagroal_append(NULL, (char*)data); + val->destroy_data = free_destroy_cb; + break; + } + case ValueBASE64: + { + val->data = (uintptr_t)pgagroal_append(NULL, (char*)data); + val->destroy_data = free_destroy_cb; + break; + } + case ValueMem: + val->data = data; + val->destroy_data = free_destroy_cb; + break; + case ValueJSON: + val->data = data; + val->destroy_data = json_destroy_cb; + break; + case ValueDeque: + val->data = data; + val->destroy_data = deque_destroy_cb; + break; + case ValueART: + val->data = data; + val->destroy_data = art_destroy_cb; + break; + default: + val->data = data; + val->destroy_data = noop_destroy_cb; + break; + } + *value = val; + return 0; + +error: + return 1; +} + +int +pgagroal_value_create_with_config(uintptr_t data, struct value_config* config, struct value** value) +{ + if (pgagroal_value_create(ValueRef, data, value)) + { + return 1; + } + if (config != NULL) + { + if (config->destroy_data != NULL) + { + (*value)->destroy_data = config->destroy_data; + } + if (config->to_string != NULL) + { + (*value)->to_string = config->to_string; + } + } + return 0; +} + +int +pgagroal_value_destroy(struct value* value) +{ + if (value == NULL) + { + return 0; + } + value->destroy_data(value->data); + free(value); + return 0; +} + +uintptr_t +pgagroal_value_data(struct value* value) +{ + if (value == NULL) + { + return 0; + } + return value->data; +} + +char* +pgagroal_value_to_string(struct value* value, int32_t format, char* tag, int indent) +{ + return value->to_string(value->data, format, tag, indent); +} + +uintptr_t +pgagroal_value_from_double(double val) +{ + union duni + { + double val; + uintptr_t data; + }; + union duni uni; + uni.val = val; + return uni.data; +} + +double +pgagroal_value_to_double(uintptr_t val) +{ + union duni + { + double val; + uintptr_t data; + }; + union duni uni; + uni.data = val; + return uni.val; +} + +uintptr_t +pgagroal_value_from_float(float val) +{ + union funi + { + float val; + uintptr_t data; + }; + union funi uni; + uni.val = val; + return uni.data; +} + +float +pgagroal_value_to_float(uintptr_t val) +{ + union funi + { + float val; + uintptr_t data; + }; + union funi uni; + uni.data = val; + return uni.val; +} + +static void +noop_destroy_cb(uintptr_t data) +{ + (void) data; +} + +static void +free_destroy_cb(uintptr_t data) +{ + free((void*) data); +} + +static void +art_destroy_cb(uintptr_t data) +{ + pgagroal_art_destroy((struct art*) data); +} + +static void +deque_destroy_cb(uintptr_t data) +{ + pgagroal_deque_destroy((struct deque*) data); +} + +static void +json_destroy_cb(uintptr_t data) +{ + pgagroal_json_destroy((struct json*) data); +} + +static char* +noop_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent) +{ + char* ret = NULL; + ret = pgagroal_indent(ret, tag, indent); + (void) data; + (void) format; + return ret; +} + +static char* +int8_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent) +{ + char* ret = NULL; + char buf[MISC_LENGTH]; + + ret = pgagroal_indent(ret, tag, indent); + memset(buf, 0, MISC_LENGTH); + snprintf(buf, MISC_LENGTH, "%" PRId8, (int8_t)data); + ret = pgagroal_append(ret, buf); + return ret; +} + +static char* +uint8_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent) +{ + char* ret = NULL; + char buf[MISC_LENGTH]; + ret = pgagroal_indent(ret, tag, indent); + memset(buf, 0, MISC_LENGTH); + snprintf(buf, MISC_LENGTH, "%" PRIu8, (uint8_t)data); + ret = pgagroal_append(ret, buf); + return ret; +} + +static char* +int16_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent) +{ + char* ret = NULL; + char buf[MISC_LENGTH]; + + ret = pgagroal_indent(ret, tag, indent); + memset(buf, 0, MISC_LENGTH); + snprintf(buf, MISC_LENGTH, "%" PRId16, (int16_t)data); + ret = pgagroal_append(ret, buf); + return ret; +} + +static char* +uint16_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent) +{ + char* ret = NULL; + char buf[MISC_LENGTH]; + + ret = pgagroal_indent(ret, tag, indent); + memset(buf, 0, MISC_LENGTH); + snprintf(buf, MISC_LENGTH, "%" PRIu16, (uint16_t)data); + ret = pgagroal_append(ret, buf); + return ret; +} + +static char* +int32_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent) +{ + char* ret = NULL; + char buf[MISC_LENGTH]; + + ret = pgagroal_indent(ret, tag, indent); + memset(buf, 0, MISC_LENGTH); + snprintf(buf, MISC_LENGTH, "%" PRId32, (int32_t)data); + ret = pgagroal_append(ret, buf); + return ret; +} + +static char* +uint32_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent) +{ + char* ret = NULL; + char buf[MISC_LENGTH]; + + ret = pgagroal_indent(ret, tag, indent); + memset(buf, 0, MISC_LENGTH); + snprintf(buf, MISC_LENGTH, "%" PRIu32, (uint32_t)data); + ret = pgagroal_append(ret, buf); + return ret; +} + +static char* +int64_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent) +{ + char* ret = NULL; + char buf[MISC_LENGTH]; + + ret = pgagroal_indent(ret, tag, indent); + memset(buf, 0, MISC_LENGTH); + snprintf(buf, MISC_LENGTH, "%" PRId64, (int64_t)data); + ret = pgagroal_append(ret, buf); + return ret; +} + +static char* +uint64_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent) +{ + char* ret = NULL; + char buf[MISC_LENGTH]; + + ret = pgagroal_indent(ret, tag, indent); + memset(buf, 0, MISC_LENGTH); + snprintf(buf, MISC_LENGTH, "%" PRIu64, (uint64_t)data); + ret = pgagroal_append(ret, buf); + return ret; +} + +static char* +float_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent) +{ + char* ret = NULL; + char buf[MISC_LENGTH]; + + ret = pgagroal_indent(ret, tag, indent); + memset(buf, 0, MISC_LENGTH); + snprintf(buf, MISC_LENGTH, "%f", pgagroal_value_to_float(data)); + ret = pgagroal_append(ret, buf); + return ret; +} + +static char* +double_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent) +{ + char* ret = NULL; + char buf[MISC_LENGTH]; + + ret = pgagroal_indent(ret, tag, indent); + memset(buf, 0, MISC_LENGTH); + snprintf(buf, MISC_LENGTH, "%f", pgagroal_value_to_double(data)); + ret = pgagroal_append(ret, buf); + + return ret; +} + +static char* +string_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent) +{ + char* ret = NULL; + char* str = (char*) data; + char buf[MISC_LENGTH]; + char* translated_string = NULL; + + ret = pgagroal_indent(ret, tag, indent); + memset(buf, 0, MISC_LENGTH); + if (str == NULL) + { + if (format == FORMAT_JSON || format == FORMAT_JSON_COMPACT) + { + snprintf(buf, MISC_LENGTH, "null"); + } + } + else if (strlen(str) == 0) + { + if (format == FORMAT_JSON || format == FORMAT_JSON_COMPACT) + { + snprintf(buf, MISC_LENGTH, "\"%s\"", str); + } + else if (format == FORMAT_TEXT) + { + snprintf(buf, MISC_LENGTH, "''"); + } + } + else + { + if (format == FORMAT_JSON || format == FORMAT_JSON_COMPACT) + { + translated_string = pgagroal_escape_string(str); + snprintf(buf, MISC_LENGTH, "\"%s\"", translated_string); + free(translated_string); + } + else if (format == FORMAT_TEXT) + { + snprintf(buf, MISC_LENGTH, "%s", str); + } + } + ret = pgagroal_append(ret, buf); + return ret; +} + +static char* +bool_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent) +{ + char* ret = NULL; + bool val = (bool) data; + ret = pgagroal_indent(ret, tag, indent); + ret = pgagroal_append(ret, val?"true":"false"); + return ret; +} + +static char* +char_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent) +{ + char* ret = NULL; + char buf[MISC_LENGTH]; + + ret = pgagroal_indent(ret, tag, indent); + memset(buf, 0, MISC_LENGTH); + snprintf(buf, MISC_LENGTH, "'%c'", (char)data); + ret = pgagroal_append(ret, buf); + + return ret; +} + +static char* +deque_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent) +{ + return pgagroal_deque_to_string((struct deque*)data, format, tag, indent); +} + +static char* +art_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent) +{ + return pgagroal_art_to_string((struct art*) data, format, tag, indent); +} + +static char* +json_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent) +{ + return pgagroal_json_to_string((struct json*)data, format, tag, indent); +} + +static char* +mem_to_string_cb(uintptr_t data, int32_t format, char* tag, int indent) +{ + char* ret = NULL; + char buf[MISC_LENGTH]; + + ret = pgagroal_indent(ret, tag, indent); + memset(buf, 0, MISC_LENGTH); + snprintf(buf, MISC_LENGTH, "%p", (void*)data); + ret = pgagroal_append(ret, buf); + + return ret; +} \ No newline at end of file diff --git a/src/libpgagroal/worker.c b/src/libpgagroal/worker.c new file mode 100644 index 00000000..2749f26f --- /dev/null +++ b/src/libpgagroal/worker.c @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include + +volatile int running = 1; +volatile int exit_code = WORKER_FAILURE; + +static void signal_cb(struct ev_loop* loop, ev_signal* w, int revents); + +void +pgagroal_worker(int client_fd, char* address, char** argv) +{ + struct ev_loop* loop = NULL; + struct signal_info signal_watcher; + struct worker_io client_io; + struct worker_io server_io; + time_t start_time; + bool started = false; + int auth_status; + struct main_configuration* config; + struct pipeline p; + bool tx_pool = false; + int32_t slot = -1; + int transfer_fd = -1; + SSL* client_ssl = NULL; + SSL* server_ssl = NULL; + + pgagroal_start_logging(); + pgagroal_memory_init(); + + config = (struct main_configuration*)shmem; + + memset(&client_io, 0, sizeof(struct worker_io)); + memset(&server_io, 0, sizeof(struct worker_io)); + + client_io.slot = -1; + server_io.slot = -1; + + start_time = time(NULL); + + pgagroal_tracking_event_basic(TRACKER_CLIENT_START, NULL, NULL); + pgagroal_tracking_event_socket(TRACKER_SOCKET_ASSOCIATE_CLIENT, client_fd); + pgagroal_set_proc_title(1, argv, "authenticating", NULL); + + pgagroal_prometheus_client_wait_add(); + /* Authentication */ + auth_status = pgagroal_authenticate(client_fd, address, &slot, &client_ssl, &server_ssl); + if (auth_status == AUTH_SUCCESS) + { + pgagroal_log_debug("pgagroal_worker: Slot %d (%d -> %d)", slot, client_fd, config->connections[slot].fd); + + pgagroal_tracking_event_socket(TRACKER_SOCKET_ASSOCIATE_SERVER, config->connections[slot].fd); + + if (config->common.log_connections) + { + pgagroal_log_info("connect: user=%s database=%s address=%s", config->connections[slot].username, + config->connections[slot].database, address); + } + + pgagroal_prometheus_client_wait_sub(); + pgagroal_prometheus_client_active_add(); + + pgagroal_pool_status(); + + // do we have to update the process title? + switch (config->update_process_title) + { + case UPDATE_PROCESS_TITLE_MINIMAL: + case UPDATE_PROCESS_TITLE_STRICT: + // pgagroal_set_proc_title will check the policy + pgagroal_set_proc_title(1, argv, config->connections[slot].username, config->connections[slot].database); + break; + case UPDATE_PROCESS_TITLE_VERBOSE: + pgagroal_set_connection_proc_title(1, argv, &config->connections[slot]); + break; + } + + if (config->pipeline == PIPELINE_PERFORMANCE) + { + p = performance_pipeline(); + } + else if (config->pipeline == PIPELINE_SESSION) + { + p = session_pipeline(); + } + else if (config->pipeline == PIPELINE_TRANSACTION) + { + p = transaction_pipeline(); + tx_pool = true; + } + else + { + pgagroal_log_error("pgagroal_worker: Unknown pipeline %d", config->pipeline); + p = session_pipeline(); + } + + ev_io_init((struct ev_io*)&client_io, p.client, client_fd, EV_READ); + client_io.client_fd = client_fd; + client_io.server_fd = config->connections[slot].fd; + client_io.slot = slot; + client_io.client_ssl = client_ssl; + client_io.server_ssl = server_ssl; + + if (config->pipeline != PIPELINE_TRANSACTION) + { + ev_io_init((struct ev_io*)&server_io, p.server, config->connections[slot].fd, EV_READ); + server_io.client_fd = client_fd; + server_io.server_fd = config->connections[slot].fd; + server_io.slot = slot; + server_io.client_ssl = client_ssl; + server_io.server_ssl = server_ssl; + } + + loop = ev_loop_new(pgagroal_libev(config->libev)); + + ev_signal_init((struct ev_signal*)&signal_watcher, signal_cb, SIGQUIT); + signal_watcher.slot = slot; + ev_signal_start(loop, (struct ev_signal*)&signal_watcher); + + p.start(loop, &client_io); + started = true; + + ev_io_start(loop, (struct ev_io*)&client_io); + if (config->pipeline != PIPELINE_TRANSACTION) + { + ev_io_start(loop, (struct ev_io*)&server_io); + } + + while (running) + { + ev_loop(loop, 0); + } + + if (config->pipeline == PIPELINE_TRANSACTION) + { + /* The slot may have been updated */ + slot = client_io.slot; + } + + pgagroal_prometheus_client_active_sub(); + } + else + { + if (config->common.log_connections) + { + pgagroal_log_info("connect: address=%s", address); + } + pgagroal_prometheus_client_wait_sub(); + } + + if (config->common.log_disconnections) + { + if (auth_status == AUTH_SUCCESS) + { + pgagroal_log_info("disconnect: user=%s database=%s address=%s", config->connections[slot].username, + config->connections[slot].database, address); + } + else + { + pgagroal_log_info("disconnect: address=%s", address); + } + } + + /* Return to pool */ + if (slot != -1) + { + if (started) + { + p.stop(loop, &client_io); + + pgagroal_prometheus_session_time(difftime(time(NULL), start_time)); + } + + if ((auth_status == AUTH_SUCCESS || auth_status == AUTH_BAD_PASSWORD) && + (exit_code == WORKER_SUCCESS || exit_code == WORKER_CLIENT_FAILURE || + (exit_code == WORKER_FAILURE && config->connections[slot].has_security != SECURITY_INVALID))) + { + if (config->pipeline != PIPELINE_TRANSACTION) + { + pgagroal_tracking_event_socket(TRACKER_SOCKET_DISASSOCIATE_SERVER, config->connections[slot].fd); + pgagroal_tracking_event_slot(TRACKER_WORKER_RETURN1, slot); + pgagroal_return_connection(slot, server_ssl, tx_pool); + } + } + else if (exit_code == WORKER_SERVER_FAILURE || exit_code == WORKER_SERVER_FATAL || exit_code == WORKER_SHUTDOWN || exit_code == WORKER_FAILOVER || + (exit_code == WORKER_FAILURE && config->connections[slot].has_security == SECURITY_INVALID)) + { + pgagroal_tracking_event_socket(TRACKER_SOCKET_DISASSOCIATE_SERVER, config->connections[slot].fd); + pgagroal_tracking_event_slot(TRACKER_WORKER_KILL1, slot); + pgagroal_kill_connection(slot, server_ssl); + } + else + { + if (pgagroal_socket_isvalid(config->connections[slot].fd) && + pgagroal_connection_isvalid(config->connections[slot].fd) && + config->connections[slot].has_security != SECURITY_INVALID) + { + pgagroal_tracking_event_socket(TRACKER_SOCKET_DISASSOCIATE_SERVER, config->connections[slot].fd); + pgagroal_tracking_event_slot(TRACKER_WORKER_RETURN2, slot); + pgagroal_return_connection(slot, server_ssl, tx_pool); + } + else + { + pgagroal_tracking_event_socket(TRACKER_SOCKET_DISASSOCIATE_SERVER, config->connections[slot].fd); + pgagroal_tracking_event_slot(TRACKER_WORKER_KILL2, slot); + pgagroal_kill_connection(slot, server_ssl); + } + } + } + + if (pgagroal_connection_get(&transfer_fd)) + { + pgagroal_log_error("pgagroal_workers: Unable to get a transfer connection"); + } + + if (pgagroal_connection_id_write(transfer_fd, CONNECTION_CLIENT_DONE)) + { + pgagroal_log_error("pgagroal_workers: Unable to write to a transfer connection"); + } + + if (pgagroal_connection_pid_write(transfer_fd, getpid())) + { + pgagroal_log_error("pgagroal_workers: Unable to write to a transfer connection"); + } + + pgagroal_disconnect(transfer_fd); + + if (client_ssl != NULL) + { + int res; + SSL_CTX* ctx = SSL_get_SSL_CTX(client_ssl); + res = SSL_shutdown(client_ssl); + if (res == 0) + { + SSL_shutdown(client_ssl); + } + SSL_free(client_ssl); + SSL_CTX_free(ctx); + } + + pgagroal_log_debug("client disconnect: %d", client_fd); + pgagroal_tracking_event_socket(TRACKER_SOCKET_DISASSOCIATE_CLIENT, client_fd); + pgagroal_disconnect(client_fd); + + pgagroal_prometheus_client_sockets_sub(); + pgagroal_prometheus_query_count_specified_reset(slot); + + pgagroal_pool_status(); + pgagroal_log_debug("After client: PID %d Slot %d (%d)", getpid(), slot, exit_code); + + if (loop) + { + ev_io_stop(loop, (struct ev_io*)&client_io); + if (config->pipeline != PIPELINE_TRANSACTION) + { + ev_io_stop(loop, (struct ev_io*)&server_io); + } + + ev_signal_stop(loop, (struct ev_signal*)&signal_watcher); + + ev_loop_destroy(loop); + } + + free(address); + + pgagroal_tracking_event_basic(TRACKER_CLIENT_STOP, NULL, NULL); + + pgagroal_memory_destroy(); + pgagroal_stop_logging(); + + exit(exit_code); +} + +static void +signal_cb(struct ev_loop* loop, ev_signal* w, int revents) +{ + struct signal_info* si; + + si = (struct signal_info*)w; + + pgagroal_log_debug("pgagroal: signal %d for slot %d", si->signal.signum, si->slot); + + exit_code = WORKER_SHUTDOWN; + running = 0; + ev_break(loop, EVBREAK_ALL); +} diff --git a/src/libpgagroal/zstandard_compression.c b/src/libpgagroal/zstandard_compression.c new file mode 100644 index 00000000..36c24860 --- /dev/null +++ b/src/libpgagroal/zstandard_compression.c @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include +#include + +#define ZSTD_DEFAULT_NUMBER_OF_WORKERS 4 + +int +pgagroal_zstdc_string(char* s, unsigned char** buffer, size_t* buffer_size) +{ + size_t input_size; + size_t max_compressed_size; + size_t compressed_size; + + input_size = strlen(s); + max_compressed_size = ZSTD_compressBound(input_size); + + *buffer = (unsigned char*)malloc(max_compressed_size); + if (*buffer == NULL) + { + pgagroal_log_error("ZSTD: Allocation failed"); + return 1; + } + + compressed_size = ZSTD_compress(*buffer, max_compressed_size, s, input_size, 1); + if (ZSTD_isError(compressed_size)) + { + pgagroal_log_error("ZSTD: Compression error: %s", ZSTD_getErrorName(compressed_size)); + free(*buffer); + return 1; + } + + *buffer_size = compressed_size; + + return 0; +} + +int +pgagroal_zstdd_string(unsigned char* compressed_buffer, size_t compressed_size, char** output_string) +{ + size_t decompressed_size; + size_t result; + + decompressed_size = ZSTD_getFrameContentSize(compressed_buffer, compressed_size); + + if (decompressed_size == ZSTD_CONTENTSIZE_ERROR) + { + pgagroal_log_error("ZSTD: Not a valid compressed buffer"); + return 1; + } + if (decompressed_size == ZSTD_CONTENTSIZE_UNKNOWN) + { + pgagroal_log_error("ZSTD: Unknown decompressed size"); + return 1; + } + + *output_string = (char*)malloc(decompressed_size + 1); + if (*output_string == NULL) + { + pgagroal_log_error("ZSTD: Allocation failed"); + return 1; + } + + result = ZSTD_decompress(*output_string, decompressed_size, compressed_buffer, compressed_size); + if (ZSTD_isError(result)) + { + pgagroal_log_error("ZSTD: Compression error: %s", ZSTD_getErrorName(result)); + free(*output_string); + return 1; + } + + (*output_string)[decompressed_size] = '\0'; + + return 0; +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 00000000..aab3d117 --- /dev/null +++ b/src/main.c @@ -0,0 +1,2718 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_LINUX +#include +#endif + +#define MAX_FDS 64 + +static void accept_main_cb(struct ev_loop* loop, struct ev_io* watcher, int revents); +static void accept_mgt_cb(struct ev_loop* loop, struct ev_io* watcher, int revents); +static void accept_transfer_cb(struct ev_loop* loop, struct ev_io* watcher, int revents); +static void accept_metrics_cb(struct ev_loop* loop, struct ev_io* watcher, int revents); +static void accept_management_cb(struct ev_loop* loop, struct ev_io* watcher, int revents); +static void shutdown_cb(struct ev_loop* loop, ev_signal* w, int revents); +static void reload_cb(struct ev_loop* loop, ev_signal* w, int revents); +static void graceful_cb(struct ev_loop* loop, ev_signal* w, int revents); +static void coredump_cb(struct ev_loop* loop, ev_signal* w, int revents); +static void idle_timeout_cb(struct ev_loop* loop, ev_periodic* w, int revents); +static void max_connection_age_cb(struct ev_loop* loop, ev_periodic* w, int revents); +static void rotate_frontend_password_cb(struct ev_loop* loop, ev_periodic* w, int revents); +static void validation_cb(struct ev_loop* loop, ev_periodic* w, int revents); +static void disconnect_client_cb(struct ev_loop* loop, ev_periodic* w, int revents); +static void frontend_user_password_startup(struct main_configuration* config); +static bool accept_fatal(int error); +static void add_client(pid_t pid); +static void remove_client(pid_t pid); +static bool reload_configuration(void); +static void create_pidfile_or_exit(void); +static void remove_pidfile(void); +static void shutdown_ports(void); + +static volatile int keep_running = 1; +static char** argv_ptr; +static struct ev_loop* main_loop = NULL; +static struct accept_io io_main[MAX_FDS]; +static struct accept_io io_mgt; +static struct accept_io io_uds; +static int* main_fds = NULL; +static int main_fds_length = -1; +static int unix_management_socket = -1; +static int unix_transfer_socket = -1; +static int unix_pgsql_socket = -1; +static struct accept_io io_metrics[MAX_FDS]; +static int* metrics_fds = NULL; +static int metrics_fds_length = -1; +static struct accept_io io_management[MAX_FDS]; +static int* management_fds = NULL; +static int management_fds_length = -1; +static struct pipeline main_pipeline; +static int known_fds[MAX_NUMBER_OF_CONNECTIONS]; +static struct client* clients = NULL; +static struct accept_io io_transfer; + +static void +start_mgt(void) +{ + memset(&io_mgt, 0, sizeof(struct accept_io)); + ev_io_init((struct ev_io*)&io_mgt, accept_mgt_cb, unix_management_socket, EV_READ); + io_mgt.socket = unix_management_socket; + io_mgt.argv = argv_ptr; + ev_io_start(main_loop, (struct ev_io*)&io_mgt); +} + +static void +shutdown_mgt(void) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + ev_io_stop(main_loop, (struct ev_io*)&io_mgt); + pgagroal_disconnect(unix_management_socket); + errno = 0; + pgagroal_remove_unix_socket(config->unix_socket_dir, MAIN_UDS); + errno = 0; +} + +static void +start_transfer(void) +{ + memset(&io_transfer, 0, sizeof(struct accept_io)); + ev_io_init((struct ev_io*)&io_transfer, accept_transfer_cb, unix_transfer_socket, EV_READ); + io_transfer.socket = unix_transfer_socket; + io_transfer.argv = argv_ptr; + ev_io_start(main_loop, (struct ev_io*)&io_transfer); +} + +static void +shutdown_transfer(void) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + ev_io_stop(main_loop, (struct ev_io*)&io_transfer); + pgagroal_disconnect(unix_transfer_socket); + errno = 0; + pgagroal_remove_unix_socket(config->unix_socket_dir, TRANSFER_UDS); + errno = 0; +} + +static void +start_uds(void) +{ + memset(&io_uds, 0, sizeof(struct accept_io)); + ev_io_init((struct ev_io*)&io_uds, accept_main_cb, unix_pgsql_socket, EV_READ); + io_uds.socket = unix_pgsql_socket; + io_uds.argv = argv_ptr; + ev_io_start(main_loop, (struct ev_io*)&io_uds); +} + +static void +shutdown_uds(void) +{ + char pgsql[MISC_LENGTH]; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + memset(&pgsql, 0, sizeof(pgsql)); + snprintf(&pgsql[0], sizeof(pgsql), ".s.PGSQL.%d", config->common.port); + + ev_io_stop(main_loop, (struct ev_io*)&io_uds); + pgagroal_disconnect(unix_pgsql_socket); + errno = 0; + pgagroal_remove_unix_socket(config->unix_socket_dir, &pgsql[0]); + errno = 0; +} + +static void +start_io(void) +{ + for (int i = 0; i < main_fds_length; i++) + { + int sockfd = *(main_fds + i); + + memset(&io_main[i], 0, sizeof(struct accept_io)); + ev_io_init((struct ev_io*)&io_main[i], accept_main_cb, sockfd, EV_READ); + io_main[i].socket = sockfd; + io_main[i].argv = argv_ptr; + ev_io_start(main_loop, (struct ev_io*)&io_main[i]); + } +} + +static void +shutdown_io(void) +{ + for (int i = 0; i < main_fds_length; i++) + { + ev_io_stop(main_loop, (struct ev_io*)&io_main[i]); + pgagroal_disconnect(io_main[i].socket); + errno = 0; + } +} + +static void +start_metrics(void) +{ + for (int i = 0; i < metrics_fds_length; i++) + { + int sockfd = *(metrics_fds + i); + + memset(&io_metrics[i], 0, sizeof(struct accept_io)); + ev_io_init((struct ev_io*)&io_metrics[i], accept_metrics_cb, sockfd, EV_READ); + io_metrics[i].socket = sockfd; + io_metrics[i].argv = argv_ptr; + ev_io_start(main_loop, (struct ev_io*)&io_metrics[i]); + } +} + +static void +shutdown_metrics(void) +{ + for (int i = 0; i < metrics_fds_length; i++) + { + ev_io_stop(main_loop, (struct ev_io*)&io_metrics[i]); + pgagroal_disconnect(io_metrics[i].socket); + errno = 0; + } +} + +static void +start_management(void) +{ + for (int i = 0; i < management_fds_length; i++) + { + int sockfd = *(management_fds + i); + + memset(&io_management[i], 0, sizeof(struct accept_io)); + ev_io_init((struct ev_io*)&io_management[i], accept_management_cb, sockfd, EV_READ); + io_management[i].socket = sockfd; + io_management[i].argv = argv_ptr; + ev_io_start(main_loop, (struct ev_io*)&io_management[i]); + } +} + +static void +shutdown_management(void) +{ + for (int i = 0; i < management_fds_length; i++) + { + ev_io_stop(main_loop, (struct ev_io*)&io_management[i]); + pgagroal_disconnect(io_management[i].socket); + errno = 0; + } +} + +static void +version(void) +{ + printf("pgagroal %s\n", PGAGROAL_VERSION); + exit(1); +} + +static void +usage(void) +{ + printf("pgagroal %s\n", PGAGROAL_VERSION); + printf(" High-performance connection pool for PostgreSQL\n"); + printf("\n"); + + printf("Usage:\n"); + printf(" pgagroal [ -c CONFIG_FILE ] [ -a HBA_FILE ] [ -d ]\n"); + printf("\n"); + printf("Options:\n"); + printf(" -c, --config CONFIG_FILE Set the path to the pgagroal.conf file\n"); + printf(" Default: %s\n", PGAGROAL_DEFAULT_CONF_FILE); + printf(" -a, --hba HBA_FILE Set the path to the pgagroal_hba.conf file\n"); + printf(" Default: %s\n", PGAGROAL_DEFAULT_HBA_FILE); + printf(" -l, --limit LIMIT_FILE Set the path to the pgagroal_databases.conf file\n"); + printf(" Default: %s\n", PGAGROAL_DEFAULT_LIMIT_FILE); + printf(" -u, --users USERS_FILE Set the path to the pgagroal_users.conf file\n"); + printf(" Default: %s\n", PGAGROAL_DEFAULT_USERS_FILE); + printf(" -F, --frontend FRONTEND_USERS_FILE Set the path to the pgagroal_frontend_users.conf file\n"); + printf(" Default: %s\n", PGAGROAL_DEFAULT_FRONTEND_USERS_FILE); + printf(" -A, --admins ADMINS_FILE Set the path to the pgagroal_admins.conf file\n"); + printf(" Default: %s\n", PGAGROAL_DEFAULT_ADMINS_FILE); + printf(" -S, --superuser SUPERUSER_FILE Set the path to the pgagroal_superuser.conf file\n"); + printf(" Default: %s\n", PGAGROAL_DEFAULT_SUPERUSER_FILE); + printf(" -d, --daemon Run as a daemon\n"); + printf(" -V, --version Display version information\n"); + printf(" -?, --help Display help\n"); + printf("\n"); + printf("pgagroal: %s\n", PGAGROAL_HOMEPAGE); + printf("Report bugs: %s\n", PGAGROAL_ISSUES); +} + +int +main(int argc, char** argv) +{ + char* configuration_path = NULL; + char* hba_path = NULL; + char* limit_path = NULL; + char* users_path = NULL; + char* frontend_users_path = NULL; + char* admins_path = NULL; + char* superuser_path = NULL; + bool daemon = false; + pid_t pid, sid; +#ifdef HAVE_LINUX + int sds; +#endif + bool has_unix_socket = false; + bool has_main_sockets = false; + void* tmp_shmem = NULL; + struct signal_info signal_watcher[6]; + struct ev_periodic idle_timeout; + struct ev_periodic max_connection_age; + struct ev_periodic validation; + struct ev_periodic disconnect_client; + struct ev_periodic rotate_frontend_password; + struct rlimit flimit; + size_t shmem_size; + size_t pipeline_shmem_size = 0; + size_t prometheus_shmem_size = 0; + size_t prometheus_cache_shmem_size = 0; + size_t tmp_size; + struct main_configuration* config = NULL; + int ret; + int c; + bool conf_file_mandatory; + char message[MISC_LENGTH]; // a generic message used for errors + argv_ptr = argv; + + while (1) + { + static struct option long_options[] = + { + {"config", required_argument, 0, 'c'}, + {"hba", required_argument, 0, 'a'}, + {"limit", required_argument, 0, 'l'}, + {"users", required_argument, 0, 'u'}, + {"frontend", required_argument, 0, 'F'}, + {"admins", required_argument, 0, 'A'}, + {"superuser", required_argument, 0, 'S'}, + {"daemon", no_argument, 0, 'd'}, + {"version", no_argument, 0, 'V'}, + {"help", no_argument, 0, '?'} + }; + int option_index = 0; + + c = getopt_long (argc, argv, "dV?a:c:l:u:F:A:S:", + long_options, &option_index); + + if (c == -1) + { + break; + } + + switch (c) + { + case 'a': + hba_path = optarg; + break; + case 'c': + configuration_path = optarg; + break; + case 'l': + limit_path = optarg; + break; + case 'u': + users_path = optarg; + break; + case 'F': + frontend_users_path = optarg; + break; + case 'A': + admins_path = optarg; + break; + case 'S': + superuser_path = optarg; + break; + case 'd': + daemon = true; + break; + case 'V': + version(); + break; + case '?': + usage(); + exit(1); + break; + default: + break; + } + } + + if (getuid() == 0) + { +#ifdef HAVE_LINUX + sd_notify(0, "STATUS=Using the root account is not allowed"); +#endif + errx(1, "Using the root account is not allowed"); + } + + shmem_size = sizeof(struct main_configuration); + if (pgagroal_create_shared_memory(shmem_size, HUGEPAGE_OFF, &shmem)) + { +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Error in creating shared memory"); +#endif + errx(1, "Error in creating shared memory"); + } + + pgagroal_init_configuration(shmem); + config = (struct main_configuration*)shmem; + + memset(&known_fds, 0, sizeof(known_fds)); + memset(message, 0, MISC_LENGTH); + + // the main configuration file is mandatory! + configuration_path = configuration_path != NULL ? configuration_path : PGAGROAL_DEFAULT_CONF_FILE; + if ((ret = pgagroal_read_configuration(shmem, configuration_path, true)) != PGAGROAL_CONFIGURATION_STATUS_OK) + { + // the configuration has some problem, build up a descriptive message + if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND) + { + snprintf(message, MISC_LENGTH, "Configuration file not found"); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG) + { + snprintf(message, MISC_LENGTH, "Too many sections"); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_KO) + { + snprintf(message, MISC_LENGTH, "Invalid configuration file"); + } + else if (ret > 0) + { + snprintf(message, MISC_LENGTH, "%d problematic or duplicated section%c", + ret, + ret > 1 ? 's' : ' '); + } + +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=%s: %s", message, configuration_path); +#endif + errx(1, "%s (file <%s>)", message, configuration_path); + } + + memcpy(&config->common.configuration_path[0], configuration_path, MIN(strlen(configuration_path), MAX_PATH - 1)); + + // the HBA file is mandatory! + hba_path = hba_path != NULL ? hba_path : PGAGROAL_DEFAULT_HBA_FILE; + memset(message, 0, MISC_LENGTH); + ret = pgagroal_read_hba_configuration(shmem, hba_path); + if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND) + { + snprintf(message, MISC_LENGTH, "HBA configuration file not found"); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=%s: %s", message, hba_path); +#endif + errx(1, "%s (file <%s>)", message, hba_path); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG) + { + snprintf(message, MISC_LENGTH, "HBA too many entries (max %d)", NUMBER_OF_HBAS); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=%s: %s", message, hba_path); +#endif + + errx(1, "%s (file <%s>)", message, hba_path); + } + + memcpy(&config->hba_path[0], hba_path, MIN(strlen(hba_path), MAX_PATH - 1)); + + conf_file_mandatory = true; +read_limit_path: + if (limit_path != NULL) + { + memset(message, 0, MISC_LENGTH); + ret = pgagroal_read_limit_configuration(shmem, limit_path); + if (ret == PGAGROAL_CONFIGURATION_STATUS_OK) + { + memcpy(&config->limit_path[0], limit_path, MIN(strlen(limit_path), MAX_PATH - 1)); + } + else if (conf_file_mandatory && ret == PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND) + { + + snprintf(message, MISC_LENGTH, "LIMIT configuration file not found"); + printf("pgagroal: %s (file <%s>)\n", message, limit_path); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=%s: %s", message, limit_path); +#endif + exit(1); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG) + { + + snprintf(message, MISC_LENGTH, "Too many limit entries"); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=%s: %s", message, limit_path); +#endif + errx(1, "%s (file <%s>)", message, limit_path); + } + + } + else + { + // the user did not specify a file on the command line + // so try the default one and allow it to be missing + limit_path = PGAGROAL_DEFAULT_LIMIT_FILE; + conf_file_mandatory = false; + goto read_limit_path; + } + + conf_file_mandatory = true; +read_users_path: + if (users_path != NULL) + { + memset(message, 0, MISC_LENGTH); + ret = pgagroal_read_users_configuration(shmem, users_path); + if (ret == PGAGROAL_CONFIGURATION_STATUS_OK) + { + memcpy(&config->users_path[0], users_path, MIN(strlen(users_path), MAX_PATH - 1)); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND && conf_file_mandatory) + { + + snprintf(message, MISC_LENGTH, "USERS configuration file not found"); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=%s : %s", message, users_path); +#endif + errx(1, "%s (file <%s>)", message, users_path); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_KO + || ret == PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT) + { + + snprintf(message, MISC_LENGTH, "Invalid master key file"); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=%s: <%s>", message, users_path); +#endif + errx(1, "%s (file <%s>)", message, users_path); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG) + { + + snprintf(message, MISC_LENGTH, "USERS: too many users defined (%d, max %d)", config->number_of_users, NUMBER_OF_USERS); + +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=%s: %s", message, users_path); +#endif + errx(1, "%s (file <%s>)", message, users_path); + } + } + else + { + // the user did not specify a file on the command line + // so try the default one and allow it to be missing + users_path = PGAGROAL_DEFAULT_USERS_FILE; + conf_file_mandatory = false; + goto read_users_path; + } + + conf_file_mandatory = true; +read_frontend_users_path: + if (frontend_users_path != NULL) + { + ret = pgagroal_read_frontend_users_configuration(shmem, frontend_users_path); + if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND && conf_file_mandatory) + { + memset(message, 0, MISC_LENGTH); + snprintf(message, MISC_LENGTH, "FRONTEND USERS configuration file not found"); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=%s: %s", message, frontend_users_path); +#endif + errx(1, "%s (file <%s>)", message, frontend_users_path); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT + || ret == PGAGROAL_CONFIGURATION_STATUS_KO) + { +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Invalid master key file: <%s>", frontend_users_path); +#endif + errx(1, "%s (file <%s>)", message, frontend_users_path); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG) + { + memset(message, 0, MISC_LENGTH); + snprintf(message, MISC_LENGTH, "FRONTEND USERS: Too many users defined %d (max %d)", + config->number_of_frontend_users, NUMBER_OF_USERS); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=%s: %s", message, frontend_users_path); +#endif + errx(1, "%s (file <%s>)", message, frontend_users_path); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_OK) + { + memcpy(&config->frontend_users_path[0], frontend_users_path, MIN(strlen(frontend_users_path), MAX_PATH - 1)); + } + } + else + { + // the user did not specify a file on the command line + // so try the default one and allow it to be missing + frontend_users_path = PGAGROAL_DEFAULT_FRONTEND_USERS_FILE; + conf_file_mandatory = false; + goto read_frontend_users_path; + } + + conf_file_mandatory = true; +read_admins_path: + if (admins_path != NULL) + { + memset(message, 0, MISC_LENGTH); + ret = pgagroal_read_admins_configuration(shmem, admins_path); + if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND && conf_file_mandatory) + { + + snprintf(message, MISC_LENGTH, "ADMINS configuration file not found"); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=%s: %s", message, admins_path); +#endif + errx(1, "%s (file <%s>)", message, admins_path); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT + || ret == PGAGROAL_CONFIGURATION_STATUS_KO) + { +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Invalid master key file: <%s>", admins_path); +#endif + errx(1, "Invalid master key file (file <%s>)", admins_path); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG) + { + snprintf(message, MISC_LENGTH, "Too many admins defined %d (max %d)", config->number_of_admins, NUMBER_OF_ADMINS); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=%s %s", message, admins_path); +#endif + errx(1, "%s (file <%s>)", message, admins_path); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_OK) + { + memcpy(&config->admins_path[0], admins_path, MIN(strlen(admins_path), MAX_PATH - 1)); + } + } + else + { + // the user did not specify a file on the command line + // so try the default one and allow it to be missing + admins_path = PGAGROAL_DEFAULT_ADMINS_FILE; + conf_file_mandatory = false; + goto read_admins_path; + } + + conf_file_mandatory = true; +read_superuser_path: + if (superuser_path != NULL) + { + ret = pgagroal_read_superuser_configuration(shmem, superuser_path); + memset(message, 0, MISC_LENGTH); + if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND && conf_file_mandatory) + { + snprintf(message, MISC_LENGTH, "SUPERUSER configuration file not found"); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=%s: %s", message, superuser_path); +#endif + errx(1, "%s (file <%s>)", message, superuser_path); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT || ret == PGAGROAL_CONFIGURATION_STATUS_KO) + { +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Invalid master key file: <%s>", superuser_path); +#endif + errx(1, "Invalid master key file (file <%s>)", superuser_path); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG) + { + snprintf(message, MISC_LENGTH, "SUPERUSER: Too many superusers defined (max 1)"); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=%s: %s", message, superuser_path); +#endif + errx(1, "%s (file <%s>)", message, superuser_path); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_OK) + { + memcpy(&config->superuser_path[0], superuser_path, MIN(strlen(superuser_path), MAX_PATH - 1)); + } + } + else + { + // the user did not specify a file on the command line + // so try the default one and allow it to be missing + superuser_path = PGAGROAL_DEFAULT_SUPERUSER_FILE; + conf_file_mandatory = false; + goto read_superuser_path; + } + + /* systemd sockets */ +#ifdef HAVE_LINUX + sds = sd_listen_fds(0); + if (sds > 0) + { + int m = 0; + + main_fds_length = 0; + + for (int i = 0; i < sds; i++) + { + int fd = SD_LISTEN_FDS_START + i; + + if (sd_is_socket(fd, AF_INET, 0, -1) || sd_is_socket(fd, AF_INET6, 0, -1)) + { + main_fds_length++; + } + } + + if (main_fds_length > 0) + { + main_fds = malloc(main_fds_length * sizeof(int)); + } + + for (int i = 0; i < sds; i++) + { + int fd = SD_LISTEN_FDS_START + i; + + if (sd_is_socket(fd, AF_UNIX, 0, -1)) + { + unix_pgsql_socket = fd; + has_unix_socket = true; + } + else if (sd_is_socket(fd, AF_INET, 0, -1) || sd_is_socket(fd, AF_INET6, 0, -1)) + { + *(main_fds + (m * sizeof(int))) = fd; + has_main_sockets = true; + m++; + } + } + } +#endif + + if (pgagroal_init_logging()) + { +#ifdef HAVE_LINUX + sd_notify(0, "STATUS=Failed to init logging"); +#endif + exit(1); + } + + if (pgagroal_start_logging()) + { +#ifdef HAVE_LINUX + sd_notify(0, "STATUS=Failed to start logging"); +#endif + errx(1, "Failed to start logging"); + } + + if (config->common.metrics > 0) + { + if (pgagroal_init_prometheus(&prometheus_shmem_size, &prometheus_shmem)) + { + #ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Error in creating and initializing prometheus shared memory"); + #endif + errx(1, "Error in creating and initializing prometheus shared memory"); + } + + if (pgagroal_init_prometheus_cache(&prometheus_cache_shmem_size, &prometheus_cache_shmem)) + { + #ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Error in creating and initializing prometheus cache shared memory"); + #endif + errx(1, "Error in creating and initializing prometheus cache shared memory"); + } + } + + if (pgagroal_validate_configuration(shmem, has_unix_socket, has_main_sockets)) + { +#ifdef HAVE_LINUX + sd_notify(0, "STATUS=Invalid configuration"); +#endif + errx(1, "Invalid configuration"); + } + + frontend_user_password_startup(config); + + if (pgagroal_validate_hba_configuration(shmem)) + { +#ifdef HAVE_LINUX + sd_notify(0, "STATUS=Invalid HBA configuration"); +#endif + errx(1, "Invalid HBA configuration"); + } + if (pgagroal_validate_limit_configuration(shmem)) + { +#ifdef HAVE_LINUX + sd_notify(0, "STATUS=Invalid LIMIT configuration"); +#endif + errx(1, "Invalid LIMIT configuration"); + } + if (pgagroal_validate_users_configuration(shmem)) + { +#ifdef HAVE_LINUX + sd_notify(0, "STATUS=Invalid USERS configuration"); +#endif + errx(1, "Invalid USERS configuration"); + } + if (pgagroal_validate_frontend_users_configuration(shmem)) + { +#ifdef HAVE_LINUX + sd_notify(0, "STATUS=Invalid FRONTEND USERS configuration"); +#endif + errx(1, "Invalid FRONTEND USERS configuration"); + } + if (pgagroal_validate_admins_configuration(shmem)) + { +#ifdef HAVE_LINUX + sd_notify(0, "STATUS=Invalid ADMINS configuration"); +#endif + errx(1, "Invalid ADMINS configuration"); + } + + if (pgagroal_resize_shared_memory(shmem_size, shmem, &tmp_size, &tmp_shmem)) + { +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Error in creating shared memory"); +#endif + errx(1, "Error in creating shared memory"); + } + if (pgagroal_destroy_shared_memory(shmem, shmem_size) == -1) + { +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Error in destroying shared memory"); +#endif + errx(1, "Error in destroying shared memory"); + } + shmem_size = tmp_size; + shmem = tmp_shmem; + config = (struct main_configuration*)shmem; + + pgagroal_memory_init(); + + if (getrlimit(RLIMIT_NOFILE, &flimit) == -1) + { +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Unable to find limit due to %s", strerror(errno)); +#endif + err(1, "Unable to find limit"); + } + + /* We are "reserving" 30 file descriptors for pgagroal main */ + if (config->max_connections > (flimit.rlim_cur - 30)) + { +#ifdef HAVE_LINUX + sd_notifyf(0, + "STATUS=max_connections is larger than the file descriptor limit (%ld available)", + (long)(flimit.rlim_cur - 30)); +#endif + errx(1, "max_connections is larger than the file descriptor limit (%ld available)", (long)(flimit.rlim_cur - 30)); + } + + if (daemon) + { + if (config->common.log_type == PGAGROAL_LOGGING_TYPE_CONSOLE) + { +#ifdef HAVE_LINUX + sd_notify(0, "STATUS=Daemon mode can't be used with console logging"); +#endif + errx(1, "Daemon mode can't be used with console logging"); + } + + pid = fork(); + + if (pid < 0) + { +#ifdef HAVE_LINUX + sd_notify(0, "STATUS=Daemon mode failed"); +#endif + errx(1, "Daemon mode failed"); + } + + if (pid > 0) + { + exit(0); + } + + /* We are a daemon now */ + umask(0); + sid = setsid(); + + if (sid < 0) + { + exit(1); + } + } + + create_pidfile_or_exit(); + + pgagroal_pool_init(); + pgagroal_initialize_random(); + + pgagroal_set_proc_title(argc, argv, "main", NULL); + + /* Bind Unix Domain Socket: Main */ + if (pgagroal_bind_unix_socket(config->unix_socket_dir, MAIN_UDS, &unix_management_socket)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s/%s", config->unix_socket_dir, MAIN_UDS); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Could not bind to %s/%s", config->unix_socket_dir, MAIN_UDS); +#endif + goto error; + } + + /* Bind Unix Domain Socket: Transfer */ + if (pgagroal_bind_unix_socket(config->unix_socket_dir, TRANSFER_UDS, &unix_transfer_socket)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s/%s", config->unix_socket_dir, TRANSFER_UDS); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Could not bind to %s/%s", config->unix_socket_dir, TRANSFER_UDS); +#endif + goto error; + } + + if (!has_unix_socket) + { + char pgsql[MISC_LENGTH]; + + memset(&pgsql, 0, sizeof(pgsql)); + snprintf(&pgsql[0], sizeof(pgsql), ".s.PGSQL.%d", config->common.port); + + if (pgagroal_bind_unix_socket(config->unix_socket_dir, &pgsql[0], &unix_pgsql_socket)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s/%s", config->unix_socket_dir, &pgsql[0]); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Could not bind to %s/%s", config->unix_socket_dir, &pgsql[0]); +#endif + goto error; + } + } + + /* Bind main socket */ + if (!has_main_sockets) + { + if (pgagroal_bind(config->common.host, config->common.port, &main_fds, &main_fds_length, config->non_blocking, config->nodelay, config->backlog)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s:%d", config->common.host, config->common.port); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Could not bind to %s:%d", config->common.host, config->common.port); +#endif + goto error; + } + } + + if (main_fds_length > MAX_FDS) + { + pgagroal_log_fatal("pgagroal: Too many descriptors %d", main_fds_length); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Too many descriptors %d", main_fds_length); +#endif + goto error; + } + + /* libev */ + main_loop = ev_default_loop(pgagroal_libev(config->libev)); + if (!main_loop) + { + pgagroal_log_fatal("pgagroal: No loop implementation (%x) (%x)", + pgagroal_libev(config->libev), ev_supported_backends()); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=No loop implementation (%x) (%x)", pgagroal_libev(config->libev), ev_supported_backends()); +#endif + goto error; + } + + ev_signal_init((struct ev_signal*)&signal_watcher[0], shutdown_cb, SIGTERM); + ev_signal_init((struct ev_signal*)&signal_watcher[1], reload_cb, SIGHUP); + ev_signal_init((struct ev_signal*)&signal_watcher[2], shutdown_cb, SIGINT); + ev_signal_init((struct ev_signal*)&signal_watcher[3], graceful_cb, SIGTRAP); + ev_signal_init((struct ev_signal*)&signal_watcher[4], coredump_cb, SIGABRT); + ev_signal_init((struct ev_signal*)&signal_watcher[5], shutdown_cb, SIGALRM); + + for (int i = 0; i < 6; i++) + { + signal_watcher[i].slot = -1; + ev_signal_start(main_loop, (struct ev_signal*)&signal_watcher[i]); + } + + if (config->pipeline == PIPELINE_PERFORMANCE) + { + main_pipeline = performance_pipeline(); + } + else if (config->pipeline == PIPELINE_SESSION) + { + if (pgagroal_tls_valid()) + { + pgagroal_log_fatal("pgagroal: Invalid TLS configuration"); +#ifdef HAVE_LINUX + sd_notify(0, "STATUS=Invalid TLS configuration"); +#endif + goto error; + } + + main_pipeline = session_pipeline(); + } + else if (config->pipeline == PIPELINE_TRANSACTION) + { + if (pgagroal_tls_valid()) + { + pgagroal_log_fatal("pgagroal: Invalid TLS configuration"); +#ifdef HAVE_LINUX + sd_notify(0, "STATUS=Invalid TLS configuration"); +#endif + goto error; + } + + main_pipeline = transaction_pipeline(); + } + else + { + pgagroal_log_fatal("pgagroal: Unknown pipeline identifier (%d)", config->pipeline); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Unknown pipeline identifier (%d)", config->pipeline); +#endif + goto error; + } + + if (main_pipeline.initialize(shmem, &pipeline_shmem, &pipeline_shmem_size)) + { + pgagroal_log_fatal("pgagroal: Pipeline initialize error (%d)", config->pipeline); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Pipeline initialize error (%d)", config->pipeline); +#endif + goto error; + } + + start_transfer(); + start_mgt(); + start_uds(); + start_io(); + + if (config->idle_timeout > 0) + { + ev_periodic_init (&idle_timeout, idle_timeout_cb, 0., + MAX(1. * config->idle_timeout / 2., 5.), 0); + ev_periodic_start (main_loop, &idle_timeout); + } + + if (config->max_connection_age > 0) + { + ev_periodic_init (&max_connection_age, max_connection_age_cb, 0., + MAX(1. * config->max_connection_age / 2., 5.), 0); + ev_periodic_start (main_loop, &max_connection_age); + } + + if (config->validation == VALIDATION_BACKGROUND) + { + ev_periodic_init (&validation, validation_cb, 0., + MAX(1. * config->background_interval, 5.), 0); + ev_periodic_start (main_loop, &validation); + } + + if (config->disconnect_client > 0) + { + ev_periodic_init (&disconnect_client, disconnect_client_cb, 0., + MIN(300., MAX(1. * config->disconnect_client / 2., 1.)), 0); + ev_periodic_start (main_loop, &disconnect_client); + } + + if (config->rotate_frontend_password_timeout > 0) + { + ev_periodic_init (&rotate_frontend_password, rotate_frontend_password_cb, 0., + config->rotate_frontend_password_timeout, 0); + ev_periodic_start (main_loop, &rotate_frontend_password); + } + + if (config->common.metrics > 0) + { + /* Bind metrics socket */ + if (pgagroal_bind(config->common.host, config->common.metrics, &metrics_fds, &metrics_fds_length, config->non_blocking, config->nodelay, config->backlog)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s:%d", config->common.host, config->common.metrics); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Could not bind to %s:%d", config->common.host, config->common.metrics); +#endif + goto error; + } + + if (metrics_fds_length > MAX_FDS) + { + pgagroal_log_fatal("pgagroal: Too many descriptors %d", metrics_fds_length); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Too many descriptors %d", metrics_fds_length); +#endif + goto error; + } + + start_metrics(); + } + + if (config->management > 0) + { + /* Bind management socket */ + if (pgagroal_bind(config->common.host, config->management, &management_fds, &management_fds_length, config->non_blocking, config->nodelay, config->backlog)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s:%d", config->common.host, config->management); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Could not bind to %s:%d", config->common.host, config->management); +#endif + goto error; + } + + if (management_fds_length > MAX_FDS) + { + pgagroal_log_fatal("pgagroal: Too many descriptors %d", management_fds_length); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Too many descriptors %d", management_fds_length); +#endif + goto error; + } + + start_management(); + } + + pgagroal_log_info("pgagroal: %s started on %s:%d", + PGAGROAL_VERSION, + config->common.host, + config->common.port); + for (int i = 0; i < main_fds_length; i++) + { + pgagroal_log_debug("Socket: %d", *(main_fds + i)); + } + pgagroal_log_debug("Unix Domain Socket: %d", unix_pgsql_socket); + pgagroal_log_debug("Management: %d", unix_management_socket); + pgagroal_log_debug("Transfer: %d", unix_transfer_socket); + for (int i = 0; i < metrics_fds_length; i++) + { + pgagroal_log_debug("Metrics: %d", *(metrics_fds + i)); + } + for (int i = 0; i < management_fds_length; i++) + { + pgagroal_log_debug("Remote management: %d", *(management_fds + i)); + } + pgagroal_libev_engines(); + pgagroal_log_debug("libev engine: %s", pgagroal_libev_engine(ev_backend(main_loop))); + pgagroal_log_debug("Pipeline: %d", config->pipeline); + pgagroal_log_debug("Pipeline size: %lu", pipeline_shmem_size); + pgagroal_log_debug("%s", OpenSSL_version(OPENSSL_VERSION)); + pgagroal_log_debug("Configuration size: %lu", shmem_size); + pgagroal_log_debug("Max connections: %d", config->max_connections); + pgagroal_log_debug("Known users: %d", config->number_of_users); + pgagroal_log_debug("Known frontend users: %d", config->number_of_frontend_users); + pgagroal_log_debug("Known admins: %d", config->number_of_admins); + pgagroal_log_debug("Known superuser: %s", strlen(config->superuser.username) > 0 ? "Yes" : "No"); + + if (!config->allow_unknown_users && config->number_of_users == 0) + { + pgagroal_log_warn("No users allowed"); + } + + if (pgagroal_can_prefill()) + { + if (!fork()) + { + shutdown_ports(); + pgagroal_prefill_if_can(false, true); + } + } + +#ifdef HAVE_LINUX + sd_notifyf(0, + "READY=1\n" + "STATUS=Running\n" + "MAINPID=%lu", (unsigned long)getpid()); +#endif + + while (keep_running) + { + ev_loop(main_loop, 0); + } + + pgagroal_log_info("pgagroal: shutdown"); +#ifdef HAVE_LINUX + sd_notify(0, "STOPPING=1"); +#endif + pgagroal_pool_shutdown(); + + if (clients != NULL) + { + struct client* c = clients; + while (c != NULL) + { + kill(c->pid, SIGQUIT); + c = c->next; + } + } + + shutdown_management(); + shutdown_metrics(); + shutdown_mgt(); + shutdown_transfer(); + shutdown_io(); + shutdown_uds(); + + for (int i = 0; i < 6; i++) + { + ev_signal_stop(main_loop, (struct ev_signal*)&signal_watcher[i]); + } + + ev_loop_destroy(main_loop); + + free(main_fds); + free(metrics_fds); + free(management_fds); + + main_pipeline.destroy(pipeline_shmem, pipeline_shmem_size); + + remove_pidfile(); + + pgagroal_stop_logging(); + pgagroal_destroy_shared_memory(prometheus_shmem, prometheus_shmem_size); + pgagroal_destroy_shared_memory(prometheus_cache_shmem, prometheus_cache_shmem_size); + pgagroal_destroy_shared_memory(shmem, shmem_size); + + pgagroal_memory_destroy(); + + return 0; + +error: + remove_pidfile(); + exit(1); +} + +static void +accept_main_cb(struct ev_loop* loop, struct ev_io* watcher, int revents) +{ + struct sockaddr_in6 client_addr; + socklen_t client_addr_length; + int client_fd; + char address[INET6_ADDRSTRLEN]; + pid_t pid; + struct accept_io* ai; + struct main_configuration* config; + + if (EV_ERROR & revents) + { + pgagroal_log_debug("accept_main_cb: invalid event: %s", strerror(errno)); + errno = 0; + return; + } + + ai = (struct accept_io*)watcher; + config = (struct main_configuration*)shmem; + + errno = 0; + + memset(&address, 0, sizeof(address)); + + client_addr_length = sizeof(client_addr); + client_fd = accept(watcher->fd, (struct sockaddr*)&client_addr, &client_addr_length); + if (client_fd == -1) + { + if (accept_fatal(errno) && keep_running) + { + char pgsql[MISC_LENGTH]; + + pgagroal_log_warn("Restarting listening port due to: %s (%d)", strerror(errno), watcher->fd); + + shutdown_io(); + shutdown_uds(); + + memset(&pgsql, 0, sizeof(pgsql)); + snprintf(&pgsql[0], sizeof(pgsql), ".s.PGSQL.%d", config->common.port); + + if (pgagroal_bind_unix_socket(config->unix_socket_dir, &pgsql[0], &unix_pgsql_socket)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s/%s", config->unix_socket_dir, &pgsql[0]); + exit(1); + } + + free(main_fds); + main_fds = NULL; + main_fds_length = 0; + + if (pgagroal_bind(config->common.host, config->common.port, &main_fds, &main_fds_length, config->non_blocking, config->nodelay, config->backlog)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s:%d", config->common.host, config->common.port); + exit(1); + } + + if (main_fds_length > MAX_FDS) + { + pgagroal_log_fatal("pgagroal: Too many descriptors %d", main_fds_length); + exit(1); + } + + if (!fork()) + { + shutdown_ports(); + pgagroal_flush(FLUSH_GRACEFULLY, "*"); + exit(0); + } + + start_io(); + start_uds(); + + for (int i = 0; i < main_fds_length; i++) + { + pgagroal_log_debug("Socket: %d", *(main_fds + i)); + } + pgagroal_log_debug("Unix Domain Socket: %d", unix_pgsql_socket); + } + else + { + pgagroal_log_debug("accept: %s (%d)", strerror(errno), watcher->fd); + } + errno = 0; + return; + } + + pgagroal_prometheus_client_sockets_add(); + + pgagroal_get_address((struct sockaddr*)&client_addr, (char*)&address, sizeof(address)); + + pgagroal_log_trace("accept_main_cb: client address: %s", address); + + pid = fork(); + if (pid == -1) + { + /* No process */ + pgagroal_log_error("Cannot create process"); + } + else if (pid > 0) + { + add_client(pid); + } + else + { + char* addr = calloc(1, strlen(address) + 1); + if (addr == NULL) + { + pgagroal_log_fatal("Cannot allocate memory for client address"); + return; + } + memcpy(addr, address, strlen(address)); + + ev_loop_fork(loop); + shutdown_ports(); + /* We are leaving the socket descriptor valid such that the client won't reuse it */ + pgagroal_worker(client_fd, addr, ai->argv); + } + + pgagroal_disconnect(client_fd); +} + +static void +accept_mgt_cb(struct ev_loop* loop, struct ev_io* watcher, int revents) +{ + struct sockaddr_in6 client_addr; + socklen_t client_addr_length; + int client_fd; + int32_t id; + pid_t pid; + char* str = NULL; + time_t start_time; + time_t end_time; + uint8_t compression = MANAGEMENT_COMPRESSION_NONE; + uint8_t encryption = MANAGEMENT_ENCRYPTION_NONE; + struct accept_io* ai; + struct json* payload = NULL; + struct json* header = NULL; + struct main_configuration* config; + + if (EV_ERROR & revents) + { + pgagroal_log_trace("accept_mgt_cb: got invalid event: %s", strerror(errno)); + return; + } + + config = (struct main_configuration*)shmem; + ai = (struct accept_io*)watcher; + + errno = 0; + + client_addr_length = sizeof(client_addr); + client_fd = accept(watcher->fd, (struct sockaddr*)&client_addr, &client_addr_length); + + pgagroal_prometheus_self_sockets_add(); + + if (client_fd == -1) + { + if (accept_fatal(errno) && keep_running) + { + pgagroal_log_warn("Restarting management due to: %s (%d)", strerror(errno), watcher->fd); + + shutdown_mgt(); + + if (pgagroal_bind_unix_socket(config->unix_socket_dir, MAIN_UDS, &unix_management_socket)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s", config->unix_socket_dir); + exit(1); + } + + start_mgt(); + + pgagroal_log_debug("Management: %d", unix_management_socket); + } + else + { + pgagroal_log_debug("accept: %s (%d)", strerror(errno), watcher->fd); + } + errno = 0; + return; + } + + /* Process management request */ + if (pgagroal_management_read_json(NULL, client_fd, &compression, &encryption, &payload)) + { + pgagroal_management_response_error(NULL, client_fd, NULL, MANAGEMENT_ERROR_BAD_PAYLOAD, compression, encryption, NULL); + pgagroal_log_error("Management: Bad payload (%d)", MANAGEMENT_ERROR_BAD_PAYLOAD); + goto error; + } + + header = (struct json*)pgagroal_json_get(payload, MANAGEMENT_CATEGORY_HEADER); + id = (int32_t)pgagroal_json_get(header, MANAGEMENT_ARGUMENT_COMMAND); + + str = pgagroal_json_to_string(payload, FORMAT_JSON, NULL, 0); + pgagroal_log_debug("Management %d: %s", id, str); + + if (id == MANAGEMENT_FLUSH) + { + pgagroal_log_debug("pgagroal: Management flush"); + + pid = fork(); + if (pid == -1) + { + pgagroal_management_response_error(NULL, client_fd, NULL, MANAGEMENT_ERROR_FLUSH_NOFORK, compression, encryption, payload); + pgagroal_log_error("Flush: No fork (%d)", MANAGEMENT_ERROR_FLUSH_NOFORK); + goto error; + } + else if (pid == 0) + { + struct json* pyl = NULL; + + shutdown_ports(); + + pgagroal_json_clone(payload, &pyl); + + pgagroal_set_proc_title(1, ai->argv, "flush", NULL); + pgagroal_request_flush(NULL, client_fd, compression, encryption, pyl); + } + } + else if (id == MANAGEMENT_ENABLEDB) + { + struct json* req = NULL; + struct json* res = NULL; + struct json* databases = NULL; + char* database = NULL; + + pgagroal_log_debug("pgagroal: Management enabledb: "); + pgagroal_pool_status(); + + start_time = time(NULL); + + req = (struct json*)pgagroal_json_get(payload, MANAGEMENT_CATEGORY_REQUEST); + database = (char*)pgagroal_json_get(req, MANAGEMENT_ARGUMENT_DATABASE); + + pgagroal_management_create_response(payload, -1, &res); + pgagroal_json_create(&databases); + + if (!strcmp("*", database)) + { + struct json* js = NULL; + + config->all_disabled = false; + memset(&config->disabled, 0, sizeof(config->disabled)); + + pgagroal_json_create(&js); + + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_DATABASE, (uintptr_t)database, ValueString); + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_ENABLED, (uintptr_t)true, ValueBool); + + pgagroal_json_append(databases, (uintptr_t)js, ValueJSON); + } + else + { + bool found = false; + for (int i = 0; !found && i < NUMBER_OF_DISABLED; i++) + { + struct json* js = NULL; + + if (!strcmp(config->disabled[i], database)) + { + memset(&config->disabled[i], 0, MAX_DATABASE_LENGTH); + + pgagroal_json_create(&js); + + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_DATABASE, (uintptr_t)database, ValueString); + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_ENABLED, (uintptr_t)true, ValueBool); + + pgagroal_json_append(databases, (uintptr_t)js, ValueJSON); + + found = true; + } + } + } + + pgagroal_json_put(res, MANAGEMENT_ARGUMENT_DATABASES, (uintptr_t)databases, ValueJSON); + + end_time = time(NULL); + + pgagroal_management_response_ok(NULL, client_fd, start_time, end_time, compression, encryption, payload); + } + else if (id == MANAGEMENT_DISABLEDB) + { + struct json* req = NULL; + struct json* res = NULL; + struct json* databases = NULL; + char* database = NULL; + + pgagroal_log_debug("pgagroal: Management disabledb: "); + pgagroal_pool_status(); + + start_time = time(NULL); + + req = (struct json*)pgagroal_json_get(payload, MANAGEMENT_CATEGORY_REQUEST); + database = (char*)pgagroal_json_get(req, MANAGEMENT_ARGUMENT_DATABASE); + + pgagroal_management_create_response(payload, -1, &res); + pgagroal_json_create(&databases); + + config->all_disabled = false; + + if (!strcmp("*", database)) + { + struct json* js = NULL; + + config->all_disabled = true; + memset(&config->disabled, 0, sizeof(config->disabled)); + + pgagroal_json_create(&js); + + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_DATABASE, (uintptr_t)database, ValueString); + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_ENABLED, (uintptr_t)false, ValueBool); + + pgagroal_json_append(databases, (uintptr_t)js, ValueJSON); + } + else + { + bool inserted = false; + for (int i = 0; !inserted && i < NUMBER_OF_DISABLED; i++) + { + struct json* js = NULL; + + if (strlen(config->disabled[i]) == 0) + { + memcpy(&config->disabled[i], database, strlen(database) > MAX_DATABASE_LENGTH ? MAX_DATABASE_LENGTH : strlen(database)); + + pgagroal_json_create(&js); + + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_DATABASE, (uintptr_t)database, ValueString); + pgagroal_json_put(js, MANAGEMENT_ARGUMENT_ENABLED, (uintptr_t)false, ValueBool); + + pgagroal_json_append(databases, (uintptr_t)js, ValueJSON); + + inserted = true; + } + } + } + + pgagroal_json_put(res, MANAGEMENT_ARGUMENT_DATABASES, (uintptr_t)databases, ValueJSON); + + end_time = time(NULL); + + pgagroal_management_response_ok(NULL, client_fd, start_time, end_time, compression, encryption, payload); + } + else if (id == MANAGEMENT_GRACEFULLY) + { + pgagroal_log_debug("pgagroal: Management gracefully"); + pgagroal_pool_status(); + + start_time = time(NULL); + + config->gracefully = true; + + end_time = time(NULL); + + pgagroal_management_response_ok(NULL, client_fd, start_time, end_time, compression, encryption, payload); + } + else if (id == MANAGEMENT_SHUTDOWN) + { + pgagroal_log_debug("pgagroal: Management shutdown"); + pgagroal_pool_status(); + + start_time = time(NULL); + + end_time = time(NULL); + + pgagroal_management_response_ok(NULL, client_fd, start_time, end_time, compression, encryption, payload); + + ev_break(loop, EVBREAK_ALL); + keep_running = 0; + } + else if (id == MANAGEMENT_CANCEL_SHUTDOWN) + { + pgagroal_log_debug("pgagroal: Management cancel shutdown"); + pgagroal_pool_status(); + + start_time = time(NULL); + + config->gracefully = false; + + end_time = time(NULL); + + pgagroal_management_response_ok(NULL, client_fd, start_time, end_time, compression, encryption, payload); + } + else if (id == MANAGEMENT_STATUS) + { + pgagroal_log_debug("pgagroal: Management status"); + pgagroal_pool_status(); + + pid = fork(); + if (pid == -1) + { + pgagroal_management_response_error(NULL, client_fd, NULL, MANAGEMENT_ERROR_STATUS_NOFORK, compression, encryption, payload); + pgagroal_log_error("Status: No fork %s (%d)", NULL, MANAGEMENT_ERROR_STATUS_NOFORK); + goto error; + } + else if (pid == 0) + { + struct json* pyl = NULL; + + shutdown_ports(); + + pgagroal_json_clone(payload, &pyl); + + pgagroal_set_proc_title(1, ai->argv, "status", NULL); + pgagroal_status(NULL, client_fd, compression, encryption, pyl); + } + } + else if (id == MANAGEMENT_DETAILS) + { + pgagroal_log_debug("pgagroal: Management details"); + pgagroal_pool_status(); + + pid = fork(); + if (pid == -1) + { + pgagroal_management_response_error(NULL, client_fd, NULL, MANAGEMENT_ERROR_STATUS_NOFORK, compression, encryption, payload); + pgagroal_log_error("Status: No fork %s (%d)", NULL, MANAGEMENT_ERROR_STATUS_NOFORK); + goto error; + } + else if (pid == 0) + { + struct json* pyl = NULL; + + shutdown_ports(); + + pgagroal_json_clone(payload, &pyl); + + pgagroal_set_proc_title(1, ai->argv, "status", NULL); + pgagroal_status_details(NULL, client_fd, compression, encryption, pyl); + } + } + else if (id == MANAGEMENT_PING) + { + struct json* response = NULL; + + pgagroal_log_debug("pgagroal: Management ping"); + + start_time = time(NULL); + + pgagroal_management_create_response(payload, -1, &response); + + end_time = time(NULL); + + pgagroal_management_response_ok(NULL, client_fd, start_time, end_time, compression, encryption, payload); + } + else if (id == MANAGEMENT_CLEAR) + { + pgagroal_log_debug("pgagroal: Management clear"); + + start_time = time(NULL); + + pgagroal_prometheus_clear(); + + end_time = time(NULL); + + pgagroal_management_response_ok(NULL, client_fd, start_time, end_time, compression, encryption, payload); + + } + else if (id == MANAGEMENT_CLEAR_SERVER) + { + pgagroal_log_debug("pgagroal: Management clear server"); + char* server = NULL; + struct json* req = NULL; + + start_time = time(NULL); + + req = (struct json*)pgagroal_json_get(payload, MANAGEMENT_CATEGORY_REQUEST); + server = (char*)pgagroal_json_get(req, MANAGEMENT_ARGUMENT_SERVER); + + pgagroal_server_clear(server); + pgagroal_prometheus_failed_servers(); + + end_time = time(NULL); + + pgagroal_management_response_ok(NULL, client_fd, start_time, end_time, compression, encryption, payload); + } + else if (id == MANAGEMENT_SWITCH_TO) + { + pgagroal_log_debug("pgagroal: Management switch to"); + int old_primary = -1; + signed char server_state; + char* server = NULL; + struct json* req = NULL; + + start_time = time(NULL); + + req = (struct json*)pgagroal_json_get(payload, MANAGEMENT_CATEGORY_REQUEST); + server = (char*)pgagroal_json_get(req, MANAGEMENT_ARGUMENT_SERVER); + + for (int i = 0; old_primary == -1 && i < config->number_of_servers; i++) + { + server_state = atomic_load(&config->servers[i].state); + if (server_state == SERVER_PRIMARY) + { + old_primary = i; + } + } + + if (!pgagroal_server_switch(server)) + { + if (!fork()) + { + shutdown_ports(); + if (old_primary != -1) + { + pgagroal_flush_server(old_primary); + } + else + { + pgagroal_flush(FLUSH_GRACEFULLY, "*"); + exit(0); + } + } + pgagroal_prometheus_failed_servers(); + } + + end_time = time(NULL); + + pgagroal_management_response_ok(NULL, client_fd, start_time, end_time, compression, encryption, payload); + } + else if (id == MANAGEMENT_RELOAD) + { + bool restart = false; + struct json* res = NULL; + + pgagroal_log_debug("pgagroal: Management reload"); + + start_time = time(NULL); + + restart = reload_configuration(); + + pgagroal_management_create_response(payload, -1, &res); + + pgagroal_json_put(res, MANAGEMENT_ARGUMENT_RESTART, (uintptr_t)restart, ValueBool); + + end_time = time(NULL); + + pgagroal_management_response_ok(NULL, client_fd, start_time, end_time, compression, encryption, payload); + } + else if (id == MANAGEMENT_CONFIG_LS) + { + struct json* response = NULL; + + start_time = time(NULL); + + pgagroal_management_create_response(payload, -1, &response); + + pgagroal_json_put(response, CONFIGURATION_ARGUMENT_MAIN_CONF_PATH, (uintptr_t)config->common.configuration_path, ValueString); + pgagroal_json_put(response, CONFIGURATION_ARGUMENT_HBA_CONF_PATH, (uintptr_t)config->hba_path, ValueString); + pgagroal_json_put(response, CONFIGURATION_ARGUMENT_LIMIT_CONF_PATH, (uintptr_t)config->limit_path, ValueString); + pgagroal_json_put(response, CONFIGURATION_ARGUMENT_USER_CONF_PATH, (uintptr_t)config->users_path, ValueString); + pgagroal_json_put(response, CONFIGURATION_ARGUMENT_FRONTEND_USERS_CONF_PATH, (uintptr_t)config->frontend_users_path, ValueString); + pgagroal_json_put(response, CONFIGURATION_ARGUMENT_ADMIN_CONF_PATH, (uintptr_t)config->admins_path, ValueString); + pgagroal_json_put(response, CONFIGURATION_ARGUMENT_SUPERUSER_CONF_PATH, (uintptr_t)config->superuser_path, ValueString); + + end_time = time(NULL); + + pgagroal_management_response_ok(NULL, client_fd, start_time, end_time, compression, encryption, payload); + } + else if (id == MANAGEMENT_CONFIG_GET) + { + pid = fork(); + if (pid == -1) + { + pgagroal_management_response_error(NULL, client_fd, NULL, MANAGEMENT_ERROR_CONF_GET_NOFORK, compression, encryption, payload); + pgagroal_log_error("Conf Get: No fork %s (%d)", NULL, MANAGEMENT_ERROR_CONF_GET_NOFORK); + goto error; + } + else if (pid == 0) + { + struct json* pyl = NULL; + + shutdown_ports(); + + pgagroal_json_clone(payload, &pyl); + + pgagroal_set_proc_title(1, ai->argv, "conf get", NULL); + pgagroal_conf_get(NULL, client_fd, compression, encryption, pyl); + } + } + else if (id == MANAGEMENT_CONFIG_SET) + { + pid = fork(); + if (pid == -1) + { + pgagroal_management_response_error(NULL, client_fd, NULL, MANAGEMENT_ERROR_CONF_SET_NOFORK, compression, encryption, payload); + pgagroal_log_error("Conf Set: No fork %s (%d)", NULL, MANAGEMENT_ERROR_CONF_SET_NOFORK); + goto error; + } + else if (pid == 0) + { + struct json* pyl = NULL; + + shutdown_ports(); + + pgagroal_json_clone(payload, &pyl); + + pgagroal_set_proc_title(1, ai->argv, "conf set", NULL); + pgagroal_conf_set(NULL, client_fd, compression, encryption, pyl); + } + } + else if (id == MANAGEMENT_GET_PASSWORD) + { + int index = -1; + char* username = NULL; + struct json* req = NULL; + struct json* res = NULL; + + start_time = time(NULL); + + req = (struct json*)pgagroal_json_get(payload, MANAGEMENT_CATEGORY_REQUEST); + username = (char*)pgagroal_json_get(req, MANAGEMENT_ARGUMENT_USERNAME); + + for (int i = 0; index == -1 && i < config->number_of_frontend_users; i++) + { + if (!strcmp(&config->frontend_users[i].username[0], username)) + { + index = i; + } + } + + pgagroal_management_create_response(payload, -1, &res); + + pgagroal_json_put(res, MANAGEMENT_ARGUMENT_USERNAME, (uintptr_t)username, ValueString); + + if (index != -1) + { + pgagroal_json_put(res, MANAGEMENT_ARGUMENT_PASSWORD, (uintptr_t)config->frontend_users[index].password, ValueString); + } + else + { + pgagroal_json_put(res, MANAGEMENT_ARGUMENT_PASSWORD, (uintptr_t)NULL, ValueString); + } + + end_time = time(NULL); + + pgagroal_management_response_ok(NULL, client_fd, start_time, end_time, compression, encryption, payload); + } + else + { + pgagroal_management_response_error(NULL, client_fd, NULL, MANAGEMENT_ERROR_UNKNOWN_COMMAND, compression, encryption, payload); + pgagroal_log_error("Unknown: %s (%d)", pgagroal_json_to_string(payload, FORMAT_JSON, NULL, 0), MANAGEMENT_ERROR_UNKNOWN_COMMAND); + goto error; + } + + if (keep_running && config->gracefully) + { + if (atomic_load(&config->active_connections) == 0) + { + pgagroal_pool_status(); + keep_running = 0; + ev_break(loop, EVBREAK_ALL); + } + } + + free(str); + + pgagroal_json_destroy(payload); + + pgagroal_disconnect(client_fd); + + pgagroal_prometheus_self_sockets_sub(); + + return; + +error: + + free(str); + + pgagroal_disconnect(client_fd); + + pgagroal_prometheus_self_sockets_sub(); +} + +static void +accept_transfer_cb(struct ev_loop* loop, struct ev_io* watcher, int revents) +{ + struct sockaddr_in6 client_addr; + socklen_t client_addr_length; + int client_fd = 0; + int id = -1; + pid_t pid = 0; + int32_t slot = -1; + int fd = -1; + struct main_configuration* config; + + if (EV_ERROR & revents) + { + pgagroal_log_trace("accept_mgt_cb: got invalid event: %s", strerror(errno)); + return; + } + + config = (struct main_configuration*)shmem; + + errno = 0; + + client_addr_length = sizeof(client_addr); + client_fd = accept(watcher->fd, (struct sockaddr*)&client_addr, &client_addr_length); + + pgagroal_prometheus_self_sockets_add(); + + if (client_fd == -1) + { + if (accept_fatal(errno) && keep_running) + { + pgagroal_log_warn("Restarting transfer due to: %s (%d)", strerror(errno), watcher->fd); + + shutdown_mgt(); + + if (pgagroal_bind_unix_socket(config->unix_socket_dir, TRANSFER_UDS, &unix_transfer_socket)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s/%s", config->unix_socket_dir, TRANSFER_UDS); + exit(1); + } + + start_mgt(); + + pgagroal_log_debug("Transfer: %d", unix_transfer_socket); + } + else + { + pgagroal_log_debug("accept: %s (%d)", strerror(errno), watcher->fd); + } + errno = 0; + return; + } + + /* Process transfer request */ + if (pgagroal_connection_id_read(client_fd, &id)) + { + goto error; + } + + if (id == CONNECTION_TRANSFER) + { + pgagroal_log_trace("pgagroal: Transfer connection"); + + if (pgagroal_connection_transfer_read(client_fd, &slot, &fd)) + { + pgagroal_log_error("pgagroal: Transfer connection: Slot %d FD %d", slot, fd); + goto error; + } + + config->connections[slot].fd = fd; + known_fds[slot] = config->connections[slot].fd; + + if (config->pipeline == PIPELINE_TRANSACTION) + { + struct client* c = clients; + while (c != NULL) + { + int c_fd = -1; + + if (pgagroal_connection_get_pid(c->pid, &c_fd)) + { + goto error; + } + + if (pgagroal_connection_id_write(c_fd, CONNECTION_CLIENT_FD)) + { + goto error; + } + + if (pgagroal_connection_transfer_write(c_fd, slot)) + { + goto error; + } + + pgagroal_disconnect(c_fd); + + c = c->next; + } + } + + pgagroal_log_debug("pgagroal: Transfer connection: Slot %d FD %d", slot, fd); + + } + else if (id == CONNECTION_RETURN) + { + pgagroal_log_trace("pgagroal: Transfer return connection"); + + if (pgagroal_connection_slot_read(client_fd, &slot)) + { + pgagroal_log_error("pgagroal: Transfer return connection: Slot %d", slot); + goto error; + } + + pgagroal_log_debug("pgagroal: Transfer return connection: Slot %d", slot); + } + else if (id == CONNECTION_KILL) + { + pgagroal_log_trace("pgagroal: Transfer kill connection"); + + if (pgagroal_connection_transfer_read(client_fd, &slot, &fd)) + { + pgagroal_log_error("pgagroal: Transfer kill connection: Slot %d FD %d", slot, fd); + goto error; + } + + if (known_fds[slot] == fd) + { + struct client* c = clients; + while (c != NULL) + { + int c_fd = -1; + + if (pgagroal_connection_get_pid(c->pid, &c_fd)) + { + goto error; + } + + if (pgagroal_connection_id_write(c_fd, CONNECTION_REMOVE_FD)) + { + goto error; + } + + if (pgagroal_connection_transfer_write(c_fd, slot)) + { + goto error; + } + + pgagroal_disconnect(c_fd); + + c = c->next; + } + + pgagroal_disconnect(fd); + known_fds[slot] = 0; + } + + pgagroal_log_debug("pgagroal: Transfer kill connection: Slot %d FD %d", slot, fd); + } + else if (id == CONNECTION_CLIENT_DONE) + { + pgagroal_log_debug("pgagroal: Transfer client done"); + + if (pgagroal_connection_pid_read(client_fd, &pid)) + { + pgagroal_log_error("pgagroal: Transfer client done: PID %d", (int)pid); + goto error; + } + + remove_client(pid); + + pgagroal_log_debug("pgagroal: Transfer client done: PID %d", (int)pid); + } + + pgagroal_disconnect(client_fd); + + pgagroal_prometheus_self_sockets_sub(); + + return; + +error: + + pgagroal_disconnect(client_fd); + + pgagroal_prometheus_self_sockets_sub(); +} + +static void +accept_metrics_cb(struct ev_loop* loop, struct ev_io* watcher, int revents) +{ + struct sockaddr_in6 client_addr; + socklen_t client_addr_length; + int client_fd; + struct main_configuration* config; + + if (EV_ERROR & revents) + { + pgagroal_log_debug("accept_metrics_cb: invalid event: %s", strerror(errno)); + errno = 0; + return; + } + + config = (struct main_configuration*)shmem; + + errno = 0; + + client_addr_length = sizeof(client_addr); + client_fd = accept(watcher->fd, (struct sockaddr*)&client_addr, &client_addr_length); + + pgagroal_prometheus_self_sockets_add(); + + if (client_fd == -1) + { + if (accept_fatal(errno) && keep_running) + { + pgagroal_log_warn("Restarting listening port due to: %s (%d)", strerror(errno), watcher->fd); + + shutdown_metrics(); + + free(metrics_fds); + metrics_fds = NULL; + metrics_fds_length = 0; + + if (pgagroal_bind(config->common.host, config->common.metrics, &metrics_fds, &metrics_fds_length, config->non_blocking, config->nodelay, config->backlog)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s:%d", config->common.host, config->common.metrics); + exit(1); + } + + if (metrics_fds_length > MAX_FDS) + { + pgagroal_log_fatal("pgagroal: Too many descriptors %d", metrics_fds_length); + exit(1); + } + + start_metrics(); + + for (int i = 0; i < metrics_fds_length; i++) + { + pgagroal_log_debug("Metrics: %d", *(metrics_fds + i)); + } + } + else + { + pgagroal_log_debug("accept: %s (%d)", strerror(errno), watcher->fd); + } + errno = 0; + return; + } + + if (!fork()) + { + ev_loop_fork(loop); + shutdown_ports(); + /* We are leaving the socket descriptor valid such that the client won't reuse it */ + pgagroal_prometheus(client_fd); + } + + pgagroal_disconnect(client_fd); + pgagroal_prometheus_self_sockets_sub(); +} + +static void +accept_management_cb(struct ev_loop* loop, struct ev_io* watcher, int revents) +{ + struct sockaddr_in6 client_addr; + socklen_t client_addr_length; + int client_fd; + char address[INET6_ADDRSTRLEN]; + struct main_configuration* config; + + if (EV_ERROR & revents) + { + pgagroal_log_debug("accept_management_cb: invalid event: %s", strerror(errno)); + errno = 0; + return; + } + + memset(&address, 0, sizeof(address)); + + config = (struct main_configuration*)shmem; + + errno = 0; + + client_addr_length = sizeof(client_addr); + client_fd = accept(watcher->fd, (struct sockaddr*)&client_addr, &client_addr_length); + + pgagroal_prometheus_self_sockets_add(); + + if (client_fd == -1) + { + if (accept_fatal(errno) && keep_running) + { + pgagroal_log_warn("Restarting listening port due to: %s (%d)", strerror(errno), watcher->fd); + + shutdown_management(); + + free(management_fds); + management_fds = NULL; + management_fds_length = 0; + + if (pgagroal_bind(config->common.host, config->management, &management_fds, &management_fds_length, config->non_blocking, config->nodelay, config->backlog)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s:%d", config->common.host, config->management); + exit(1); + } + + if (management_fds_length > MAX_FDS) + { + pgagroal_log_fatal("pgagroal: Too many descriptors %d", management_fds_length); + exit(1); + } + + start_management(); + + for (int i = 0; i < management_fds_length; i++) + { + pgagroal_log_debug("Remote management: %d", *(management_fds + i)); + } + } + else + { + pgagroal_log_debug("accept: %s (%d)", strerror(errno), watcher->fd); + } + errno = 0; + return; + } + + pgagroal_get_address((struct sockaddr*)&client_addr, (char*)&address, sizeof(address)); + + if (!fork()) + { + char* addr = calloc(1, strlen(address) + 1); + if (addr == NULL) + { + pgagroal_log_fatal("Couldn't allocate address"); + return; + } + memcpy(addr, address, strlen(address)); + + ev_loop_fork(loop); + shutdown_ports(); + /* We are leaving the socket descriptor valid such that the client won't reuse it */ + pgagroal_remote_management(client_fd, addr); + } + + pgagroal_disconnect(client_fd); + pgagroal_prometheus_self_sockets_sub(); +} + +static void +shutdown_cb(struct ev_loop* loop, ev_signal* w, int revents) +{ + pgagroal_log_debug("pgagroal: shutdown requested"); + pgagroal_pool_status(); + ev_break(loop, EVBREAK_ALL); + keep_running = 0; +} + +static void +reload_cb(struct ev_loop* loop, ev_signal* w, int revents) +{ + pgagroal_log_debug("pgagroal: reload requested"); + reload_configuration(); +} + +static void +graceful_cb(struct ev_loop* loop, ev_signal* w, int revents) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + pgagroal_log_debug("pgagroal: gracefully requested"); + + pgagroal_pool_status(); + config->gracefully = true; + + if (atomic_load(&config->active_connections) == 0) + { + pgagroal_pool_status(); + keep_running = 0; + ev_break(loop, EVBREAK_ALL); + } +} + +static void +coredump_cb(struct ev_loop* loop, ev_signal* w, int revents) +{ + pgagroal_log_info("pgagroal: core dump requested"); + pgagroal_pool_status(); + abort(); +} + +static void +idle_timeout_cb(struct ev_loop* loop, ev_periodic* w, int revents) +{ + if (EV_ERROR & revents) + { + pgagroal_log_trace("idle_timeout_cb: got invalid event: %s", strerror(errno)); + return; + } + + /* pgagroal_idle_timeout() is always in a fork() */ + if (!fork()) + { + shutdown_ports(); + pgagroal_idle_timeout(); + } +} + +static void +max_connection_age_cb(struct ev_loop* loop, ev_periodic* w, int revents) +{ + if (EV_ERROR & revents) + { + pgagroal_log_trace("max_connection_age_cb: got invalid event: %s", strerror(errno)); + return; + } + + /* max_connection_age() is always in a fork() */ + if (!fork()) + { + shutdown_ports(); + pgagroal_max_connection_age(); + } +} + +static void +validation_cb(struct ev_loop* loop, ev_periodic* w, int revents) +{ + if (EV_ERROR & revents) + { + pgagroal_log_trace("validation_cb: got invalid event: %s", strerror(errno)); + return; + } + + /* pgagroal_validation() is always in a fork() */ + if (!fork()) + { + shutdown_ports(); + pgagroal_validation(); + } +} + +static void +disconnect_client_cb(struct ev_loop* loop, ev_periodic* w, int revents) +{ + if (EV_ERROR & revents) + { + pgagroal_log_trace("disconnect_client_cb: got invalid event: %s", strerror(errno)); + return; + } + + /* main_pipeline.periodic is always in a fork() */ + if (!fork()) + { + shutdown_ports(); + main_pipeline.periodic(); + } +} + +static void +rotate_frontend_password_cb(struct ev_loop* loop, ev_periodic* w, int revents) +{ + char* pwd; + + if (EV_ERROR & revents) + { + pgagroal_log_trace("rotate_frontend_password_cb: got invalid event: %s", strerror(errno)); + return; + } + + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + for (int i = 0; i < config->number_of_frontend_users; i++) + { + if (pgagroal_generate_password(config->rotate_frontend_password_length, &pwd)) + { + pgagroal_log_debug("rotate_frontend_password_cb: unable to rotate password"); + return; + } + memcpy(&config->frontend_users[i].password, pwd, strlen(pwd) + 1); + pgagroal_log_trace("rotate_frontend_password_cb: current pass for username=%s:%s", config->frontend_users[i].username, config->frontend_users[i].password); + free(pwd); + } +} + +static bool +accept_fatal(int error) +{ + switch (error) + { + case EAGAIN: + case ENETDOWN: + case EPROTO: + case ENOPROTOOPT: + case EHOSTDOWN: +#ifdef HAVE_LINUX + case ENONET: +#endif + case EHOSTUNREACH: + case EOPNOTSUPP: + case ENETUNREACH: + return false; + break; + } + + return true; +} + +static void +frontend_user_password_startup(struct main_configuration* config) +{ + char* pwd = NULL; + + if (config->number_of_frontend_users == 0 && config->rotate_frontend_password_timeout > 0) + { + for (int i = 0; i < config->number_of_users; i++) + { + memcpy(&config->frontend_users[i].username, config->users[i].username, strlen(config->users[i].username)); + if (pgagroal_generate_password(config->rotate_frontend_password_length, &pwd)) + { + pgagroal_log_debug("frontend_user_password_startup: unable to generate random password at startup"); + return; + } + memcpy(&config->frontend_users[i].password, pwd, strlen(pwd) + 1); + pgagroal_log_trace("frontend_user_password_startup: frontend user with username=%s initiated", config->frontend_users[i].username); + free(pwd); + } + config->number_of_frontend_users = config->number_of_users; + } + +} + +static void +add_client(pid_t pid) +{ + struct client* c = NULL; + + c = (struct client*)malloc(sizeof(struct client)); + c->pid = pid; + c->next = NULL; + + if (clients == NULL) + { + clients = c; + } + else + { + struct client* last = NULL; + + last = clients; + + while (last->next != NULL) + { + last = last->next; + } + + last->next = c; + } +} + +static void +remove_client(pid_t pid) +{ + struct client* c = NULL; + struct client* p = NULL; + + c = clients; + p = NULL; + + if (c != NULL) + { + while (c->pid != pid) + { + p = c; + c = c->next; + + if (c == NULL) + { + return; + } + } + + if (c == clients) + { + clients = c->next; + } + else + { + p->next = c->next; + } + + free(c); + } +} + +static bool +reload_configuration(void) +{ + bool restart = false; + char pgsql[MISC_LENGTH]; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + shutdown_io(); + shutdown_uds(); + shutdown_metrics(); + shutdown_management(); + + pgagroal_reload_configuration(&restart); + + memset(&pgsql, 0, sizeof(pgsql)); + snprintf(&pgsql[0], sizeof(pgsql), ".s.PGSQL.%d", config->common.port); + + if (pgagroal_bind_unix_socket(config->unix_socket_dir, &pgsql[0], &unix_pgsql_socket)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s/%s", config->unix_socket_dir, &pgsql[0]); + goto error; + } + + free(main_fds); + main_fds = NULL; + main_fds_length = 0; + + if (pgagroal_bind(config->common.host, config->common.port, &main_fds, &main_fds_length, config->non_blocking, config->nodelay, config->backlog)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s:%d", config->common.host, config->common.port); + goto error; + } + + if (main_fds_length > MAX_FDS) + { + pgagroal_log_fatal("pgagroal: Too many descriptors %d", main_fds_length); + goto error; + } + + start_io(); + start_uds(); + + if (config->common.metrics > 0) + { + free(metrics_fds); + metrics_fds = NULL; + metrics_fds_length = 0; + + /* Bind metrics socket */ + if (pgagroal_bind(config->common.host, config->common.metrics, &metrics_fds, &metrics_fds_length, config->non_blocking, config->nodelay, config->backlog)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s:%d", config->common.host, config->common.metrics); + goto error; + } + + if (metrics_fds_length > MAX_FDS) + { + pgagroal_log_fatal("pgagroal: Too many descriptors %d", metrics_fds_length); + goto error; + } + + start_metrics(); + } + + if (config->management > 0) + { + free(management_fds); + management_fds = NULL; + management_fds_length = 0; + + /* Bind management socket */ + if (pgagroal_bind(config->common.host, config->management, &management_fds, &management_fds_length, config->non_blocking, config->nodelay, config->backlog)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s:%d", config->common.host, config->management); + goto error; + } + + if (management_fds_length > MAX_FDS) + { + pgagroal_log_fatal("pgagroal: Too many descriptors %d", management_fds_length); + goto error; + } + + start_management(); + } + + for (int i = 0; i < main_fds_length; i++) + { + pgagroal_log_debug("Socket: %d", *(main_fds + i)); + } + pgagroal_log_debug("Unix Domain Socket: %d", unix_pgsql_socket); + for (int i = 0; i < metrics_fds_length; i++) + { + pgagroal_log_debug("Metrics: %d", *(metrics_fds + i)); + } + for (int i = 0; i < management_fds_length; i++) + { + pgagroal_log_debug("Remote management: %d", *(management_fds + i)); + } + + if (restart) + { + remove_pidfile(); + } + + exit(0); + +error: + remove_pidfile(); + exit(1); +} + +/** + * Creates the pid file for the running pooler. + * If a pid file already exists, or if the file cannot be written, + * the function kills (exits) the current process. + * + */ +static void +create_pidfile_or_exit(void) +{ + char buffer[64]; + pid_t pid; + int r; + int fd; + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + if (strlen(config->pidfile) > 0) + { + pid = getpid(); + + fd = open(config->pidfile, O_WRONLY | O_CREAT | O_EXCL, 0640); + if (errno == EEXIST) + { + errx(1, "PID file <%s> exists, is there another instance running ?", config->pidfile); + } + else if (errno == EACCES) + { + errx(1, "PID file <%s> cannot be created due to lack of permissions", config->pidfile); + } + else if (fd < 0) + { + err(1, "Could not create PID file <%s>", config->pidfile); + } + + snprintf(&buffer[0], sizeof(buffer), "%u\n", (unsigned)pid); + + r = write(fd, &buffer[0], strlen(buffer)); + if (r < 0) + { + errx(1, "Could not write into PID file <%s>", config->pidfile); + } + + close(fd); + } +} + +static void +remove_pidfile(void) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + if (strlen(config->pidfile) > 0) + { + if (unlink(config->pidfile)) + { + warn("Cannot remove PID file <%s>", config->pidfile); + } + } +} + +static void +shutdown_ports(void) +{ + struct main_configuration* config; + + config = (struct main_configuration*)shmem; + + shutdown_io(); + + if (config->common.metrics > 0) + { + shutdown_metrics(); + } + + if (config->management > 0) + { + shutdown_management(); + } +} diff --git a/src/vault.c b/src/vault.c new file mode 100644 index 00000000..9d60632e --- /dev/null +++ b/src/vault.c @@ -0,0 +1,895 @@ +/* + * Copyright (C) 2025 The pgagroal community + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* system */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_LINUX +#include +#endif + +#define CLIENTSSL_ON_SERVERSSL_ON 0 +#define CLIENTSSL_ON_SERVERSSL_OFF 1 +#define CLIENTSSL_OFF_SERVERSSL_ON 2 +#define CLIENTSSL_OFF_SERVERSSL_OFF 3 + +#define MAX_FDS 64 + +static void accept_vault_cb(struct ev_loop* loop, struct ev_io* watcher, int revents); +static void accept_metrics_cb(struct ev_loop* loop, struct ev_io* watcher, int revents); +static void shutdown_cb(struct ev_loop* loop, ev_signal* w, int revents); +static bool accept_fatal(int error); +static int connect_pgagroal(struct vault_configuration* config, char* username, char* password, SSL** s_ssl, int* client_socket); +static void route_users(char* username, char** response, SSL* s_ssl, int client_fd); +static void route_not_found(char** response); +static void route_found(char** response, char* password); +static void route_redirect(char** response, char* redirect_link); +static int router(SSL* ccl, SSL* ssl, int client_fd); +static bool is_ssl_request(int client_fd); +static int get_connection_state(struct vault_configuration* config, int client_fd); + +static volatile int keep_running = 1; +static char** argv_ptr; +static struct ev_loop* main_loop = NULL; +static struct accept_io io_main[MAX_FDS]; +static struct accept_io io_metrics[MAX_FDS]; +static int* metrics_fds = NULL; +static int metrics_fds_length = -1; +static int* server_fds = NULL; +static int server_fds_length = -1; + +static int +router(SSL* c_ssl, SSL* s_ssl, int client_fd) +{ + int exit_code = 0; + int connection_state; + ssize_t bytes_write; + struct vault_configuration* config; + char* response = NULL; + char method[8]; + char path[128]; + char buffer[HTTP_BUFFER_SIZE]; + char username[MAX_USERNAME_LENGTH + 1]; // Assuming username is less than 128 characters + char* redirect_link = NULL; + + config = (struct vault_configuration*)shmem; + memset(&response, 0, sizeof(response)); + memset(&buffer, 0, sizeof(buffer)); + + connection_state = get_connection_state(config, client_fd); + switch (connection_state) + { + case CLIENTSSL_ON_SERVERSSL_ON: + if (accept_ssl_vault(config, client_fd, &c_ssl)) + { + pgagroal_log_error("accept_ssl_vault: SSL connection failed"); + exit_code = 1; + goto exit; + } + pgagroal_read_socket(c_ssl, client_fd, buffer, sizeof(buffer)); + sscanf(buffer, "%7s %127s", method, path); + break; + case CLIENTSSL_OFF_SERVERSSL_ON: + pgagroal_read_socket(c_ssl, client_fd, buffer, sizeof(buffer)); + sscanf(buffer, "%7s %127s", method, path); + redirect_link = pgagroal_append(redirect_link, "https://"); + redirect_link = pgagroal_append(redirect_link, config->common.host); + redirect_link = pgagroal_append(redirect_link, ":"); + redirect_link = pgagroal_append_int(redirect_link, config->common.port); + redirect_link = pgagroal_append(redirect_link, path); + route_redirect(&response, redirect_link); + pgagroal_log_error("client must initiate tls handshake"); + goto send; + case CLIENTSSL_OFF_SERVERSSL_OFF: + pgagroal_read_socket(c_ssl, client_fd, buffer, sizeof(buffer)); + sscanf(buffer, "%7s %127s", method, path); + break; + case CLIENTSSL_ON_SERVERSSL_OFF: + pgagroal_log_error("client requests tls connection to http server"); + default: + return 1; + } + + // Parse URL parameters for GET requests only + if (strcmp(method, "GET") == 0) + { + // Call the appropriate handler function for the URL path + if (strncmp(path, "/users/", 7) == 0 && strcmp(method, "GET") == 0) // Only one '/' + { + // Extract the username from the path + sscanf(path, "/users/%128s", username); + // Call the appropriate handler function with the username + route_users(username, &response, s_ssl, client_fd); + } + else + { + route_not_found(&response); + } + } + else + { + route_not_found(&response); + } + +send: + // Send the response + bytes_write = pgagroal_write_socket(c_ssl, client_fd, response, strlen(response)); + if (bytes_write <= 0) + { + exit_code = 1; + } + pgagroal_prometheus_client_sockets_sub(); + free(response); +exit: + return exit_code; +} + +static void +route_users(char* username, char** response, SSL* s_ssl, int client_fd) +{ + struct vault_configuration* config = (struct vault_configuration*)shmem; + int client_pgagroal_fd = -1; + struct json* read = NULL; + struct json* res = NULL; + char* password = NULL; + + // Connect to pgagroal management port + if (connect_pgagroal(config, config->vault_server.user.username, config->vault_server.user.password, &s_ssl, &client_pgagroal_fd)) // Change NULL to ssl + { + pgagroal_log_error("pgagroal-vault: Couldn't connect to %s:%d", config->vault_server.server.host, config->vault_server.server.port); + // Send Error Response + route_not_found(response); + return; + } + + // Call GET_PASSWORD at management port + if (pgagroal_management_request_get_password(s_ssl, client_pgagroal_fd, username, COMPRESSION_NONE, ENCRYPTION_AES_256_CBC, MANAGEMENT_OUTPUT_FORMAT_JSON)) + { + pgagroal_log_error("pgagroal-vault: Couldn't get password from the management"); + // Send Error Response + route_not_found(response); + return; + } + + if (pgagroal_management_read_json(s_ssl, client_pgagroal_fd, NULL, NULL, &read)) + { + pgagroal_log_warn("pgagroal-vault: Couldn't receive the result"); + } + + if (read != NULL) + { + res = (struct json*)pgagroal_json_get(read, MANAGEMENT_CATEGORY_RESPONSE); + password = (char*)pgagroal_json_get(res, MANAGEMENT_ARGUMENT_PASSWORD); + } + + if (password == NULL || strlen(password) == 0) // user not found + { + pgagroal_log_warn("pgagroal-vault: Couldn't find the user: %s", username); + route_not_found(response); + } + else + { + route_found(response, password); + } + + pgagroal_json_destroy(read); +} + +static void +route_not_found(char** response) +{ + char* tmp_response = NULL; + memset(&tmp_response, 0, sizeof(tmp_response)); + tmp_response = pgagroal_append(tmp_response, "HTTP/1.1 404 Not Found\r\n\r\n"); + *response = tmp_response; +} + +static void +route_found(char** response, char* password) +{ + char* tmp_response = NULL; + memset(&tmp_response, 0, sizeof(tmp_response)); + tmp_response = pgagroal_append(tmp_response, "HTTP/1.1 200 OK\r\n"); + tmp_response = pgagroal_append(tmp_response, "Content-Type: text/plain\r\n"); + tmp_response = pgagroal_append(tmp_response, "\r\n\r\n"); + tmp_response = pgagroal_append(tmp_response, password); + tmp_response = pgagroal_append(tmp_response, "\r\n"); + *response = tmp_response; +} + +static void +route_redirect(char** response, char* redirect_link) +{ + char* tmp_response = NULL; + tmp_response = pgagroal_append(tmp_response, "HTTP/1.1 301 Moved Permanently\r\n"); + tmp_response = pgagroal_append(tmp_response, "Content-Length: 0\r\n"); + tmp_response = pgagroal_append(tmp_response, "Location: "); + tmp_response = pgagroal_append(tmp_response, redirect_link); + tmp_response = pgagroal_append(tmp_response, "\r\n"); + *response = tmp_response; +} + +static int +connect_pgagroal(struct vault_configuration* config, char* username, char* password, SSL** s_ssl, int* client_socket) +{ + SSL* s = NULL; + + if (pgagroal_connect(config->vault_server.server.host, config->vault_server.server.port, client_socket, false, false, false)) + { + pgagroal_disconnect(*client_socket); + return 1; + } + + pgagroal_log_debug("connect_pgagroal: Authenticating the remote management access to %s:%d", config->vault_server.server.host, config->vault_server.server.port); + username = config->vault_server.user.username; + + for (int i = 0; i < strlen(password); i++) + { + if ((unsigned char)(*(password + i)) & 0x80) + { + + pgagroal_log_debug("pgagroal-vault: Bad credentials for %s", username); + return 1; + } + } + + /* Authenticate */ + if (pgagroal_remote_management_scram_sha256(username, password, *client_socket, &s) != AUTH_SUCCESS) + { + pgagroal_log_debug("pgagroal-vault: Bad credentials for %s", username); + pgagroal_disconnect(*client_socket); + return 1; + } + + *s_ssl = s; + + return 0; +} + +static bool +is_ssl_request(int client_fd) +{ + ssize_t peek_bytes; + char peek_buffer[HTTP_BUFFER_SIZE]; + bool ssl_req = false; + + // MSG_Peek + peek_bytes = recv(client_fd, peek_buffer, sizeof(peek_buffer), MSG_PEEK); + if (peek_bytes <= 0) + { + pgagroal_log_error("unable to peek network data from client"); + close(client_fd); + exit(1); + } + + // Check for SSL request by matching `Client Hello` bytes + if ( + ((unsigned char)peek_buffer[0] == 0x16) && + ((unsigned char)peek_buffer[1] == 0x03) && + ((unsigned char)peek_buffer[2] == 0x01 || (unsigned char)peek_buffer[2] == 0x02 || (unsigned char)peek_buffer[2] == 0x03 || (unsigned char)peek_buffer[2] == 0x04) + ) + { + ssl_req = true; + } + + return ssl_req; +} + +static int +get_connection_state(struct vault_configuration* config, int client_fd) +{ + if (config->common.tls) + { + if (is_ssl_request(client_fd)) + { + return CLIENTSSL_ON_SERVERSSL_ON; + } + else + { + return CLIENTSSL_OFF_SERVERSSL_ON; + } + } + else if (is_ssl_request(client_fd)) + { + return CLIENTSSL_ON_SERVERSSL_OFF; + } + return CLIENTSSL_OFF_SERVERSSL_OFF; +} + +static void +start_vault_io(void) +{ + for (int i = 0; i < server_fds_length; i++) + { + int sockfd = *(server_fds + i); + + memset(&io_main[i], 0, sizeof(struct accept_io)); + ev_io_init((struct ev_io*)&io_main, accept_vault_cb, sockfd, EV_READ); + io_main[i].socket = sockfd; + io_main[i].argv = argv_ptr; + ev_io_start(main_loop, (struct ev_io*)&io_main[i]); + } +} + +static void +shutdown_vault_io(void) +{ + for (int i = 0; i < server_fds_length; i++) + { + ev_io_stop(main_loop, (struct ev_io*)&io_main[i]); + pgagroal_disconnect(io_main[i].socket); + errno = 0; + } +} + +static void +start_metrics(void) +{ + for (int i = 0; i < metrics_fds_length; i++) + { + int sockfd = *(metrics_fds + i); + + memset(&io_metrics[i], 0, sizeof(struct accept_io)); + ev_io_init((struct ev_io*)&io_metrics[i], accept_metrics_cb, sockfd, EV_READ); + io_metrics[i].socket = sockfd; + io_metrics[i].argv = argv_ptr; + ev_io_start(main_loop, (struct ev_io*)&io_metrics[i]); + } +} + +static void +shutdown_metrics(void) +{ + for (int i = 0; i < metrics_fds_length; i++) + { + ev_io_stop(main_loop, (struct ev_io*)&io_metrics[i]); + pgagroal_disconnect(io_metrics[i].socket); + errno = 0; + } +} + +static void +shutdown_ports(void) +{ + struct vault_configuration* config; + + config = (struct vault_configuration*)shmem; + + shutdown_vault_io(); + + if (config->common.metrics > 0) + { + shutdown_metrics(); + } +} + +static void +usage(void) +{ + printf("pgagroal-vault %s\n", PGAGROAL_VERSION); + printf(" Simple vault that hosts an HTTP server to handle user frontend password requests\n"); + printf("\n"); + + printf("Usage:\n"); + printf(" pgagroal-vault [ -c CONFIG_FILE ] [ -u USERS_FILE ] \n"); + printf("\n"); + printf("Options:\n"); + printf(" -c, --config CONFIG_FILE Set the path to the pgagroal_vault.conf file\n"); + printf(" Default: %s\n", PGAGROAL_DEFAULT_VAULT_CONF_FILE); + printf(" -u, --users USERS_FILE Set the password for the admin user of management port\n"); + printf(" -?, --help Display help\n"); + printf("\n"); + printf("pgagroal: %s\n", PGAGROAL_HOMEPAGE); + printf("Report bugs: %s\n", PGAGROAL_ISSUES); +} + +int +main(int argc, char** argv) +{ + int ret; + int exit_code = 0; + char* configuration_path = NULL; + char* users_path = NULL; + struct signal_info signal_watcher[1]; // Can add more + int c; + int option_index = 0; + size_t prometheus_shmem_size = 0; + size_t prometheus_cache_shmem_size = 0; + size_t size; + struct vault_configuration* config = NULL; + char message[MISC_LENGTH]; // a generic message used for errors + + while (1) + { + static struct option long_options[] = + { + {"config", required_argument, 0, 'c'}, + {"users", required_argument, 0, 'u'}, + {"help", no_argument, 0, '?'} + }; + + c = getopt_long(argc, argv, "?c:u:", + long_options, &option_index); + + if (c == -1) + { + break; + } + + switch (c) + { + case 'c': + configuration_path = optarg; + break; + case 'u': + users_path = optarg; + break; + case '?': + usage(); + exit(1); + break; + default: + break; + } + } + + if (getuid() == 0) + { + errx(1, "pgagroal-vault: Using the root account is not allowed"); + } + + size = sizeof(struct vault_configuration); + if (pgagroal_create_shared_memory(size, HUGEPAGE_OFF, &shmem)) + { + errx(1, "pgagroal-vault: Error creating shared memory"); + } + + memset(message, 0, MISC_LENGTH); + + pgagroal_vault_init_configuration(shmem); + config = (struct vault_configuration*)shmem; + + configuration_path = configuration_path != NULL ? configuration_path : PGAGROAL_DEFAULT_VAULT_CONF_FILE; + if ((ret = pgagroal_vault_read_configuration(shmem, configuration_path, false)) != PGAGROAL_CONFIGURATION_STATUS_OK) + { + // the configuration has some problem, build up a descriptive message + if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND) + { + snprintf(message, MISC_LENGTH, "Configuration file not found"); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG) + { + snprintf(message, MISC_LENGTH, "Too many sections"); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_KO) + { + snprintf(message, MISC_LENGTH, "Invalid configuration file"); + } + else if (ret > 0) + { + snprintf(message, MISC_LENGTH, "%d problematic or duplicated section%c", + ret, + ret > 1 ? 's' : ' '); + } + + errx(1, "pgagroal-vault: %s (file <%s>)", message, configuration_path); + } + + memcpy(&config->common.configuration_path[0], configuration_path, MIN(strlen(configuration_path), MAX_PATH - 1)); + + if (pgagroal_init_logging()) + { + exit(1); + } + + if (pgagroal_start_logging()) + { + errx(1, "Failed to start logging"); + } + + if (config->common.metrics > 0) + { + if (pgagroal_vault_init_prometheus(&prometheus_shmem_size, &prometheus_shmem)) + { + #ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Error in creating and initializing prometheus shared memory"); + #endif + errx(1, "Error in creating and initializing prometheus shared memory"); + } + + if (pgagroal_init_prometheus_cache(&prometheus_cache_shmem_size, &prometheus_cache_shmem)) + { + #ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Error in creating and initializing prometheus cache shared memory"); + #endif + errx(1, "Error in creating and initializing prometheus cache shared memory"); + } + } + + if (pgagroal_vault_validate_configuration(shmem)) + { + errx(1, "pgagroal-vault: Invalid VAULT configuration"); + } + + config = (struct vault_configuration*)shmem; + + // -- Read the USERS file -- +read_users_path: + if (users_path != NULL) + { + memset(message, 0, MISC_LENGTH); + ret = pgagroal_vault_read_users_configuration(shmem, users_path); + if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_NOT_FOUND) + { + + snprintf(message, MISC_LENGTH, "USERS configuration file not found"); + errx(1, "pgagroal-vault: %s (file <%s>)", message, users_path); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_CANNOT_DECRYPT) + { + errx(1, "pgagroal-vault: Invalid entry in the file"); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_FILE_TOO_BIG) + { + snprintf(message, MISC_LENGTH, "Too many users defined %d (max %d)", config->number_of_users, NUMBER_OF_ADMINS); + errx(1, "pgagroal-vault: %s (file <%s>)", message, users_path); + } + else if (ret == PGAGROAL_CONFIGURATION_STATUS_OK) + { + memcpy(&config->users_path[0], users_path, MIN(strlen(users_path), MAX_PATH - 1)); + } + } + else + { + // the user did not specify a file on the command line + // so try the default one and allow it to be missing + users_path = PGAGROAL_DEFAULT_VAULT_USERS_FILE; + goto read_users_path; + } + + // -- Bind & Listen at the given hostname and port -- + + if (pgagroal_bind(config->common.host, config->common.port, &server_fds, &server_fds_length, false, false, -1)) + { + errx(1, "pgagroal-vault: Could not bind to %s:%d", config->common.host, config->common.port); + } + + if (server_fds_length > MAX_FDS) + { + pgagroal_log_fatal("pgagroal: Too many descriptors %d", server_fds_length); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Too many descriptors %d", server_fds_length); +#endif + exit(1); + } + + // -- Initialize the watcher and start loop -- + main_loop = ev_default_loop(0); + + if (!main_loop) + { + errx(1, "pgagroal-vault: No loop implementation"); + } + + ev_signal_init((struct ev_signal*)&signal_watcher[0], shutdown_cb, SIGTERM); + + for (int i = 0; i < 1; i++) + { + signal_watcher[i].slot = -1; + ev_signal_start(main_loop, (struct ev_signal*)&signal_watcher[i]); + } + + start_vault_io(); + + if (config->common.metrics > 0) + { + /* Bind metrics socket */ + if (pgagroal_bind(config->common.host, config->common.metrics, &metrics_fds, &metrics_fds_length, false, false, -1)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s:%d", config->common.host, config->common.metrics); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Could not bind to %s:%d", config->common.host, config->common.metrics); +#endif + exit(1); + } + + if (metrics_fds_length > MAX_FDS) + { + pgagroal_log_fatal("pgagroal: Too many descriptors %d", metrics_fds_length); +#ifdef HAVE_LINUX + sd_notifyf(0, "STATUS=Too many descriptors %d", metrics_fds_length); +#endif + exit(1); + } + + start_metrics(); + } + + pgagroal_log_info("pgagroal-vault %s: Started on %s:%d", + PGAGROAL_VERSION, + config->common.host, + config->common.port); + for (int i = 0; i < server_fds_length; i++) + { + pgagroal_log_debug("Socket: %d", *(server_fds + i)); + } + for (int i = 0; i < metrics_fds_length; i++) + { + pgagroal_log_debug("Metrics: %d", *(metrics_fds + i)); + } + + while (keep_running) + { + ev_loop(main_loop, 0); + } + + pgagroal_log_info("pgagroal-vault: shutdown"); + + shutdown_ports(); + + for (int i = 0; i < 1; i++) + { + ev_signal_stop(main_loop, (struct ev_signal*)&signal_watcher[i]); + } + + ev_loop_destroy(main_loop); + + // -- Free all memory -- + free(metrics_fds); + free(server_fds); + pgagroal_stop_logging(); + pgagroal_destroy_shared_memory(shmem, size); + + return exit_code; +} + +static void +shutdown_cb(struct ev_loop* loop, ev_signal* w, int revents) +{ + pgagroal_log_debug("pgagroal-vault: Shutdown requested"); + ev_break(loop, EVBREAK_ALL); + keep_running = 0; +} + +static void +accept_vault_cb(struct ev_loop* loop, struct ev_io* watcher, int revents) +{ + struct sockaddr_in6 client_addr; + socklen_t client_addr_length; + int client_fd; + char address[INET6_ADDRSTRLEN]; + pid_t pid; + SSL* c_ssl = NULL; + SSL* s_ssl = NULL; + struct vault_configuration* config; + + if (EV_ERROR & revents) + { + pgagroal_log_debug("accept_vault_cb: Invalid event: %s", strerror(errno)); + errno = 0; + return; + } + + config = (struct vault_configuration*)shmem; + + memset(&address, 0, sizeof(address)); + + client_addr_length = sizeof(client_addr); + client_fd = accept(watcher->fd, (struct sockaddr*)&client_addr, &client_addr_length); + + if (client_fd == -1) + { + if (accept_fatal(errno) && keep_running) + { + pgagroal_log_warn("accept_vault_cb: Restarting listening port due to: %s (%d)", strerror(errno), watcher->fd); + + shutdown_vault_io(); + + free(server_fds); + server_fds = NULL; + + if (pgagroal_bind(config->common.host, config->common.port, &server_fds, &server_fds_length, false, false, -1)) + { + pgagroal_log_fatal("pgagroal-vault: Could not bind to %s:%d", config->common.host, config->common.port); + exit(1); + } + + if (!fork()) + { + shutdown_ports(); + } + + start_vault_io(); + pgagroal_log_debug("Socket: %d", *server_fds); + } + else + { + pgagroal_log_debug("accept: %s (%d)", strerror(errno), watcher->fd); + } + errno = 0; + return; + } + + pgagroal_prometheus_client_sockets_add(); + pgagroal_get_address((struct sockaddr*)&client_addr, (char*)&address, sizeof(address)); + + pgagroal_log_trace("accept_vault_cb: client address: %s", address); + + pid = fork(); + if (pid == -1) + { + /* No process */ + pgagroal_log_error("accept_vault_cb: Couldn't create process"); + } + else if (pid == 0) + { + char* addr = calloc(1, strlen(address) + 1); + if (addr == NULL) + { + pgagroal_log_fatal("accept_vault_cb: Couldn't allocate memory for client address"); + return; + } + memcpy(addr, address, strlen(address)); + + ev_loop_fork(loop); + shutdown_ports(); + + // Handle http request + if (router(c_ssl, s_ssl, client_fd)) + { + pgagroal_log_error("Couldn't write to client"); + pgagroal_disconnect(client_fd); + exit(1); + } + + exit(0); + } + + pgagroal_disconnect(client_fd); +} + +static void +accept_metrics_cb(struct ev_loop* loop, struct ev_io* watcher, int revents) +{ + struct sockaddr_in6 client_addr; + socklen_t client_addr_length; + int client_fd; + struct vault_configuration* config; + + if (EV_ERROR & revents) + { + pgagroal_log_debug("accept_metrics_cb: invalid event: %s", strerror(errno)); + errno = 0; + return; + } + + config = (struct vault_configuration*)shmem; + + client_addr_length = sizeof(client_addr); + client_fd = accept(watcher->fd, (struct sockaddr*)&client_addr, &client_addr_length); + + pgagroal_prometheus_self_sockets_add(); + + if (client_fd == -1) + { + if (accept_fatal(errno) && keep_running) + { + pgagroal_log_warn("Restarting listening port due to: %s (%d)", strerror(errno), watcher->fd); + + shutdown_metrics(); + + free(metrics_fds); + metrics_fds = NULL; + metrics_fds_length = 0; + + if (pgagroal_bind(config->common.host, config->common.metrics, &metrics_fds, &metrics_fds_length, false, false, -1)) + { + pgagroal_log_fatal("pgagroal: Could not bind to %s:%d", config->common.host, config->common.metrics); + exit(1); + } + + if (metrics_fds_length > MAX_FDS) + { + pgagroal_log_fatal("pgagroal: Too many descriptors %d", metrics_fds_length); + exit(1); + } + + start_metrics(); + + for (int i = 0; i < metrics_fds_length; i++) + { + pgagroal_log_debug("Metrics: %d", *(metrics_fds + i)); + } + } + else + { + pgagroal_log_debug("accept: %s (%d)", strerror(errno), watcher->fd); + } + errno = 0; + return; + } + + if (!fork()) + { + ev_loop_fork(loop); + shutdown_ports(); + /* We are leaving the socket descriptor valid such that the client won't reuse it */ + pgagroal_vault_prometheus(client_fd); + } + + pgagroal_disconnect(client_fd); + pgagroal_prometheus_self_sockets_sub(); +} + +static bool +accept_fatal(int error) +{ + switch (error) + { + case EAGAIN: + case ENETDOWN: + case EPROTO: + case ENOPROTOOPT: + case EHOSTDOWN: +#ifdef HAVE_LINUX + case ENONET: +#endif + case EHOSTUNREACH: + case EOPNOTSUPP: + case ENETUNREACH: + return false; + break; + } + + return true; +} diff --git a/uncrustify.cfg b/uncrustify.cfg new file mode 100644 index 00000000..19220fb8 --- /dev/null +++ b/uncrustify.cfg @@ -0,0 +1,3398 @@ +# Uncrustify-0.74.0 + +# +# General options +# + +# The type of line endings. +# +# Default: auto +newlines = auto # lf/crlf/cr/auto + +# The original size of tabs in the input. +# +# Default: 8 +input_tab_size = 3 # unsigned number + +# The size of tabs in the output (only used if align_with_tabs=true). +# +# Default: 8 +output_tab_size = 3 # unsigned number + +# The ASCII value of the string escape char, usually 92 (\) or (Pawn) 94 (^). +# +# Default: 92 +string_escape_char = 92 # unsigned number + +# Alternate string escape char (usually only used for Pawn). +# Only works right before the quote char. +string_escape_char2 = 0 # unsigned number + +# Replace tab characters found in string literals with the escape sequence \t +# instead. +string_replace_tab_chars = false # true/false + +# Allow interpreting '>=' and '>>=' as part of a template in code like +# 'void f(list>=val);'. If true, 'assert(x<0 && y>=3)' will be broken. +# Improvements to template detection may make this option obsolete. +tok_split_gte = false # true/false + +# Disable formatting of NL_CONT ('\\n') ended lines (e.g. multi-line macros). +disable_processing_nl_cont = false # true/false + +# Specify the marker used in comments to disable processing of part of the +# file. +# +# Default: *INDENT-OFF* +disable_processing_cmt = " *INDENT-OFF*" # string + +# Specify the marker used in comments to (re)enable processing in a file. +# +# Default: *INDENT-ON* +enable_processing_cmt = " *INDENT-ON*" # string + +# Enable parsing of digraphs. +enable_digraphs = false # true/false + +# Option to allow both disable_processing_cmt and enable_processing_cmt +# strings, if specified, to be interpreted as ECMAScript regular expressions. +# If true, a regex search will be performed within comments according to the +# specified patterns in order to disable/enable processing. +processing_cmt_as_regex = false # true/false + +# Add or remove the UTF-8 BOM (recommend 'remove'). +utf8_bom = ignore # ignore/add/remove/force/not_defined + +# If the file contains bytes with values between 128 and 255, but is not +# UTF-8, then output as UTF-8. +utf8_byte = false # true/false + +# Force the output encoding to UTF-8. +utf8_force = false # true/false + +# +# Spacing options +# + +# Add or remove space around non-assignment symbolic operators ('+', '/', '%', +# '<<', and so forth). +sp_arith = force # ignore/add/remove/force/not_defined + +# Add or remove space around arithmetic operators '+' and '-'. +# +# Overrides sp_arith. +sp_arith_additive = force # ignore/add/remove/force/not_defined + +# Add or remove space around assignment operator '=', '+=', etc. +sp_assign = force # ignore/add/remove/force/not_defined + +# Add or remove space around '=' in C++11 lambda capture specifications. +# +# Overrides sp_assign. +sp_cpp_lambda_assign = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after the capture specification of a C++11 lambda when +# an argument list is present, as in '[] (int x){ ... }'. +sp_cpp_lambda_square_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after the capture specification of a C++11 lambda with +# no argument list is present, as in '[] { ... }'. +sp_cpp_lambda_square_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after the opening parenthesis and before the closing +# parenthesis of a argument list of a C++11 lambda, as in +# '[]( int x ){ ... }'. +sp_cpp_lambda_argument_list = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after the argument list of a C++11 lambda, as in +# '[](int x) { ... }'. +sp_cpp_lambda_paren_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between a lambda body and its call operator of an +# immediately invoked lambda, as in '[]( ... ){ ... } ( ... )'. +sp_cpp_lambda_fparen = ignore # ignore/add/remove/force/not_defined + +# Add or remove space around assignment operator '=' in a prototype. +# +# If set to ignore, use sp_assign. +sp_assign_default = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before assignment operator '=', '+=', etc. +# +# Overrides sp_assign. +sp_before_assign = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after assignment operator '=', '+=', etc. +# +# Overrides sp_assign. +sp_after_assign = ignore # ignore/add/remove/force/not_defined + +# Add or remove space in 'NS_ENUM ('. +sp_enum_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space around assignment '=' in enum. +sp_enum_assign = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before assignment '=' in enum. +# +# Overrides sp_enum_assign. +sp_enum_before_assign = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after assignment '=' in enum. +# +# Overrides sp_enum_assign. +sp_enum_after_assign = ignore # ignore/add/remove/force/not_defined + +# Add or remove space around assignment ':' in enum. +sp_enum_colon = ignore # ignore/add/remove/force/not_defined + +# Add or remove space around preprocessor '##' concatenation operator. +# +# Default: add +sp_pp_concat = add # ignore/add/remove/force/not_defined + +# Add or remove space after preprocessor '#' stringify operator. +# Also affects the '#@' charizing operator. +sp_pp_stringify = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before preprocessor '#' stringify operator +# as in '#define x(y) L#y'. +sp_before_pp_stringify = ignore # ignore/add/remove/force/not_defined + +# Add or remove space around boolean operators '&&' and '||'. +sp_bool = force # ignore/add/remove/force/not_defined + +# Add or remove space around compare operator '<', '>', '==', etc. +sp_compare = force # ignore/add/remove/force/not_defined + +# Add or remove space inside '(' and ')'. +sp_inside_paren = remove # ignore/add/remove/force/not_defined + +# Add or remove space between nested parentheses, i.e. '((' vs. ') )'. +sp_paren_paren = remove # ignore/add/remove/force/not_defined + +# Add or remove space between back-to-back parentheses, i.e. ')(' vs. ') ('. +sp_cparen_oparen = remove # ignore/add/remove/force/not_defined + +# Whether to balance spaces inside nested parentheses. +sp_balance_nested_parens = false # true/false + +# Add or remove space between ')' and '{'. +sp_paren_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between nested braces, i.e. '{{' vs. '{ {'. +sp_brace_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before pointer star '*'. +sp_before_ptr_star = remove # ignore/add/remove/force/not_defined + +# Add or remove space before pointer star '*' that isn't followed by a +# variable name. If set to ignore, sp_before_ptr_star is used instead. +sp_before_unnamed_ptr_star = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between pointer stars '*', as in 'int ***a;'. +sp_between_ptr_star = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after pointer star '*', if followed by a word. +# +# Overrides sp_type_func. +sp_after_ptr_star = force # ignore/add/remove/force/not_defined + +# Add or remove space after pointer caret '^', if followed by a word. +sp_after_ptr_block_caret = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after pointer star '*', if followed by a qualifier. +sp_after_ptr_star_qualifier = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after a pointer star '*', if followed by a function +# prototype or function definition. +# +# Overrides sp_after_ptr_star and sp_type_func. +sp_after_ptr_star_func = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after a pointer star '*' in the trailing return of a +# function prototype or function definition. +sp_after_ptr_star_trailing = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between the pointer star '*' and the name of the variable +# in a function pointer definition. +sp_ptr_star_func_var = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after a pointer star '*', if followed by an open +# parenthesis, as in 'void* (*)()'. +sp_ptr_star_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before a pointer star '*', if followed by a function +# prototype or function definition. +sp_before_ptr_star_func = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before a pointer star '*' in the trailing return of a +# function prototype or function definition. +sp_before_ptr_star_trailing = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before a reference sign '&'. +sp_before_byref = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before a reference sign '&' that isn't followed by a +# variable name. If set to ignore, sp_before_byref is used instead. +sp_before_unnamed_byref = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after reference sign '&', if followed by a word. +# +# Overrides sp_type_func. +sp_after_byref = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after a reference sign '&', if followed by a function +# prototype or function definition. +# +# Overrides sp_after_byref and sp_type_func. +sp_after_byref_func = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before a reference sign '&', if followed by a function +# prototype or function definition. +sp_before_byref_func = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between type and word. In cases where total removal of +# whitespace would be a syntax error, a value of 'remove' is treated the same +# as 'force'. +# +# This also affects some other instances of space following a type that are +# not covered by other options; for example, between the return type and +# parenthesis of a function type template argument, between the type and +# parenthesis of an array parameter, or between 'decltype(...)' and the +# following word. +# +# Default: force +sp_after_type = force # ignore/add/remove/force/not_defined + +# Add or remove space between 'decltype(...)' and word, +# brace or function call. +sp_after_decltype = ignore # ignore/add/remove/force/not_defined + +# (D) Add or remove space before the parenthesis in the D constructs +# 'template Foo(' and 'class Foo('. +sp_before_template_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between 'template' and '<'. +# If set to ignore, sp_before_angle is used. +sp_template_angle = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before '<'. +sp_before_angle = ignore # ignore/add/remove/force/not_defined + +# Add or remove space inside '<' and '>'. +sp_inside_angle = ignore # ignore/add/remove/force/not_defined + +# Add or remove space inside '<>'. +sp_inside_angle_empty = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between '>' and ':'. +sp_angle_colon = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after '>'. +sp_after_angle = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between '>' and '(' as found in 'new List(foo);'. +sp_angle_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between '>' and '()' as found in 'new List();'. +sp_angle_paren_empty = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between '>' and a word as in 'List m;' or +# 'template static ...'. +sp_angle_word = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between '>' and '>' in '>>' (template stuff). +# +# Default: add +sp_angle_shift = add # ignore/add/remove/force/not_defined + +# (C++11) Permit removal of the space between '>>' in 'foo >'. Note +# that sp_angle_shift cannot remove the space without this option. +sp_permit_cpp11_shift = false # true/false + +# Add or remove space before '(' of control statements ('if', 'for', 'switch', +# 'while', etc.). +sp_before_sparen = force # ignore/add/remove/force/not_defined + +# Add or remove space inside '(' and ')' of control statements other than +# 'for'. +sp_inside_sparen = remove # ignore/add/remove/force/not_defined + +# Add or remove space after '(' of control statements other than 'for'. +# +# Overrides sp_inside_sparen. +sp_inside_sparen_open = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before ')' of control statements other than 'for'. +# +# Overrides sp_inside_sparen. +sp_inside_sparen_close = ignore # ignore/add/remove/force/not_defined + +# Add or remove space inside '(' and ')' of 'for' statements. +sp_inside_for = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after '(' of 'for' statements. +# +# Overrides sp_inside_for. +sp_inside_for_open = remove # ignore/add/remove/force/not_defined + +# Add or remove space before ')' of 'for' statements. +# +# Overrides sp_inside_for. +sp_inside_for_close = remove # ignore/add/remove/force/not_defined + +# Add or remove space between '((' or '))' of control statements. +sp_sparen_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after ')' of control statements. +sp_after_sparen = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between ')' and '{' of control statements. +sp_sparen_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between 'do' and '{'. +sp_do_brace_open = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between '}' and 'while'. +sp_brace_close_while = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between 'while' and '('. Overrides sp_before_sparen. +sp_while_paren_open = ignore # ignore/add/remove/force/not_defined + +# (D) Add or remove space between 'invariant' and '('. +sp_invariant_paren = ignore # ignore/add/remove/force/not_defined + +# (D) Add or remove space after the ')' in 'invariant (C) c'. +sp_after_invariant_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before empty statement ';' on 'if', 'for' and 'while'. +sp_special_semi = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before ';'. +# +# Default: remove +sp_before_semi = remove # ignore/add/remove/force/not_defined + +# Add or remove space before ';' in non-empty 'for' statements. +sp_before_semi_for = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before a semicolon of an empty left part of a for +# statement, as in 'for ( ; ; )'. +sp_before_semi_for_empty = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between the semicolons of an empty middle part of a for +# statement, as in 'for ( ; ; )'. +sp_between_semi_for_empty = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after ';', except when followed by a comment. +# +# Default: add +sp_after_semi = add # ignore/add/remove/force/not_defined + +# Add or remove space after ';' in non-empty 'for' statements. +# +# Default: force +sp_after_semi_for = force # ignore/add/remove/force/not_defined + +# Add or remove space after the final semicolon of an empty part of a for +# statement, as in 'for ( ; ; )'. +sp_after_semi_for_empty = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before '[' (except '[]'). +sp_before_square = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before '[' for a variable definition. +# +# Default: remove +sp_before_vardef_square = remove # ignore/add/remove/force/not_defined + +# Add or remove space before '[' for asm block. +sp_before_square_asm_block = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before '[]'. +sp_before_squares = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before C++17 structured bindings. +sp_cpp_before_struct_binding = ignore # ignore/add/remove/force/not_defined + +# Add or remove space inside a non-empty '[' and ']'. +sp_inside_square = ignore # ignore/add/remove/force/not_defined + +# Add or remove space inside '[]'. +sp_inside_square_empty = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space inside a non-empty Objective-C boxed array '@[' and +# ']'. If set to ignore, sp_inside_square is used. +sp_inside_square_oc_array = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after ',', i.e. 'a,b' vs. 'a, b'. +sp_after_comma = force # ignore/add/remove/force/not_defined + +# Add or remove space before ',', i.e. 'a,b' vs. 'a ,b'. +# +# Default: remove +sp_before_comma = remove # ignore/add/remove/force/not_defined + +# (C#) Add or remove space between ',' and ']' in multidimensional array type +# like 'int[,,]'. +sp_after_mdatype_commas = ignore # ignore/add/remove/force/not_defined + +# (C#) Add or remove space between '[' and ',' in multidimensional array type +# like 'int[,,]'. +sp_before_mdatype_commas = ignore # ignore/add/remove/force/not_defined + +# (C#) Add or remove space between ',' in multidimensional array type +# like 'int[,,]'. +sp_between_mdatype_commas = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between an open parenthesis and comma, +# i.e. '(,' vs. '( ,'. +# +# Default: force +sp_paren_comma = force # ignore/add/remove/force/not_defined + +# Add or remove space after the variadic '...' when preceded by a +# non-punctuator. +# The value REMOVE will be overriden with FORCE +sp_after_ellipsis = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before the variadic '...' when preceded by a +# non-punctuator. +# The value REMOVE will be overriden with FORCE +sp_before_ellipsis = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between a type and '...'. +sp_type_ellipsis = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between a '*' and '...'. +sp_ptr_type_ellipsis = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before pointer star '*'. +sp_before_ptr_star = remove # ignore/add/remove/force/not_defined + +# Add or remove space between ')' and '...'. +sp_paren_ellipsis = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between '&&' and '...'. +sp_byref_ellipsis = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between ')' and a qualifier such as 'const'. +sp_paren_qualifier = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between ')' and 'noexcept'. +sp_paren_noexcept = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after class ':'. +sp_after_class_colon = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before class ':'. +sp_before_class_colon = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after class constructor ':'. +# +# Default: add +sp_after_constr_colon = add # ignore/add/remove/force/not_defined + +# Add or remove space before class constructor ':'. +# +# Default: add +sp_before_constr_colon = add # ignore/add/remove/force/not_defined + +# Add or remove space before case ':'. +# +# Default: remove +sp_before_case_colon = remove # ignore/add/remove/force/not_defined + +# Add or remove space between 'operator' and operator sign. +sp_after_operator = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between the operator symbol and the open parenthesis, as +# in 'operator ++('. +sp_after_operator_sym = ignore # ignore/add/remove/force/not_defined + +# Overrides sp_after_operator_sym when the operator has no arguments, as in +# 'operator *()'. +sp_after_operator_sym_empty = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after C/D cast, i.e. 'cast(int)a' vs. 'cast(int) a' or +# '(int)a' vs. '(int) a'. +sp_after_cast = ignore # ignore/add/remove/force/not_defined + +# Add or remove spaces inside cast parentheses. +sp_inside_paren_cast = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between the type and open parenthesis in a C++ cast, +# i.e. 'int(exp)' vs. 'int (exp)'. +sp_cpp_cast_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between 'sizeof' and '('. +sp_sizeof_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between 'sizeof' and '...'. +sp_sizeof_ellipsis = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between 'sizeof...' and '('. +sp_sizeof_ellipsis_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between '...' and a parameter pack. +sp_ellipsis_parameter_pack = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between a parameter pack and '...'. +sp_parameter_pack_ellipsis = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between 'decltype' and '('. +sp_decltype_paren = ignore # ignore/add/remove/force/not_defined + +# (Pawn) Add or remove space after the tag keyword. +sp_after_tag = ignore # ignore/add/remove/force/not_defined + +# Add or remove space inside enum '{' and '}'. +sp_inside_braces_enum = ignore # ignore/add/remove/force/not_defined + +# Add or remove space inside struct/union '{' and '}'. +sp_inside_braces_struct = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space inside Objective-C boxed dictionary '{' and '}' +sp_inside_braces_oc_dict = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after open brace in an unnamed temporary +# direct-list-initialization +# if statement is a brace_init_lst +# works only if sp_brace_brace is set to ignore. +sp_after_type_brace_init_lst_open = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before close brace in an unnamed temporary +# direct-list-initialization +# if statement is a brace_init_lst +# works only if sp_brace_brace is set to ignore. +sp_before_type_brace_init_lst_close = ignore # ignore/add/remove/force/not_defined + +# Add or remove space inside an unnamed temporary direct-list-initialization +# if statement is a brace_init_lst +# works only if sp_brace_brace is set to ignore +# works only if sp_before_type_brace_init_lst_close is set to ignore. +sp_inside_type_brace_init_lst = ignore # ignore/add/remove/force/not_defined + +# Add or remove space inside '{' and '}'. +sp_inside_braces = ignore # ignore/add/remove/force/not_defined + +# Add or remove space inside '{}'. +sp_inside_braces_empty = ignore # ignore/add/remove/force/not_defined + +# Add or remove space around trailing return operator '->'. +sp_trailing_return = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between return type and function name. A minimum of 1 +# is forced except for pointer return types. +sp_type_func = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between type and open brace of an unnamed temporary +# direct-list-initialization. +sp_type_brace_init_lst = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between function name and '(' on function declaration. +sp_func_proto_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between function name and '()' on function declaration +# without parameters. +sp_func_proto_paren_empty = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between function name and '(' with a typedef specifier. +sp_func_type_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between alias name and '(' of a non-pointer function type typedef. +sp_func_def_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between function name and '()' on function definition +# without parameters. +sp_func_def_paren_empty = ignore # ignore/add/remove/force/not_defined + +# Add or remove space inside empty function '()'. +# Overrides sp_after_angle unless use_sp_after_angle_always is set to true. +sp_inside_fparens = remove # ignore/add/remove/force/not_defined + +# Add or remove space inside function '(' and ')'. +sp_inside_fparen = remove # ignore/add/remove/force/not_defined + +# Add or remove space inside the first parentheses in a function type, as in +# 'void (*x)(...)'. +sp_inside_tparen = remove # ignore/add/remove/force/not_defined + +# Add or remove space between the ')' and '(' in a function type, as in +# 'void (*x)(...)'. +sp_after_tparen_close = remove # ignore/add/remove/force/not_defined + +# Add or remove space between ']' and '(' when part of a function call. +sp_square_fparen = remove # ignore/add/remove/force/not_defined + +# Add or remove space between ')' and '{' of function. +sp_fparen_brace = remove # ignore/add/remove/force/not_defined + +# Add or remove space between ')' and '{' of a function call in object +# initialization. +# +# Overrides sp_fparen_brace. +sp_fparen_brace_initializer = ignore # ignore/add/remove/force/not_defined + +# (Java) Add or remove space between ')' and '{{' of double brace initializer. +sp_fparen_dbrace = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between function name and '(' on function calls. +sp_func_call_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between function name and '()' on function calls without +# parameters. If set to ignore (the default), sp_func_call_paren is used. +sp_func_call_paren_empty = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between the user function name and '(' on function +# calls. You need to set a keyword to be a user function in the config file, +# like: +# set func_call_user tr _ i18n +sp_func_call_user_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space inside user function '(' and ')'. +sp_func_call_user_inside_fparen = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between nested parentheses with user functions, +# i.e. '((' vs. '( ('. +sp_func_call_user_paren_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between a constructor/destructor and the open +# parenthesis. +sp_func_class_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between a constructor without parameters or destructor +# and '()'. +sp_func_class_paren_empty = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after 'return'. +# +# Default: force +sp_return = force # ignore/add/remove/force/not_defined + +# Add or remove space between 'return' and '('. +sp_return_paren = force # ignore/add/remove/force/not_defined + +# Add or remove space between 'return' and '{'. +sp_return_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between '__attribute__' and '('. +sp_attribute_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between 'defined' and '(' in '#if defined (FOO)'. +sp_defined_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between 'throw' and '(' in 'throw (something)'. +sp_throw_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between 'throw' and anything other than '(' as in +# '@throw [...];'. +sp_after_throw = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between 'catch' and '(' in 'catch (something) { }'. +# If set to ignore, sp_before_sparen is used. +sp_catch_paren = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space between '@catch' and '(' +# in '@catch (something) { }'. If set to ignore, sp_catch_paren is used. +sp_oc_catch_paren = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space before Objective-C protocol list +# as in '@protocol Protocol' or '@interface MyClass : NSObject'. +sp_before_oc_proto_list = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space between class name and '(' +# in '@interface className(categoryName):BaseClass' +sp_oc_classname_paren = ignore # ignore/add/remove/force/not_defined + +# (D) Add or remove space between 'version' and '(' +# in 'version (something) { }'. If set to ignore, sp_before_sparen is used. +sp_version_paren = ignore # ignore/add/remove/force/not_defined + +# (D) Add or remove space between 'scope' and '(' +# in 'scope (something) { }'. If set to ignore, sp_before_sparen is used. +sp_scope_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between 'super' and '(' in 'super (something)'. +# +# Default: remove +sp_super_paren = remove # ignore/add/remove/force/not_defined + +# Add or remove space between 'this' and '(' in 'this (something)'. +# +# Default: remove +sp_this_paren = remove # ignore/add/remove/force/not_defined + +# Add or remove space between a macro name and its definition. +sp_macro = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between a macro function ')' and its definition. +sp_macro_func = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between 'else' and '{' if on the same line. +sp_else_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between '}' and 'else' if on the same line. +sp_brace_else = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between '}' and the name of a typedef on the same line. +sp_brace_typedef = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before the '{' of a 'catch' statement, if the '{' and +# 'catch' are on the same line, as in 'catch (decl) {'. +sp_catch_brace = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space before the '{' of a '@catch' statement, if the '{' +# and '@catch' are on the same line, as in '@catch (decl) {'. +# If set to ignore, sp_catch_brace is used. +sp_oc_catch_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between '}' and 'catch' if on the same line. +sp_brace_catch = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space between '}' and '@catch' if on the same line. +# If set to ignore, sp_brace_catch is used. +sp_oc_brace_catch = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between 'finally' and '{' if on the same line. +sp_finally_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between '}' and 'finally' if on the same line. +sp_brace_finally = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between 'try' and '{' if on the same line. +sp_try_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between get/set and '{' if on the same line. +sp_getset_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between a variable and '{' for C++ uniform +# initialization. +sp_word_brace_init_lst = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between a variable and '{' for a namespace. +# +# Default: add +sp_word_brace_ns = add # ignore/add/remove/force/not_defined + +# Add or remove space before the '::' operator. +sp_before_dc = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after the '::' operator. +sp_after_dc = ignore # ignore/add/remove/force/not_defined + +# (D) Add or remove around the D named array initializer ':' operator. +sp_d_array_colon = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after the '!' (not) unary operator. +# +# Default: remove +sp_not = remove # ignore/add/remove/force/not_defined + +# Add or remove space after the '~' (invert) unary operator. +# +# Default: remove +sp_inv = remove # ignore/add/remove/force/not_defined + +# Add or remove space after the '&' (address-of) unary operator. This does not +# affect the spacing after a '&' that is part of a type. +# +# Default: remove +sp_addr = remove # ignore/add/remove/force/not_defined + +# Add or remove space around the '.' or '->' operators. +# +# Default: remove +sp_member = remove # ignore/add/remove/force/not_defined + +# Add or remove space after the '*' (dereference) unary operator. This does +# not affect the spacing after a '*' that is part of a type. +# +# Default: remove +sp_deref = remove # ignore/add/remove/force/not_defined + +# Add or remove space after '+' or '-', as in 'x = -5' or 'y = +7'. +# +# Default: remove +sp_sign = remove # ignore/add/remove/force/not_defined + +# Add or remove space between '++' and '--' the word to which it is being +# applied, as in '(--x)' or 'y++;'. +# +# Default: remove +sp_incdec = remove # ignore/add/remove/force/not_defined + +# Add or remove space before a backslash-newline at the end of a line. +# +# Default: add +sp_before_nl_cont = add # ignore/add/remove/force/not_defined + +# (OC) Add or remove space after the scope '+' or '-', as in '-(void) foo;' +# or '+(int) bar;'. +sp_after_oc_scope = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space after the colon in message specs, +# i.e. '-(int) f:(int) x;' vs. '-(int) f: (int) x;'. +sp_after_oc_colon = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space before the colon in message specs, +# i.e. '-(int) f: (int) x;' vs. '-(int) f : (int) x;'. +sp_before_oc_colon = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space after the colon in immutable dictionary expression +# 'NSDictionary *test = @{@"foo" :@"bar"};'. +sp_after_oc_dict_colon = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space before the colon in immutable dictionary expression +# 'NSDictionary *test = @{@"foo" :@"bar"};'. +sp_before_oc_dict_colon = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space after the colon in message specs, +# i.e. '[object setValue:1];' vs. '[object setValue: 1];'. +sp_after_send_oc_colon = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space before the colon in message specs, +# i.e. '[object setValue:1];' vs. '[object setValue :1];'. +sp_before_send_oc_colon = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space after the (type) in message specs, +# i.e. '-(int)f: (int) x;' vs. '-(int)f: (int)x;'. +sp_after_oc_type = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space after the first (type) in message specs, +# i.e. '-(int) f:(int)x;' vs. '-(int)f:(int)x;'. +sp_after_oc_return_type = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space between '@selector' and '(', +# i.e. '@selector(msgName)' vs. '@selector (msgName)'. +# Also applies to '@protocol()' constructs. +sp_after_oc_at_sel = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space between '@selector(x)' and the following word, +# i.e. '@selector(foo) a:' vs. '@selector(foo)a:'. +sp_after_oc_at_sel_parens = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space inside '@selector' parentheses, +# i.e. '@selector(foo)' vs. '@selector( foo )'. +# Also applies to '@protocol()' constructs. +sp_inside_oc_at_sel_parens = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space before a block pointer caret, +# i.e. '^int (int arg){...}' vs. ' ^int (int arg){...}'. +sp_before_oc_block_caret = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space after a block pointer caret, +# i.e. '^int (int arg){...}' vs. '^ int (int arg){...}'. +sp_after_oc_block_caret = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space between the receiver and selector in a message, +# as in '[receiver selector ...]'. +sp_after_oc_msg_receiver = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space after '@property'. +sp_after_oc_property = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove space between '@synchronized' and the open parenthesis, +# i.e. '@synchronized(foo)' vs. '@synchronized (foo)'. +sp_after_oc_synchronized = ignore # ignore/add/remove/force/not_defined + +# Add or remove space around the ':' in 'b ? t : f'. +sp_cond_colon = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before the ':' in 'b ? t : f'. +# +# Overrides sp_cond_colon. +sp_cond_colon_before = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after the ':' in 'b ? t : f'. +# +# Overrides sp_cond_colon. +sp_cond_colon_after = ignore # ignore/add/remove/force/not_defined + +# Add or remove space around the '?' in 'b ? t : f'. +sp_cond_question = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before the '?' in 'b ? t : f'. +# +# Overrides sp_cond_question. +sp_cond_question_before = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after the '?' in 'b ? t : f'. +# +# Overrides sp_cond_question. +sp_cond_question_after = ignore # ignore/add/remove/force/not_defined + +# In the abbreviated ternary form '(a ?: b)', add or remove space between '?' +# and ':'. +# +# Overrides all other sp_cond_* options. +sp_cond_ternary_short = ignore # ignore/add/remove/force/not_defined + +# Fix the spacing between 'case' and the label. Only 'ignore' and 'force' make +# sense here. +sp_case_label = ignore # ignore/add/remove/force/not_defined + +# (D) Add or remove space around the D '..' operator. +sp_range = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after ':' in a Java/C++11 range-based 'for', +# as in 'for (Type var : expr)'. +sp_after_for_colon = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before ':' in a Java/C++11 range-based 'for', +# as in 'for (Type var : expr)'. +sp_before_for_colon = ignore # ignore/add/remove/force/not_defined + +# (D) Add or remove space between 'extern' and '(' as in 'extern (C)'. +sp_extern_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after the opening of a C++ comment, as in '// A'. +sp_cmt_cpp_start = ignore # ignore/add/remove/force/not_defined + +# Add or remove space in a C++ region marker comment, as in '// BEGIN'. +# A region marker is defined as a comment which is not preceded by other text +# (i.e. the comment is the first non-whitespace on the line), and which starts +# with either 'BEGIN' or 'END'. +# +# Overrides sp_cmt_cpp_start. +sp_cmt_cpp_region = ignore # ignore/add/remove/force/not_defined + +# If true, space added with sp_cmt_cpp_start will be added after Doxygen +# sequences like '///', '///<', '//!' and '//!<'. +sp_cmt_cpp_doxygen = false # true/false + +# If true, space added with sp_cmt_cpp_start will be added after Qt translator +# or meta-data comments like '//:', '//=', and '//~'. +sp_cmt_cpp_qttr = false # true/false + +# Add or remove space between #else or #endif and a trailing comment. +sp_endif_cmt = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after 'new', 'delete' and 'delete[]'. +sp_after_new = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between 'new' and '(' in 'new()'. +sp_between_new_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between ')' and type in 'new(foo) BAR'. +sp_after_newop_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space inside parenthesis of the new operator +# as in 'new(foo) BAR'. +sp_inside_newop_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after the open parenthesis of the new operator, +# as in 'new(foo) BAR'. +# +# Overrides sp_inside_newop_paren. +sp_inside_newop_paren_open = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before the close parenthesis of the new operator, +# as in 'new(foo) BAR'. +# +# Overrides sp_inside_newop_paren. +sp_inside_newop_paren_close = ignore # ignore/add/remove/force/not_defined + +# Add or remove space before a trailing comment. +sp_before_tr_cmt = ignore # ignore/add/remove/force/not_defined + +# Number of spaces before a trailing comment. +sp_num_before_tr_cmt = 0 # unsigned number + +# Add or remove space before an embedded comment. +# +# Default: force +sp_before_emb_cmt = force # ignore/add/remove/force/not_defined + +# Number of spaces before an embedded comment. +# +# Default: 1 +sp_num_before_emb_cmt = 1 # unsigned number + +# Add or remove space after an embedded comment. +# +# Default: force +sp_after_emb_cmt = force # ignore/add/remove/force/not_defined + +# Number of spaces after an embedded comment. +# +# Default: 1 +sp_num_after_emb_cmt = 1 # unsigned number + +# (Java) Add or remove space between an annotation and the open parenthesis. +sp_annotation_paren = ignore # ignore/add/remove/force/not_defined + +# If true, vbrace tokens are dropped to the previous token and skipped. +sp_skip_vbrace_tokens = false # true/false + +# Add or remove space after 'noexcept'. +sp_after_noexcept = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after '_'. +sp_vala_after_translation = ignore # ignore/add/remove/force/not_defined + +# If true, a is inserted after #define. +force_tab_after_define = false # true/false + +# +# Indenting options +# + +# The number of columns to indent per level. Usually 2, 3, 4, or 8. +# +# Default: 8 +indent_columns = 3 # unsigned number + +# The continuation indent. If non-zero, this overrides the indent of '(', '[' +# and '=' continuation indents. Negative values are OK; negative value is +# absolute and not increased for each '(' or '[' level. +# +# For FreeBSD, this is set to 4. +indent_continue = 0 # number + +# The continuation indent, only for class header line(s). If non-zero, this +# overrides the indent of 'class' continuation indents. +indent_continue_class_head = 0 # unsigned number + +# Whether to indent empty lines (i.e. lines which contain only spaces before +# the newline character). +indent_single_newlines = false # true/false + +# The continuation indent for func_*_param if they are true. If non-zero, this +# overrides the indent. +indent_param = 0 # unsigned number + +# How to use tabs when indenting code. +# +# 0: Spaces only +# 1: Indent with tabs to brace level, align with spaces (default) +# 2: Indent and align with tabs, using spaces when not on a tabstop +# +# Default: 1 +indent_with_tabs = 0 # unsigned number + +# Whether to indent comments that are not at a brace level with tabs on a +# tabstop. Requires indent_with_tabs=2. If false, will use spaces. +indent_cmt_with_tabs = false # true/false + +# Whether to indent strings broken by '\' so that they line up. +indent_align_string = false # true/false + +# The number of spaces to indent multi-line XML strings. +# Requires indent_align_string=true. +indent_xml_string = 0 # unsigned number + +# Spaces to indent '{' from level. +indent_brace = 0 # unsigned number + +# Whether braces are indented to the body level. +indent_braces = false # true/false + +# Whether to disable indenting function braces if indent_braces=true. +indent_braces_no_func = false # true/false + +# Whether to disable indenting class braces if indent_braces=true. +indent_braces_no_class = false # true/false + +# Whether to disable indenting struct braces if indent_braces=true. +indent_braces_no_struct = false # true/false + +# Whether to indent based on the size of the brace parent, +# i.e. 'if' => 3 spaces, 'for' => 4 spaces, etc. +indent_brace_parent = false # true/false + +# Whether to indent based on the open parenthesis instead of the open brace +# in '({\n'. +indent_paren_open_brace = false # true/false + +# (C#) Whether to indent the brace of a C# delegate by another level. +indent_cs_delegate_brace = false # true/false + +# (C#) Whether to indent a C# delegate (to handle delegates with no brace) by +# another level. +indent_cs_delegate_body = false # true/false + +# Whether to indent the body of a 'namespace'. +indent_namespace = false # true/false + +# Whether to indent only the first namespace, and not any nested namespaces. +# Requires indent_namespace=true. +indent_namespace_single_indent = false # true/false + +# The number of spaces to indent a namespace block. +# If set to zero, use the value indent_columns +indent_namespace_level = 0 # unsigned number + +# If the body of the namespace is longer than this number, it won't be +# indented. Requires indent_namespace=true. 0 means no limit. +indent_namespace_limit = 0 # unsigned number + +# Whether the 'extern "C"' body is indented. +indent_extern = false # true/false + +# Whether the 'class' body is indented. +indent_class = false # true/false + +# Additional indent before the leading base class colon. +# Negative values decrease indent down to the first column. +# Requires a newline break before colon (see pos_class_colon +# and nl_class_colon) +indent_before_class_colon = 0 # number + +# Whether to indent the stuff after a leading base class colon. +indent_class_colon = false # true/false + +# Whether to indent based on a class colon instead of the stuff after the +# colon. Requires indent_class_colon=true. +indent_class_on_colon = false # true/false + +# Whether to indent the stuff after a leading class initializer colon. +indent_constr_colon = false # true/false + +# Virtual indent from the ':' for leading member initializers. +# +# Default: 2 +indent_ctor_init_leading = 2 # unsigned number + +# Virtual indent from the ':' for following member initializers. +# +# Default: 2 +indent_ctor_init_following = 2 # unsigned number + +# Additional indent for constructor initializer list. +# Negative values decrease indent down to the first column. +indent_ctor_init = 0 # number + +# Whether to indent 'if' following 'else' as a new block under the 'else'. +# If false, 'else\nif' is treated as 'else if' for indenting purposes. +indent_else_if = false # true/false + +# Amount to indent variable declarations after a open brace. +# +# <0: Relative +# >=0: Absolute +indent_var_def_blk = 0 # number + +# Whether to indent continued variable declarations instead of aligning. +indent_var_def_cont = false # true/false + +# How to indent continued shift expressions ('<<' and '>>'). +# Set align_left_shift=false when using this. +# 0: Align shift operators instead of indenting them (default) +# 1: Indent by one level +# -1: Preserve original indentation +indent_shift = 0 # number + +# Whether to force indentation of function definitions to start in column 1. +indent_func_def_force_col1 = false # true/false + +# Whether to indent continued function call parameters one indent level, +# rather than aligning parameters under the open parenthesis. +indent_func_call_param = false # true/false + +# Whether to indent continued function definition parameters one indent level, +# rather than aligning parameters under the open parenthesis. +indent_func_def_param = false # true/false + +# for function definitions, only if indent_func_def_param is false +# Allows to align params when appropriate and indent them when not +# behave as if it was true if paren position is more than this value +# if paren position is more than the option value +indent_func_def_param_paren_pos_threshold = 0 # unsigned number + +# Whether to indent continued function call prototype one indent level, +# rather than aligning parameters under the open parenthesis. +indent_func_proto_param = false # true/false + +# Whether to indent continued function call declaration one indent level, +# rather than aligning parameters under the open parenthesis. +indent_func_class_param = false # true/false + +# Whether to indent continued class variable constructors one indent level, +# rather than aligning parameters under the open parenthesis. +indent_func_ctor_var_param = false # true/false + +# Whether to indent continued template parameter list one indent level, +# rather than aligning parameters under the open parenthesis. +indent_template_param = false # true/false + +# Double the indent for indent_func_xxx_param options. +# Use both values of the options indent_columns and indent_param. +indent_func_param_double = false # true/false + +# Indentation column for standalone 'const' qualifier on a function +# prototype. +indent_func_const = 0 # unsigned number + +# Indentation column for standalone 'throw' qualifier on a function +# prototype. +indent_func_throw = 0 # unsigned number + +# How to indent within a macro followed by a brace on the same line +# This allows reducing the indent in macros that have (for example) +# `do { ... } while (0)` blocks bracketing them. +# +# true: add an indent for the brace on the same line as the macro +# false: do not add an indent for the brace on the same line as the macro +# +# Default: true +indent_macro_brace = true # true/false + +# The number of spaces to indent a continued '->' or '.'. +# Usually set to 0, 1, or indent_columns. +indent_member = 0 # unsigned number + +# Whether lines broken at '.' or '->' should be indented by a single indent. +# The indent_member option will not be effective if this is set to true. +indent_member_single = false # true/false + +# Spaces to indent single line ('//') comments on lines before code. +indent_single_line_comments_before = 0 # unsigned number + +# Spaces to indent single line ('//') comments on lines after code. +indent_single_line_comments_after = 0 # unsigned number + +# When opening a paren for a control statement (if, for, while, etc), increase +# the indent level by this value. Negative values decrease the indent level. +indent_sparen_extra = 0 # number + +# Whether to indent trailing single line ('//') comments relative to the code +# instead of trying to keep the same absolute column. +indent_relative_single_line_comments = false # true/false + +# Spaces to indent 'case' from 'switch'. Usually 0 or indent_columns. +# It might be wise to choose the same value for the option indent_case_brace. +indent_switch_case = 3 # unsigned number + +# Spaces to indent the body of a 'switch' before any 'case'. +# Usually the same as indent_columns or indent_switch_case. +indent_switch_body = 0 # unsigned number + +# Spaces to indent '{' from 'case'. By default, the brace will appear under +# the 'c' in case. Usually set to 0 or indent_columns. Negative values are OK. +# It might be wise to choose the same value for the option indent_switch_case. +indent_case_brace = 0 # number + +# indent 'break' with 'case' from 'switch'. +indent_switch_break_with_case = false # true/false + +# Whether to indent preprocessor statements inside of switch statements. +# +# Default: true +indent_switch_pp = true # true/false + +# Spaces to shift the 'case' line, without affecting any other lines. +# Usually 0. +indent_case_shift = 0 # unsigned number + +# Whether to align comments before 'case' with the 'case'. +# +# Default: true +indent_case_comment = true # true/false + +# Whether to indent comments not found in first column. +# +# Default: true +indent_comment = true # true/false + +# Whether to indent comments found in first column. +indent_col1_comment = false # true/false + +# Whether to indent multi string literal in first column. +indent_col1_multi_string_literal = false # true/false + +# Align comments on adjacent lines that are this many columns apart or less. +# +# Default: 3 +indent_comment_align_thresh = 3 # unsigned number + +# Whether to ignore indent for goto labels. +indent_ignore_label = false # true/false + +# How to indent goto labels. Requires indent_ignore_label=false. +# +# >0: Absolute column where 1 is the leftmost column +# <=0: Subtract from brace indent +# +# Default: 1 +indent_label = 1 # number + +# How to indent access specifiers that are followed by a +# colon. +# +# >0: Absolute column where 1 is the leftmost column +# <=0: Subtract from brace indent +# +# Default: 1 +indent_access_spec = 1 # number + +# Whether to indent the code after an access specifier by one level. +# If true, this option forces 'indent_access_spec=0'. +indent_access_spec_body = false # true/false + +# If an open parenthesis is followed by a newline, whether to indent the next +# line so that it lines up after the open parenthesis (not recommended). +indent_paren_nl = false # true/false + +# How to indent a close parenthesis after a newline. +# +# 0: Indent to body level (default) +# 1: Align under the open parenthesis +# 2: Indent to the brace level +indent_paren_close = 0 # unsigned number + +# Whether to indent the open parenthesis of a function definition, +# if the parenthesis is on its own line. +indent_paren_after_func_def = false # true/false + +# Whether to indent the open parenthesis of a function declaration, +# if the parenthesis is on its own line. +indent_paren_after_func_decl = false # true/false + +# Whether to indent the open parenthesis of a function call, +# if the parenthesis is on its own line. +indent_paren_after_func_call = false # true/false + +# How to indent a comma when inside braces. +# 0: Indent by one level (default) +# 1: Align under the open brace +# -1: Preserve original indentation +indent_comma_brace = 0 # number + +# How to indent a comma when inside parentheses. +# 0: Indent by one level (default) +# 1: Align under the open parenthesis +# -1: Preserve original indentation +indent_comma_paren = 0 # number + +# How to indent a Boolean operator when inside parentheses. +# 0: Indent by one level (default) +# 1: Align under the open parenthesis +# -1: Preserve original indentation +indent_bool_paren = 0 # number + +# Whether to indent a semicolon when inside a for parenthesis. +# If true, aligns under the open for parenthesis. +indent_semicolon_for_paren = false # true/false + +# Whether to align the first expression to following ones +# if indent_bool_paren=true. +indent_first_bool_expr = false # true/false + +# Whether to align the first expression to following ones +# if indent_semicolon_for_paren=true. +indent_first_for_expr = false # true/false + +# If an open square is followed by a newline, whether to indent the next line +# so that it lines up after the open square (not recommended). +indent_square_nl = false # true/false + +# (ESQL/C) Whether to preserve the relative indent of 'EXEC SQL' bodies. +indent_preserve_sql = false # true/false + +# Whether to align continued statements at the '='. If false or if the '=' is +# followed by a newline, the next line is indent one tab. +# +# Default: true +indent_align_assign = true # true/false + +# If true, the indentation of the chunks after a '=' sequence will be set at +# LHS token indentation column before '='. +indent_off_after_assign = false # true/false + +# Whether to align continued statements at the '('. If false or the '(' is +# followed by a newline, the next line indent is one tab. +# +# Default: true +indent_align_paren = true # true/false + +# (OC) Whether to indent Objective-C code inside message selectors. +indent_oc_inside_msg_sel = false # true/false + +# (OC) Whether to indent Objective-C blocks at brace level instead of usual +# rules. +indent_oc_block = false # true/false + +# (OC) Indent for Objective-C blocks in a message relative to the parameter +# name. +# +# =0: Use indent_oc_block rules +# >0: Use specified number of spaces to indent +indent_oc_block_msg = 0 # unsigned number + +# (OC) Minimum indent for subsequent parameters +indent_oc_msg_colon = 0 # unsigned number + +# (OC) Whether to prioritize aligning with initial colon (and stripping spaces +# from lines, if necessary). +# +# Default: true +indent_oc_msg_prioritize_first_colon = true # true/false + +# (OC) Whether to indent blocks the way that Xcode does by default +# (from the keyword if the parameter is on its own line; otherwise, from the +# previous indentation level). Requires indent_oc_block_msg=true. +indent_oc_block_msg_xcode_style = false # true/false + +# (OC) Whether to indent blocks from where the brace is, relative to a +# message keyword. Requires indent_oc_block_msg=true. +indent_oc_block_msg_from_keyword = false # true/false + +# (OC) Whether to indent blocks from where the brace is, relative to a message +# colon. Requires indent_oc_block_msg=true. +indent_oc_block_msg_from_colon = false # true/false + +# (OC) Whether to indent blocks from where the block caret is. +# Requires indent_oc_block_msg=true. +indent_oc_block_msg_from_caret = false # true/false + +# (OC) Whether to indent blocks from where the brace caret is. +# Requires indent_oc_block_msg=true. +indent_oc_block_msg_from_brace = false # true/false + +# When indenting after virtual brace open and newline add further spaces to +# reach this minimum indent. +indent_min_vbrace_open = 0 # unsigned number + +# Whether to add further spaces after regular indent to reach next tabstop +# when indenting after virtual brace open and newline. +indent_vbrace_open_on_tabstop = false # true/false + +# How to indent after a brace followed by another token (not a newline). +# true: indent all contained lines to match the token +# false: indent all contained lines to match the brace +# +# Default: true +indent_token_after_brace = true # true/false + +# Whether to indent the body of a C++11 lambda. +indent_cpp_lambda_body = false # true/false + +# How to indent compound literals that are being returned. +# true: add both the indent from return & the compound literal open brace +# (i.e. 2 indent levels) +# false: only indent 1 level, don't add the indent for the open brace, only +# add the indent for the return. +# +# Default: true +indent_compound_literal_return = true # true/false + +# (C#) Whether to indent a 'using' block if no braces are used. +# +# Default: true +indent_using_block = true # true/false + +# How to indent the continuation of ternary operator. +# +# 0: Off (default) +# 1: When the `if_false` is a continuation, indent it under `if_false` +# 2: When the `:` is a continuation, indent it under `?` +indent_ternary_operator = 0 # unsigned number + +# Whether to indent the statements inside ternary operator. +indent_inside_ternary_operator = false # true/false + +# If true, the indentation of the chunks after a `return` sequence will be set at return indentation column. +indent_off_after_return = false # true/false + +# If true, the indentation of the chunks after a `return new` sequence will be set at return indentation column. +indent_off_after_return_new = false # true/false + +# If true, the tokens after return are indented with regular single indentation. By default (false) the indentation is after the return token. +indent_single_after_return = false # true/false + +# Whether to ignore indent and alignment for 'asm' blocks (i.e. assume they +# have their own indentation). +indent_ignore_asm_block = false # true/false + +# Don't indent the close parenthesis of a function definition, +# if the parenthesis is on its own line. +donot_indent_func_def_close_paren = false # true/false + +# +# Newline adding and removing options +# + +# Whether to collapse empty blocks between '{' and '}'. +# If true, overrides nl_inside_empty_func +nl_collapse_empty_body = false # true/false + +# Don't split one-line braced assignments, as in 'foo_t f = { 1, 2 };'. +nl_assign_leave_one_liners = false # true/false + +# Don't split one-line braced statements inside a 'class xx { }' body. +nl_class_leave_one_liners = false # true/false + +# Don't split one-line enums, as in 'enum foo { BAR = 15 };' +nl_enum_leave_one_liners = false # true/false + +# Don't split one-line get or set functions. +nl_getset_leave_one_liners = false # true/false + +# (C#) Don't split one-line property get or set functions. +nl_cs_property_leave_one_liners = false # true/false + +# Don't split one-line function definitions, as in 'int foo() { return 0; }'. +# might modify nl_func_type_name +nl_func_leave_one_liners = false # true/false + +# Don't split one-line C++11 lambdas, as in '[]() { return 0; }'. +nl_cpp_lambda_leave_one_liners = false # true/false + +# Don't split one-line if/else statements, as in 'if(...) b++;'. +nl_if_leave_one_liners = false # true/false + +# Don't split one-line while statements, as in 'while(...) b++;'. +nl_while_leave_one_liners = false # true/false + +# Don't split one-line do statements, as in 'do { b++; } while(...);'. +nl_do_leave_one_liners = false # true/false + +# Don't split one-line for statements, as in 'for(...) b++;'. +nl_for_leave_one_liners = false # true/false + +# (OC) Don't split one-line Objective-C messages. +nl_oc_msg_leave_one_liner = false # true/false + +# (OC) Add or remove newline between method declaration and '{'. +nl_oc_mdef_brace = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove newline between Objective-C block signature and '{'. +nl_oc_block_brace = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove blank line before '@interface' statement. +nl_oc_before_interface = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove blank line before '@implementation' statement. +nl_oc_before_implementation = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove blank line before '@end' statement. +nl_oc_before_end = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove newline between '@interface' and '{'. +nl_oc_interface_brace = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove newline between '@implementation' and '{'. +nl_oc_implementation_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove newlines at the start of the file. +nl_start_of_file = ignore # ignore/add/remove/force/not_defined + +# The minimum number of newlines at the start of the file (only used if +# nl_start_of_file is 'add' or 'force'). +nl_start_of_file_min = 0 # unsigned number + +# Add or remove newline at the end of the file. +nl_end_of_file = ignore # ignore/add/remove/force/not_defined + +# The minimum number of newlines at the end of the file (only used if +# nl_end_of_file is 'add' or 'force'). +nl_end_of_file_min = 0 # unsigned number + +# Add or remove newline between '=' and '{'. +nl_assign_brace = ignore # ignore/add/remove/force/not_defined + +# (D) Add or remove newline between '=' and '['. +nl_assign_square = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between '[]' and '{'. +nl_tsquare_brace = ignore # ignore/add/remove/force/not_defined + +# (D) Add or remove newline after '= ['. Will also affect the newline before +# the ']'. +nl_after_square_assign = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between a function call's ')' and '{', as in +# 'list_for_each(item, &list) { }'. +nl_fcall_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between 'enum' and '{'. +nl_enum_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between 'enum' and 'class'. +nl_enum_class = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between 'enum class' and the identifier. +nl_enum_class_identifier = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between 'enum class' type and ':'. +nl_enum_identifier_colon = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between 'enum class identifier :' and type. +nl_enum_colon_type = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between 'struct and '{'. +nl_struct_brace = add # ignore/add/remove/force/not_defined + +# Add or remove newline between 'union' and '{'. +nl_union_brace = add # ignore/add/remove/force/not_defined + +# Add or remove newline between 'if' and '{'. +nl_if_brace = add # ignore/add/remove/force/not_defined + +# Add or remove newline between '}' and 'else'. +nl_brace_else = add # ignore/add/remove/force/not_defined + +# Add or remove newline between 'else if' and '{'. If set to ignore, +# nl_if_brace is used instead. +nl_elseif_brace = add # ignore/add/remove/force/not_defined + +# Add or remove newline between 'else' and '{'. +nl_else_brace = add # ignore/add/remove/force/not_defined + +# Add or remove newline between 'else' and 'if'. +nl_else_if = remove # ignore/add/remove/force/not_defined + +# Add or remove newline before '{' opening brace +nl_before_opening_brace_func_class_def = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline before 'if'/'else if' closing parenthesis. +nl_before_if_closing_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between '}' and 'finally'. +nl_brace_finally = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between 'finally' and '{'. +nl_finally_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between 'try' and '{'. +nl_try_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between get/set and '{'. +nl_getset_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between 'for' and '{'. +nl_for_brace = add # ignore/add/remove/force/not_defined + +# Add or remove newline before the '{' of a 'catch' statement, as in +# 'catch (decl) {'. +nl_catch_brace = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove newline before the '{' of a '@catch' statement, as in +# '@catch (decl) {'. If set to ignore, nl_catch_brace is used. +nl_oc_catch_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between '}' and 'catch'. +nl_brace_catch = ignore # ignore/add/remove/force/not_defined + +# (OC) Add or remove newline between '}' and '@catch'. If set to ignore, +# nl_brace_catch is used. +nl_oc_brace_catch = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between '}' and ']'. +nl_brace_square = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between '}' and ')' in a function invocation. +nl_brace_fparen = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between 'while' and '{'. +nl_while_brace = add # ignore/add/remove/force/not_defined + +# (D) Add or remove newline between 'scope (x)' and '{'. +nl_scope_brace = ignore # ignore/add/remove/force/not_defined + +# (D) Add or remove newline between 'unittest' and '{'. +nl_unittest_brace = ignore # ignore/add/remove/force/not_defined + +# (D) Add or remove newline between 'version (x)' and '{'. +nl_version_brace = ignore # ignore/add/remove/force/not_defined + +# (C#) Add or remove newline between 'using' and '{'. +nl_using_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between two open or close braces. Due to general +# newline/brace handling, REMOVE may not work. +nl_brace_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between 'do' and '{'. +nl_do_brace = add # ignore/add/remove/force/not_defined + +# Add or remove newline between '}' and 'while' of 'do' statement. +nl_brace_while = add # ignore/add/remove/force/not_defined + +# Add or remove newline between 'switch' and '{'. +nl_switch_brace = add # ignore/add/remove/force/not_defined + +# Add or remove newline between 'synchronized' and '{'. +nl_synchronized_brace = ignore # ignore/add/remove/force/not_defined + +# Add a newline between ')' and '{' if the ')' is on a different line than the +# if/for/etc. +# +# Overrides nl_for_brace, nl_if_brace, nl_switch_brace, nl_while_switch and +# nl_catch_brace. +nl_multi_line_cond = false # true/false + +# Add a newline after '(' if an if/for/while/switch condition spans multiple +# lines +nl_multi_line_sparen_open = ignore # ignore/add/remove/force/not_defined + +# Add a newline before ')' if an if/for/while/switch condition spans multiple +# lines. Overrides nl_before_if_closing_paren if both are specified. +nl_multi_line_sparen_close = ignore # ignore/add/remove/force/not_defined + +# Force a newline in a define after the macro name for multi-line defines. +nl_multi_line_define = false # true/false + +# Whether to add a newline before 'case', and a blank line before a 'case' +# statement that follows a ';' or '}'. +nl_before_case = false # true/false + +# Whether to add a newline after a 'case' statement. +nl_after_case = false # true/false + +# Add or remove newline between a case ':' and '{'. +# +# Overrides nl_after_case. +nl_case_colon_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between ')' and 'throw'. +nl_before_throw = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between 'namespace' and '{'. +nl_namespace_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline after 'template<...>' of a template class. +nl_template_class = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline after 'template<...>' of a template class declaration. +# +# Overrides nl_template_class. +nl_template_class_decl = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline after 'template<>' of a specialized class declaration. +# +# Overrides nl_template_class_decl. +nl_template_class_decl_special = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline after 'template<...>' of a template class definition. +# +# Overrides nl_template_class. +nl_template_class_def = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline after 'template<>' of a specialized class definition. +# +# Overrides nl_template_class_def. +nl_template_class_def_special = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline after 'template<...>' of a template function. +nl_template_func = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline after 'template<...>' of a template function +# declaration. +# +# Overrides nl_template_func. +nl_template_func_decl = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline after 'template<>' of a specialized function +# declaration. +# +# Overrides nl_template_func_decl. +nl_template_func_decl_special = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline after 'template<...>' of a template function +# definition. +# +# Overrides nl_template_func. +nl_template_func_def = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline after 'template<>' of a specialized function +# definition. +# +# Overrides nl_template_func_def. +nl_template_func_def_special = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline after 'template<...>' of a template variable. +nl_template_var = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between 'template<...>' and 'using' of a templated +# type alias. +nl_template_using = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between 'class' and '{'. +nl_class_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline before or after (depending on pos_class_comma, +# may not be IGNORE) each',' in the base class list. +nl_class_init_args = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline after each ',' in the constructor member +# initialization. Related to nl_constr_colon, pos_constr_colon and +# pos_constr_comma. +nl_constr_init_args = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline before first element, after comma, and after last +# element, in 'enum'. +nl_enum_own_lines = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between return type and function name in a function +# definition. +# might be modified by nl_func_leave_one_liners +nl_func_type_name = force # ignore/add/remove/force/not_defined + +# Add or remove newline between return type and function name inside a class +# definition. If set to ignore, nl_func_type_name or nl_func_proto_type_name +# is used instead. +nl_func_type_name_class = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between class specification and '::' +# in 'void A::f() { }'. Only appears in separate member implementation (does +# not appear with in-line implementation). +nl_func_class_scope = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between function scope and name, as in +# 'void A :: f() { }'. +nl_func_scope_name = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between return type and function name in a prototype. +nl_func_proto_type_name = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between a function name and the opening '(' in the +# declaration. +nl_func_paren = ignore # ignore/add/remove/force/not_defined + +# Overrides nl_func_paren for functions with no parameters. +nl_func_paren_empty = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between a function name and the opening '(' in the +# definition. +nl_func_def_paren = ignore # ignore/add/remove/force/not_defined + +# Overrides nl_func_def_paren for functions with no parameters. +nl_func_def_paren_empty = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between a function name and the opening '(' in the +# call. +nl_func_call_paren = ignore # ignore/add/remove/force/not_defined + +# Overrides nl_func_call_paren for functions with no parameters. +nl_func_call_paren_empty = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline after '(' in a function declaration. +nl_func_decl_start = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline after '(' in a function definition. +nl_func_def_start = ignore # ignore/add/remove/force/not_defined + +# Overrides nl_func_decl_start when there is only one parameter. +nl_func_decl_start_single = ignore # ignore/add/remove/force/not_defined + +# Overrides nl_func_def_start when there is only one parameter. +nl_func_def_start_single = ignore # ignore/add/remove/force/not_defined + +# Whether to add a newline after '(' in a function declaration if '(' and ')' +# are in different lines. If false, nl_func_decl_start is used instead. +nl_func_decl_start_multi_line = false # true/false + +# Whether to add a newline after '(' in a function definition if '(' and ')' +# are in different lines. If false, nl_func_def_start is used instead. +nl_func_def_start_multi_line = false # true/false + +# Add or remove newline after each ',' in a function declaration. +nl_func_decl_args = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline after each ',' in a function definition. +nl_func_def_args = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline after each ',' in a function call. +nl_func_call_args = ignore # ignore/add/remove/force/not_defined + +# Whether to add a newline after each ',' in a function declaration if '(' +# and ')' are in different lines. If false, nl_func_decl_args is used instead. +nl_func_decl_args_multi_line = false # true/false + +# Whether to add a newline after each ',' in a function definition if '(' +# and ')' are in different lines. If false, nl_func_def_args is used instead. +nl_func_def_args_multi_line = false # true/false + +# Add or remove newline before the ')' in a function declaration. +nl_func_decl_end = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline before the ')' in a function definition. +nl_func_def_end = ignore # ignore/add/remove/force/not_defined + +# Overrides nl_func_decl_end when there is only one parameter. +nl_func_decl_end_single = ignore # ignore/add/remove/force/not_defined + +# Overrides nl_func_def_end when there is only one parameter. +nl_func_def_end_single = ignore # ignore/add/remove/force/not_defined + +# Whether to add a newline before ')' in a function declaration if '(' and ')' +# are in different lines. If false, nl_func_decl_end is used instead. +nl_func_decl_end_multi_line = false # true/false + +# Whether to add a newline before ')' in a function definition if '(' and ')' +# are in different lines. If false, nl_func_def_end is used instead. +nl_func_def_end_multi_line = false # true/false + +# Add or remove newline between '()' in a function declaration. +nl_func_decl_empty = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between '()' in a function definition. +nl_func_def_empty = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between '()' in a function call. +nl_func_call_empty = ignore # ignore/add/remove/force/not_defined + +# Whether to add a newline after '(' in a function call, +# has preference over nl_func_call_start_multi_line. +nl_func_call_start = ignore # ignore/add/remove/force/not_defined + +# Whether to add a newline before ')' in a function call. +nl_func_call_end = ignore # ignore/add/remove/force/not_defined + +# Whether to add a newline after '(' in a function call if '(' and ')' are in +# different lines. +nl_func_call_start_multi_line = false # true/false + +# Whether to add a newline after each ',' in a function call if '(' and ')' +# are in different lines. +nl_func_call_args_multi_line = false # true/false + +# Whether to add a newline before ')' in a function call if '(' and ')' are in +# different lines. +nl_func_call_end_multi_line = false # true/false + +# Whether to respect nl_func_call_XXX option in case of closure args. +nl_func_call_args_multi_line_ignore_closures = false # true/false + +# Whether to add a newline after '<' of a template parameter list. +nl_template_start = false # true/false + +# Whether to add a newline after each ',' in a template parameter list. +nl_template_args = false # true/false + +# Whether to add a newline before '>' of a template parameter list. +nl_template_end = false # true/false + +# (OC) Whether to put each Objective-C message parameter on a separate line. +# See nl_oc_msg_leave_one_liner. +nl_oc_msg_args = false # true/false + +# Add or remove newline between function signature and '{'. +nl_fdef_brace = add # ignore/add/remove/force/not_defined + +# Add or remove newline between function signature and '{', +# if signature ends with ')'. Overrides nl_fdef_brace. +nl_fdef_brace_cond = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between C++11 lambda signature and '{'. +nl_cpp_ldef_brace = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline between 'return' and the return expression. +nl_return_expr = ignore # ignore/add/remove/force/not_defined + +# Whether to add a newline after semicolons, except in 'for' statements. +nl_after_semicolon = false # true/false + +# (Java) Add or remove newline between the ')' and '{{' of the double brace +# initializer. +nl_paren_dbrace_open = ignore # ignore/add/remove/force/not_defined + +# Whether to add a newline after the type in an unnamed temporary +# direct-list-initialization. +nl_type_brace_init_lst = ignore # ignore/add/remove/force/not_defined + +# Whether to add a newline after the open brace in an unnamed temporary +# direct-list-initialization. +nl_type_brace_init_lst_open = ignore # ignore/add/remove/force/not_defined + +# Whether to add a newline before the close brace in an unnamed temporary +# direct-list-initialization. +nl_type_brace_init_lst_close = ignore # ignore/add/remove/force/not_defined + +# Whether to add a newline before '{'. +nl_before_brace_open = false # true/false + +# Whether to add a newline after '{'. +nl_after_brace_open = false # true/false + +# Whether to add a newline between the open brace and a trailing single-line +# comment. Requires nl_after_brace_open=true. +nl_after_brace_open_cmt = false # true/false + +# Whether to add a newline after a virtual brace open with a non-empty body. +# These occur in un-braced if/while/do/for statement bodies. +nl_after_vbrace_open = false # true/false + +# Whether to add a newline after a virtual brace open with an empty body. +# These occur in un-braced if/while/do/for statement bodies. +nl_after_vbrace_open_empty = false # true/false + +# Whether to add a newline after '}'. Does not apply if followed by a +# necessary ';'. +nl_after_brace_close = false # true/false + +# Whether to add a newline after a virtual brace close, +# as in 'if (foo) a++; return;'. +nl_after_vbrace_close = false # true/false + +# Add or remove newline between the close brace and identifier, +# as in 'struct { int a; } b;'. Affects enumerations, unions and +# structures. If set to ignore, uses nl_after_brace_close. +nl_brace_struct_var = ignore # ignore/add/remove/force/not_defined + +# Whether to alter newlines in '#define' macros. +nl_define_macro = false # true/false + +# Whether to alter newlines between consecutive parenthesis closes. The number +# of closing parentheses in a line will depend on respective open parenthesis +# lines. +nl_squeeze_paren_close = false # true/false + +# Whether to remove blanks after '#ifxx' and '#elxx', or before '#elxx' and +# '#endif'. Does not affect top-level #ifdefs. +nl_squeeze_ifdef = false # true/false + +# Makes the nl_squeeze_ifdef option affect the top-level #ifdefs as well. +nl_squeeze_ifdef_top_level = false # true/false + +# Add or remove blank line before 'if'. +nl_before_if = ignore # ignore/add/remove/force/not_defined + +# Add or remove blank line after 'if' statement. Add/Force work only if the +# next token is not a closing brace. +nl_after_if = ignore # ignore/add/remove/force/not_defined + +# Add or remove blank line before 'for'. +nl_before_for = ignore # ignore/add/remove/force/not_defined + +# Add or remove blank line after 'for' statement. +nl_after_for = ignore # ignore/add/remove/force/not_defined + +# Add or remove blank line before 'while'. +nl_before_while = ignore # ignore/add/remove/force/not_defined + +# Add or remove blank line after 'while' statement. +nl_after_while = ignore # ignore/add/remove/force/not_defined + +# Add or remove blank line before 'switch'. +nl_before_switch = ignore # ignore/add/remove/force/not_defined + +# Add or remove blank line after 'switch' statement. +nl_after_switch = ignore # ignore/add/remove/force/not_defined + +# Add or remove blank line before 'synchronized'. +nl_before_synchronized = ignore # ignore/add/remove/force/not_defined + +# Add or remove blank line after 'synchronized' statement. +nl_after_synchronized = ignore # ignore/add/remove/force/not_defined + +# Add or remove blank line before 'do'. +nl_before_do = ignore # ignore/add/remove/force/not_defined + +# Add or remove blank line after 'do/while' statement. +nl_after_do = ignore # ignore/add/remove/force/not_defined + +# Ignore nl_before_{if,for,switch,do,synchronized} if the control +# statement is immediately after a case statement. +# if nl_before_{if,for,switch,do} is set to remove, this option +# does nothing. +nl_before_ignore_after_case = false # true/false + +# Whether to put a blank line before 'return' statements, unless after an open +# brace. +nl_before_return = false # true/false + +# Whether to put a blank line after 'return' statements, unless followed by a +# close brace. +nl_after_return = false # true/false + +# Whether to put a blank line before a member '.' or '->' operators. +nl_before_member = ignore # ignore/add/remove/force/not_defined + +# (Java) Whether to put a blank line after a member '.' or '->' operators. +nl_after_member = ignore # ignore/add/remove/force/not_defined + +# Whether to double-space commented-entries in 'struct'/'union'/'enum'. +nl_ds_struct_enum_cmt = false # true/false + +# Whether to force a newline before '}' of a 'struct'/'union'/'enum'. +# (Lower priority than eat_blanks_before_close_brace.) +nl_ds_struct_enum_close_brace = false # true/false + +# Add or remove newline before or after (depending on pos_class_colon) a class +# colon, as in 'class Foo : public Bar'. +nl_class_colon = ignore # ignore/add/remove/force/not_defined + +# Add or remove newline around a class constructor colon. The exact position +# depends on nl_constr_init_args, pos_constr_colon and pos_constr_comma. +nl_constr_colon = ignore # ignore/add/remove/force/not_defined + +# Whether to collapse a two-line namespace, like 'namespace foo\n{ decl; }' +# into a single line. If true, prevents other brace newline rules from turning +# such code into four lines. If true, it also preserves one-liner namespaces. +nl_namespace_two_to_one_liner = false # true/false + +# Whether to remove a newline in simple unbraced if statements, turning them +# into one-liners, as in 'if(b)\n i++;' => 'if(b) i++;'. +nl_create_if_one_liner = false # true/false + +# Whether to remove a newline in simple unbraced for statements, turning them +# into one-liners, as in 'for (...)\n stmt;' => 'for (...) stmt;'. +nl_create_for_one_liner = false # true/false + +# Whether to remove a newline in simple unbraced while statements, turning +# them into one-liners, as in 'while (expr)\n stmt;' => 'while (expr) stmt;'. +nl_create_while_one_liner = false # true/false + +# Whether to collapse a function definition whose body (not counting braces) +# is only one line so that the entire definition (prototype, braces, body) is +# a single line. +nl_create_func_def_one_liner = false # true/false + +# Whether to split one-line simple list definitions into three lines by +# adding newlines, as in 'int a[12] = { 0 };'. +nl_create_list_one_liner = false # true/false + +# Whether to split one-line simple unbraced if statements into two lines by +# adding a newline, as in 'if(b) i++;'. +nl_split_if_one_liner = false # true/false + +# Whether to split one-line simple unbraced for statements into two lines by +# adding a newline, as in 'for (...) stmt;'. +nl_split_for_one_liner = false # true/false + +# Whether to split one-line simple unbraced while statements into two lines by +# adding a newline, as in 'while (expr) stmt;'. +nl_split_while_one_liner = false # true/false + +# Don't add a newline before a cpp-comment in a parameter list of a function +# call. +donot_add_nl_before_cpp_comment = false # true/false + +# +# Blank line options +# + +# The maximum number of consecutive newlines (3 = 2 blank lines). +nl_max = 2 # unsigned number + +# The maximum number of consecutive newlines in a function. +nl_max_blank_in_func = 0 # unsigned number + +# The number of newlines inside an empty function body. +# This option overrides eat_blanks_after_open_brace and +# eat_blanks_before_close_brace, but is ignored when +# nl_collapse_empty_body=true +nl_inside_empty_func = 0 # unsigned number + +# The number of newlines before a function prototype. +nl_before_func_body_proto = 0 # unsigned number + +# The number of newlines before a multi-line function definition. Where +# applicable, this option is overridden with eat_blanks_after_open_brace=true +nl_before_func_body_def = 0 # unsigned number + +# The number of newlines before a class constructor/destructor prototype. +nl_before_func_class_proto = 0 # unsigned number + +# The number of newlines before a class constructor/destructor definition. +nl_before_func_class_def = 0 # unsigned number + +# The number of newlines after a function prototype. +nl_after_func_proto = 0 # unsigned number + +# The number of newlines after a function prototype, if not followed by +# another function prototype. +nl_after_func_proto_group = 0 # unsigned number + +# The number of newlines after a class constructor/destructor prototype. +nl_after_func_class_proto = 0 # unsigned number + +# The number of newlines after a class constructor/destructor prototype, +# if not followed by another constructor/destructor prototype. +nl_after_func_class_proto_group = 0 # unsigned number + +# Whether one-line method definitions inside a class body should be treated +# as if they were prototypes for the purposes of adding newlines. +# +# Requires nl_class_leave_one_liners=true. Overrides nl_before_func_body_def +# and nl_before_func_class_def for one-liners. +nl_class_leave_one_liner_groups = false # true/false + +# The number of newlines after '}' of a multi-line function body. +nl_after_func_body = 0 # unsigned number + +# The number of newlines after '}' of a multi-line function body in a class +# declaration. Also affects class constructors/destructors. +# +# Overrides nl_after_func_body. +nl_after_func_body_class = 0 # unsigned number + +# The number of newlines after '}' of a single line function body. Also +# affects class constructors/destructors. +# +# Overrides nl_after_func_body and nl_after_func_body_class. +nl_after_func_body_one_liner = 0 # unsigned number + +# The minimum number of blank lines after a block of variable definitions +# at the top of a function body. If any preprocessor directives appear +# between the opening brace of the function and the variable block, then +# it is considered as not at the top of the function.Newlines are added +# before trailing preprocessor directives, if any exist. +# +# 0: No change (default). +nl_var_def_blk_end_func_top = 0 # unsigned number + +# The number of newlines before a block of typedefs. If nl_after_access_spec +# is non-zero, that option takes precedence. +# +# 0: No change (default). +nl_typedef_blk_start = 0 # unsigned number + +# The number of newlines after a block of typedefs. +# +# 0: No change (default). +nl_typedef_blk_end = 0 # unsigned number + +# The maximum number of consecutive newlines within a block of typedefs. +# +# 0: No change (default). +nl_typedef_blk_in = 0 # unsigned number + +# The number of newlines before a block of variable definitions not at the top +# of a function body. If nl_after_access_spec is non-zero, that option takes +# precedence. +# +# 0: No change (default). +nl_var_def_blk_start = 0 # unsigned number + +# The number of newlines after a block of variable definitions not at the top +# of a function body. +# +# 0: No change (default). +nl_var_def_blk_end = 0 # unsigned number + +# The maximum number of consecutive newlines within a block of variable +# definitions. +# +# 0: No change (default). +nl_var_def_blk_in = 0 # unsigned number + +# The minimum number of newlines before a multi-line comment. +# Doesn't apply if after a brace open or another multi-line comment. +nl_before_block_comment = 0 # unsigned number + +# The minimum number of newlines before a single-line C comment. +# Doesn't apply if after a brace open or other single-line C comments. +nl_before_c_comment = 0 # unsigned number + +# The minimum number of newlines before a CPP comment. +# Doesn't apply if after a brace open or other CPP comments. +nl_before_cpp_comment = 0 # unsigned number + +# Whether to force a newline after a multi-line comment. +nl_after_multiline_comment = false # true/false + +# Whether to force a newline after a label's colon. +nl_after_label_colon = false # true/false + +# The number of newlines before a struct definition. +nl_before_struct = 0 # unsigned number + +# The number of newlines after '}' or ';' of a struct/enum/union definition. +nl_after_struct = 0 # unsigned number + +# The number of newlines before a class definition. +nl_before_class = 0 # unsigned number + +# The number of newlines after '}' or ';' of a class definition. +nl_after_class = 0 # unsigned number + +# The number of newlines before a namespace. +nl_before_namespace = 0 # unsigned number + +# The number of newlines after '{' of a namespace. This also adds newlines +# before the matching '}'. +# +# 0: Apply eat_blanks_after_open_brace or eat_blanks_before_close_brace if +# applicable, otherwise no change. +# +# Overrides eat_blanks_after_open_brace and eat_blanks_before_close_brace. +nl_inside_namespace = 0 # unsigned number + +# The number of newlines after '}' of a namespace. +nl_after_namespace = 0 # unsigned number + +# The number of newlines before an access specifier label. This also includes +# the Qt-specific 'signals:' and 'slots:'. Will not change the newline count +# if after a brace open. +# +# 0: No change (default). +nl_before_access_spec = 0 # unsigned number + +# The number of newlines after an access specifier label. This also includes +# the Qt-specific 'signals:' and 'slots:'. Will not change the newline count +# if after a brace open. +# +# 0: No change (default). +# +# Overrides nl_typedef_blk_start and nl_var_def_blk_start. +nl_after_access_spec = 0 # unsigned number + +# The number of newlines between a function definition and the function +# comment, as in '// comment\n void foo() {...}'. +# +# 0: No change (default). +nl_comment_func_def = 0 # unsigned number + +# The number of newlines after a try-catch-finally block that isn't followed +# by a brace close. +# +# 0: No change (default). +nl_after_try_catch_finally = 0 # unsigned number + +# (C#) The number of newlines before and after a property, indexer or event +# declaration. +# +# 0: No change (default). +nl_around_cs_property = 0 # unsigned number + +# (C#) The number of newlines between the get/set/add/remove handlers. +# +# 0: No change (default). +nl_between_get_set = 0 # unsigned number + +# (C#) Add or remove newline between property and the '{'. +nl_property_brace = ignore # ignore/add/remove/force/not_defined + +# Whether to remove blank lines after '{'. +eat_blanks_after_open_brace = false # true/false + +# Whether to remove blank lines before '}'. +eat_blanks_before_close_brace = false # true/false + +# How aggressively to remove extra newlines not in preprocessor. +# +# 0: No change (default) +# 1: Remove most newlines not handled by other config +# 2: Remove all newlines and reformat completely by config +nl_remove_extra_newlines = 0 # unsigned number + +# (Java) Add or remove newline after an annotation statement. Only affects +# annotations that are after a newline. +nl_after_annotation = ignore # ignore/add/remove/force/not_defined + +# (Java) Add or remove newline between two annotations. +nl_between_annotation = ignore # ignore/add/remove/force/not_defined + +# The number of newlines before a whole-file #ifdef. +# +# 0: No change (default). +nl_before_whole_file_ifdef = 0 # unsigned number + +# The number of newlines after a whole-file #ifdef. +# +# 0: No change (default). +nl_after_whole_file_ifdef = 0 # unsigned number + +# The number of newlines before a whole-file #endif. +# +# 0: No change (default). +nl_before_whole_file_endif = 0 # unsigned number + +# The number of newlines after a whole-file #endif. +# +# 0: No change (default). +nl_after_whole_file_endif = 0 # unsigned number + +# +# Positioning options +# + +# The position of arithmetic operators in wrapped expressions. +pos_arith = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of assignment in wrapped expressions. Do not affect '=' +# followed by '{'. +pos_assign = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of Boolean operators in wrapped expressions. +pos_bool = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of comparison operators in wrapped expressions. +pos_compare = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of conditional operators, as in the '?' and ':' of +# 'expr ? stmt : stmt', in wrapped expressions. +pos_conditional = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of the comma in wrapped expressions. +pos_comma = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of the comma in enum entries. +pos_enum_comma = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of the comma in the base class list if there is more than one +# line. Affects nl_class_init_args. +pos_class_comma = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of the comma in the constructor initialization list. +# Related to nl_constr_colon, nl_constr_init_args and pos_constr_colon. +pos_constr_comma = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of trailing/leading class colon, between class and base class +# list. Affects nl_class_colon. +pos_class_colon = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of colons between constructor and member initialization. +# Related to nl_constr_colon, nl_constr_init_args and pos_constr_comma. +pos_constr_colon = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of shift operators in wrapped expressions. +pos_shift = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# +# Line splitting options +# + +# Try to limit code width to N columns. +code_width = 0 # unsigned number + +# Whether to fully split long 'for' statements at semi-colons. +ls_for_split_full = false # true/false + +# Whether to fully split long function prototypes/calls at commas. +# The option ls_code_width has priority over the option ls_func_split_full. +ls_func_split_full = false # true/false + +# Whether to split lines as close to code_width as possible and ignore some +# groupings. +# The option ls_code_width has priority over the option ls_func_split_full. +ls_code_width = false # true/false + +# +# Code alignment options (not left column spaces/tabs) +# + +# Whether to keep non-indenting tabs. +align_keep_tabs = false # true/false + +# Whether to use tabs for aligning. +align_with_tabs = false # true/false + +# Whether to bump out to the next tab when aligning. +align_on_tabstop = false # true/false + +# Whether to right-align numbers. +align_number_right = false # true/false + +# Whether to keep whitespace not required for alignment. +align_keep_extra_space = false # true/false + +# Whether to align variable definitions in prototypes and functions. +align_func_params = false # true/false + +# The span for aligning parameter definitions in function on parameter name. +# +# 0: Don't align (default). +align_func_params_span = 0 # unsigned number + +# The threshold for aligning function parameter definitions. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_func_params_thresh = 0 # number + +# The gap for aligning function parameter definitions. +align_func_params_gap = 0 # unsigned number + +# The span for aligning constructor value. +# +# 0: Don't align (default). +align_constr_value_span = 0 # unsigned number + +# The threshold for aligning constructor value. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_constr_value_thresh = 0 # number + +# The gap for aligning constructor value. +align_constr_value_gap = 0 # unsigned number + +# Whether to align parameters in single-line functions that have the same +# name. The function names must already be aligned with each other. +align_same_func_call_params = false # true/false + +# The span for aligning function-call parameters for single line functions. +# +# 0: Don't align (default). +align_same_func_call_params_span = 0 # unsigned number + +# The threshold for aligning function-call parameters for single line +# functions. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_same_func_call_params_thresh = 0 # number + +# The span for aligning variable definitions. +# +# 0: Don't align (default). +align_var_def_span = 0 # unsigned number + +# How to consider (or treat) the '*' in the alignment of variable definitions. +# +# 0: Part of the type 'void * foo;' (default) +# 1: Part of the variable 'void *foo;' +# 2: Dangling 'void *foo;' +# Dangling: the '*' will not be taken into account when aligning. +align_var_def_star_style = 0 # unsigned number + +# How to consider (or treat) the '&' in the alignment of variable definitions. +# +# 0: Part of the type 'long & foo;' (default) +# 1: Part of the variable 'long &foo;' +# 2: Dangling 'long &foo;' +# Dangling: the '&' will not be taken into account when aligning. +align_var_def_amp_style = 0 # unsigned number + +# The threshold for aligning variable definitions. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_var_def_thresh = 0 # number + +# The gap for aligning variable definitions. +align_var_def_gap = 0 # unsigned number + +# Whether to align the colon in struct bit fields. +align_var_def_colon = false # true/false + +# The gap for aligning the colon in struct bit fields. +align_var_def_colon_gap = 0 # unsigned number + +# Whether to align any attribute after the variable name. +align_var_def_attribute = false # true/false + +# Whether to align inline struct/enum/union variable definitions. +align_var_def_inline = false # true/false + +# The span for aligning on '=' in assignments. +# +# 0: Don't align (default). +align_assign_span = 0 # unsigned number + +# The span for aligning on '=' in function prototype modifier. +# +# 0: Don't align (default). +align_assign_func_proto_span = 0 # unsigned number + +# The threshold for aligning on '=' in assignments. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_assign_thresh = 0 # number + +# How to apply align_assign_span to function declaration "assignments", i.e. +# 'virtual void foo() = 0' or '~foo() = {default|delete}'. +# +# 0: Align with other assignments (default) +# 1: Align with each other, ignoring regular assignments +# 2: Don't align +align_assign_decl_func = 0 # unsigned number + +# The span for aligning on '=' in enums. +# +# 0: Don't align (default). +align_enum_equ_span = 0 # unsigned number + +# The threshold for aligning on '=' in enums. +# Use a negative number for absolute thresholds. +# +# 0: no limit (default). +align_enum_equ_thresh = 0 # number + +# The span for aligning class member definitions. +# +# 0: Don't align (default). +align_var_class_span = 0 # unsigned number + +# The threshold for aligning class member definitions. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_var_class_thresh = 0 # number + +# The gap for aligning class member definitions. +align_var_class_gap = 0 # unsigned number + +# The span for aligning struct/union member definitions. +# +# 0: Don't align (default). +align_var_struct_span = 0 # unsigned number + +# The threshold for aligning struct/union member definitions. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_var_struct_thresh = 0 # number + +# The gap for aligning struct/union member definitions. +align_var_struct_gap = 0 # unsigned number + +# The span for aligning struct initializer values. +# +# 0: Don't align (default). +align_struct_init_span = 0 # unsigned number + +# The span for aligning single-line typedefs. +# +# 0: Don't align (default). +align_typedef_span = 0 # unsigned number + +# The minimum space between the type and the synonym of a typedef. +align_typedef_gap = 0 # unsigned number + +# How to align typedef'd functions with other typedefs. +# +# 0: Don't mix them at all (default) +# 1: Align the open parenthesis with the types +# 2: Align the function type name with the other type names +align_typedef_func = 0 # unsigned number + +# How to consider (or treat) the '*' in the alignment of typedefs. +# +# 0: Part of the typedef type, 'typedef int * pint;' (default) +# 1: Part of type name: 'typedef int *pint;' +# 2: Dangling: 'typedef int *pint;' +# Dangling: the '*' will not be taken into account when aligning. +align_typedef_star_style = 0 # unsigned number + +# How to consider (or treat) the '&' in the alignment of typedefs. +# +# 0: Part of the typedef type, 'typedef int & intref;' (default) +# 1: Part of type name: 'typedef int &intref;' +# 2: Dangling: 'typedef int &intref;' +# Dangling: the '&' will not be taken into account when aligning. +align_typedef_amp_style = 0 # unsigned number + +# The span for aligning comments that end lines. +# +# 0: Don't align (default). +align_right_cmt_span = 0 # unsigned number + +# Minimum number of columns between preceding text and a trailing comment in +# order for the comment to qualify for being aligned. Must be non-zero to have +# an effect. +align_right_cmt_gap = 0 # unsigned number + +# If aligning comments, whether to mix with comments after '}' and #endif with +# less than three spaces before the comment. +align_right_cmt_mix = false # true/false + +# Whether to only align trailing comments that are at the same brace level. +align_right_cmt_same_level = false # true/false + +# Minimum column at which to align trailing comments. Comments which are +# aligned beyond this column, but which can be aligned in a lesser column, +# may be "pulled in". +# +# 0: Ignore (default). +align_right_cmt_at_col = 0 # unsigned number + +# The span for aligning function prototypes. +# +# 0: Don't align (default). +align_func_proto_span = 0 # unsigned number + +# How to consider (or treat) the '*' in the alignment of function prototypes. +# +# 0: Part of the type 'void * foo();' (default) +# 1: Part of the function 'void *foo();' +# 2: Dangling 'void *foo();' +# Dangling: the '*' will not be taken into account when aligning. +align_func_proto_star_style = 0 # unsigned number + +# How to consider (or treat) the '&' in the alignment of function prototypes. +# +# 0: Part of the type 'long & foo();' (default) +# 1: Part of the function 'long &foo();' +# 2: Dangling 'long &foo();' +# Dangling: the '&' will not be taken into account when aligning. +align_func_proto_amp_style = 0 # unsigned number + +# The threshold for aligning function prototypes. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_func_proto_thresh = 0 # number + +# Minimum gap between the return type and the function name. +align_func_proto_gap = 0 # unsigned number + +# Whether to align function prototypes on the 'operator' keyword instead of +# what follows. +align_on_operator = false # true/false + +# Whether to mix aligning prototype and variable declarations. If true, +# align_var_def_XXX options are used instead of align_func_proto_XXX options. +align_mix_var_proto = false # true/false + +# Whether to align single-line functions with function prototypes. +# Uses align_func_proto_span. +align_single_line_func = false # true/false + +# Whether to align the open brace of single-line functions. +# Requires align_single_line_func=true. Uses align_func_proto_span. +align_single_line_brace = false # true/false + +# Gap for align_single_line_brace. +align_single_line_brace_gap = 0 # unsigned number + +# (OC) The span for aligning Objective-C message specifications. +# +# 0: Don't align (default). +align_oc_msg_spec_span = 0 # unsigned number + +# Whether and how to align backslashes that split a macro onto multiple lines. +# This will not work right if the macro contains a multi-line comment. +# +# 0: Do nothing (default) +# 1: Align the backslashes in the column at the end of the longest line +# 2: Align with the backslash that is farthest to the left, or, if that +# backslash is farther left than the end of the longest line, at the end of +# the longest line +# 3: Align with the backslash that is farthest to the right +align_nl_cont = 0 # unsigned number + +# Whether to align macro functions and variables together. +align_pp_define_together = false # true/false + +# The span for aligning on '#define' bodies. +# +# =0: Don't align (default) +# >0: Number of lines (including comments) between blocks +align_pp_define_span = 0 # unsigned number + +# The minimum space between label and value of a preprocessor define. +align_pp_define_gap = 0 # unsigned number + +# Whether to align lines that start with '<<' with previous '<<'. +# +# Default: true +align_left_shift = true # true/false + +# Whether to align comma-separated statements following '<<' (as used to +# initialize Eigen matrices). +align_eigen_comma_init = false # true/false + +# Whether to align text after 'asm volatile ()' colons. +align_asm_colon = false # true/false + +# (OC) Span for aligning parameters in an Objective-C message call +# on the ':'. +# +# 0: Don't align. +align_oc_msg_colon_span = 0 # unsigned number + +# (OC) Whether to always align with the first parameter, even if it is too +# short. +align_oc_msg_colon_first = false # true/false + +# (OC) Whether to align parameters in an Objective-C '+' or '-' declaration +# on the ':'. +align_oc_decl_colon = false # true/false + +# (OC) Whether to not align parameters in an Objectve-C message call if first +# colon is not on next line of the message call (the same way Xcode does +# aligment) +align_oc_msg_colon_xcode_like = false # true/false + +# +# Comment modification options +# + +# Try to wrap comments at N columns. +cmt_width = 0 # unsigned number + +# How to reflow comments. +# +# 0: No reflowing (apart from the line wrapping due to cmt_width) (default) +# 1: No touching at all +# 2: Full reflow (enable cmt_indent_multi for indent with line wrapping due to cmt_width) +cmt_reflow_mode = 0 # unsigned number + +# Path to a file that contains regular expressions describing patterns for +# which the end of one line and the beginning of the next will be folded into +# the same sentence or paragraph during full comment reflow. The regular +# expressions are described using ECMAScript syntax. The syntax for this +# specification is as follows, where "..." indicates the custom regular +# expression and "n" indicates the nth end_of_prev_line_regex and +# beg_of_next_line_regex regular expression pair: +# +# end_of_prev_line_regex[1] = "...$" +# beg_of_next_line_regex[1] = "^..." +# end_of_prev_line_regex[2] = "...$" +# beg_of_next_line_regex[2] = "^..." +# . +# . +# . +# end_of_prev_line_regex[n] = "...$" +# beg_of_next_line_regex[n] = "^..." +# +# Note that use of this option overrides the default reflow fold regular +# expressions, which are internally defined as follows: +# +# end_of_prev_line_regex[1] = "[\w,\]\)]$" +# beg_of_next_line_regex[1] = "^[\w,\[\(]" +# end_of_prev_line_regex[2] = "\.$" +# beg_of_next_line_regex[2] = "^[A-Z]" +cmt_reflow_fold_regex_file = "" # string + +# Whether to indent wrapped lines to the start of the encompassing paragraph +# during full comment reflow (cmt_reflow_mode = 2). Overrides the value +# specified by cmt_sp_after_star_cont. +# +# Note that cmt_align_doxygen_javadoc_tags overrides this option for +# paragraphs associated with javadoc tags +cmt_reflow_indent_to_paragraph_start = false # true/false + +# Whether to convert all tabs to spaces in comments. If false, tabs in +# comments are left alone, unless used for indenting. +cmt_convert_tab_to_spaces = false # true/false + +# Whether to apply changes to multi-line comments, including cmt_width, +# keyword substitution and leading chars. +# +# Default: true +cmt_indent_multi = true # true/false + +# Whether to align doxygen javadoc-style tags ('@param', '@return', etc.) +# and corresponding fields such that groups of consecutive block tags, +# parameter names, and descriptions align with one another. Overrides that +# which is specified by the cmt_sp_after_star_cont. If cmt_width > 0, it may +# be necessary to enable cmt_indent_multi and set cmt_reflow_mode = 2 +# in order to achieve the desired alignment for line-wrapping. +cmt_align_doxygen_javadoc_tags = false # true/false + +# The number of spaces to insert after the star and before doxygen +# javadoc-style tags (@param, @return, etc). Requires enabling +# cmt_align_doxygen_javadoc_tags. Overrides that which is specified by the +# cmt_sp_after_star_cont. +# +# Default: 1 +cmt_sp_before_doxygen_javadoc_tags = 1 # unsigned number + +# Whether to change trailing, single-line c-comments into cpp-comments. +cmt_trailing_single_line_c_to_cpp = false # true/false + +# Whether to group c-comments that look like they are in a block. +cmt_c_group = false # true/false + +# Whether to put an empty '/*' on the first line of the combined c-comment. +cmt_c_nl_start = false # true/false + +# Whether to add a newline before the closing '*/' of the combined c-comment. +cmt_c_nl_end = false # true/false + +# Whether to change cpp-comments into c-comments. +cmt_cpp_to_c = false # true/false + +# Whether to group cpp-comments that look like they are in a block. Only +# meaningful if cmt_cpp_to_c=true. +cmt_cpp_group = false # true/false + +# Whether to put an empty '/*' on the first line of the combined cpp-comment +# when converting to a c-comment. +# +# Requires cmt_cpp_to_c=true and cmt_cpp_group=true. +cmt_cpp_nl_start = false # true/false + +# Whether to add a newline before the closing '*/' of the combined cpp-comment +# when converting to a c-comment. +# +# Requires cmt_cpp_to_c=true and cmt_cpp_group=true. +cmt_cpp_nl_end = false # true/false + +# Whether to put a star on subsequent comment lines. +cmt_star_cont = false # true/false + +# The number of spaces to insert at the start of subsequent comment lines. +cmt_sp_before_star_cont = 0 # unsigned number + +# The number of spaces to insert after the star on subsequent comment lines. +cmt_sp_after_star_cont = 0 # unsigned number + +# For multi-line comments with a '*' lead, remove leading spaces if the first +# and last lines of the comment are the same length. +# +# Default: true +cmt_multi_check_last = true # true/false + +# For multi-line comments with a '*' lead, remove leading spaces if the first +# and last lines of the comment are the same length AND if the length is +# bigger as the first_len minimum. +# +# Default: 4 +cmt_multi_first_len_minimum = 4 # unsigned number + +# Path to a file that contains text to insert at the beginning of a file if +# the file doesn't start with a C/C++ comment. If the inserted text contains +# '$(filename)', that will be replaced with the current file's name. +cmt_insert_file_header = "" # string + +# Path to a file that contains text to insert at the end of a file if the +# file doesn't end with a C/C++ comment. If the inserted text contains +# '$(filename)', that will be replaced with the current file's name. +cmt_insert_file_footer = "" # string + +# Path to a file that contains text to insert before a function definition if +# the function isn't preceded by a C/C++ comment. If the inserted text +# contains '$(function)', '$(javaparam)' or '$(fclass)', these will be +# replaced with, respectively, the name of the function, the javadoc '@param' +# and '@return' stuff, or the name of the class to which the member function +# belongs. +cmt_insert_func_header = "" # string + +# Path to a file that contains text to insert before a class if the class +# isn't preceded by a C/C++ comment. If the inserted text contains '$(class)', +# that will be replaced with the class name. +cmt_insert_class_header = "" # string + +# Path to a file that contains text to insert before an Objective-C message +# specification, if the method isn't preceded by a C/C++ comment. If the +# inserted text contains '$(message)' or '$(javaparam)', these will be +# replaced with, respectively, the name of the function, or the javadoc +# '@param' and '@return' stuff. +cmt_insert_oc_msg_header = "" # string + +# Whether a comment should be inserted if a preprocessor is encountered when +# stepping backwards from a function name. +# +# Applies to cmt_insert_oc_msg_header, cmt_insert_func_header and +# cmt_insert_class_header. +cmt_insert_before_preproc = false # true/false + +# Whether a comment should be inserted if a function is declared inline to a +# class definition. +# +# Applies to cmt_insert_func_header. +# +# Default: true +cmt_insert_before_inlines = true # true/false + +# Whether a comment should be inserted if the function is a class constructor +# or destructor. +# +# Applies to cmt_insert_func_header. +cmt_insert_before_ctor_dtor = false # true/false + +# +# Code modifying options (non-whitespace) +# + +# Add or remove braces on a single-line 'do' statement. +mod_full_brace_do = add # ignore/add/remove/force/not_defined + +# Add or remove braces on a single-line 'for' statement. +mod_full_brace_for = add # ignore/add/remove/force/not_defined + +# (Pawn) Add or remove braces on a single-line function definition. +mod_full_brace_function = add # ignore/add/remove/force/not_defined + +# Add or remove braces on a single-line 'if' statement. Braces will not be +# removed if the braced statement contains an 'else'. +mod_full_brace_if = add # ignore/add/remove/force/not_defined + +# Whether to enforce that all blocks of an 'if'/'else if'/'else' chain either +# have, or do not have, braces. Overrides mod_full_brace_if. +# +# 0: Don't override mod_full_brace_if +# 1: Add braces to all blocks if any block needs braces and remove braces if +# they can be removed from all blocks +# 2: Add braces to all blocks if any block already has braces, regardless of +# whether it needs them +# 3: Add braces to all blocks if any block needs braces and remove braces if +# they can be removed from all blocks, except if all blocks have braces +# despite none needing them +mod_full_brace_if_chain = 0 # unsigned number + +# Whether to add braces to all blocks of an 'if'/'else if'/'else' chain. +# If true, mod_full_brace_if_chain will only remove braces from an 'if' that +# does not have an 'else if' or 'else'. +mod_full_brace_if_chain_only = false # true/false + +# Add or remove braces on single-line 'while' statement. +mod_full_brace_while = ignore # ignore/add/remove/force/not_defined + +# Add or remove braces on single-line 'using ()' statement. +mod_full_brace_using = ignore # ignore/add/remove/force/not_defined + +# Don't remove braces around statements that span N newlines +mod_full_brace_nl = 0 # unsigned number + +# Whether to prevent removal of braces from 'if'/'for'/'while'/etc. blocks +# which span multiple lines. +# +# Affects: +# mod_full_brace_for +# mod_full_brace_if +# mod_full_brace_if_chain +# mod_full_brace_if_chain_only +# mod_full_brace_while +# mod_full_brace_using +# +# Does not affect: +# mod_full_brace_do +# mod_full_brace_function +mod_full_brace_nl_block_rem_mlcond = false # true/false + +# Add or remove unnecessary parenthesis on 'return' statement. +mod_paren_on_return = ignore # ignore/add/remove/force/not_defined + +# (Pawn) Whether to change optional semicolons to real semicolons. +mod_pawn_semicolon = false # true/false + +# Whether to fully parenthesize Boolean expressions in 'while' and 'if' +# statement, as in 'if (a && b > c)' => 'if (a && (b > c))'. +mod_full_paren_if_bool = false # true/false + +# Whether to remove superfluous semicolons. +mod_remove_extra_semicolon = false # true/false + +# Whether to remove duplicate include. +mod_remove_duplicate_include = false # true/false + +# If a function body exceeds the specified number of newlines and doesn't have +# a comment after the close brace, a comment will be added. +mod_add_long_function_closebrace_comment = 0 # unsigned number + +# If a namespace body exceeds the specified number of newlines and doesn't +# have a comment after the close brace, a comment will be added. +mod_add_long_namespace_closebrace_comment = 0 # unsigned number + +# If a class body exceeds the specified number of newlines and doesn't have a +# comment after the close brace, a comment will be added. +mod_add_long_class_closebrace_comment = 0 # unsigned number + +# If a switch body exceeds the specified number of newlines and doesn't have a +# comment after the close brace, a comment will be added. +mod_add_long_switch_closebrace_comment = 0 # unsigned number + +# If an #ifdef body exceeds the specified number of newlines and doesn't have +# a comment after the #endif, a comment will be added. +mod_add_long_ifdef_endif_comment = 0 # unsigned number + +# If an #ifdef or #else body exceeds the specified number of newlines and +# doesn't have a comment after the #else, a comment will be added. +mod_add_long_ifdef_else_comment = 0 # unsigned number + +# Whether to take care of the case by the mod_sort_xx options. +mod_sort_case_sensitive = false # true/false + +# Whether to sort consecutive single-line 'import' statements. +mod_sort_import = false # true/false + +# (C#) Whether to sort consecutive single-line 'using' statements. +mod_sort_using = false # true/false + +# Whether to sort consecutive single-line '#include' statements (C/C++) and +# '#import' statements (Objective-C). Be aware that this has the potential to +# break your code if your includes/imports have ordering dependencies. +mod_sort_include = false # true/false + +# Whether to prioritize '#include' and '#import' statements that contain +# filename without extension when sorting is enabled. +mod_sort_incl_import_prioritize_filename = false # true/false + +# Whether to prioritize '#include' and '#import' statements that does not +# contain extensions when sorting is enabled. +mod_sort_incl_import_prioritize_extensionless = false # true/false + +# Whether to prioritize '#include' and '#import' statements that contain +# angle over quotes when sorting is enabled. +mod_sort_incl_import_prioritize_angle_over_quotes = false # true/false + +# Whether to ignore file extension in '#include' and '#import' statements +# for sorting comparison. +mod_sort_incl_import_ignore_extension = false # true/false + +# Whether to group '#include' and '#import' statements when sorting is enabled. +mod_sort_incl_import_grouping_enabled = false # true/false + +# Whether to move a 'break' that appears after a fully braced 'case' before +# the close brace, as in 'case X: { ... } break;' => 'case X: { ... break; }'. +mod_move_case_break = false # true/false + +# Add or remove braces around a fully braced case statement. Will only remove +# braces if there are no variable declarations in the block. +mod_case_brace = ignore # ignore/add/remove/force/not_defined + +# Whether to remove a void 'return;' that appears as the last statement in a +# function. +mod_remove_empty_return = false # true/false + +# Add or remove the comma after the last value of an enumeration. +mod_enum_last_comma = ignore # ignore/add/remove/force/not_defined + +# (OC) Whether to organize the properties. If true, properties will be +# rearranged according to the mod_sort_oc_property_*_weight factors. +mod_sort_oc_properties = false # true/false + +# (OC) Weight of a class property modifier. +mod_sort_oc_property_class_weight = 0 # number + +# (OC) Weight of 'atomic' and 'nonatomic'. +mod_sort_oc_property_thread_safe_weight = 0 # number + +# (OC) Weight of 'readwrite' when organizing properties. +mod_sort_oc_property_readwrite_weight = 0 # number + +# (OC) Weight of a reference type specifier ('retain', 'copy', 'assign', +# 'weak', 'strong') when organizing properties. +mod_sort_oc_property_reference_weight = 0 # number + +# (OC) Weight of getter type ('getter=') when organizing properties. +mod_sort_oc_property_getter_weight = 0 # number + +# (OC) Weight of setter type ('setter=') when organizing properties. +mod_sort_oc_property_setter_weight = 0 # number + +# (OC) Weight of nullability type ('nullable', 'nonnull', 'null_unspecified', +# 'null_resettable') when organizing properties. +mod_sort_oc_property_nullability_weight = 0 # number + +# +# Preprocessor options +# + +# Add or remove indentation of preprocessor directives inside #if blocks +# at brace level 0 (file-level). +pp_indent = ignore # ignore/add/remove/force/not_defined + +# Whether to indent #if/#else/#endif at the brace level. If false, these are +# indented from column 1. +pp_indent_at_level = false # true/false + +# Specifies the number of columns to indent preprocessors per level +# at brace level 0 (file-level). If pp_indent_at_level=false, also specifies +# the number of columns to indent preprocessors per level +# at brace level > 0 (function-level). +# +# Default: 1 +pp_indent_count = 1 # unsigned number + +# Add or remove space after # based on pp level of #if blocks. +pp_space_after = ignore # ignore/add/remove/force/not_defined + +# Sets the number of spaces per level added with pp_space. +pp_space_count = 0 # unsigned number + +# The indent for '#region' and '#endregion' in C# and '#pragma region' in +# C/C++. Negative values decrease indent down to the first column. +pp_indent_region = 0 # number + +# Whether to indent the code between #region and #endregion. +pp_region_indent_code = false # true/false + +# If pp_indent_at_level=true, sets the indent for #if, #else and #endif when +# not at file-level. Negative values decrease indent down to the first column. +# +# =0: Indent preprocessors using output_tab_size +# >0: Column at which all preprocessors will be indented +pp_indent_if = 0 # number + +# Whether to indent the code between #if, #else and #endif. +pp_if_indent_code = false # true/false + +# Whether to indent the body of an #if that encompasses all the code in the file. +pp_indent_in_guard = false # true/false + +# Whether to indent '#define' at the brace level. If false, these are +# indented from column 1. +pp_define_at_level = false # true/false + +# Whether to indent '#include' at the brace level. +pp_include_at_level = false # true/false + +# Whether to ignore the '#define' body while formatting. +pp_ignore_define_body = false # true/false + +# Whether to indent case statements between #if, #else, and #endif. +# Only applies to the indent of the preprocesser that the case statements +# directly inside of. +# +# Default: true +pp_indent_case = true # true/false + +# Whether to indent whole function definitions between #if, #else, and #endif. +# Only applies to the indent of the preprocesser that the function definition +# is directly inside of. +# +# Default: true +pp_indent_func_def = true # true/false + +# Whether to indent extern C blocks between #if, #else, and #endif. +# Only applies to the indent of the preprocesser that the extern block is +# directly inside of. +# +# Default: true +pp_indent_extern = true # true/false + +# How to indent braces directly inside #if, #else, and #endif. +# Requires pp_if_indent_code=true and only applies to the indent of the +# preprocessor that the braces are directly inside of. +# 0: No extra indent +# 1: Indent by one level +# -1: Preserve original indentation +# +# Default: 1 +pp_indent_brace = 1 + +# +# Sort includes options +# + +# The regex for include category with priority 0. +include_category_0 = "" # string + +# The regex for include category with priority 1. +include_category_1 = "" # string + +# The regex for include category with priority 2. +include_category_2 = "" # string + +# +# Use or Do not Use options +# + +# true: indent_func_call_param will be used (default) +# false: indent_func_call_param will NOT be used +# +# Default: true +use_indent_func_call_param = true # true/false + +# The value of the indentation for a continuation line is calculated +# differently if the statement is: +# - a declaration: your case with QString fileName ... +# - an assignment: your case with pSettings = new QSettings( ... +# +# At the second case the indentation value might be used twice: +# - at the assignment +# - at the function call (if present) +# +# To prevent the double use of the indentation value, use this option with the +# value 'true'. +# +# true: indent_continue will be used only once +# false: indent_continue will be used every time (default) +use_indent_continue_only_once = false # true/false + +# The value might be used twice: +# - at the assignment +# - at the opening brace +# +# To prevent the double use of the indentation value, use this option with the +# value 'true'. +# +# true: indentation will be used only once +# false: indentation will be used every time (default) +indent_cpp_lambda_only_once = false # true/false + +# Whether sp_after_angle takes precedence over sp_inside_fparen. This was the +# historic behavior, but is probably not the desired behavior, so this is off +# by default. +use_sp_after_angle_always = false # true/false + +# Whether to apply special formatting for Qt SIGNAL/SLOT macros. Essentially, +# this tries to format these so that they match Qt's normalized form (i.e. the +# result of QMetaObject::normalizedSignature), which can slightly improve the +# performance of the QObject::connect call, rather than how they would +# otherwise be formatted. +# +# See options_for_QT.cpp for details. +# +# Default: true +use_options_overriding_for_qt_macros = true # true/false + +# If true: the form feed character is removed from the list of whitespace +# characters. See https://en.cppreference.com/w/cpp/string/byte/isspace. +use_form_feed_no_more_as_whitespace_character = false # true/false + +# +# Warn levels - 1: error, 2: warning (default), 3: note +# + +# (C#) Warning is given if doing tab-to-\t replacement and we have found one +# in a C# verbatim string literal. +# +# Default: 2 +warn_level_tabs_found_in_verbatim_string_literals = 2 # unsigned number + +# Limit the number of loops. +# Used by uncrustify.cpp to exit from infinite loop. +# 0: no limit. +debug_max_number_of_loops = 0 # number + +# Set the number of the line to protocol; +# Used in the function prot_the_line if the 2. parameter is zero. +# 0: nothing protocol. +debug_line_number_to_protocol = 0 # number + +# Set the number of second(s) before terminating formatting the current file, +# 0: no timeout. +# only for linux +debug_timeout = 0 # number + +# Set the number of characters to be printed if the text is too long, +# 0: do not truncate. +debug_truncate = 0 # unsigned number + +# Meaning of the settings: +# Ignore - do not do any changes +# Add - makes sure there is 1 or more space/brace/newline/etc +# Force - makes sure there is exactly 1 space/brace/newline/etc, +# behaves like Add in some contexts +# Remove - removes space/brace/newline/etc +# +# +# - Token(s) can be treated as specific type(s) with the 'set' option: +# `set tokenType tokenString [tokenString...]` +# +# Example: +# `set BOOL __AND__ __OR__` +# +# tokenTypes are defined in src/token_enum.h, use them without the +# 'CT_' prefix: 'CT_BOOL' => 'BOOL' +# +# +# - Token(s) can be treated as type(s) with the 'type' option. +# `type tokenString [tokenString...]` +# +# Example: +# `type int c_uint_8 Rectangle` +# +# This can also be achieved with `set TYPE int c_uint_8 Rectangle` +# +# +# To embed whitespace in tokenStrings use the '\' escape character, or quote +# the tokenStrings. These quotes are supported: "'` +# +# +# - Support for the auto detection of languages through the file ending can be +# added using the 'file_ext' command. +# `file_ext langType langString [langString..]` +# +# Example: +# `file_ext CPP .ch .cxx .cpp.in` +# +# langTypes are defined in uncrusify_types.h in the lang_flag_e enum, use +# them without the 'LANG_' prefix: 'LANG_CPP' => 'CPP' +# +# +# - Custom macro-based indentation can be set up using 'macro-open', +# 'macro-else' and 'macro-close'. +# `(macro-open | macro-else | macro-close) tokenString` +# +# Example: +# `macro-open BEGIN_TEMPLATE_MESSAGE_MAP` +# `macro-open BEGIN_MESSAGE_MAP` +# `macro-close END_MESSAGE_MAP` +# +# +# option(s) with 'not default' value: 0 +# diff --git a/uncrustify.sh b/uncrustify.sh new file mode 100755 index 00000000..13242820 --- /dev/null +++ b/uncrustify.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +function indent() +{ + for f in $1 + do + uncrustify -c uncrustify.cfg -q --replace --no-backup $f + done +} + +indent "src/*.c" +indent "src/include/*.h" +indent "src/libpgagroal/*.c"

\n"); + data = pgagroal_append(data, " Metrics\n"); + data = pgagroal_append(data, "