Tests: Set api.pwhash and dns.blocking.mode using PATCH /api/config
Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
parent
4abba45878
commit
140a365806
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"config": {
|
||||
"api": {
|
||||
"pwhash": "183c1b634da0078fcf5b0af84bdcbb3e817708c3f22b329be84165f4bad1ae48"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"config": {
|
||||
"dns": {
|
||||
"blocking": {
|
||||
"mode": "IP"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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[@]}"
|
||||
|
|
Loading…
Reference in New Issue