diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index e57be732fe9b41fc3b0e16b1b66675a74472e8a2..1cca92441905277aecbe4baeb20ab400c1ab97e9 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -19,5 +19,5 @@ jobs: uses: codespell-project/actions-codespell@master with: ignore_words_file: .github/.codespellignore - skip: ./src/database/sqlite3.c,./src/database/sqlite3.h,./src/database/shell.c,./src/lua,./src/dnsmasq,./src/tre-regex,./.git,./test/libs,./src/webserver/civetweb,./src/zip/miniz,./src/api/docs/content/external,./src/database/sqlite3_rsync.c + skip: ./src/database/sqlite3.c,./src/database/sqlite3.h,./src/database/shell.c,./src/lua,./src/dnsmasq,./src/tre-regex,./.git,./test/libs,./src/webserver/civetweb,./src/zip/miniz,./src/api/docs/content/external,./src/database/sqlite3_rsync.c,./package-lock.json exclude_file: .github/.codespellignore_lines diff --git a/.github/workflows/openapi-validator.yml b/.github/workflows/openapi-validator.yml index 8827d074a10f1f4b35ca2db34b954c09a4e7168a..3e3fd916557d8ef6ecb44ad3deeac5497f5a1800 100644 --- a/.github/workflows/openapi-validator.yml +++ b/.github/workflows/openapi-validator.yml @@ -3,7 +3,7 @@ name: API validation on: [push] env: - CI: true + FORCE_COLOR: 2 jobs: openapi-validator: @@ -12,12 +12,13 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.2 - - name: Set Node.js version - uses: actions/setup-node@v4 + - name: Set up Node.js + uses: actions/setup-node@v4.3.0 with: node-version: "20" + cache: npm - name: Install npm dependencies run: npm ci diff --git a/.gitignore b/.gitignore index cf04c4932b01745aa1645c06fbcfd04fbd823873..11057f61e577fd30393865ee5b31c44e9c28f9d1 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ version~ .vscode/* !.vscode/c_cpp_properties.json /build/ +.codechecker/ # __pycache__ files (API tests) __pycache__/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 04deada9b10382e6f551405a483faaed4ee5786b..272900c49cdd021803f42d7c95a643119cef58a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,6 @@ set(CMAKE_C_STANDARD 17) project(PIHOLE_FTL C) -set(DNSMASQ_VERSION pi-hole-v2.91rc5+1) +set(DNSMASQ_VERSION pi-hole-v2.91) add_subdirectory(src) diff --git a/README.md b/README.md index add4fea6d39e87c9521373cc48a6ca8756dcc391..e2d4c056474986ed0224acd12a8e0711b6bbb59c 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,14 @@ <p align="center"> - <a href="https://pi-hole.net"> - <picture> - <source media="(prefers-color-scheme: dark)" srcset="https://pi-hole.github.io/graphics/Vortex/Vortex_Vertical_wordmark_darkmode.png"> - <source media="(prefers-color-scheme: light)" srcset="https://pi-hole.github.io/graphics/Vortex/Vortex_Vertical_wordmark_lightmode.png"> - <img src="https://pi-hole.github.io/graphics/Vortex/Vortex_Vertical_wordmark_lightmode.png" width="80" alt="Pi-hole website"> - </picture> + <a href="https://pi-hole.net/"> + <img src="https://raw.githubusercontent.com/pi-hole/graphics/refs/heads/master/Vortex/vortex_with_text.svg" alt="Pi-hole logo" width="80" height="128"> </a> - <br/> - <b>Network-wide ad blocking via your own Linux hardware</b><br/><br/> - <a href="https://pi-hole.net"> - <picture> - <source media="(prefers-color-scheme: dark)" srcset="https://pi-hole.github.io/graphics/FTLDNS/FTLDNS_darkmode.png"> - <source media="(prefers-color-scheme: light)" srcset="https://pi-hole.github.io/graphics/FTLDNS/FTLDNS.png"> - <img src="https://pi-hole.github.io/graphics/FTLDNS/FTLDNS.png" alt="FTLDNS"> - </picture> + <br> + <strong>Network-wide ad blocking via your own Linux hardware</strong> + <br> + <br> + <a href="https://pi-hole.net/"> + <img src="https://raw.githubusercontent.com/pi-hole/graphics/refs/heads/master/FTLDNS/FTLDNS.svg" alt="FTLDNS logo" width="500" height="128"> </a> - <br/> </p> FTLDNS (`pihole-FTL`) provides an interactive API and also generates statistics for Pi-hole[®](https://pi-hole.net/trademark-rules-and-brand-guidelines/)'s Web interface. @@ -36,4 +29,4 @@ FTLDNS (`pihole-FTL`) is automatically installed when installing Pi-hole. ### IMPORTANT ->FTLDNS will *disable* any existing installations of `dnsmasq`. This is because FTLDNS *is* `dnsmasq` + Pi-hole's code, so both cannot run simultaneously. +>FTLDNS will *disable* any existing installations of `dnsmasq`. This is because FTLDNS *is* `dnsmasq` + Pi-hole's code, so both cannot run simultaneously. diff --git a/build.sh b/build.sh index 1d22c9ea6339fcbe406abda6c52ef8324a3a08b9..bebdc6362dcb6f88dee9dee1d35f13109d3810ac 100755 --- a/build.sh +++ b/build.sh @@ -157,8 +157,13 @@ else cmake .. fi -# Build the sources with the number of available cores -cmake --build . -- -j "$(nproc)" +# If MAKEFLAGS is unset, we set it to "-j$(nproc)" +if [[ -z "${MAKEFLAGS}" ]]; then + MAKEFLAGS="-j$(nproc)" +fi + +# Build the sources +cmake --build . -- ${MAKEFLAGS} # Checksum verification ./pihole-FTL verify diff --git a/package-lock.json b/package-lock.json index 3ac9ba290ad28b45d367d62c3611389a638da8f6..9a71d10805a72ce48d12efbc0aa0a67475aa356a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,7 +1,7 @@ { "name": "pihole-ftl", "version": "1.0.0", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -9,8 +9,8 @@ "version": "1.0.0", "license": "EUPL-1.2", "devDependencies": { - "openapi-enforcer": "^1.13.1", - "openapi-examples-validator": "^4.2.1" + "openapi-enforcer": "^1.23.0", + "openapi-examples-validator": "^5.0.0" } }, "node_modules/@apidevtools/json-schema-ref-parser": { @@ -18,6 +18,7 @@ "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz", "integrity": "sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w==", "dev": true, + "license": "MIT", "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.6", @@ -25,109 +26,127 @@ "js-yaml": "^4.1.0" } }, - "node_modules/@apidevtools/json-schema-ref-parser/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/@jsdevtools/ono": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, + "license": "MIT", "dependencies": { - "follow-redirects": "^1.14.0" + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "node_modules/decimal.js": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz", - "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==", - "dev": true + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/drange": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/drange/-/drange-1.1.1.tgz", + "integrity": "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } }, "node_modules/errno": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/errno/-/errno-1.0.0.tgz", "integrity": "sha512-3zV5mFS1E8/1bPxt/B0xxzI1snsg3uSCIh6Zo1qKg6iMw93hzPANk9oBFzSFBFrwuVoQuE3rLoouAUfwOAj1wQ==", "dev": true, + "license": "MIT", "dependencies": { "prr": "~1.0.1" }, @@ -135,84 +154,60 @@ "errno": "cli.js" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", "dev": true, "funding": [ { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" } ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } + "license": "BSD-3-Clause" }, "node_modules/foreach": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==", - "dev": true - }, - "node_modules/format-util": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/format-util/-/format-util-1.0.5.tgz", - "integrity": "sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "minimatch": "^5.0.1", + "once": "^1.3.0" }, "engines": { - "node": "*" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -221,8 +216,10 @@ "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -232,16 +229,17 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, + "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -252,600 +250,193 @@ "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.2.tgz", "integrity": "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==", "dev": true, + "license": "MIT", "dependencies": { "foreach": "^2.0.4" } }, + "node_modules/json-schema-ref-parser": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz", + "integrity": "sha512-qcP2lmGy+JUoQJ4DOQeLaZDqH9qSkeGCK3suKWxJXS82dg728Mn3j97azDMaOUmJAN4uCq91LdPx4K7E8F1a7Q==", + "deprecated": "Please switch to @apidevtools/json-schema-ref-parser", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "9.0.9" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonpath-plus": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", + "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true, + "license": "MIT" }, "node_modules/lodash.flatmap": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.flatmap/-/lodash.flatmap-4.5.0.tgz", - "integrity": "sha1-74y/QI9uSCaGYzRTBcaswLd4cC4=", - "dev": true + "integrity": "sha512-/OcpcAGWlrZyoHGeHh3cAoa6nGdX6QYtmzNP84Jqol6UEQQ2gIaU3H+0eICcjcKGl0/XF8LWOujNn9lffsnaOg==", + "dev": true, + "license": "MIT" }, "node_modules/lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", - "dev": true + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true, + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=10" } }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, + "license": "ISC", "dependencies": { "wrappy": "1" } }, - "node_modules/ono": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/ono/-/ono-4.0.11.tgz", - "integrity": "sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g==", - "dev": true, - "dependencies": { - "format-util": "^1.0.3" - } - }, "node_modules/openapi-enforcer": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/openapi-enforcer/-/openapi-enforcer-1.13.1.tgz", - "integrity": "sha512-NDDmyonl3bgxP+RabJsTPxn8EEza4Aet5zEocfX6nP9LL0J52aNFMYxNHoAuqoY3zQcZG2pnm3DMi2gYiRYbQg==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/openapi-enforcer/-/openapi-enforcer-1.23.0.tgz", + "integrity": "sha512-Ja6kvNQ28jvCHpotNZkB129/dBg2IslMDV56aUSl1Dcs+bcd8lGJTq1NatGYsspGpURXvQnFA/q2K8V3AQoO3Q==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "axios": "^0.21.1", - "json-schema-ref-parser": "^6.1.0" - } - }, - "node_modules/openapi-enforcer/node_modules/json-schema-ref-parser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-6.1.0.tgz", - "integrity": "sha512-pXe9H1m6IgIpXmE5JSb8epilNTGsmTb2iPohAXpOdhqGFbQjNeHHsZxU+C8w6T81GZxSPFLeUoqDJmzxx5IGuw==", - "deprecated": "Please switch to @apidevtools/json-schema-ref-parser", - "dev": true, - "dependencies": { - "call-me-maybe": "^1.0.1", - "js-yaml": "^3.12.1", - "ono": "^4.0.11" + "js-yaml": "^4.1.0", + "randexp": "^0.5.3" } }, "node_modules/openapi-examples-validator": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/openapi-examples-validator/-/openapi-examples-validator-4.7.1.tgz", - "integrity": "sha512-/OZZHhJkiaMQhkVfD0vFNOrRCBhkOL+X4/uhC55CJH0giuLUPQgFB5/iU26DCq1sZo2j9L70XYPcdHoM4PS+Xw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/openapi-examples-validator/-/openapi-examples-validator-5.0.0.tgz", + "integrity": "sha512-UbyFT66im7n8yJWBMHup2VBKzWjIX0v9y+RroExPHqArUx/1kCdb39BfDra+cPiiZIwU9YYGtxwBwYyCCJAVgg==", "dev": true, + "license": "MIT", "dependencies": { - "ajv": "^6.12.6", - "ajv-oai": "1.2.1", + "ajv": "^8.12.0", + "ajv-draft-04": "^1.0.0", + "ajv-formats": "^2.1.1", "commander": "^6.2.1", "errno": "^1.0.0", - "glob": "^7.2.0", + "glob": "^8.1.0", "json-pointer": "^0.6.2", "json-schema-ref-parser": "^9.0.9", - "jsonpath-plus": "^6.0.1", + "jsonpath-plus": "^7.2.0", "lodash.clonedeep": "^4.5.0", "lodash.flatmap": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.merge": "^4.6.2", - "yaml": "^1.10.2" + "yaml": "^2.2.2" }, "bin": { "openapi-examples-validator": "dist/cli.js" }, "engines": { - "node": ">=10" + "node": ">=16" } }, - "node_modules/openapi-examples-validator/node_modules/ajv-oai": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ajv-oai/-/ajv-oai-1.2.1.tgz", - "integrity": "sha512-gj7dnSdLyjWKid3uQI16u5wQNpkyqivjtCuvI4BWezeOzYTj5YHt4otH9GOBCaXY3FEbzQeWsp6C2qc18+BXDA==", + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", "dev": true, - "dependencies": { - "decimal.js": "^10.2.0" - }, - "peerDependencies": { - "ajv": "6.x" - } + "license": "MIT" }, - "node_modules/openapi-examples-validator/node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/openapi-examples-validator/node_modules/json-schema-ref-parser": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz", - "integrity": "sha512-qcP2lmGy+JUoQJ4DOQeLaZDqH9qSkeGCK3suKWxJXS82dg728Mn3j97azDMaOUmJAN4uCq91LdPx4K7E8F1a7Q==", - "deprecated": "Please switch to @apidevtools/json-schema-ref-parser", + "node_modules/randexp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz", + "integrity": "sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==", "dev": true, + "license": "MIT", "dependencies": { - "@apidevtools/json-schema-ref-parser": "9.0.9" + "drange": "^1.0.2", + "ret": "^0.2.0" }, "engines": { - "node": ">=10" - } - }, - "node_modules/openapi-examples-validator/node_modules/jsonpath-plus": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-6.0.1.tgz", - "integrity": "sha512-EvGovdvau6FyLexFH2OeXfIITlgIbgZoAZe3usiySeaIDm5QS+A10DKNpaPBBqqRSZr2HN6HVNXxtwUAr2apEw==", - "dev": true, - "engines": { - "node": ">=10.0.0" + "node": ">=4" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true - }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=6" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" + "node": ">=4" } }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "engines": { - "node": ">= 6" - } - } - }, - "dependencies": { - "@apidevtools/json-schema-ref-parser": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz", - "integrity": "sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w==", - "dev": true, - "requires": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.6", - "call-me-maybe": "^1.0.1", - "js-yaml": "^4.1.0" - }, - "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - } - } - }, - "@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "dev": true - }, - "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } + "license": "ISC" }, - "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dev": true, - "requires": { - "follow-redirects": "^1.14.0" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "decimal.js": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz", - "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==", - "dev": true - }, - "errno": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/errno/-/errno-1.0.0.tgz", - "integrity": "sha512-3zV5mFS1E8/1bPxt/B0xxzI1snsg3uSCIh6Zo1qKg6iMw93hzPANk9oBFzSFBFrwuVoQuE3rLoouAUfwOAj1wQ==", - "dev": true, - "requires": { - "prr": "~1.0.1" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "dev": true - }, - "foreach": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", - "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==", - "dev": true - }, - "format-util": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/format-util/-/format-util-1.0.5.tgz", - "integrity": "sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "json-pointer": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.2.tgz", - "integrity": "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==", - "dev": true, - "requires": { - "foreach": "^2.0.4" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true - }, - "lodash.flatmap": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.flatmap/-/lodash.flatmap-4.5.0.tgz", - "integrity": "sha1-74y/QI9uSCaGYzRTBcaswLd4cC4=", - "dev": true - }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", - "dev": true - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "ono": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/ono/-/ono-4.0.11.tgz", - "integrity": "sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g==", - "dev": true, - "requires": { - "format-util": "^1.0.3" - } - }, - "openapi-enforcer": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/openapi-enforcer/-/openapi-enforcer-1.13.1.tgz", - "integrity": "sha512-NDDmyonl3bgxP+RabJsTPxn8EEza4Aet5zEocfX6nP9LL0J52aNFMYxNHoAuqoY3zQcZG2pnm3DMi2gYiRYbQg==", - "dev": true, - "requires": { - "axios": "^0.21.1", - "json-schema-ref-parser": "^6.1.0" - }, - "dependencies": { - "json-schema-ref-parser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-6.1.0.tgz", - "integrity": "sha512-pXe9H1m6IgIpXmE5JSb8epilNTGsmTb2iPohAXpOdhqGFbQjNeHHsZxU+C8w6T81GZxSPFLeUoqDJmzxx5IGuw==", - "dev": true, - "requires": { - "call-me-maybe": "^1.0.1", - "js-yaml": "^3.12.1", - "ono": "^4.0.11" - } - } - } - }, - "openapi-examples-validator": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/openapi-examples-validator/-/openapi-examples-validator-4.7.1.tgz", - "integrity": "sha512-/OZZHhJkiaMQhkVfD0vFNOrRCBhkOL+X4/uhC55CJH0giuLUPQgFB5/iU26DCq1sZo2j9L70XYPcdHoM4PS+Xw==", + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", "dev": true, - "requires": { - "ajv": "^6.12.6", - "ajv-oai": "1.2.1", - "commander": "^6.2.1", - "errno": "^1.0.0", - "glob": "^7.2.0", - "json-pointer": "^0.6.2", - "json-schema-ref-parser": "^9.0.9", - "jsonpath-plus": "^6.0.1", - "lodash.clonedeep": "^4.5.0", - "lodash.flatmap": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.merge": "^4.6.2", - "yaml": "^1.10.2" + "license": "ISC", + "bin": { + "yaml": "bin.mjs" }, - "dependencies": { - "ajv-oai": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ajv-oai/-/ajv-oai-1.2.1.tgz", - "integrity": "sha512-gj7dnSdLyjWKid3uQI16u5wQNpkyqivjtCuvI4BWezeOzYTj5YHt4otH9GOBCaXY3FEbzQeWsp6C2qc18+BXDA==", - "dev": true, - "requires": { - "decimal.js": "^10.2.0" - } - }, - "commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true - }, - "json-schema-ref-parser": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz", - "integrity": "sha512-qcP2lmGy+JUoQJ4DOQeLaZDqH9qSkeGCK3suKWxJXS82dg728Mn3j97azDMaOUmJAN4uCq91LdPx4K7E8F1a7Q==", - "dev": true, - "requires": { - "@apidevtools/json-schema-ref-parser": "9.0.9" - } - }, - "jsonpath-plus": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-6.0.1.tgz", - "integrity": "sha512-EvGovdvau6FyLexFH2OeXfIITlgIbgZoAZe3usiySeaIDm5QS+A10DKNpaPBBqqRSZr2HN6HVNXxtwUAr2apEw==", - "dev": true - } - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" + "engines": { + "node": ">= 14" } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true } } } diff --git a/package.json b/package.json index c99272f9af5cb5b03cf4debdb6d96f050076fe7d..f844e1426abc253abd2137bbff70e9a9e08658bc 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test": "npm run openapi-enforcer && npm run validate-examples" }, "devDependencies": { - "openapi-enforcer": "^1.13.1", - "openapi-examples-validator": "^4.2.1" + "openapi-enforcer": "^1.23.0", + "openapi-examples-validator": "^5.0.0" } } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e8581d7d5e3d471c37817433ac6ed836f14c2df3..82885f23c24a1b4379c76546f09d0f3dc5ddb76a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -31,7 +31,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) # SQLITE_OMIT_SHARED_CACHE: This option builds SQLite without support for shared cache mode. The sqlite3_enable_shared_cache() is omitted along with a fair amount of logic within the B-Tree subsystem associated with shared cache management. This compile-time option is recommended most applications as it results in improved performance and reduced library footprint. # SQLITE_DEFAULT_FOREIGN_KEYS=1: This macro determines whether enforcement of foreign key constraints is enabled or disabled by default for new database connections. # SQLITE_DQS=0: This setting disables the double-quoted string literal misfeature. -# SQLITE_ENABLE_DBPAGE_VTAB: Enables the SQLITE_DBPAGE virtual table. Warning: writing to the SQLITE_DBPAGE virtual table can very easily cause unrecoverably database corruption. +# SQLITE_ENABLE_DBPAGE_VTAB: Enables the SQLITE_DBPAGE virtual table. Warning: writing to the SQLITE_DBPAGE virtual table can very easily cause unrecoverably database corruption. It is needed for the .recover SQLite3 command. # SQLITE_TEMP_STORE=1: This option sets the default value for the temp_store pragma to 1. This means that temporary tables and indices are always stored on disk. This is the default setting. # SQLITE_DEFAULT_CACHE_SIZE=-16384: Allow up to 16 MiB of cache to be used by SQLite3 (default is 2000 kiB) # SQLITE_DEFAULT_SYNCHRONOUS=1: Use normal synchronous mode (default is 2) diff --git a/src/api/auth.h b/src/api/auth.h index 9f5cc9b5bff7a2aecbc32f8c1d50d8a21f3d3d1c..058bf634036d882c6724c81f5a671e43e7ca9810 100644 --- a/src/api/auth.h +++ b/src/api/auth.h @@ -43,7 +43,7 @@ // flaw, the browser (primarily Internet Explorer) will not reveal the cookie to // a third party. #define FTL_SET_COOKIE "Set-Cookie: sid=%s; SameSite=Strict; Path=/; Max-Age=%u; HttpOnly\r\n" -#define FTL_DELETE_COOKIE "Set-Cookie: sid=deleted; SameSite=Strict; Path=/; Max-Age=-1\r\n" +#define FTL_DELETE_COOKIE "Set-Cookie: sid=deleted; SameSite=Strict; Path=/; Max-Age=-1; Expires=Thu, 01 Jan 1970 00:00:00 GMT;\r\n" struct session { bool used; diff --git a/src/api/docs/content/specs/config.yaml b/src/api/docs/content/specs/config.yaml index 215c6c012e49e6c0dbadb163202d84371081a0a7..8e7a06b9e474b9a8689dc6f72b7ae62349975e20 100644 --- a/src/api/docs/content/specs/config.yaml +++ b/src/api/docs/content/specs/config.yaml @@ -261,6 +261,8 @@ components: type: boolean iCloudPrivateRelay: type: boolean + designatedResolver: + type: boolean reply: type: object properties: @@ -413,6 +415,8 @@ components: type: array items: type: string + serve_all: + type: boolean session: type: object properties: @@ -432,6 +436,8 @@ components: type: string webhome: type: string + prefix: + type: string interface: type: object properties: @@ -685,6 +691,7 @@ components: specialDomains: mozillaCanary: true iCloudPrivateRelay: true + designatedResolver: true reply: host: force4: false @@ -749,11 +756,13 @@ components: port: 80,[::]:80 threads: 0 headers: + - "X-DNS-Prefetch-Control: off" - "Content-Security-Policy: default-src 'self' 'unsafe-inline';" - "X-Frame-Options: DENY" - "X-XSS-Protection: 0" - "X-Content-Type-Options: nosniff" - "Referrer-Policy: strict-origin-when-cross-origin" + serve_all: false session: timeout: 300 restore: true @@ -762,6 +771,7 @@ components: paths: webroot: "/var/www/html" webhome: "/admin/" + prefix: "" interface: boxed: true theme: "default-darker" diff --git a/src/api/docs/content/specs/docs.yaml b/src/api/docs/content/specs/docs.yaml index ecd62879638d94508e2656b3c1c8dfefa96619cb..16dd0b80176db1e8412f6d9a0f18188c3ceecdbd 100644 --- a/src/api/docs/content/specs/docs.yaml +++ b/src/api/docs/content/specs/docs.yaml @@ -10,7 +10,7 @@ components: operationId: "get_docs" security: [] description: | - This API hook returns the embedded API documentation rendered as HTML. + This API hook returns the embedded API documentation rendered as HTML. Note that this endpoint is supposed to be accessed from web browsers only. The automatically generated `curl` example will *not* create a *full* (as in: self-contained) clone of the documentation. responses: '200': description: OK diff --git a/src/api/docs/content/specs/info.yaml b/src/api/docs/content/specs/info.yaml index 6be2b1acd1e87f3fd7c8160e009c70ad59bb6846..d5bf45fdfc81eaa44d77a8fdb9560ee9b88d4d1a 100644 --- a/src/api/docs/content/specs/info.yaml +++ b/src/api/docs/content/specs/info.yaml @@ -461,6 +461,10 @@ components: type: integer description: Number of available processors example: 8 + "%cpu": + type: number + description: Total CPU usage in percent (may be higher than 100% on multi-core systems and negative if the value cannot be computed) + example: 0.0 load: type: object description: 1, 5, and 15 minute load averages @@ -532,7 +536,7 @@ components: value: 40.0 max: null crit: null - path: "temp1" + sensor: "temp1" - name: "nct6793" path: "hwmon2" source: "devices/platform/nct6775.656" @@ -541,12 +545,12 @@ components: value: 59.5 max: null crit: null - path: "temp1" + sensor: "temp1" - name: CPUTIN value: 40.0 max: null crit: null - path: "temp2" + sensor: "temp2" - name: "k10temp" path: "hwmon3" source: "devices/pci0000:00/0000:00:18.3" @@ -555,12 +559,12 @@ components: value: 58.875 max: 70.0 crit: null - path: "temp1" + sensor: "temp1" - name: "Tctl" value: 58.875 max: null crit: null - path: "temp2" + sensor: "temp2" - name: "coretemp" path: "hwmon4" source: "devices/platform/coretemp.0" @@ -569,17 +573,17 @@ components: value: 48.0 max: 100.0 crit: 100.0 - path: "temp1" + sensor: "temp1" - name: "Core 0" value: 48.0 max: 100.0 crit: 100.0 - path: "temp2" + sensor: "temp2" - name: "Core 1" value: 47.0 max: 100.0 crit: 100.0 - path: "temp3" + sensor: "temp3" cpu_temp: type: number description: CPU temperature (best guess, may be *null* if no sensor can be reliably identified, please report if you encounter this) diff --git a/src/api/docs/content/specs/lists.yaml b/src/api/docs/content/specs/lists.yaml index 19c3fddab62ee0d05e0aec23d470527235c45d0a..b1f2b721ec0ecbf8b4c2fecca0c7dbd3cddd6057 100644 --- a/src/api/docs/content/specs/lists.yaml +++ b/src/api/docs/content/specs/lists.yaml @@ -194,7 +194,24 @@ components: content: application/json: schema: - $ref: 'lists.yaml#/components/schemas/lists/post' + type: array + items: + type: object + properties: + item: + type: string + description: group name + type: + type: string + enum: + - "allow" + - "block" + description: Type of list + example: + - item: "https://hosts-file.net/ad_servers.txt" + type: "block" + - item: "https://hosts-file.net/ad_servers2.txt" + type: "block" responses: '204': description: Items deleted diff --git a/src/api/docs/content/specs/queries.yaml b/src/api/docs/content/specs/queries.yaml index aa7df5e631437728cc719d79d6786724858e7dfb..117c298a433527a1a2b3a4f458000a848526b03c 100644 --- a/src/api/docs/content/specs/queries.yaml +++ b/src/api/docs/content/specs/queries.yaml @@ -19,7 +19,7 @@ components: - Only show queries *from* a given timestamp on: Use parameter `from` - Only show queries *until* a given timestamp: Use parameter `until` - - Only show queries sent to a specific upstream destination (may also be `cache` or `blocklist`): Use parameter `upstream` + - Only show queries sent to a specific upstream destination (may also be `cache`, `blocklist`, or `permitted`): Use parameter `upstream` - Only show queries for specific domains: Use parameter `domain` - Only show queries for specific clients: Use parameter `client` @@ -250,7 +250,7 @@ components: time: 19 list_id: NULL upstream: "localhost#5353" - dbid: 112421354 + id: 112421354 ede: code: 0 text: null @@ -268,7 +268,7 @@ components: time: 12.3 list_id: NULL upstream: "localhost#5353" - dbid: 112421355 + id: 112421355 ede: code: 0 text: null diff --git a/src/api/history.c b/src/api/history.c index d6beada8d04ed0b8cc599f1ddc41fa3df1c698ac..6a20c9beae033621c9f488702b88232360b63d58 100644 --- a/src/api/history.c +++ b/src/api/history.c @@ -102,7 +102,7 @@ int api_history_clients(struct ftl_conn *api) JSON_ADD_ITEM_TO_OBJECT(json, "history", history); cJSON *clients = JSON_NEW_ARRAY(); JSON_ADD_ITEM_TO_OBJECT(json, "clients", clients); - JSON_SEND_OBJECT_UNLOCK(json); + JSON_SEND_OBJECT(json); } // Get number of clients to return diff --git a/src/api/info.c b/src/api/info.c index 9a690d31318887a24c2a014daeff9ad67aa939f5..f060e4c0ff2c32fd0f8b5b624d75223283296aa9 100644 --- a/src/api/info.c +++ b/src/api/info.c @@ -32,7 +32,7 @@ #include "datastructure.h" // uname() #include <sys/utsname.h> -// get_cpu_percentage() +// get_ftl_cpu_percentage() #include "daemon.h" // getProcessMemory() #include "procps.h" @@ -220,6 +220,8 @@ int get_system_obj(struct ftl_conn *api, cJSON *system) cJSON *cpu = JSON_NEW_OBJECT(); // Number of available processors JSON_ADD_NUMBER_TO_OBJECT(cpu, "nprocs", nprocs); + // Averaged total CPU usage in percent + JSON_ADD_NUMBER_TO_OBJECT(cpu, "%cpu", get_total_cpu_percentage()); // 1, 5, and 15 minute load averages (we need to convert them) cJSON *raw = JSON_NEW_ARRAY(); @@ -599,7 +601,7 @@ static int get_ftl_obj(struct ftl_conn *api, cJSON *ftl) parse_proc_meminfo(&mem); getProcessMemory(&pmem, mem.total); JSON_ADD_NUMBER_TO_OBJECT(ftl, "%mem", pmem.VmRSS_percent); - JSON_ADD_NUMBER_TO_OBJECT(ftl, "%cpu", get_cpu_percentage()); + JSON_ADD_NUMBER_TO_OBJECT(ftl, "%cpu", get_ftl_cpu_percentage()); JSON_ADD_BOOL_TO_OBJECT(ftl, "allow_destructive", config.webserver.api.allow_destructive.v.b); @@ -762,7 +764,12 @@ int get_version_obj(struct ftl_conn *api, cJSON *version) // Loop over KEY=VALUE parts in the versions file while((read = getline(&line, &len, fp)) != -1) { - if (parse_line(line, &key, &value)) + // Skip empty lines + if(read <= 1) + continue; + + // Parse lines and skip those without values + if (!parse_line(line, &key, &value) || strlen(value) == 0) continue; if(strcmp(key, "CORE_BRANCH") == 0) diff --git a/src/api/padd.c b/src/api/padd.c index 21a44cfa5dc2749cd0266e140637d2256be1a513..5c1d680cc5dd2d37865edfeb83bd51e533eb47e5 100644 --- a/src/api/padd.c +++ b/src/api/padd.c @@ -296,7 +296,7 @@ int api_padd(struct ftl_conn *api) parse_proc_meminfo(&mem); getProcessMemory(&pmem, mem.total); JSON_ADD_NUMBER_TO_OBJECT(json, "%mem", pmem.VmRSS_percent); - JSON_ADD_NUMBER_TO_OBJECT(json, "%cpu", get_cpu_percentage()); + JSON_ADD_NUMBER_TO_OBJECT(json, "%cpu", get_ftl_cpu_percentage()); JSON_ADD_NUMBER_TO_OBJECT(json, "pid", getpid()); // info/sensors -> CPU temp sensor diff --git a/src/api/queries.c b/src/api/queries.c index 0ae502821f5d4262a5176f03ffa1b146fb3a0f9f..e109bc17365a06af7e31c396f200c2065aa58225 100644 --- a/src/api/queries.c +++ b/src/api/queries.c @@ -157,6 +157,10 @@ int api_queries_suggestions(struct ftl_conn *api) JSON_REF_STR_IN_ARRAY(dnssec, string); } + // Add special "permitted" upstream which is the sum of forwarded and + // cached queries + JSON_REF_STR_IN_ARRAY(upstream, "permitted"); + cJSON *suggestions = JSON_NEW_OBJECT(); JSON_ADD_ITEM_TO_OBJECT(suggestions, "domain", domain); JSON_ADD_ITEM_TO_OBJECT(suggestions, "client_ip", client_ip); @@ -347,6 +351,12 @@ int api_queries(struct ftl_conn *api) add_querystr_string(api, querystr, "q.status IN ", get_cached_statuslist(), &where); filtering = true; } + else if(strcmp(upstreamname, "permitted") == 0) + { + // Pseudo-upstream for permitted queries + add_querystr_string(api, querystr, "q.status IN ", get_permitted_statuslist(), &where); + filtering = true; + } else { if(is_wildcard(upstreamname)) diff --git a/src/api/teleporter.c b/src/api/teleporter.c index 6a835e8bb86c35e1c027affcb06998d4799e3b74..cb56295b46aa114704cfcbe653074fa10ae95d4d 100644 --- a/src/api/teleporter.c +++ b/src/api/teleporter.c @@ -219,6 +219,16 @@ static int process_received_tar_gz(struct ftl_conn *api, struct upload_data *dat static int api_teleporter_POST(struct ftl_conn *api) { + // Check if this is an app session and reject the request if app sudo + // mode is disabled + if(api->session != NULL && api->session->app && !config.webserver.api.app_sudo.v.b) + { + return send_json_error(api, 403, + "forbidden", + "Unable to change configuration (read-only)", + "The current app session is not allowed to modify Pi-hole config settings (webserver.api.app_sudo is false)"); + } + struct upload_data data; memset(&data, 0, sizeof(struct upload_data)); const struct mg_request_info *req_info = mg_get_request_info(api->conn); diff --git a/src/config/config.c b/src/config/config.c index 775c7a1a7e749e3ff48ba9d6ca04813a8fe86f3d..59fcb23f81b752d0abaadb6cca87ec866c3c4b0b 100644 --- a/src/config/config.c +++ b/src/config/config.c @@ -673,6 +673,12 @@ static void initConfig(struct config *conf) conf->dns.specialDomains.iCloudPrivateRelay.d.b = true; conf->dns.specialDomains.iCloudPrivateRelay.c = validate_stub; // Only type-based checking + conf->dns.specialDomains.designatedResolver.k = "dns.specialDomains.designatedResolver"; + conf->dns.specialDomains.designatedResolver.h = "Should Pi-hole always reply with NODATA to all queries to zone resolver.arpa to prevent devices from bypassing Pi-hole using Discovery of Designated Resolvers? This is based on recommendations at the end of RFC 9462, section 4."; + conf->dns.specialDomains.designatedResolver.t = CONF_BOOL; + conf->dns.specialDomains.designatedResolver.d.b = true; + conf->dns.specialDomains.designatedResolver.c = validate_stub; // Only type-based checking + // sub-struct dns.reply_addr conf->dns.reply.host.force4.k = "dns.reply.host.force4"; conf->dns.reply.host.force4.h = "Use a specific IPv4 address for the Pi-hole host? By default, FTL determines the address of the interface a query arrived on and uses this address for replying to A queries with the most suitable address for the requesting client. This setting can be used to use a fixed, rather than the dynamically obtained, address when Pi-hole responds to the following names: [ \"pi.hole\", \"<the device's hostname>\", \"pi.hole.<local domain>\", \"<the device's hostname>.<local domain>\" ]"; @@ -1015,11 +1021,12 @@ static void initConfig(struct config *conf) conf->webserver.threads.c = validate_stub; // Only type-based checking conf->webserver.headers.k = "webserver.headers"; - conf->webserver.headers.h = "Additional HTTP headers added to the web server responses.\n The headers are added to all responses, including those for the API.\n Note about the default additional headers:\n - Content-Security-Policy: [...] 'unsafe-inline' is both required by Chart.js styling some elements directly, and index.html containing some inlined Javascript code.\n - X-Frame-Options: DENY: The page can not be displayed in a frame, regardless of the site attempting to do so.\n - X-Xss-Protection: 0: Disables XSS filtering in browsers that support it. This header is usually enabled by default in browsers, and is not recommended as it can hurt the security of the site. (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection).\n - X-Content-Type-Options: nosniff: Marker used by the server to indicate that the MIME types advertised in the Content-Type headers should not be changed and be followed. This allows to opt-out of MIME type sniffing, or, in other words, it is a way to say that the webmasters knew what they were doing. Site security testers usually expect this header to be set.\n - Referrer-Policy: strict-origin-when-cross-origin: A referrer will be sent for same-site origins, but cross-origin requests will send no referrer information.\n The latter four headers are set as expected by https://securityheaders.io"; + conf->webserver.headers.h = "Additional HTTP headers added to the web server responses.\n The headers are added to all responses, including those for the API.\n Note about the default additional headers:\n - X-DNS-Prefetch-Control: off: Usually browsers proactively perform domain name resolution on links that the user may choose to follow. We disable DNS prefetching here.\n - Content-Security-Policy: [...] 'unsafe-inline' is both required by Chart.js styling some elements directly, and index.html containing some inlined Javascript code.\n - X-Frame-Options: DENY: The page can not be displayed in a frame, regardless of the site attempting to do so.\n - X-Xss-Protection: 0: Disables XSS filtering in browsers that support it. This header is usually enabled by default in browsers, and is not recommended as it can hurt the security of the site. (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection).\n - X-Content-Type-Options: nosniff: Marker used by the server to indicate that the MIME types advertised in the Content-Type headers should not be changed and be followed. This allows to opt-out of MIME type sniffing, or, in other words, it is a way to say that the webmasters knew what they were doing. Site security testers usually expect this header to be set.\n - Referrer-Policy: strict-origin-when-cross-origin: A referrer will be sent for same-site origins, but cross-origin requests will send no referrer information.\n The latter four headers are set as expected by https://securityheaders.io"; conf->webserver.headers.a = cJSON_CreateStringReference("array of HTTP headers"); conf->webserver.headers.t = CONF_JSON_STRING_ARRAY; conf->webserver.headers.f = FLAG_RESTART_FTL; conf->webserver.headers.d.json = cJSON_CreateArray(); + cJSON_AddItemReferenceToArray(conf->webserver.headers.d.json, cJSON_CreateStringReference("X-DNS-Prefetch-Control: off")); cJSON_AddItemReferenceToArray(conf->webserver.headers.d.json, cJSON_CreateStringReference("Content-Security-Policy: default-src 'self' 'unsafe-inline';")); cJSON_AddItemReferenceToArray(conf->webserver.headers.d.json, cJSON_CreateStringReference("X-Frame-Options: DENY")); cJSON_AddItemReferenceToArray(conf->webserver.headers.d.json, cJSON_CreateStringReference("X-XSS-Protection: 0")); @@ -1027,6 +1034,12 @@ static void initConfig(struct config *conf) cJSON_AddItemReferenceToArray(conf->webserver.headers.d.json, cJSON_CreateStringReference("Referrer-Policy: strict-origin-when-cross-origin")); conf->webserver.headers.c = validate_stub; // Only type-based checking + conf->webserver.serve_all.k = "webserver.serve_all"; + conf->webserver.serve_all.h = "Should the web server serve all files in webserver.paths.webroot directory? If disabled, only files within the path defined through webserver.paths.webhome and /api will be served."; + conf->webserver.serve_all.t = CONF_BOOL; + conf->webserver.serve_all.d.b = false; + conf->webserver.serve_all.c = validate_stub; + conf->webserver.tls.cert.k = "webserver.tls.cert"; conf->webserver.tls.cert.h = "Path to the TLS (SSL) certificate file. All directories along the path must be readable and accessible by the user running FTL (typically 'pihole'). This option is only required when at least one of webserver.port is TLS. The file must be in PEM format, and it must have both, private key and certificate (the *.pem file created must contain a 'CERTIFICATE' section as well as a 'RSA PRIVATE KEY' section).\n The *.pem file can be created using\n cp server.crt server.pem\n cat server.key >> server.pem\n if you have these files instead"; conf->webserver.tls.cert.a = cJSON_CreateStringReference("<valid TLS certificate file (*.pem)>"); @@ -1064,6 +1077,14 @@ static void initConfig(struct config *conf) conf->webserver.paths.webhome.d.s = (char*)"/admin/"; conf->webserver.paths.webhome.c = validate_filepath_two_slash; + conf->webserver.paths.prefix.k = "webserver.paths.prefix"; + conf->webserver.paths.prefix.h = "Prefix where the web interface is served\n This is useful when you are using a reverse proxy serving the web interface, e.g., at http://<ip>/pihole/admin/ instead of http://<ip>/admin/. In this example, the prefix would be \"/pihole\". Note that the prefix has to be stripped away by the reverse proxy, e.g., for traefik:\n - traefik.http.routers.pihole.rule=PathPrefix(`/pihole`)\n - traefik.http.middlewares.piholehttp.stripprefix.prefixes=/pihole\n The prefix should start with a slash. If you don't use a prefix, leave this field empty. Setting this field to an incorrect value may result in the web interface not being accessible.\n Don't use this setting if you are not using a reverse proxy!"; + conf->webserver.paths.prefix.a = cJSON_CreateStringReference("valid URL prefix or empty"); + conf->webserver.paths.prefix.t = CONF_STRING; + conf->webserver.paths.prefix.f = FLAG_RESTART_FTL; + conf->webserver.paths.prefix.d.s = (char*)""; + conf->webserver.paths.prefix.c = validate_filepath_empty; + // sub-struct interface conf->webserver.interface.boxed.k = "webserver.interface.boxed"; conf->webserver.interface.boxed.h = "Should the web interface use the boxed layout?"; diff --git a/src/config/config.h b/src/config/config.h index 9c7078cd0e0bb6b3ce485a6909727ab0cd6f8fe7..974eb853928533f1d8243cc127b13f939740de75 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -162,6 +162,7 @@ struct config { struct { struct conf_item mozillaCanary; struct conf_item iCloudPrivateRelay; + struct conf_item designatedResolver; } specialDomains; struct { struct { @@ -244,6 +245,7 @@ struct config { struct conf_item port; struct conf_item threads; struct conf_item headers; + struct conf_item serve_all; struct { struct conf_item timeout; struct conf_item restore; @@ -254,6 +256,7 @@ struct config { struct { struct conf_item webroot; struct conf_item webhome; + struct conf_item prefix; } paths; struct { struct conf_item boxed; diff --git a/src/config/dnsmasq_config.c b/src/config/dnsmasq_config.c index 36726e2f3dfdf34097a2b72b8f115143ef9b0bee..4b35b207574d57952791863eaed005d278919d8f 100644 --- a/src/config/dnsmasq_config.c +++ b/src/config/dnsmasq_config.c @@ -568,6 +568,15 @@ bool __attribute__((nonnull(1,3))) write_dnsmasq_config(struct config *conf, boo } } + // The domain "pi.hole" is purely local, never forward it to upstream servers + fputs("# Local domain for Pi-hole\n", pihole_conf); + fputs("# This domain is purely local and should never be forwarded to any\n", pihole_conf); + fputs("# upstream servers. We add a false A-record to this domain to prevent\n", pihole_conf); + fputs("# NXDOMAIN responses for queries on this domain. The actual response\n", pihole_conf); + fputs("# is handled by FTL at runtime\n", pihole_conf); + fputs("local=/pi.hole/\n", pihole_conf); + fputs("host-record=pi.hole,0.0.0.0\n", pihole_conf); + if(conf->dhcp.active.v.b) { fputs("# DHCP server setting\n", pihole_conf); diff --git a/src/config/env.c b/src/config/env.c index ad52c2564b094a7a3d18a8984cc4d434906e9301..e5d57361e1cb4415b64ff73e725fc3a51ff556c3 100644 --- a/src/config/env.c +++ b/src/config/env.c @@ -580,8 +580,8 @@ bool __attribute__((nonnull(1,2,3))) readEnvValue(struct conf_item *conf_item, s cJSON_Delete(conf_item->v.json); conf_item->v.json = cJSON_CreateArray(); // Parse envvar array and generate a JSON array (env var - // arrays are ;-delimited) - const char delim[] =";"; + // arrays are ; or \n-delimited) + const char delim[] =";\n"; const char *elem = strtok(envvar_copy, delim); while(elem != NULL) { diff --git a/src/config/toml_reader.c b/src/config/toml_reader.c index a42b65e8d72bb645301ea70be900f51d2b1747b6..0bb0edd6000563f5ba09fe530e5b3eb8502e1a47 100644 --- a/src/config/toml_reader.c +++ b/src/config/toml_reader.c @@ -197,7 +197,10 @@ bool readFTLtoml(struct config *oldconf, struct config *newconf, { log_debug(DEBUG_CONFIG, "%s CHANGED", new_conf_item->k); if(new_conf_item->f & FLAG_RESTART_FTL && restart != NULL) + { + log_info("Restarting FTL due to change of %s", new_conf_item->k); *restart = true; + } // Check if this item changed the password, if so, we need to // invalidate all currently active sessions @@ -208,7 +211,10 @@ bool readFTLtoml(struct config *oldconf, struct config *newconf, // Migrate config from old to new if(migrate_config(toml, newconf) && restart != NULL) + { + log_info("Restarting FTL due to migration of configuration"); *restart = true; + } // Report debug config if enabled set_debug_flags(newconf); diff --git a/src/daemon.c b/src/daemon.c index ca866f76967d8b7853238e2d2e7deb0cb2684a6c..45e399fb891719dbc5166aab49a22016d78767f7 100644 --- a/src/daemon.c +++ b/src/daemon.c @@ -39,6 +39,8 @@ #include <locale.h> // freeEnvVars() #include "config/env.h" +// parse_proc_stat() +#include "procps.h" // destroy_entropy() #include "webserver/x509.h" @@ -441,8 +443,8 @@ void cleanup(const int ret) log_info("########## FTL terminated after%s (code %i)! ##########", buffer, ret); } -static float last_clock = 0.0f; -static float cpu_usage = 0.0f; +static float ftl_cpu_usage = 0.0f; +static float total_cpu_usage = 0.0f; void calc_cpu_usage(const unsigned int interval) { // Get the current resource usage @@ -459,18 +461,46 @@ void calc_cpu_usage(const unsigned int interval) // kernel mode by this process since the total time since the last call // to this function. 100% means one core is fully used, 200% means two // cores are fully used, etc. - const float this_clock = usage.ru_utime.tv_sec + usage.ru_stime.tv_sec + 1e-6 * (usage.ru_utime.tv_usec + usage.ru_stime.tv_usec); + const float ftl_cpu_time = usage.ru_utime.tv_sec + usage.ru_stime.tv_sec + 1e-6 * (usage.ru_utime.tv_usec + usage.ru_stime.tv_usec); // Calculate the CPU usage in this interval - cpu_usage = 100.0 * (this_clock - last_clock) / interval; + static float last_ftl_cpu_time = 0.0f; + ftl_cpu_usage = 100.0 * (ftl_cpu_time - last_ftl_cpu_time) / interval; // Store the current time for the next call to this function - last_clock = this_clock; + last_ftl_cpu_time = ftl_cpu_time; + + // The number of clock ticks per second + static long user_hz = 0; + if(user_hz == 0) + user_hz = sysconf(_SC_CLK_TCK); + + // Calculate the total CPU usage + unsigned long total_total, total_idle; + if(!parse_proc_stat(&total_total, &total_idle)) + { + total_cpu_usage = -1.0f; + return; + } + + // Calculate the CPU usage since the last call to this function + static unsigned long last_total_total = 0, last_total_idle = 0; + if(total_total - last_total_total > 0) + total_cpu_usage = 100.0 * (total_total - last_total_total - (total_idle - last_total_idle)) / (total_total - last_total_total); + + // Store the current time for the next call to this function + last_total_idle = total_idle; + last_total_total = total_total; +} + +float __attribute__((pure)) get_ftl_cpu_percentage(void) +{ + return ftl_cpu_usage; } -float __attribute__((pure)) get_cpu_percentage(void) +float __attribute__((pure)) get_total_cpu_percentage(void) { - return cpu_usage; + return total_cpu_usage; } ssize_t getrandom_fallback(void *buf, size_t buflen, unsigned int flags) diff --git a/src/daemon.h b/src/daemon.h index 45f5504d126c9ae20c4d0cab9ecceabf4c358f61..17170c690a931983abb1cc2d78d3959311195c37 100644 --- a/src/daemon.h +++ b/src/daemon.h @@ -23,7 +23,8 @@ bool is_fork(const pid_t mpid, const pid_t pid) __attribute__ ((const)); void cleanup(const int ret); void set_nice(void); void calc_cpu_usage(const unsigned int interval); -float get_cpu_percentage(void) __attribute__((pure)); +float get_ftl_cpu_percentage(void) __attribute__((pure)); +float get_total_cpu_percentage(void) __attribute__((pure)); bool ipv6_enabled(void); void init_locale(void); diff --git a/src/database/database-thread.c b/src/database/database-thread.c index 7f5ae04a67a7aec59a1685c3af5a27ae8eec9980..8cb84aa97e9390988f0e868c39853a2b59864c0d 100644 --- a/src/database/database-thread.c +++ b/src/database/database-thread.c @@ -131,8 +131,7 @@ void *DB_thread(void *val) break; // Store queries in on-disk database - if(config.database.maxDBdays.v.ui > 0 && - now - lastDBsave >= (time_t)config.database.DBinterval.v.ui) + if(now - lastDBsave >= (time_t)config.database.DBinterval.v.ui) { // Update lastDBsave timer lastDBsave = now - now%config.database.DBinterval.v.ui; diff --git a/src/database/query-table.c b/src/database/query-table.c index 7cd94a00d2735fa5da42d3f7d83d0e443baefcdc..d6afdf049e3b0e6baef51c66f9433cd8ba828ffd 100644 --- a/src/database/query-table.c +++ b/src/database/query-table.c @@ -586,88 +586,129 @@ bool import_queries_from_disk(void) // seconds. This is to give queries some time to complete before they are // exported to disk. When final is true, we export all queries (nothing is going // to be added to the in-memory database anymore). -bool export_queries_to_disk(bool final) +bool export_queries_to_disk(const bool final) { + int rc = 0; bool okay = false; + unsigned int insertions = 0; const double time = double_time() - (final ? 0.0 : REPLY_TIMEOUT); // Only try to export to database if it is known to not be broken if(FTLDBerror()) - return false; - - const char *querystr = "INSERT INTO disk.query_storage SELECT * FROM query_storage WHERE id > ? AND timestamp < ?"; - - log_debug(DEBUG_DATABASE, "Storing queries on disk WHERE id > %lu (max is %lu) and timestamp < %f", - last_disk_db_idx, last_mem_db_idx, time); + return false; // Start database timer timer_start(DATABASE_WRITE_TIMER); // Start transaction sqlite3 *memdb = get_memdb(); - SQL_bool(memdb, "BEGIN TRANSACTION"); - - // Prepare SQLite3 statement sqlite3_stmt *stmt = NULL; - log_debug(DEBUG_DATABASE, "Accessing in-memory database"); - int rc = sqlite3_prepare_v2(memdb, querystr, -1, &stmt, NULL); - if( rc != SQLITE_OK ){ - log_err("export_queries_to_disk(): SQL error prepare: %s", sqlite3_errstr(rc)); - return false; - } + SQL_bool(memdb, "BEGIN TRANSACTION"); - // Bind index - if((rc = sqlite3_bind_int64(stmt, 1, last_disk_db_idx)) != SQLITE_OK) + // Only store queries if database.maxDBdays > 0 + if(config.database.maxDBdays.v.ui > 0) { - log_err("export_queries_to_disk(): Failed to bind id: %s", sqlite3_errstr(rc)); - return false; - } + const char *querystr = "INSERT INTO disk.query_storage SELECT * FROM query_storage WHERE id > ? AND timestamp < ?"; - // Bind upper time limit - // This prevents queries from the last 30 seconds from being stored - // immediately on-disk to give them some time to complete before finally - // exported. We do not limit anything when storing during termination. - if((rc = sqlite3_bind_double(stmt, 2, time)) != SQLITE_OK) - { - log_err("export_queries_to_disk(): Failed to bind time: %s", sqlite3_errstr(rc)); - return false; - } + log_debug(DEBUG_DATABASE, "Storing queries on disk WHERE id > %lu (max is %lu) and timestamp < %f", + last_disk_db_idx, last_mem_db_idx, time); - // Perform step - if((rc = sqlite3_step(stmt)) == SQLITE_DONE) - okay = true; - else - { - log_err("export_queries_to_disk(): Failed to export queries: %s", sqlite3_errstr(rc)); - log_info(" SQL query was: \"%s\"", querystr); - log_info(" with parameters: id = %lu, timestamp = %f", last_disk_db_idx, time); - } + // Prepare SQLite3 statement + log_debug(DEBUG_DATABASE, "Accessing in-memory database"); + if((rc = sqlite3_prepare_v2(memdb, querystr, -1, &stmt, NULL)) != SQLITE_OK) + { + log_err("export_queries_to_disk(): SQL error prepare: %s", sqlite3_errstr(rc)); + return false; + } - // Get number of queries actually inserted by the INSERT INTO ... SELECT * FROM ... - const unsigned int insertions = sqlite3_changes(memdb); + // Bind index + if((rc = sqlite3_bind_int64(stmt, 1, last_disk_db_idx)) != SQLITE_OK) + { + log_err("export_queries_to_disk(): Failed to bind id: %s", sqlite3_errstr(rc)); + return false; + } - // Finalize statement - sqlite3_finalize(stmt); + // Bind upper time limit + // This prevents queries from the last 30 seconds from being stored + // immediately on-disk to give them some time to complete before finally + // exported. We do not limit anything when storing during termination. + if((rc = sqlite3_bind_double(stmt, 2, time)) != SQLITE_OK) + { + log_err("export_queries_to_disk(): Failed to bind time: %s", sqlite3_errstr(rc)); + return false; + } - // Update last_disk_db_idx - // Prepare SQLite3 statement - log_debug(DEBUG_DATABASE, "Accessing in-memory database"); - rc = sqlite3_prepare_v2(memdb, "SELECT MAX(id) FROM disk.query_storage;", -1, &stmt, NULL); - if(rc != SQLITE_OK) - { - log_err("export_queries_to_disk(): SQL error prepare: %s", sqlite3_errstr(rc)); - return false; - } + // Perform step + if((rc = sqlite3_step(stmt)) == SQLITE_DONE) + okay = true; + else + { + log_err("export_queries_to_disk(): Failed to export queries: %s", sqlite3_errstr(rc)); + log_info(" SQL query was: \"%s\"", querystr); + log_info(" with parameters: id = %lu, timestamp = %f", last_disk_db_idx, time); + } - // Perform step - if((rc = sqlite3_step(stmt)) == SQLITE_ROW) - last_disk_db_idx = sqlite3_column_int64(stmt, 0); - else - log_err("Failed to get MAX(id) from query_storage: %s", - sqlite3_errstr(rc)); + // Get number of queries actually inserted by the INSERT INTO ... SELECT * FROM ... + insertions = sqlite3_changes(memdb); - // Finalize statement - sqlite3_finalize(stmt); + // Finalize statement + sqlite3_finalize(stmt); + + + // Update last_disk_db_idx + // Prepare SQLite3 statement + rc = sqlite3_prepare_v2(memdb, "SELECT MAX(id) FROM disk.query_storage;", -1, &stmt, NULL); + if(rc != SQLITE_OK) + { + log_err("export_queries_to_disk(): SQL error prepare: %s", sqlite3_errstr(rc)); + return false; + } + + // Perform step + if((rc = sqlite3_step(stmt)) == SQLITE_ROW) + last_disk_db_idx = sqlite3_column_int64(stmt, 0); + else + log_err("Failed to get MAX(id) from query_storage: %s", + sqlite3_errstr(rc)); + + // Finalize statement + sqlite3_finalize(stmt); + + /* + * If there are any insertions, we: + * 1. Insert (or replace) the last timestamp into the `disk.ftl` table. + * 2. Update the total queries counter in the `disk.counters` table. + * 3. Update the blocked queries counter in the `disk.counters` table. + * + * Note that new_total does not need to match the total number of + * insertions here as storing queries to the database happens + * time-delayed. In the end, the total number of queries will be + * correct (after final synchronization during FTL shutdown). + */ + if(insertions > 0) + { + if((rc = dbquery(memdb, "INSERT OR REPLACE INTO disk.ftl (id, value) VALUES ( %i, %f );", DB_LASTTIMESTAMP, new_last_timestamp)) != SQLITE_OK) + log_err("export_queries_to_disk(): Cannot update timestamp: %s", sqlite3_errstr(rc)); + + if((rc = dbquery(memdb, "UPDATE disk.counters SET value = value + %u WHERE id = %i;", new_total, DB_TOTALQUERIES)) != SQLITE_OK) + log_err("export_queries_to_disk(): Cannot update total queries counter: %s", sqlite3_errstr(rc)); + else + // Success + new_total = 0; + + if((rc = dbquery(memdb, "UPDATE disk.counters SET value = value + %u WHERE id = %i;", new_blocked, DB_BLOCKEDQUERIES)) != SQLITE_OK) + log_err("export_queries_to_disk(): Cannot update blocked queries counter: %s", sqlite3_errstr(rc)); + else + // Success + new_blocked = 0; + } + + // Update number of queries in the disk database + disk_db_num = get_number_of_queries_in_DB(memdb, "disk.query_storage"); + + // All temp queries were stored to disk, update the IDs + last_disk_db_idx += insertions; + } // Export linking tables and current AUTOINCREMENT values to the disk database const char *subtable_names[] = { @@ -694,35 +735,6 @@ bool export_queries_to_disk(bool final) log_debug(DEBUG_DATABASE, "Exported %i rows to disk.%s", sqlite3_changes(memdb), subtable_names[i]); } - /* - * If there are any insertions, we: - * 1. Insert (or replace) the last timestamp into the `disk.ftl` table. - * 2. Update the total queries counter in the `disk.counters` table. - * 3. Update the blocked queries counter in the `disk.counters` table. - * - * Note that new_total does not need to match the total number of - * insertions here as storing queries to the database happens - * time-delayed. In the end, the total number of queries will be - * correct (after final synchronization during FTL shutdown). - */ - if(insertions > 0) - { - if((rc = dbquery(memdb, "INSERT OR REPLACE INTO disk.ftl (id, value) VALUES ( %i, %f );", DB_LASTTIMESTAMP, new_last_timestamp)) != SQLITE_OK) - log_err("export_queries_to_disk(): Cannot update timestamp: %s", sqlite3_errstr(rc)); - - if((rc = dbquery(memdb, "UPDATE disk.counters SET value = value + %u WHERE id = %i;", new_total, DB_TOTALQUERIES)) != SQLITE_OK) - log_err("export_queries_to_disk(): Cannot update total queries counter: %s", sqlite3_errstr(rc)); - else - // Success - new_total = 0; - - if((rc = dbquery(memdb, "UPDATE disk.counters SET value = value + %u WHERE id = %i;", new_blocked, DB_BLOCKEDQUERIES)) != SQLITE_OK) - log_err("export_queries_to_disk(): Cannot update blocked queries counter: %s", sqlite3_errstr(rc)); - else - // Success - new_blocked = 0; - } - // End transaction if((rc = sqlite3_exec(memdb, "END TRANSACTION", NULL, NULL, NULL)) != SQLITE_OK) { @@ -730,14 +742,8 @@ bool export_queries_to_disk(bool final) return false; } - // Update number of queries in the disk database - disk_db_num = get_number_of_queries_in_DB(memdb, "disk.query_storage"); - - // All temp queries were stored to disk, update the IDs - last_disk_db_idx += insertions; - log_debug(DEBUG_DATABASE, "Exported %u rows for disk.query_storage (took %.1f ms, last SQLite ID %lu)", - insertions, timer_elapsed_msec(DATABASE_WRITE_TIMER), last_disk_db_idx); + insertions, timer_elapsed_msec(DATABASE_WRITE_TIMER), last_disk_db_idx); return okay; } diff --git a/src/database/query-table.h b/src/database/query-table.h index 691ba1e5e98f817579ce64f17da6379f2c83aab3..ca9e2076fd3a83d9b045b30417c303836190ee17 100644 --- a/src/database/query-table.h +++ b/src/database/query-table.h @@ -114,7 +114,7 @@ bool import_queries_from_disk(void); bool attach_database(sqlite3* db, const char **message, const char *path, const char *alias); bool detach_database(sqlite3* db, const char **message, const char *alias); int get_number_of_queries_in_DB(sqlite3 *db, const char *tablename); -bool export_queries_to_disk(bool final); +bool export_queries_to_disk(const bool final); bool delete_old_queries_from_db(const bool use_memdb, const double mintime); bool add_additional_info_column(sqlite3 *db); void DB_read_queries(void); diff --git a/src/datastructure.c b/src/datastructure.c index 1c8fc2a7fce6f5b7e97c5e3834c7c4b3b6a0af0b..79c7354eef10831b884e5096aee3ff546694619b 100644 --- a/src/datastructure.c +++ b/src/datastructure.c @@ -410,13 +410,32 @@ int _findClientID(const char *clientIP, const bool count, const bool aliasclient return clientID; } -void change_clientcount(clientsData *client, int total, int blocked, int overTimeIdx, int overTimeMod) +/** + * @brief Updates the client count, blocked count, and overtime data for a given + * client. + * + * This function modifies the client's count and blocked count by the specified + * amounts. Additionally, if a valid overtime index is provided, it updates the + * overtime data for the client and the global overtime array. This update can + * be avoided by setting overTimeIdx to -1. + * + * @param client Pointer to the clientsData structure representing the client. + * @param total The amount to add to the client's count. + * @param blocked The amount to add to the client's blocked count. + * @param overTimeIdx The index of the overtime slot to update. Must be between + * 0 and OVERTIME_SLOTS - 1 or -1 to skip updating the overtime data. + * @param overTimeMod The amount to add to the specified overtime slot. + */ +void change_clientcount(clientsData *client, const int total, const int blocked, + const int overTimeIdx, const int overTimeMod) { client->count += total; client->blockedcount += blocked; if(overTimeIdx > -1 && (unsigned int)overTimeIdx < OVERTIME_SLOTS) { overTime[overTimeIdx].total += overTimeMod; + log_debug(DEBUG_OVERTIME, "overTime[%d].total += %d = %d", + overTimeIdx, overTimeMod, overTime[overTimeIdx].total); client->overTime[overTimeIdx] += overTimeMod; } @@ -1013,6 +1032,29 @@ const char * __attribute__ ((pure)) get_cached_statuslist(void) return cached_list; } +static char permitted_list[32] = { 0 }; +const char * __attribute__ ((pure)) get_permitted_statuslist(void) +{ + if(permitted_list[0] != '\0') + return permitted_list; + + // Build a list of permitted query statuses + unsigned int first = 0; + // Open parenthesis + permitted_list[0] = '('; + for(enum query_status status = 0; status < QUERY_STATUS_MAX; status++) + if(!is_blocked(status)) + snprintf(permitted_list + strlen(permitted_list), + sizeof(permitted_list) - strlen(permitted_list), + "%s%d", first++ < 1 ? "" : ",", status); + + // Close parenthesis + const size_t len = strlen(permitted_list); + permitted_list[len] = ')'; + permitted_list[len + 1] = '\0'; + return permitted_list; +} + unsigned int __attribute__ ((pure)) get_blocked_count(void) { int blocked = 0; @@ -1217,19 +1259,43 @@ void _query_set_status(queriesData *query, const enum query_status new_status, c // ... update overTime counters, ... const int timeidx = getOverTimeID(query->timestamp); if(is_blocked(old_status) && !init) + { overTime[timeidx].blocked--; + log_debug(DEBUG_OVERTIME, "overTime[%d].blocked-- = %d (old_status = %s), ID = %d", + timeidx, overTime[timeidx].blocked, get_query_status_str(old_status), query->id); + } if(is_blocked(new_status)) + { overTime[timeidx].blocked++; + log_debug(DEBUG_OVERTIME, "overTime[%d].blocked++ = %d (new_status = %s), ID = %d", + timeidx, overTime[timeidx].blocked, get_query_status_str(new_status), query->id); + } if((old_status == QUERY_CACHE || old_status == QUERY_CACHE_STALE) && !init) + { overTime[timeidx].cached--; + log_debug(DEBUG_OVERTIME, "overTime[%d].cached-- = %d (old_status = %s), ID = %d", + timeidx, overTime[timeidx].cached, get_query_status_str(old_status), query->id); + } if(new_status == QUERY_CACHE || new_status == QUERY_CACHE_STALE) + { overTime[timeidx].cached++; + log_debug(DEBUG_OVERTIME, "overTime[%d].cached++ = %d (new_status = %s), ID = %d", + timeidx, overTime[timeidx].cached, get_query_status_str(new_status), query->id); + } if(old_status == QUERY_FORWARDED && !init) + { overTime[timeidx].forwarded--; + log_debug(DEBUG_OVERTIME, "overTime[%d].forwarded-- = %d (old_status = %s), ID = %d", + timeidx, overTime[timeidx].forwarded, get_query_status_str(old_status), query->id); + } if(new_status == QUERY_FORWARDED) + { overTime[timeidx].forwarded++; + log_debug(DEBUG_OVERTIME, "overTime[%d].forwarded++ = %d (new_status = %s), ID = %d", + timeidx, overTime[timeidx].forwarded, get_query_status_str(new_status), query->id); + } // ... and set new status query->status = new_status; diff --git a/src/datastructure.h b/src/datastructure.h index 6866908d0c3f1487770e5035cadfa0e50e149c3d..d1f3a714d8eba1dec1f905a78e3d8f42dc04ee3c 100644 --- a/src/datastructure.h +++ b/src/datastructure.h @@ -148,6 +148,7 @@ bool is_blocked(const enum query_status status) __attribute__ ((const)); bool is_cached(const enum query_status status) __attribute__ ((const)); const char *get_blocked_statuslist(void) __attribute__ ((pure)); const char *get_cached_statuslist(void) __attribute__ ((pure)); +const char *get_permitted_statuslist(void) __attribute__ ((pure)); unsigned int get_blocked_count(void) __attribute__ ((pure)); unsigned int get_forwarded_count(void) __attribute__ ((pure)); unsigned int get_cached_count(void) __attribute__ ((pure)); @@ -163,7 +164,7 @@ const char *getCNAMEDomainString(const queriesData *query); const char *getClientIPString(const queriesData *query); const char *getClientNameString(const queriesData *query); -void change_clientcount(clientsData *client, int total, int blocked, int overTimeIdx, int overTimeMod); +void change_clientcount(clientsData *client, const int total, const int blocked, const int overTimeIdx, const int overTimeMod); const char *get_query_type_str(const enum query_type type, const queriesData *query, char buffer[20]); const char *get_query_status_str(const enum query_status status) __attribute__ ((const)); const char *get_query_dnssec_str(const enum dnssec_status dnssec) __attribute__ ((const)); diff --git a/src/dnsmasq/dnsmasq.h b/src/dnsmasq/dnsmasq.h index ec21aeb7e074c2bae7cebb1f1171cd59ae0553a4..8e166dd14c78426dcebd8ff6eb45282e93099428 100644 --- a/src/dnsmasq/dnsmasq.h +++ b/src/dnsmasq/dnsmasq.h @@ -283,7 +283,8 @@ struct event_desc { #define OPT_LOCALHOST_SERVICE 72 #define OPT_LOG_PROTO 73 #define OPT_NO_0x20 74 -#define OPT_LAST 75 +#define OPT_DO_0x20 75 +#define OPT_LAST 76 #define OPTION_BITS (sizeof(unsigned int)*8) #define OPTION_SIZE ( (OPT_LAST/OPTION_BITS)+((OPT_LAST%OPTION_BITS)!=0) ) diff --git a/src/dnsmasq/forward.c b/src/dnsmasq/forward.c index c30314f2871d709663b9dca4cbaba2d2ccdb2dce..e831482d37fb60cdbb116ca7bc90c499b56a6254 100644 --- a/src/dnsmasq/forward.c +++ b/src/dnsmasq/forward.c @@ -365,8 +365,6 @@ static void forward_query(int udpfd, union mysockaddr *udpaddr, flags = 0; } - master = daemon->serverarray[first]; - /* don't forward A or AAAA queries for simple names, except the empty name */ if (!flags && option_bool(OPT_NODOTS_LOCAL) && @@ -378,7 +376,9 @@ static void forward_query(int udpfd, union mysockaddr *udpaddr, /* Configured answer. */ if (flags || ede == EDE_NOT_READY) goto reply; - + + master = daemon->serverarray[first]; + if (!(forward = get_new_frec(now, master, 0))) goto reply; /* table full - flags == 0, return REFUSED */ @@ -402,7 +402,7 @@ static void forward_query(int udpfd, union mysockaddr *udpaddr, forward->new_id = get_id(); header->id = ntohs(forward->new_id); - forward->frec_src.encode_bitmap = option_bool(OPT_NO_0x20) ? 0 : rand32(); + forward->frec_src.encode_bitmap = (!option_bool(OPT_NO_0x20) && option_bool(OPT_DO_0x20)) ? rand32() : 0; forward->frec_src.encode_bigmap = NULL; p = (unsigned char *)(header+1); if (!extract_name(header, plen, &p, (char *)&forward->frec_src.encode_bitmap, EXTR_NAME_FLIP, 1)) @@ -3342,12 +3342,12 @@ static struct frec *lookup_frec(char *target, int class, int rrtype, int id, int struct dns_header *header; int compare_mode = EXTR_NAME_COMPARE; - /* Only compare case-sensitive when matching frec to a recieved answer, + /* Only compare case-sensitive when matching frec to a received answer, NOT when looking for a duplicated question. */ if (flags & FREC_ANSWER) { flags &= ~FREC_ANSWER; - if (!option_bool(OPT_NO_0x20)) + if (!option_bool(OPT_NO_0x20) && option_bool(OPT_DO_0x20)) compare_mode = EXTR_NAME_NOCASE; } diff --git a/src/dnsmasq/option.c b/src/dnsmasq/option.c index 9c850a9be2323e6d0bcb7d19e9d96fd204ace437..0a73ac94408470bfe18ada9ab08476c0125817ca 100644 --- a/src/dnsmasq/option.c +++ b/src/dnsmasq/option.c @@ -198,6 +198,7 @@ struct myoption { #define LOPT_DNSSEC_LIMITS 385 #define LOPT_PXE_OPT 386 #define LOPT_NO_ENCODE 387 +#define LOPT_DO_ENCODE 388 #ifdef HAVE_GETOPT_LONG static const struct option opts[] = @@ -253,6 +254,7 @@ static const struct myoption opts[] = { "no-negcache", 0, 0, 'N' }, { "no-round-robin", 0, 0, LOPT_NORR }, { "no-0x20-encode", 0, 0, LOPT_NO_ENCODE }, + { "do-0x20-encode", 0, 0, LOPT_DO_ENCODE }, { "cache-rr", 1, 0, LOPT_CACHE_RR }, { "addn-hosts", 1, 0, 'H' }, { "hostsdir", 1, 0, LOPT_HOST_INOTIFY }, @@ -598,6 +600,7 @@ static struct { { LOPT_QUIET_TFTP, OPT_QUIET_TFTP, NULL, gettext_noop("Do not log routine TFTP."), NULL }, { LOPT_NORR, OPT_NORR, NULL, gettext_noop("Suppress round-robin ordering of DNS records."), NULL }, { LOPT_NO_ENCODE, OPT_NO_0x20, NULL, gettext_noop("Suppress DNS bit 0x20 encoding."), NULL }, + { LOPT_DO_ENCODE, OPT_DO_0x20, NULL, gettext_noop("Enable DNS bit 0x20 encoding."), NULL }, { LOPT_NO_IDENT, OPT_NO_IDENT, NULL, gettext_noop("Do not add CHAOS TXT records."), NULL }, { LOPT_CACHE_RR, ARG_DUP, "<RR-type>", gettext_noop("Cache this DNS resource record type."), NULL }, { LOPT_MAX_PROCS, ARG_ONE, "<integer>", gettext_noop("Maximum number of concurrent tcp connections."), NULL }, diff --git a/src/dnsmasq/rfc1035.c b/src/dnsmasq/rfc1035.c index 13ebe03b8c15d3961689b7e062a1b1782b4f6011..4f89428d699bdfbaead80b293d01502cbbc69810 100644 --- a/src/dnsmasq/rfc1035.c +++ b/src/dnsmasq/rfc1035.c @@ -36,9 +36,9 @@ int extract_name(struct dns_header *header, size_t plen, unsigned char **pp, { unsigned char *cp = (unsigned char *)name, *p = *pp, *p1 = NULL; unsigned int j, l, namelen = 0, hops = 0; - unsigned int bigmap_counter = 0, bigmap_posn = 0, bigmap_size, bitmap; + unsigned int bigmap_counter = 0, bigmap_posn = 0, bigmap_size = parm, bitmap = 0; int retvalue = 1, case_insens = 1, isExtract = 0, flip = 0, extrabytes = (int)parm; - unsigned int *bigmap; + unsigned int *bigmap = (unsigned int *)name; if (func == EXTR_NAME_EXTRACT) isExtract = 1, *cp = 0; @@ -47,9 +47,7 @@ int extract_name(struct dns_header *header, size_t plen, unsigned char **pp, else if (func == EXTR_NAME_FLIP) { flip = 1, extrabytes = 0; - bigmap = (unsigned int *)name; name = NULL; - bigmap_size = parm; } while (1) diff --git a/src/dnsmasq_interface.c b/src/dnsmasq_interface.c index 8310e4d981c4a20e49bfee845eb1d39174cb639e..e87d6d8bcf5c194c1d7078999856d41854b24a2c 100644 --- a/src/dnsmasq_interface.c +++ b/src/dnsmasq_interface.c @@ -1433,6 +1433,33 @@ static bool special_domain(const queriesData *query, const char *domain) return true; } + // RFC 9462: Designated Resolver Domain + // The domain "_dns.resolver.arpa" is reserved for use by DNS resolvers + // that are designated by the network administrator to provide special + // services to the network, e.g., DoH, DoQ, or DoT. + // + // Example (Google's 8.8.8.8): + // ;; QUESTION SECTION: + // ;_dns.resolver.arpa. IN SVCB + // + // ;; ANSWER SECTION: + // _dns.resolver.arpa. 86400 IN SVCB 1 dns.google. alpn="dot" + // _dns.resolver.arpa. 86400 IN SVCB 2 dns.google. alpn="h2,h3" key7="/dns-query{?dns}" + // + // RFC 9462, Section 4 says: + // + // If the recursive resolver that receives this query has no Designated + // Resolvers, it SHOULD return NODATA for queries to the "resolver.arpa" + // zone, to provide a consistent and accurate signal to clients that it + // does not have a Designated Resolver. + if(config.dns.specialDomains.designatedResolver.v.b && + strlen(domain) > 13 && strcasecmp(&domain[strlen(domain) - 13], "resolver.arpa") == 0) + { + blockingreason = "Designated Resolver domain"; + force_next_DNS_reply = REPLY_NODATA; + return true; + } + return false; } diff --git a/src/files.c b/src/files.c index c482bfd75ead3abd0044671921cccf67bbfb96ac..5b1d99174809571758ac694f34b3d7b7d2d6f003 100644 --- a/src/files.c +++ b/src/files.c @@ -556,17 +556,17 @@ void rotate_files(const char *path, char **first_file) } // Credits: https://stackoverflow.com/a/55410469 -int parse_line(char *line, char **key, char **value) +bool parse_line(char *line, char **key, char **value) { char *ptr = strchr(line, '='); if (ptr == NULL) - return -1; + return false; *ptr++ = '\0'; *key = trim(line); *value = trim(ptr); - return 0; + return true; } // Get symlink target diff --git a/src/files.h b/src/files.h index 4b13d44c415293932f2e7635895ff902a804e27a..b5f160199b74a694b9e1f0f257e80b36aeacd75e 100644 --- a/src/files.h +++ b/src/files.h @@ -39,7 +39,7 @@ bool files_different(const char *pathA, const char *pathB, unsigned int from); bool sha256sum(const char *path, uint8_t checksum[SHA256_DIGEST_SIZE], const bool skip_end); enum verify_result verify_FTL(bool verbose); -int parse_line(char *line, char **key, char **value); +bool parse_line(char *line, char **key, char **value); char *get_hwmon_target(const char *path) __attribute__((malloc)); diff --git a/src/gc.c b/src/gc.c index 280752b1cd482d91be8112ef141acbc0fc59c601..f340fb96593d41a8c140aec90db2da3a87492dba 100644 --- a/src/gc.c +++ b/src/gc.c @@ -394,16 +394,23 @@ void runGC(const time_t now, time_t *lastGCrun, const bool flush) if(query->timestamp > mintime) break; + // Check if this query is blocked + const bool blocked = is_blocked(query->status); + // Adjust client counter (total and overTime) const int timeidx = getOverTimeID(query->timestamp); clientsData *client = getClient(query->clientID, true); if(client != NULL) - change_clientcount(client, -1, 0, timeidx, -1); + change_clientcount(client, -1, blocked ? -1 : 0, timeidx, -1); // Adjust domain counter (no overTime information) domainsData *domain = getDomain(query->domainID, true); if(domain != NULL) + { domain->count--; + if(blocked) + domain->blockedcount--; + } // Adjust upstream counter (no overTime information) if(query->upstreamID > -1) @@ -414,46 +421,6 @@ void runGC(const time_t now, time_t *lastGCrun, const bool flush) upstream->count--; } - // Change other counters according to status of this query - switch(query->status) - { - case QUERY_UNKNOWN: - // Unknown (?) - break; - case QUERY_FORWARDED: // (fall through) - case QUERY_RETRIED: // (fall through) - case QUERY_RETRIED_DNSSEC: - // Forwarded to an upstream DNS server - break; - case QUERY_CACHE: - case QUERY_CACHE_STALE: - // Answered from local cache _or_ local config - break; - case QUERY_GRAVITY: // Blocked by Pi-hole's blocking lists (fall through) - case QUERY_DENYLIST: // Exact blocked (fall through) - case QUERY_REGEX: // Regex blocked (fall through) - case QUERY_EXTERNAL_BLOCKED_IP: // Blocked by upstream provider (fall through) - case QUERY_EXTERNAL_BLOCKED_NXRA: // Blocked by upstream provider (fall through) - case QUERY_EXTERNAL_BLOCKED_NULL: // Blocked by upstream provider (fall through) - case QUERY_EXTERNAL_BLOCKED_EDE15: // Blocked by upstream provider (fall through) - case QUERY_GRAVITY_CNAME: // Gravity domain in CNAME chain (fall through) - case QUERY_REGEX_CNAME: // Regex denied domain in CNAME chain (fall through) - case QUERY_DENYLIST_CNAME: // Exactly denied domain in CNAME chain (fall through) - case QUERY_DBBUSY: // Blocked because gravity database was busy - case QUERY_SPECIAL_DOMAIN: // Blocked by special domain handling - overTime[timeidx].blocked--; - if(domain != NULL) - domain->blockedcount--; - if(client != NULL) - change_clientcount(client, 0, -1, -1, 0); - break; - case QUERY_IN_PROGRESS: // fall through - case QUERY_STATUS_MAX: // fall through - default: - // Don't have to do anything here - break; - } - // Update reply counters counters->reply[query->reply]--; log_debug(DEBUG_STATUS, "reply type %u removed (GC), ID = %d, new count = %u", query->reply, query->id, counters->reply[query->reply]); diff --git a/src/lua/ftl_lua.c b/src/lua/ftl_lua.c index 422c0a88b19fa3b8f63a0bc561cb5fb5bc4d47eb..2f82563f958f8e6188231f46c471b8776009ae68 100644 --- a/src/lua/ftl_lua.c +++ b/src/lua/ftl_lua.c @@ -23,6 +23,8 @@ #include "datastructure.h" #include "api/api.h" #include "scripts/scripts.h" +// get_prefix_webhome(), get_api_uri() +#include "webserver/webserver.h" // prototype for luaopen_pihole() #include "lualib.h" @@ -122,14 +124,24 @@ static void get_abspath(char abs_filename[1024], char rel_filename[1024], const strncpy(abs_filename, config.webserver.paths.webroot.v.s, abs_filename_len); abs_filename_len -= strlen(config.webserver.paths.webroot.v.s); } + + // Add prefix to rel_filename if applicable + if(rel_filename != NULL && config.webserver.paths.prefix.v.s[0] != '\0') + { + strncpy(rel_filename, config.webserver.paths.prefix.v.s, rel_filename_len); + rel_filename_len -= strlen(config.webserver.paths.prefix.v.s); + } + + // Add webhome to abs_filename and rel_filename if applicable if(config.webserver.paths.webhome.v.s != NULL) { strncat(abs_filename, config.webserver.paths.webhome.v.s, abs_filename_len); abs_filename_len -= strlen(config.webserver.paths.webhome.v.s); + // Add webhome to rel_filename if(rel_filename != NULL) { - strncpy(rel_filename, config.webserver.paths.webhome.v.s, rel_filename_len); + strncat(rel_filename, config.webserver.paths.webhome.v.s, rel_filename_len); rel_filename_len -= strlen(config.webserver.paths.webhome.v.s); } } @@ -171,6 +183,10 @@ static int pihole_fileversion(lua_State *L) { return 1; // number of results } + // Cast to long long to avoid warnings on 32-bit systems + log_debug(DEBUG_API, "File \"%s\" -> \"%s\" last modified at %lld", + abspath, relpath, (long long)filestat.st_mtime); + // Return filename + modification time lua_pushfstring(L, "%s?v=%d", relpath, filestat.st_mtime); return 1; // number of results @@ -205,7 +221,7 @@ static int pihole_webtheme(lua_State *L) { // pihole.webhome() static int pihole_webhome(lua_State *L) { // Get name of currently set webhome - lua_pushstring(L, config.webserver.paths.webhome.v.s); + lua_pushstring(L, get_prefix_webhome()); return 1; // number of results } @@ -240,6 +256,42 @@ static int pihole_needLogin(lua_State *L) { return 1; // number of results } +// pihole.api_url() +static int pihole_api_url(lua_State *L) { + // Return API URL + lua_pushstring(L, get_api_uri()); + + return 1; // number of results +} + +// pihole.format_path() +static int pihole_format_path(lua_State *L) { + // Get current page (first argument to LUA function) + const char *page = luaL_checkstring(L, 1); + + // Strip leading webhome from page (if it exists) + if (config.webserver.paths.webhome.v.s != NULL) + { + const size_t webhome_len = strlen(config.webserver.paths.webhome.v.s); + if (strncmp(page, config.webserver.paths.webhome.v.s, webhome_len) == 0) + page += webhome_len; + } + + // Convert all / to - + for (char *p = (char *)page; *p != '\0'; p++) + if (*p == '/') + *p = '-'; + + // Substitute "index" for empty string (dashboard landing page) + if (page[0] == '\0') + page = "index"; + + // Return the formatted page string + lua_pushstring(L, page); + + return 1; // number of results +} + static const luaL_Reg piholelib[] = { {"ftl_version", pihole_ftl_version}, {"hostname", pihole_hostname}, @@ -249,6 +301,8 @@ static const luaL_Reg piholelib[] = { {"include", pihole_include}, {"boxedlayout", pihole_boxedlayout}, {"needLogin", pihole_needLogin}, + {"api_url", pihole_api_url}, + {"format_path", pihole_format_path}, {NULL, NULL} }; diff --git a/src/main.c b/src/main.c index a62810d75c450dd307661002a8f5df0d63c5e3f6..4b50de8e21560ad7a43479f7f9c550353903eafe 100644 --- a/src/main.c +++ b/src/main.c @@ -149,12 +149,9 @@ int main (int argc, char *argv[]) // be terminating immediately sleepms(250); - // Save new queries to database (if database is used) - if(config.database.maxDBdays.v.ui > 0) - { - export_queries_to_disk(true); - log_info("Finished final database update"); - } + // Save new queries to database + export_queries_to_disk(true); + log_info("Finished final database update"); cleanup(exit_code); diff --git a/src/ntp/client.c b/src/ntp/client.c index 7f6113825ed1618e5b9472ff14355339e0dae7f4..159b5b62561164bf2abbe0e03b4de7dc6f6b9b5f 100644 --- a/src/ntp/client.c +++ b/src/ntp/client.c @@ -344,6 +344,21 @@ static bool reply(const int fd, const char *server, struct addrinfo *saddr, stru return false; } + // Check and increment stratum + // Stratum 16 indicates unsynchronised source, and strata 0 // and > 16 + // are reserved. (RFC 5905 fig 11) + // Primary servers are assigned stratum one; secondary servers at each + // lower level are assigned stratum numbers one greater than the + // preceding level. (RFC 5905 s3) + + if (buf[1] < 1 || buf[1] > 15) + { + log_warn("Received NTP reply has invalid or unsynchronised stratum, ignoring"); + return false; + } + + ntp_stratum = buf[1] + 1; + // Calculate delay and offset const double T1 = ntp->org / FRAC; const double T2 = rec / FRAC; diff --git a/src/ntp/ntp.h b/src/ntp/ntp.h index 7adbe8adcd693d0dc8dc5a18467537f74038b681..73dffc96189a7fded343f76dd75bc5f7e587e6a3 100644 --- a/src/ntp/ntp.h +++ b/src/ntp/ntp.h @@ -71,6 +71,7 @@ bool ntp_sync_rtc(void); extern uint64_t ntp_last_sync; extern uint32_t ntp_root_delay; extern uint32_t ntp_root_dispersion; +extern uint8_t ntp_stratum; #endif // NTP_H diff --git a/src/ntp/server.c b/src/ntp/server.c index f2db8188e802d634866a66127406a31f40c35bf2..20bd434edf5884ea9edec9fd874898d525620098 100644 --- a/src/ntp/server.c +++ b/src/ntp/server.c @@ -48,6 +48,7 @@ uint64_t ntp_last_sync = 0u; uint32_t ntp_root_delay = 0u; uint32_t ntp_root_dispersion = 0u; +uint8_t ntp_stratum = 0u; // RFC 5905 Appendix A.4: Kernel System Clock Interface uint64_t gettime64(void) @@ -87,10 +88,8 @@ static bool ntp_reply(const int socket_fd, const struct sockaddr *saddr_p, const // 4 and set mode = 4 ("server") send_buf[0] = (0x04 << 3) + 0x04; - // Set stratum to "secondary server" as we have derived time via - // external NTP as well. May be set to 1 if we want to be a primary - // server (synchronized by a hardware clock with GPS, etc.) - send_buf[1] = 0x02; + // Set stratum (one greater than upstream server) + send_buf[1] = ntp_stratum; // Copy Poll value from client send_buf[2] = recv_buf[2]; diff --git a/src/overTime.c b/src/overTime.c index 5c327f4925de0c637c891643163b52b911af3e59..63ef489e23448048030c592bec3fc8ddea002909 100644 --- a/src/overTime.c +++ b/src/overTime.c @@ -175,7 +175,8 @@ void moveOverTimeMemory(const time_t mintime) (unsigned long)oldestOverTimeIS, (unsigned long)oldestOverTimeSHOULD, moveOverTime); // Check if the move over amount is valid. This prevents errors if the - // function is called before GC is necessary. + // function is called before GC is necessary. Also return if there is + // nothing to move (moveOverTime == 0). if(!(moveOverTime > 0 && moveOverTime < OVERTIME_SLOTS)) return; diff --git a/src/procps.c b/src/procps.c index febdc45eb3ccc3466ada9ef7b77dc8fb3a7f8bb0..e74c577542a1f7ebb57fc43868441ce6a97105f0 100644 --- a/src/procps.c +++ b/src/procps.c @@ -264,3 +264,98 @@ bool parse_proc_meminfo(struct proc_meminfo *mem) // Return success return true; } + + +/** + * @brief Parses the /proc/stat file to extract CPU statistics. + * + * This function reads the /proc/stat file to gather CPU usage statistics, + * including user, system, idle, iowait, irq, softirq, steal, guest, and guest_nice times. + * It calculates the total CPU time and idle time, and stores these values in the provided + * pointers. + * + * @param total_sum Pointer to store the total CPU time. + * @param idle_sum Pointer to store the idle CPU time. + * @return true if the parsing is successful, false otherwise. + */ +bool parse_proc_stat(unsigned long *total_sum, unsigned long *idle_sum) +{ + FILE *statfile = fopen("/proc/stat", "r"); + if(statfile == NULL) + return false; + + unsigned long user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice; + /* + user (1) Time spent in user mode. (includes guest and guest_nice time) + + nice (2) Time spent in user mode with low priority (nice). + + system (3) Time spent in system mode. + + idle (4) Time spent in the idle task. This value should be USER_HZ times + the second entry in the /proc/uptime pseudo-file. + + iowait (since Linux 2.5.41) + (5) Time waiting for I/O to complete. + + irq (since Linux 2.6.0-test4) + (6) Time servicing interrupts. + + softirq (since Linux 2.6.0-test4) + (7) Time servicing softirqs. + + steal (since Linux 2.6.11) + (8) Stolen time, which is the time spent in other operating systems + when running in a virtualized environment + + guest (since Linux 2.6.24) + (9) Time spent running a virtual CPU for guest operating systems + under the control of the Linux kernel. + + guest_nice (since Linux 2.6.33) + (10) Time spent running a niced guest (virtual CPU for guest operat- + ing systems under the control of the Linux kernel). + */ + + // Read the file until we find the first line starting with "cpu " + char line[256]; + while(fgets(line, sizeof(line), statfile)) + { + if(strncmp(line, "cpu ", 4) == 0) + { + if(sscanf(line, "cpu %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu", + &user, &nice, &system, &idle, + &iowait, &irq, &softirq, &steal, + &guest, &guest_nice) != 10) + { + log_debug(DEBUG_ANY, "Failed to parse CPU line in /proc/stat"); + fclose(statfile); + return false; + } + + break; + } + } + + if (feof(statfile)) { + log_warn("No CPU line found in /proc/stat"); + fclose(statfile); + return false; + } + + fclose(statfile); + + // Guest time is already accounted in usertime + user -= guest; + nice -= guest_nice; + + // Fields existing on kernels >= 2.6 + // (and RHEL's patched kernel 2.4...) + const unsigned long long int sys_all = system + irq + softirq; + const unsigned long long int virtual = guest + guest_nice; + const unsigned long long int busy_sum = user + nice + sys_all + steal + virtual; + *idle_sum = idle + iowait; + *total_sum = busy_sum + *idle_sum; + + return true; +} diff --git a/src/procps.h b/src/procps.h index 71a2a64c495138df6740c6453565adacadb67615..f5f6fae71eb9b41f7bff584f4da2310137150abd 100644 --- a/src/procps.h +++ b/src/procps.h @@ -48,5 +48,6 @@ struct proc_meminfo { bool getProcessMemory(struct proc_mem *mem, const unsigned long total_memory); bool parse_proc_meminfo(struct proc_meminfo *mem); +bool parse_proc_stat(unsigned long *total_sum, unsigned long *idle_sum); #endif // PROCPS_H diff --git a/src/signals.c b/src/signals.c index 92ebb511f43affdeebaeb360dc11b3a7e50fc8cf..e0323300d14d35fc481273830b60b58d66d06e08 100644 --- a/src/signals.c +++ b/src/signals.c @@ -139,6 +139,19 @@ void generate_backtrace(void) #endif } +/** + * @brief Terminates the program due to an error. + * + * This function sets the exit code to indicate failure and raises a SIGTERM + * signal to terminate the main process. It is intended to be called when a + * critical error occurs that requires the program to exit. + */ +static void terminate_error(void) +{ + exit_code = EXIT_FAILURE; + raise(SIGTERM); +} + static void __attribute__((noreturn)) signal_handler(int sig, siginfo_t *si, void *context) { (void)context; @@ -260,15 +273,27 @@ static void __attribute__((noreturn)) signal_handler(int sig, siginfo_t *si, voi log_info("Asking parent pihole-FTL (PID %i) to shut down", (int)mpid); kill(mpid, SIGRTMIN+2); log_info("FTL fork terminated!"); + + // Terminate fork indicating failure + exit(EXIT_FAILURE); + } + else if(gettid() != getpid()) + { + // This is a thread, signal to the main process to shut down + log_info("Shutting down thread..."); + terminate_error(); + + // Exit the thread here, it failed anyway + pthread_exit(NULL); } else { // This is the main process cleanup(EXIT_FAILURE); - } - // Terminate process indicating failure - exit(EXIT_FAILURE); + // Terminate process indicating failure + exit(EXIT_FAILURE); + } } static void SIGRT_handler(int signum, siginfo_t *si, void *context) @@ -301,8 +326,7 @@ static void SIGRT_handler(int signum, siginfo_t *si, void *context) else if(rtsig == 2) { // Terminate FTL indicating failure - exit_code = EXIT_FAILURE; - raise(SIGTERM); + terminate_error(); } else if(rtsig == 3) { diff --git a/src/webserver/json_macros.h b/src/webserver/json_macros.h index c6eb6184eeef404e3f2d88168cd951cefa740daf..ffe25945d54c9d29989f0c019a8ec3f3229df412 100644 --- a/src/webserver/json_macros.h +++ b/src/webserver/json_macros.h @@ -223,7 +223,7 @@ }) #define JSON_SEND_OBJECT_CODE(object, code)({ \ - if((code) < 100 || (code) == 204 || (code) == 304) \ + if((code) < 200 || (code) == 204 || (code) == 304) \ { \ /* HTTP codes 1xx, 204 and 304 must not have a body */ \ send_http_code(api, NULL, code, ""); \ diff --git a/src/webserver/lua_web.c b/src/webserver/lua_web.c index 508f48ec3ab6038cc8529668c7b3fb09cf5490f8..0b18d0e1d782818d8b7f24174c01899dc026c877 100644 --- a/src/webserver/lua_web.c +++ b/src/webserver/lua_web.c @@ -18,41 +18,14 @@ #include "config/config.h" // log_web() #include "log.h" - // directory_exists() #include "files.h" static char *login_uri = NULL, *admin_api_uri = NULL; -void allocate_lua(void) +void allocate_lua(char *login_uri_in, char *admin_api_uri_in) { - // Build login URI string (webhome + login) - // Append "login" to webhome string - const size_t login_uri_len = strlen(config.webserver.paths.webhome.v.s); - login_uri = calloc(login_uri_len + 6, sizeof(char)); - memcpy(login_uri, config.webserver.paths.webhome.v.s, login_uri_len); - strcpy(login_uri + login_uri_len, "login"); - login_uri[login_uri_len + 5u] = '\0'; - log_debug(DEBUG_API, "Login URI: %s", login_uri); - - // Build "wrong" API URI string (webhome + api) - // Append "api" to webhome string - const size_t admin_api_uri_len = strlen(config.webserver.paths.webhome.v.s); - admin_api_uri = calloc(admin_api_uri_len + 4, sizeof(char)); - memcpy(admin_api_uri, config.webserver.paths.webhome.v.s, admin_api_uri_len); - strcpy(admin_api_uri + admin_api_uri_len, "api"); - admin_api_uri[admin_api_uri_len + 3u] = '\0'; - log_debug(DEBUG_API, "Admin API URI: %s", admin_api_uri); -} - -void free_lua(void) -{ - // Free login_uri - if(login_uri != NULL) - free(login_uri); - - // Free admin_api_uri - if(admin_api_uri != NULL) - free(admin_api_uri); + login_uri = login_uri_in; + admin_api_uri = admin_api_uri_in; } void init_lua(const struct mg_connection *conn, void *L, unsigned context_flags) @@ -80,13 +53,25 @@ int request_handler(struct mg_connection *conn, void *cbdata) return 0; } + // Check if we are allowed to serve this directory by checking the + // configuration setting webserver.serve_all and the requested URI to + // start with something else than config.webserver.paths.webhome. If so, + // send error 404 + if(!config.webserver.serve_all.v.b && + strncmp(req_info->local_uri_raw, config.webserver.paths.webhome.v.s, strlen(config.webserver.paths.webhome.v.s)) != 0) + { + log_debug(DEBUG_WEBSERVER, "Not serving %s, returning 404", req_info->local_uri_raw); + mg_send_http_error(conn, 404, "Not Found"); + return 404; + } + // Build minimal api struct to check authentication struct ftl_conn api = { 0 }; api.conn = conn; api.request = req_info; api.now = double_time(); - // Check if the request is for the API under /admin/api + // Check if the request is for the API under <webhome>api // (it is posted at /api) if(strncmp(req_info->local_uri_raw, admin_api_uri, strlen(admin_api_uri)) == 0) { @@ -127,10 +112,10 @@ int request_handler(struct mg_connection *conn, void *cbdata) if(!authorized) { // User is not authenticated, redirect to login page - log_web("Authentication required, redirecting to %slogin", - config.webserver.paths.webhome.v.s); - mg_printf(conn, "HTTP/1.1 302 Found\r\nLocation: %slogin\r\n\r\n", - config.webserver.paths.webhome.v.s); + log_web("Authentication required, redirecting to %s%slogin", + config.webserver.paths.prefix.v.s, config.webserver.paths.webhome.v.s); + mg_printf(conn, "HTTP/1.1 302 Found\r\nLocation: %s%slogin\r\n\r\n", + config.webserver.paths.prefix.v.s, config.webserver.paths.webhome.v.s); return 302; } } @@ -141,8 +126,10 @@ int request_handler(struct mg_connection *conn, void *cbdata) if(authorized) { // User is already authenticated, redirect to index page - log_web("User is already authenticated, redirecting to %s", config.webserver.paths.webhome.v.s); - mg_printf(conn, "HTTP/1.1 302 Found\r\nLocation: %s\r\n\r\n", config.webserver.paths.webhome.v.s); + log_web("User is already authenticated, redirecting to %s%s", + config.webserver.paths.prefix.v.s, config.webserver.paths.webhome.v.s); + mg_printf(conn, "HTTP/1.1 302 Found\r\nLocation: %s%s\r\n\r\n", + config.webserver.paths.prefix.v.s, config.webserver.paths.webhome.v.s); return 302; } } diff --git a/src/webserver/lua_web.h b/src/webserver/lua_web.h index 5c0b2fe63609fcdd2525080a20894cff908f9706..b5ebf67a88bdd57c306f1629dcda9b9a41d5bc9b 100644 --- a/src/webserver/lua_web.h +++ b/src/webserver/lua_web.h @@ -13,8 +13,7 @@ // definition of struct mg_connection #include "http-common.h" -void allocate_lua(void); -void free_lua(void); +void allocate_lua(char *login_uri_in, char *admin_api_uri_in); void init_lua(const struct mg_connection *conn, void *L, unsigned context_flags); int request_handler(struct mg_connection *conn, void *cbdata); diff --git a/src/webserver/webserver.c b/src/webserver/webserver.c index 047a526d6b4c72c303474d049d70a1011fcc8fda..a40e11b7fc586b8dc26142493a16469cd9b36a7e 100644 --- a/src/webserver/webserver.c +++ b/src/webserver/webserver.c @@ -34,10 +34,79 @@ // Server context handle static struct mg_context *ctx = NULL; static char *error_pages = NULL; +static char *prefix_webhome = NULL; +static char *api_uri = NULL; +static char *admin_api_uri = NULL; +static char *login_uri = NULL; // Private prototypes static char *append_to_path(char *path, const char *append); +/** + * @brief Constructs various web paths used by the webserver. + * + * @return true if all paths are successfully constructed and allocated, false otherwise. + */ +static bool build_webpaths(void) +{ + // Construct error_pages path + error_pages = append_to_path(config.webserver.paths.webroot.v.s, config.webserver.paths.webhome.v.s); + log_debug(DEBUG_API, "Error pages path: %s", error_pages); + if(error_pages == NULL) + { + log_err("Failed to allocate memory for error_pages path!"); + return false; + } + + // Construct prefix_webhome path + prefix_webhome = append_to_path(config.webserver.paths.prefix.v.s, config.webserver.paths.webhome.v.s); + log_debug(DEBUG_API, "Prefix webhome path: %s", prefix_webhome); + if(prefix_webhome == NULL) + { + log_err("Failed to allocate memory for prefix_webhome path!"); + return false; + } + + // Construct api_url path + api_uri = append_to_path(config.webserver.paths.prefix.v.s, "/api"); + log_debug(DEBUG_API, "API URI path: %s", api_uri); + if(api_uri == NULL) + { + log_err("Failed to allocate memory for api_uri path!"); + return false; + } + + // Construct admin_api_uri path + admin_api_uri = append_to_path(prefix_webhome, "api"); + log_debug(DEBUG_API, "Admin API URI path: %s", admin_api_uri); + if(admin_api_uri == NULL) + { + log_err("Failed to allocate memory for admin_api_uri path!"); + return false; + } + + // Construct login_uri path + login_uri = append_to_path(config.webserver.paths.webhome.v.s, "login"); + log_debug(DEBUG_API, "Login URI path: %s", login_uri); + if(login_uri == NULL) + { + log_err("Failed to allocate memory for login_uri path!"); + return false; + } + + return true; +} + +char * __attribute__((pure)) get_prefix_webhome(void) +{ + return prefix_webhome; +} + +char * __attribute__((pure)) get_api_uri(void) +{ + return api_uri; +} + static int redirect_root_handler(struct mg_connection *conn, void *input) { // Get requested host @@ -94,13 +163,14 @@ static int redirect_root_handler(struct mg_connection *conn, void *input) if(strcmp(uri, "/") == 0) { log_debug(DEBUG_API, "Redirecting / --308--> %s", - config.webserver.paths.webhome.v.s); - mg_send_http_redirect(conn, config.webserver.paths.webhome.v.s, 308); + prefix_webhome); + mg_send_http_redirect(conn, prefix_webhome, 308); return 1; } } // else: Not redirecting + log_debug(DEBUG_API, "Not redirecting %s", uri); return 0; } @@ -113,11 +183,11 @@ static int redirect_admin_handler(struct mg_connection *conn, void *input) const char *uri = request->local_uri_raw; log_debug(DEBUG_API, "Redirecting %s --308--> %s", - uri, config.webserver.paths.webhome.v.s); + uri, prefix_webhome); } - // 308 Permanent Redirect from /admin -> /admin/ - mg_send_http_redirect(conn, config.webserver.paths.webhome.v.s, 308); + // 308 Permanent Redirect from [prefix]<webhome without trailing slash> -> [prefix]<webhome> + mg_send_http_redirect(conn, prefix_webhome, 308); return 1; } @@ -127,6 +197,20 @@ static int redirect_lp_handler(struct mg_connection *conn, void *input) const struct mg_request_info *request = mg_get_request_info(conn); const char *uri = request->local_uri_raw; const size_t uri_len = strlen(uri); + + // Check if we are allowed to serve this directory by checking the + // configuration setting webserver.serve_all and the requested URI to + // start with something else than config.webserver.paths.webhome. If so, + // send error 404 + if(!config.webserver.serve_all.v.b && + strncmp(uri, config.webserver.paths.webhome.v.s, strlen(config.webserver.paths.webhome.v.s)) != 0) + { + log_debug(DEBUG_WEBSERVER, "Not serving %s, returning 404", uri); + mg_send_http_error(conn, 404, "Not Found"); + return 404; + } + + // Get query string const char *query_string = request->query_string; const size_t query_len = query_string != NULL ? strlen(query_string) : 0; @@ -142,7 +226,7 @@ static int redirect_lp_handler(struct mg_connection *conn, void *input) // Copy everything from before the ".lp" to the new URI to effectively // remove it - strncpy(new_uri, uri, uri_len - 3); + strncat(new_uri, uri, uri_len - 3); // Append query string to the new URI if present if(query_len > 0) @@ -347,9 +431,10 @@ unsigned short get_api_string(char **buf, const bool domain) // We add this at buffer + 1 because the first byte is the // length of the string, which we don't know yet char *api_str = calloc(MAX_URL_LEN, sizeof(char)); - const ssize_t this_len = snprintf(api_str, MAX_URL_LEN, "http%s://%s:%d/api/", + const ssize_t this_len = snprintf(api_str, MAX_URL_LEN, "http%s://%s:%d%s/api/", server_ports[i].is_secure ? "s" : "", - addr, server_ports[i].port); + addr, server_ports[i].port, + config.webserver.paths.prefix.v.s); // Check if snprintf() failed if(this_len < 0) { @@ -404,16 +489,19 @@ void http_init(void) // Get maximum number of threads for webserver char num_threads[16] = { 0 }; - if(config.webserver.threads.v.ui == 0) + unsigned int threads = config.webserver.threads.v.ui; + if(threads == 0) { // For compatibility with older versions, set the number of // threads to the default value (50) if it was 0. Before Pi-hole // FTL v6.0.4, the number of threads was computed in dependence // of the number of CPUs available. This is no longer the case. - config.webserver.threads.v.ui = 50; + threads = 50; } - snprintf(num_threads, sizeof(num_threads), "%u", config.webserver.threads.v.ui); + snprintf(num_threads, sizeof(num_threads), "%u", threads); + + // Ensure null termination for safety num_threads[sizeof(num_threads) - 1] = '\0'; /* Initialize the library */ @@ -432,16 +520,18 @@ void http_init(void) return; } - // Construct error_pages path - error_pages = append_to_path(config.webserver.paths.webroot.v.s, config.webserver.paths.webhome.v.s); - if(error_pages == NULL) + if(!build_webpaths()) { - log_err("Failed to allocate memory for error_pages path!"); + log_err("Failed to build web paths, web interface will not be available!"); return; } // Construct additional headers char *webheaders = strdup(""); + if (webheaders == NULL) { + log_err("Failed to allocate memory for webheaders!"); + return; + } cJSON *header; cJSON_ArrayForEach(header, config.webserver.headers.v.json) { @@ -475,6 +565,8 @@ void http_init(void) "authentication_domain", config.webserver.domain.v.s, "additional_header", webheaders, "index_files", "index.html,index.htm,index.lp", + "enable_keep_alive", "yes", + "keep_alive_timeout_ms", "5000", NULL, NULL, NULL, NULL, // Leave slots for access control list (ACL) and TLS configuration at the end NULL @@ -574,24 +666,28 @@ void http_init(void) return; } - // Register API handler + // Get server ports + get_server_ports(); + + // Register API handler, use "/api" even when a prefix is defined as the + // prefix should be stripped away by the reverse proxy mg_set_request_handler(ctx, "/api", api_handler, NULL); - // Register / -> /admin/ redirect handler mg_set_request_handler(ctx, "/$", redirect_root_handler, NULL); - // Register /admin -> /admin/ redirect handler - if(strlen(config.webserver.paths.webhome.v.s) > 1) + // Register [prefix]<webhome without trailing slash> -> [<prefix>]<webhome> redirect handler + if(strlen(config.webserver.paths.webhome.v.s) > 1 && config.webserver.paths.webhome.v.s[strlen(config.webserver.paths.webhome.v.s)-1] == '/') { - // Construct webhome_matcher path - char *webhome_matcher = NULL; - webhome_matcher = strdup(config.webserver.paths.webhome.v.s); - webhome_matcher[strlen(webhome_matcher)-1] = '$'; + // Replace trailing slash with end-of-string marker for matcher + char *prefix_webhome_matcher = strdup(prefix_webhome); + prefix_webhome_matcher[strlen(prefix_webhome_matcher)-1] = '$'; + log_debug(DEBUG_API, "Redirecting %s --308--> %s", - webhome_matcher, config.webserver.paths.webhome.v.s); - // String is internally duplicated during request configuration - mg_set_request_handler(ctx, webhome_matcher, redirect_admin_handler, NULL); - free(webhome_matcher); + prefix_webhome, config.webserver.paths.webhome.v.s); + mg_set_request_handler(ctx, prefix_webhome_matcher, redirect_admin_handler, NULL); + // prefix_webhome_matcher is internally duplicated during + // request configuration so it can be freed here + free(prefix_webhome_matcher); } // Register **.lp -> ** redirect handler @@ -601,7 +697,7 @@ void http_init(void) mg_set_request_handler(ctx, "**", request_handler, NULL); // Prepare prerequisites for Lua - allocate_lua(); + allocate_lua(login_uri, admin_api_uri); // Restore sessions from database init_api(); @@ -699,16 +795,26 @@ void http_terminate(void) /* Un-initialize the library */ mg_exit_library(); - // Free Lua-related resources - free_lua(); - // Remove CLI password remove_cli_password(); // Free error_pages path if(error_pages != NULL) - { free(error_pages); - error_pages = NULL; - } + + // Free webhome_matcher path + if(prefix_webhome != NULL) + free(prefix_webhome); + + // Free api_uri path + if(api_uri != NULL) + free(api_uri); + + // Free admin_api_uri path + if(admin_api_uri != NULL) + free(admin_api_uri); + + // Free login_uri path + if(login_uri != NULL) + free(login_uri); } diff --git a/src/webserver/webserver.h b/src/webserver/webserver.h index 4a671f293504026f62b57f111441f66dbba21840..dcadf021b20cba555265ee2d32c4fa2be376b102 100644 --- a/src/webserver/webserver.h +++ b/src/webserver/webserver.h @@ -22,5 +22,7 @@ void http_terminate(void); in_port_t get_https_port(void) __attribute__((pure)); unsigned short get_api_string(char **buf, const bool domain); +char *get_prefix_webhome(void) __attribute__((pure)); +char *get_api_uri(void) __attribute__((pure)); #endif // WEBSERVER_H diff --git a/test/pihole.toml b/test/pihole.toml index 9d3a48eff8f555e34d2c310350b31bf0fe166210..376c105a121051bb749e9b1d7dc3169679ec37a1 100644 --- a/test/pihole.toml +++ b/test/pihole.toml @@ -1,7 +1,7 @@ -# Pi-hole configuration file (v6.0.3-14-gbb1ff28a-dirty) +# Pi-hole configuration file (v6.0.4-20-g70547d28-dirty) # Encoding: UTF-8 # This file is managed by pihole-FTL -# Last updated on 2025-03-03 11:05:52 UTC +# Last updated on 2025-03-07 18:48:00 UTC [dns] # Array of upstream DNS servers used by Pi-hole @@ -319,6 +319,11 @@ # https://developer.apple.com/support/prepare-your-network-for-icloud-private-relay iCloudPrivateRelay = true + # Should Pi-hole always reply with NODATA to all queries to zone resolver.arpa to + # prevent devices from bypassing Pi-hole using Discovery of Designated Resolvers? This + # is based on recommendations at the end of RFC 9462, section 4. + designatedResolver = true + [dns.reply.host] # Use a specific IPv4 address for the Pi-hole host? By default, FTL determines the # address of the interface a query arrived on and uses this address for replying to A @@ -678,6 +683,9 @@ # Additional HTTP headers added to the web server responses. # The headers are added to all responses, including those for the API. # Note about the default additional headers: + # - X-DNS-Prefetch-Control: off: Usually browsers proactively perform domain name + # resolution on links that the user may choose to follow. We disable DNS prefetching + # here. # - Content-Security-Policy: [...] 'unsafe-inline' is both required by Chart.js # styling some elements directly, and index.html containing some inlined Javascript # code. @@ -699,6 +707,7 @@ # Possible values are: # array of HTTP headers headers = [ + "X-DNS-Prefetch-Control: off", "Content-Security-Policy: default-src 'self' 'unsafe-inline';", "X-Frame-Options: DENY", "X-XSS-Protection: 0", @@ -706,6 +715,11 @@ "Referrer-Policy: strict-origin-when-cross-origin" ] + # Should the web server serve all files in webserver.paths.webroot directory? If + # disabled, only files within the path defined through webserver.paths.webhome and + # /api will be served. + serve_all = false + [webserver.session] # Session timeout in seconds. If a session is inactive for more than this time, it will # be terminated. Sessions are continuously refreshed by the web interface, preventing @@ -749,6 +763,22 @@ # <valid subpath>, both slashes are needed! webhome = "/admin/" + # Prefix where the web interface is served + # This is useful when you are using a reverse proxy serving the web interface, e.g., + # at http://<ip>/pihole/admin/ instead of http://<ip>/admin/. In this example, the + # prefix would be "/pihole". Note that the prefix has to be stripped away by the + # reverse proxy, e.g., for traefik: + # - traefik.http.routers.pihole.rule=PathPrefix(`/pihole`) + # - traefik.http.middlewares.piholehttp.stripprefix.prefixes=/pihole + # The prefix should start with a slash. If you don't use a prefix, leave this field + # empty. Setting this field to an incorrect value may result in the web interface not + # being accessible. + # Don't use this setting if you are not using a reverse proxy! + # + # Possible values are: + # valid URL prefix or empty + prefix = "" + [webserver.interface] # Should the web interface use the boxed layout? boxed = true @@ -1178,7 +1208,7 @@ all = true ### CHANGED, default = false # Configuration statistics: -# 153 total entries out of which 96 entries are default +# 156 total entries out of which 99 entries are default # --> 57 entries are modified # 3 entries are forced through environment: # - misc.nice diff --git a/test/test_suite.bats b/test/test_suite.bats index 24130b188185b12da096696370f3c29a874b5ce3..685508c33e4558027401ac08aa2fea86afb818a4 100644 --- a/test/test_suite.bats +++ b/test/test_suite.bats @@ -1771,8 +1771,19 @@ [[ ${lines[0]} == "0" ]] } -# This test should run before a password it set +@test "Lua server page outside /admin is not served by default" { + run bash -c 'curl -sI 127.0.0.1/broken_lua' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == "HTTP/1.1 404 Not Found"* ]] +} + +# This test should run before a password is set @test "Lua server page is generating proper backtrace" { + # Enable serving of Lua pages outside /admin + run bash -c './pihole-FTL --config webserver.serve_all true' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == 'true' ]] + run bash -c 'sleep 1' # Run a page with a syntax error run bash -c 'curl -s 127.0.0.1/broken_lua' printf "%s\n" "${lines[@]}"