Tests: Set api.pwhash and dns.blocking.mode using PATCH /api/config

Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
DL6ER 2023-01-09 19:08:53 +01:00
parent 4abba45878
commit 140a365806
No known key found for this signature in database
GPG Key ID: 00135ACBD90B28DD
15 changed files with 676 additions and 3553 deletions

3887
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,16 +15,11 @@
"url": "https://github.com/pi-hole/FTL/issues"
},
"scripts": {
"set-threshold": "echo '{\"warnings\": 0}' > .thresholdrc",
"delete-threshold": "rm .thresholdrc",
"lint-openapi": "lint-openapi -p -c test/api/ibm-openapi-validator.rc src/api/docs/content/specs/main.yaml 2> /dev/null",
"openapi-validator": "npm run set-threshold && npm run lint-openapi && npm run delete-threshold",
"openapi-enforcer": "node test/api/openapi-enforcer.js",
"validate-examples": "openapi-examples-validator src/api/docs/content/specs/main.yaml",
"test": "npm run openapi-enforcer && npm run openapi-validator && npm run validate-examples"
"test": "npm run openapi-enforcer && npm run validate-examples"
},
"devDependencies": {
"ibm-openapi-validator": "^0.34.1",
"openapi-enforcer": "^1.13.1",
"openapi-examples-validator": "^4.2.1"
}

View File

@ -47,6 +47,22 @@ components:
$ref: 'config.yaml#/components/examples/config_two'
config:
$ref: 'config.yaml#/components/examples/config'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: 'config.yaml#/components/schemas/config'
examples:
config:
$ref: 'config.yaml#/components/examples/config'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: 'common.yaml#/components/errors/unauthorized'
schemas:
config:
type: object

View File

@ -346,7 +346,7 @@ void initConfig(void)
config.api.pwhash.h = "API password hash";
config.api.pwhash.a = "<valid Pi-hole password hash>";
config.api.pwhash.t = CONF_STRING;
config.api.pwhash.d.s = NULL;
config.api.pwhash.d.s = (char*)"";
config.api.exclude_clients.k = "api.exclude_clients";
config.api.exclude_clients.h = "Array of clients to be excluded from certain API responses";
@ -630,6 +630,9 @@ void initConfig(void)
// JSON objects really need to be duplicated as the config
// structure stores only a pointer to memory somewhere else
conf_item->v.json = cJSON_Duplicate(conf_item->d.json, true);
else if(conf_item->t == CONF_STRING_ALLOCATED)
// Allocated string: Make our own copy
conf_item->v.s = strdup(conf_item->d.s);
else
// Ordinary value: Simply copy the union over
memcpy(&conf_item->v, &conf_item->d, sizeof(conf_item->d));

View File

@ -46,21 +46,21 @@ bool get_blockingstatus(void) __attribute__((pure));
void set_blockingstatus(bool enabled);
union conf_value {
bool b;
int i;
unsigned int ui;
long l;
unsigned long ul;
char *s;
enum ptr_type ptr_type;
enum busy_reply busy_reply;
enum blocking_mode blocking_mode;
enum refresh_hostnames refresh_hostnames;
enum privacy_level privacy_level;
enum debug_flag debug_flag;
struct in_addr in_addr;
struct in6_addr in6_addr;
cJSON *json;
bool b; // boolean value
int i; // integer value
unsigned int ui; // unsigned int value
long l; // long value
unsigned long ul; // unsigned long value
char *s; // char * value
enum ptr_type ptr_type; // enum ptr_type value
enum busy_reply busy_reply; // enum busy_reply value
enum blocking_mode blocking_mode; // enum blocking_mode value
enum refresh_hostnames refresh_hostnames; // enum refresh_hostnames value
enum privacy_level privacy_level; // enum privacy_level value
enum debug_flag debug_flag; // enum debug_flag value
struct in_addr in_addr; // struct in_addr value
struct in6_addr in6_addr; // struct in6_addr value
cJSON *json; // cJSON * value
};
enum conf_type {

View File

@ -58,6 +58,7 @@ bool readFTLtoml(void)
unsigned int level = config_path_depth(conf_item);
// Parse tree of properties
bool item_available = true;
toml_table_t *table[MAX_CONFIG_PATH_DEPTH] = { 0 };
for(unsigned int j = 0; j < level-1; j++)
{
@ -66,10 +67,15 @@ bool readFTLtoml(void)
if(!table[j])
{
log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST", conf_item->k);
item_available = false;
break;
}
}
// Skip this config item if it does not exist
if(!item_available)
continue;
// Try to parse config item
readTOMLvalue(conf_item, conf_item->p[level-1], table[level-2]);
}

View File

@ -1748,6 +1748,9 @@ void FTL_dnsmasq_reload(void)
if(config.debug.caps.v.b)
check_capabilities();
// Report blocking mode
log_info("Blocking status is %s", config.dns.blocking.active.v.b ? "enabled" : "disabled");
// Set resolver as ready
resolver_ready = true;
}
@ -3298,7 +3301,7 @@ void FTL_dnsmasq_log(const char *payload, const int length)
int check_struct_sizes(void)
{
int result = 0;
result += check_one_struct("struct config", sizeof(struct config), 5832, 4212);
result += check_one_struct("struct config", sizeof(struct config), 6120, 4420);
result += check_one_struct("queriesData", sizeof(queriesData), 72, 64);
result += check_one_struct("upstreamsData", sizeof(upstreamsData), 640, 628);
result += check_one_struct("clientsData", sizeof(clientsData), 672, 652);

View File

@ -33,7 +33,7 @@ static volatile pid_t mpid = -1;
static time_t FTLstarttime = 0;
extern volatile int exit_code;
volatile sig_atomic_t thread_cancellable[THREADS_MAX] = { false };
volatile sig_atomic_t thread_cancellable[THREADS_MAX] = { true };
const char *thread_names[THREADS_MAX] = { "" };
// Return the (null-terminated) name of the calling thread

View File

@ -1,116 +0,0 @@
{
"shared": {
"operations": {
"no_operation_id": "warning",
"operation_id_case_convention": [
"warning",
"lower_snake_case"
],
"no_summary": "warning",
"no_array_responses": "error",
"parameter_order": "warning",
"undefined_tag": "warning",
"unused_tag": "warning",
"operation_id_naming_convention": "off"
},
"pagination": {
"pagination_style": "warning"
},
"parameters": {
"no_parameter_description": "error",
"param_name_case_convention": [
"error",
"lower_snake_case"
],
"invalid_type_format_pair": "error",
"content_type_parameter": "error",
"accept_type_parameter": "error",
"authorization_parameter": "warning",
"required_param_has_default": "warning"
},
"paths": {
"missing_path_parameter": "error",
"duplicate_path_parameter": "warning",
"snake_case_only": "warning",
"paths_case_convention": [
"error",
"lower_snake_case"
]
},
"responses": {
"inline_response_schema": "warning"
},
"security_definitions": {
"unused_security_schemes": "warning",
"unused_security_scopes": "warning"
},
"security": {
"invalid_non_empty_security_array": "error"
},
"schemas": {
"invalid_type_format_pair": "error",
"snake_case_only": "warning",
"no_schema_description": "warning",
"no_property_description": "warning",
"description_mentions_json": "warning",
"array_of_arrays": "warning",
"inconsistent_property_type": [
"warning",
[
"code",
"default",
"type",
"value"
]
],
"property_case_convention": [
"error",
"lower_snake_case"
],
"property_case_collision": "error",
"enum_case_convention": [
"warning",
"lower_snake_case"
],
"undefined_required_properties": "warning"
},
"walker": {
"no_empty_descriptions": "error",
"has_circular_references": "warning",
"$ref_siblings": "warning",
"duplicate_sibling_description": "warning",
"incorrect_ref_pattern": "warning"
}
},
"swagger2": {
"operations": {
"no_consumes_for_put_or_post": "error",
"get_op_has_consumes": "warning",
"no_produces": "warning"
}
},
"oas3": {
"operations": {
"no_request_body_content": "error",
"no_request_body_name": "warning"
},
"parameters": {
"no_in_property": "error",
"invalid_in_property": "error",
"missing_schema_or_content": "error",
"has_schema_and_content": "error"
},
"responses": {
"no_response_codes": "error",
"no_success_response_codes": "warning",
"no_response_body": "warning",
"ibm_status_code_guidelines": "warning"
},
"schemas": {
"json_or_param_binary_string": "warning"
}
},
"spectral": {
"rules": {}
}
}

View File

@ -0,0 +1,7 @@
{
"config": {
"api": {
"pwhash": "183c1b634da0078fcf5b0af84bdcbb3e817708c3f22b329be84165f4bad1ae48"
}
}
}

View File

@ -0,0 +1,9 @@
{
"config": {
"dns": {
"blocking": {
"mode": "IP"
}
}
}
}

View File

@ -45,6 +45,8 @@ class FTLAPI():
# Check if we are already logged in or authentication is not
# required
if response is None:
raise Exception("No response from FTL API")
if 'session' in response and response['session']['valid']:
if 'session' not in response:
raise Exception("FTL returned invalid challenge item")
@ -53,23 +55,33 @@ class FTLAPI():
pwhash = None
if password is None:
# Try to read the password hash from setupVars.conf
# Try to obtain the password hash from pihole-FTL.toml
try:
with open("/etc/pihole/setupVars.conf", "r") as f:
with open("/etc/pihole/pihole-FTL.toml", "r") as f:
# Iterate over all lines
for line in f:
if line.startswith("WEBPASSWORD="):
pwhash = line[12:].strip()
# Find the line with the password hash
if line.startswith(" pwhash = "):
# Remove quotes and whitespace
line = line.split("=")[1].split("\"")
if len(line) > 2:
pwhash = line[1].strip()
break
except Exception as e:
self.errors.append("Exception when reading setupVars.conf: " + str(e))
# Could not read pihole-FTL.toml, throw an error
raise Exception("Could not read pihole-FTL.toml: " + str(e))
if pwhash is None:
# The password hash was not found in pihole-FTL.toml, throw an error
raise Exception("No password hash found in pihole-FTL.toml")
else:
# Generate password hash
pwhash = sha256(password.encode("ascii")).hexdigest()
pwhash = sha256(pwhash.encode("ascii")).hexdigest()
print("Using password hash: \"" + pwhash + "\"")
# Get the challenge from FTL
challenge = challenge["challenge"].encode("ascii")
challenge = response["challenge"].encode("ascii")
response = sha256(challenge + b":" + pwhash.encode("ascii")).hexdigest()
response = self.POST("/api/auth", {"response": response})
if 'session' not in response:

View File

@ -3,7 +3,7 @@
# Do not edit the file while FTL is
# running or your changes may be overwritten
#
# Last update: 2023-01-07 18:28:45
# Last update: 2023-01-09 16:26:00
[dns]
# Should FTL walk CNAME paths?
@ -38,9 +38,13 @@
# TTL for blocked queries [seconds]
blockTTL = 2
# How should FTL reply to blocked queries?
# Possible values are: [ "NULL", "IP-NODATA-AAAA", "IP", "NXDOMAIN" ]
blockingmode = "NULL"
[dns.blocking]
# Should FTL block queries?
active = true
# How should FTL reply to blocked queries?
# Possible values are: [ "NULL", "IP-NODATA-AAAA", "IP", "NXDOMAIN", "NODATA" ]
mode = "NULL"
[dns.specialDomains]
# Should FTL handle use-application-dns.net specifically and always return NXDOMAIN?
@ -118,12 +122,12 @@
[database.network]
# Should FTL anaylze the local ARP cache?
parseARPcache = false ### CHANGED, default = true
parseARPcache = true
# How long should IP addresses be kept in the network_addresses table [days]?
expire = 365
[http]
[api]
# Does local clients need to authenticate to access the API?
localAPIauth = true
@ -133,6 +137,19 @@
# How long should a session be considered valid after login [seconds]?
sessionTimeout = 300
# API password hash
# Possible values are: <valid Pi-hole password hash>
pwhash = ""
# Array of clients to be excluded from certain API responses
# Possible values are: array of IP addresses and/or hostnames, e.g. [ "192.168.2.56", "fe80::341", "localhost" ]
exclude_clients = [ "1.2.3.4" ] ### CHANGED, default = [ ]
# Array of domains to be excluded from certain API responses
# Possible values are: array of IP addresses and/or hostnames, e.g. [ "google.de", "pi-hole.net" ]
exclude_domains = [ ]
[http]
# On which domain is the web interface served?
# Possible values are: <valid domain>
domain = "pi.hole"

View File

@ -46,11 +46,9 @@ rm -rf /etc/pihole/pihole-FTL.db
./pihole-FTL sqlite3 /etc/pihole/pihole-FTL.db < test/pihole-FTL.db.sql
chown pihole:pihole /etc/pihole/pihole-FTL.db
# Prepare setupVars.conf
echo "BLOCKING_ENABLED=true" > /etc/pihole/setupVars.conf
# Prepare pihole-FTL.toml
cp test/pihole-FTL.toml /etc/pihole/pihole-FTL.toml
chown pihole:pihole /etc/pihole/pihole-FTL.toml
# Prepare dnsmasq.conf
cp test/dnsmasq.conf /etc/dnsmasq.conf

View File

@ -945,46 +945,6 @@
[[ ${lines[0]} == "Error 404: Not Found" ]]
}
@test "API authorization (without password): No login required" {
run bash -c 'curl -s 127.0.0.1:8080/api/auth'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == '{"challenge":null,"session":{"valid":true,"sid":null,"validity":-1}}' ]]
}
@test "API authorization (with password): FTL challenges us" {
# Password: ABC
echo "WEBPASSWORD=183c1b634da0078fcf5b0af84bdcbb3e817708c3f22b329be84165f4bad1ae48" >> /etc/pihole/setupVars.conf
run bash -c 'curl -s 127.0.0.1:8080/api/auth | jq ".challenge | length"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "64" ]]
}
@test "API authorization (with password): Incorrect response is rejected" {
run bash -c 'curl -s -X POST 127.0.0.1:8080/api/auth -d "{\"response\":\"0123456789012345678901234567890123456789012345678901234567890123\"}" | jq .session.valid'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "false" ]]
}
@test "API authorization (with password): Correct password is accepted" {
computeResponse() {
local pwhash challenge response
pwhash="${1}"
challenge="${2}"
response=$(echo -n "${challenge}:${pwhash}" | sha256sum | sed 's/\s.*$//')
echo "${response}"
}
pwhash="183c1b634da0078fcf5b0af84bdcbb3e817708c3f22b329be84165f4bad1ae48"
challenge="$(curl -s -X GET 127.0.0.1:8080/api/auth | jq --raw-output .challenge)"
printf "Challenge: %s\n" "${challenge}"
response="$(computeResponse "$pwhash" "$challenge")"
printf "Response: %s\n" "${response}"
session="$(curl -s -X POST 127.0.0.1:8080/api/auth -d "{\"response\":\"$response\"}")"
printf "Session: %s\n" "${session}"
run jq .session.valid <<< "${session}"
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "true" ]]
}
@test "LUA: Interpreter returns FTL version" {
run bash -c './pihole-FTL lua -e "print(pihole.ftl_version())"'
printf "%s\n" "${lines[@]}"
@ -1204,7 +1164,7 @@
}
@test "Pi-hole uses dns.reply.blocking.IPv4/6 for blocked domain" {
sed -i "s/blockingmode = \"NULL\"/blockingmode = \"IP\"/" /etc/pihole/pihole-FTL.toml
run bash -c 'curl -X PATCH http://127.0.0.1:8080/api/config -d "@test/api/json/blocking_mode_IP.json"'
run bash -c "kill -HUP $(cat /run/pihole-FTL.pid)"
sleep 2
run bash -c "dig A denied.ftl +short @127.0.0.1"
@ -1215,6 +1175,46 @@
[[ "${lines[0]}" == "fe80::11" ]]
}
@test "API authorization (without password): No login required" {
run bash -c 'curl -s 127.0.0.1:8080/api/auth'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == '{"challenge":null,"session":{"valid":true,"sid":null,"validity":-1}}' ]]
}
@test "API authorization (with password): FTL challenges us" {
# Password: ABC
run bash -c 'curl -X PATCH http://127.0.0.1:8080/api/config -d "@test/api/json/add_password.json"'
run bash -c 'curl -s 127.0.0.1:8080/api/auth | jq ".challenge | length"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "64" ]]
}
@test "API authorization (with password): Incorrect response is rejected" {
run bash -c 'curl -s -X POST 127.0.0.1:8080/api/auth -d "{\"response\":\"0123456789012345678901234567890123456789012345678901234567890123\"}" | jq .session.valid'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "false" ]]
}
@test "API authorization (with password): Correct password is accepted" {
computeResponse() {
local pwhash challenge response
pwhash="${1}"
challenge="${2}"
response=$(echo -n "${challenge}:${pwhash}" | sha256sum | sed 's/\s.*$//')
echo "${response}"
}
pwhash="183c1b634da0078fcf5b0af84bdcbb3e817708c3f22b329be84165f4bad1ae48"
challenge="$(curl -s -X GET 127.0.0.1:8080/api/auth | jq --raw-output .challenge)"
printf "Challenge: %s\n" "${challenge}"
response="$(computeResponse "$pwhash" "$challenge")"
printf "Response: %s\n" "${response}"
session="$(curl -s -X POST 127.0.0.1:8080/api/auth -d "{\"response\":\"$response\"}")"
printf "Session: %s\n" "${session}"
run jq .session.valid <<< "${session}"
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "true" ]]
}
@test "API validation" {
run python3 test/api/checkAPI.py
printf "%s\n" "${lines[@]}"