Merge branch 'development-v6' into tweak/conf_writing

Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
DL6ER 2023-11-13 22:58:18 +01:00
commit fdad1b7be2
No known key found for this signature in database
GPG Key ID: 00135ACBD90B28DD
29 changed files with 762 additions and 116 deletions

View File

@ -1,5 +1,8 @@
name: Codespell
on:
push:
branches:
- '**'
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

View File

@ -198,7 +198,7 @@ static bool encode_uint8_t_array_to_base32(const uint8_t *in, const size_t in_le
}
static uint32_t last_code = 0;
bool verifyTOTP(const uint32_t incode)
enum totp_status verifyTOTP(const uint32_t incode)
{
// Decode base32 secret
uint8_t decoded_secret[RFC6238_SECRET_LEN];
@ -228,15 +228,16 @@ bool verifyTOTP(const uint32_t incode)
{
log_warn("2FA code has already been used (%i, %u), please wait %lu seconds",
i, gencode, (unsigned long)(RFC6238_X - (now % RFC6238_X)));
return false;
return TOTP_REUSED;
}
log_info("2FA code verified successfully at %i", i);
const char *which = i == -1 ? "previous" : i == 0 ? "current" : "next";
log_debug(DEBUG_API, "2FA code from %s time step is valid", which);
last_code = gencode;
return true;
return TOTP_CORRECT;
}
}
return false;
return TOTP_INVALID;
}
// Print TOTP code to stdout (for CLI use)

View File

@ -92,7 +92,12 @@ int api_auth_session_delete(struct ftl_conn *api);
bool is_local_api_user(const char *remote_addr) __attribute__((pure));
// 2FA methods
bool verifyTOTP(const uint32_t code);
enum totp_status {
TOTP_INVALID,
TOTP_CORRECT,
TOTP_REUSED,
} __attribute__ ((packed));
enum totp_status verifyTOTP(const uint32_t code);
int generateTOTP(struct ftl_conn *api);
int printTOTP(void);
int generateAppPw(struct ftl_conn *api);

View File

@ -26,7 +26,8 @@
// database session functions
#include "database/session-table.h"
static struct session auth_data[API_MAX_CLIENTS] = {{false, false, {false, false}, 0, 0, {0}, {0}, {0}, {0}}};
static uint16_t max_sessions = 0;
static struct session *auth_data = NULL;
static void add_request_info(struct ftl_conn *api, const char *csrf)
{
@ -43,13 +44,23 @@ static void add_request_info(struct ftl_conn *api, const char *csrf)
void init_api(void)
{
// Restore sessions from database
restore_db_sessions(auth_data);
max_sessions = config.webserver.api.max_sessions.v.u16;
auth_data = calloc(max_sessions, sizeof(struct session));
if(auth_data == NULL)
{
log_crit("Could not allocate memory for API sessions, check config value of webserver.api.max_sessions");
exit(EXIT_FAILURE);
}
restore_db_sessions(auth_data, max_sessions);
}
void free_api(void)
{
// Store sessions in database
backup_db_sessions(auth_data);
backup_db_sessions(auth_data, max_sessions);
max_sessions = 0;
free(auth_data);
auth_data = NULL;
}
// Is this client connecting from localhost?
@ -187,7 +198,7 @@ int check_client_auth(struct ftl_conn *api, const bool is_api)
}
}
for(unsigned int i = 0; i < API_MAX_CLIENTS; i++)
for(unsigned int i = 0; i < max_sessions; i++)
{
if(auth_data[i].used &&
auth_data[i].valid_until >= now &&
@ -253,7 +264,7 @@ static int get_all_sessions(struct ftl_conn *api, cJSON *json)
{
const time_t now = time(NULL);
cJSON *sessions = JSON_NEW_ARRAY();
for(int i = 0; i < API_MAX_CLIENTS; i++)
for(int i = 0; i < max_sessions; i++)
{
if(!auth_data[i].used)
continue;
@ -316,7 +327,7 @@ static int get_session_object(struct ftl_conn *api, cJSON *json, const int user_
static void delete_session(const int user_id)
{
// Skip if nothing to be done here
if(user_id < 0 || user_id >= API_MAX_CLIENTS)
if(user_id < 0 || user_id >= max_sessions)
return;
// Zero out this session (also sets valid to false == 0)
@ -326,7 +337,7 @@ static void delete_session(const int user_id)
void delete_all_sessions(void)
{
// Zero out all sessions without looping
memset(auth_data, 0, sizeof(auth_data));
memset(auth_data, 0, max_sessions*sizeof(*auth_data));
}
static int send_api_auth_status(struct ftl_conn *api, const int user_id, const time_t now)
@ -516,18 +527,27 @@ int api_auth(struct ftl_conn *api)
NULL);
}
if(!verifyTOTP(json_totp->valueint))
enum totp_status totp = verifyTOTP(json_totp->valueint);
if(totp == TOTP_REUSED)
{
// 2FA token has been reused
return send_json_error(api, 401,
"unauthorized",
"Reused 2FA token",
"wait for new token");
}
else if(totp != TOTP_CORRECT)
{
// 2FA token is invalid
return send_json_error(api, 401,
"unauthorized",
"Invalid 2FA token",
NULL);
"unauthorized",
"Invalid 2FA token",
NULL);
}
}
// Find unused authentication slot
for(unsigned int i = 0; i < API_MAX_CLIENTS; i++)
for(unsigned int i = 0; i < max_sessions; i++)
{
// Expired slow, mark as unused
if(auth_data[i].used &&
@ -585,16 +605,22 @@ int api_auth(struct ftl_conn *api)
}
if(user_id == API_AUTH_UNAUTHORIZED)
{
log_warn("No free API seats available, not authenticating client");
log_warn("No free API seats available (webserver.api.max_sessions = %u), not authenticating client",
max_sessions);
return send_json_error(api, 429,
"api_seats_exceeded",
"API seats exceeded",
"increase webserver.api.max_sessions");
}
}
else if(result == PASSWORD_RATE_LIMITED)
{
// Rate limited
return send_json_error(api, 429,
"too_many_requests",
"Too many requests",
"login rate limiting");
"rate_limiting",
"Rate-limiting login attempts",
NULL);
}
else
{
@ -621,7 +647,7 @@ int api_auth_session_delete(struct ftl_conn *api)
return send_json_error(api, 400, "bad_request", "Missing or invalid session ID", NULL);
// Check if session ID is valid
if(uid <= API_AUTH_UNAUTHORIZED || uid >= API_MAX_CLIENTS)
if(uid <= API_AUTH_UNAUTHORIZED || uid >= max_sessions)
return send_json_error(api, 400, "bad_request", "Session ID out of bounds", NULL);
// Check if session is used

View File

@ -11,9 +11,6 @@
#ifndef AUTH_H
#define AUTH_H
// How many authenticated API clients are allowed simultaneously? [.]
#define API_MAX_CLIENTS 16
// crypto library
#include <nettle/sha2.h>
#include <nettle/base64.h>

View File

@ -778,11 +778,7 @@ static int api_config_patch(struct ftl_conn *api)
// Rewrite HOSTS file if required
if(rewrite_hosts)
{
write_custom_list();
// Reload HOSTS file
kill(main_pid(), SIGHUP);
}
}
else
{
@ -976,11 +972,7 @@ static int api_config_put_delete(struct ftl_conn *api)
// Rewrite HOSTS file if required
if(rewrite_hosts)
{
write_custom_list();
// Reload HOSTS file
kill(main_pid(), SIGHUP);
}
// Send empty reply with matching HTTP status code
// 201 - Created or 204 - No content

View File

@ -80,6 +80,8 @@ components:
$ref: 'auth.yaml#/components/examples/errors/no_password'
password_inval:
$ref: 'auth.yaml#/components/examples/errors/password_inval'
totp_missing:
$ref: 'auth.yaml#/components/examples/errors/totp_missing'
'401':
description: Unauthorized
content:
@ -88,6 +90,11 @@ components:
allOf:
- $ref: 'common.yaml#/components/errors/unauthorized'
- $ref: 'common.yaml#/components/schemas/took'
examples:
totp_invalid:
$ref: 'auth.yaml#/components/examples/errors/totp_invalid'
totp_reused:
$ref: 'auth.yaml#/components/examples/errors/totp_reused'
'429':
description: Too Many Requests
content:
@ -96,6 +103,11 @@ components:
allOf:
- $ref: 'common.yaml#/components/errors/too_many_requests'
- $ref: 'common.yaml#/components/schemas/took'
examples:
rate_limit:
$ref: 'auth.yaml#/components/examples/errors/rate_limit'
no_seats:
$ref: 'auth.yaml#/components/examples/errors/no_seats'
delete:
summary: Delete session
tags:
@ -486,6 +498,13 @@ components:
key: "bad_request"
message: "Field password has to be of type 'string'"
hint: null
totp_missing:
summary: Bad request (2FA token missing)
value:
error:
key: "bad_request"
message: "No 2FA token found in JSON payload"
hint: null
missing_session_id:
summary: Bad request (missing session ID)
value:
@ -507,6 +526,34 @@ components:
key: "bad_request"
message: "Session ID not in use"
hint: null
totp_invalid:
summary: 2FA token invalid
value:
error:
key: "unauthorized"
message: "Invalid 2FA token"
hint: null
totp_reused:
summary: 2FA token reused
value:
error:
key: "unauthorized"
message: "Reused 2FA token"
hint: "wait for new token"
rate_limit:
summary: Rate limit exceeded
value:
error:
key: "rate_limiting"
message: "Rate-limiting login attempts"
hint: null
no_seats:
summary: No free API seats available
value:
error:
key: "api_seats_exceeded"
message: "API seats exceeded"
hint: "increase webserver.api.max_sessions"
parameters:
id:
in: path

View File

@ -206,6 +206,8 @@ components:
type: boolean
expandHosts:
type: boolean
domain:
type: string
bogusPriv:
type: boolean
dnssec:
@ -301,6 +303,8 @@ components:
type: string
domain:
type: string
description: |
*Note:* This setting is deprecated and will be removed in a future release. Use dns.domain instead.
leaseTime:
type: string
ipv6:
@ -386,6 +390,8 @@ components:
type: boolean
searchAPIauth:
type: boolean
max_sessions:
type: integer
prettyJSON:
type: boolean
password:
@ -450,6 +456,8 @@ components:
type: integer
addr2line:
type: boolean
etc_dnsmasq_d:
type: boolean
privacylevel:
type: integer
dnsmasq_lines:
@ -583,6 +591,7 @@ components:
- "192.168.2.123 mymusicbox"
domainNeeded: true
expandHosts: true
domain: "lan"
bogusPriv: true
dnssec: true
interface: "eth0"
@ -666,6 +675,7 @@ components:
api:
localAPIauth: false
searchAPIauth: false
max_sessions: 16
prettyJSON: false
password: "********"
pwhash: ''
@ -694,6 +704,7 @@ components:
delay_startup: 10
addr2line: true
privacylevel: 0
etc_dnsmasq_d: false
dnsmasq_lines: [ ]
check:
load: true

View File

@ -309,6 +309,7 @@ void parse_args(int argc, char* argv[])
exit(read_teleporter_zip_from_disk(argv[2]) ? EXIT_SUCCESS : EXIT_FAILURE);
}
// Generate X.509 certificate
if(argc > 1 && strcmp(argv[1], "--gen-x509") == 0)
{
if(argc < 3 || argc > 5)
@ -327,6 +328,55 @@ void parse_args(int argc, char* argv[])
exit(generate_certificate(argv[2], rsa, domain) ? EXIT_SUCCESS : EXIT_FAILURE);
}
// Parse X.509 certificate
if(argc > 1 &&
(strcmp(argv[1], "--read-x509") == 0 ||
strcmp(argv[1], "--read-x509-key") == 0))
{
if(argc < 2 || argc > 4)
{
printf("Usage: %s %s [<input file>] [<domain>]\n", argv[0], argv[1]);
printf("Example: %s %s /etc/pihole/tls.pem\n", argv[0], argv[1]);
printf(" with domain: %s %s /etc/pihole/tls.pem pi.hole\n", argv[0], argv[1]);
exit(EXIT_FAILURE);
}
// Option parsing
// Should we report on the private key?
const bool private_key = strcmp(argv[1], "--read-x509-key") == 0;
// If no certificate file is given, we use the one from the config
const char *certfile = NULL;
if(argc == 2)
{
readFTLconf(&config, false);
certfile = config.webserver.tls.cert.v.s;
}
else
certfile = argv[2];
// If no domain is given, we only check the certificate
const char *domain = argc > 3 ? argv[3] : NULL;
// Enable stdout printing
cli_mode = true;
log_ctrl(false, true);
enum cert_check result = read_certificate(certfile, domain, private_key);
if(argc < 4)
exit(result == CERT_OKAY ? EXIT_SUCCESS : EXIT_FAILURE);
else if(result == CERT_DOMAIN_MATCH)
{
printf("Certificate matches domain %s\n", argv[3]);
exit(EXIT_SUCCESS);
}
else
{
printf("Certificate does not match domain %s\n", argv[3]);
exit(EXIT_FAILURE);
}
}
// If the first argument is "gravity" (e.g., /usr/bin/pihole-FTL gravity),
// we offer some specialized gravity tools
if(argc > 1 && (strcmp(argv[1], "gravity") == 0 || strcmp(argv[1], "antigravity") == 0))
@ -812,6 +862,16 @@ void parse_args(int argc, char* argv[])
printf(" an RSA (4096 bit) key will be generated instead.\n\n");
printf(" Usage: %spihole-FTL --gen-x509 %soutfile %s[rsa]%s\n\n", green, cyan, purple, normal);
printf("%sTLS X.509 certificate parser:%s\n", yellow, normal);
printf(" Parse the given X.509 certificate and optionally check if\n");
printf(" it matches a given domain. If no domain is given, only a\n");
printf(" human-readable output string is printed.\n\n");
printf(" If no certificate file is given, the one from the config\n");
printf(" is used (if applicable). If --read-x509-key is used, details\n");
printf(" about the private key are printed as well.\n\n");
printf(" Usage: %spihole-FTL --read-x509 %s[certfile] %s[domain]%s\n", green, cyan, purple, normal);
printf(" Usage: %spihole-FTL --read-x509-key %s[certfile] %s[domain]%s\n\n", green, cyan, purple, normal);
printf("%sGravity tools:%s\n", yellow, normal);
printf(" Check domains in a given file for validity using Pi-hole's\n");
printf(" gravity filters. The expected input format is one domain\n");

View File

@ -441,10 +441,8 @@ int set_config_from_CLI(const char *key, const char *value)
}
else if(conf_item == &config.dns.hosts)
{
// We need to rewrite the custom.list file but do not need to
// restart dnsmasq. If dnsmasq is going to be restarted anyway,
// this is not necessary as the file will be rewritten during
// the restart
// We need to rewrite the custom.list file but do not
// need to restart dnsmasq
write_custom_list();
}

View File

@ -477,6 +477,13 @@ void initConfig(struct config *conf)
conf->dns.expandHosts.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dns.expandHosts.d.b = false;
conf->dns.domain.k = "dns.domain";
conf->dns.domain.h = "The DNS domain used by your Pi-hole to expand hosts and for DHCP.\n\n Only if DHCP is enabled below: For DHCP, this has two effects; firstly it causes the DHCP server to return the domain to any hosts which request it, and secondly it sets the domain which it is legal for DHCP-configured hosts to claim. The intention is to constrain hostnames so that an untrusted host on the LAN cannot advertise its name via DHCP as e.g. \"google.com\" and capture traffic not meant for it. If no domain suffix is specified, then any DHCP hostname with a domain part (ie with a period) will be disallowed and logged. If a domain is specified, then hostnames with a domain part are allowed, provided the domain part matches the suffix. In addition, when a suffix is set then hostnames without a domain part have the suffix added as an optional domain part. For instance, we can set domain=mylab.com and have a machine whose DHCP hostname is \"laptop\". The IP address for that machine is available both as \"laptop\" and \"laptop.mylab.com\".\n\n You can disable setting a domain by setting this option to an empty string.";
conf->dns.domain.a = cJSON_CreateStringReference("<any valid domain>");
conf->dns.domain.t = CONF_STRING;
conf->dns.domain.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dns.domain.d.s = (char*)"lan";
conf->dns.bogusPriv.k = "dns.bogusPriv";
conf->dns.bogusPriv.h = "Should all reverse lookups for private IP ranges (i.e., 192.168.x.y, etc) which are not found in /etc/hosts or the DHCP leases file be answered with \"no such domain\" rather than being forwarded upstream?";
conf->dns.bogusPriv.t = CONF_BOOL;
@ -528,7 +535,7 @@ void initConfig(struct config *conf)
conf->dns.cnameRecords.k = "dns.cnameRecords";
conf->dns.cnameRecords.h = "List of CNAME records which indicate that <cname> is really <target>. If the <TTL> is given, it overwrites the value of local-ttl";
conf->dns.cnameRecords.a = cJSON_CreateStringReference("Array of static leases each on in one of the following forms: \"<cname>,<target>[,<TTL>]\"");
conf->dns.cnameRecords.a = cJSON_CreateStringReference("Array of CNAMEs each on in one of the following forms: \"<cname>,<target>[,<TTL>]\"");
conf->dns.cnameRecords.t = CONF_JSON_STRING_ARRAY;
conf->dns.cnameRecords.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dns.cnameRecords.d.json = cJSON_CreateArray();
@ -706,7 +713,7 @@ void initConfig(struct config *conf)
conf->dhcp.router.d.s = (char*)"";
conf->dhcp.domain.k = "dhcp.domain";
conf->dhcp.domain.h = "The DNS domain used by your Pi-hole";
conf->dhcp.domain.h = "The DNS domain used by your Pi-hole (*** DEPRECATED ***)\n This setting is deprecated and will be removed in a future version. Please use dns.domain instead. Setting it to any non-default value will overwrite the value of dns.domain if it is still set to its default value.";
conf->dhcp.domain.a = cJSON_CreateStringReference("<any valid domain>");
conf->dhcp.domain.t = CONF_STRING;
conf->dhcp.domain.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
@ -824,13 +831,14 @@ void initConfig(struct config *conf)
conf->webserver.acl.k = "webserver.acl";
conf->webserver.acl.h = "Webserver access control list (ACL) allowing for restrictions to be put on the list of IP addresses which have access to the web server. The ACL is a comma separated list of IP subnets, where each subnet is prepended by either a - or a + sign. A plus sign means allow, where a minus sign means deny. If a subnet mask is omitted, such as -1.2.3.4, this means to deny only that single IP address. If this value is not set (empty string), all accesses are allowed. Otherwise, the default setting is to deny all accesses. On each request the full list is traversed, and the last (!) match wins. IPv6 addresses may be specified in CIDR-form [a:b::c]/64.\n\n Example 1: acl = \"+127.0.0.1,+[::1]\"\n ---> deny all access, except from 127.0.0.1 and ::1,\n Example 2: acl = \"+192.168.0.0/16\"\n ---> deny all accesses, except from the 192.168.0.0/16 subnet,\n Example 3: acl = \"+[::]/0\" ---> allow only IPv6 access.";
conf->webserver.acl.a = cJSON_CreateStringReference("<valid ACL>");
conf->webserver.acl.f = FLAG_ADVANCED_SETTING;
conf->webserver.acl.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->webserver.acl.t = CONF_STRING;
conf->webserver.acl.d.s = (char*)"";
conf->webserver.port.k = "webserver.port";
conf->webserver.port.h = "Ports to be used by the webserver.\n Comma-separated list of ports to listen on. It is possible to specify an IP address to bind to. In this case, an IP address and a colon must be prepended to the port number. For example, to bind to the loopback interface on port 80 (IPv4) and to all interfaces port 8080 (IPv4), use \"127.0.0.1:80,8080\". \"[::]:80\" can be used to listen to IPv6 connections to port 80. IPv6 addresses of network interfaces can be specified as well, e.g. \"[::1]:80\" for the IPv6 loopback interface. [::]:80 will bind to port 80 IPv6 only.\n In order to use port 80 for all interfaces, both IPv4 and IPv6, use either the configuration \"80,[::]:80\" (create one socket for IPv4 and one for IPv6 only), or \"+80\" (create one socket for both, IPv4 and IPv6). The + notation to use IPv4 and IPv6 will only work if no network interface is specified. Depending on your operating system version and IPv6 network environment, some configurations might not work as expected, so you have to test to find the configuration most suitable for your needs. In case \"+80\" does not work for your environment, you need to use \"80,[::]:80\".\n If the port is TLS/SSL, a letter 's' must be appended, for example, \"80,443s\" will open port 80 and port 443, and connections on port 443 will be encrypted. For non-encrypted ports, it is allowed to append letter 'r' (as in redirect). Redirected ports will redirect all their traffic to the first configured SSL port. For example, if webserver.port is \"80r,443s\", then all HTTP traffic coming at port 80 will be redirected to HTTPS port 443.";
conf->webserver.port.a = cJSON_CreateStringReference("comma-separated list of <[ip_address:]port>");
conf->webserver.port.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->webserver.port.t = CONF_STRING;
conf->webserver.port.d.s = (char*)"80,[::]:80,443s,[::]:443s";
@ -862,14 +870,14 @@ void initConfig(struct config *conf)
conf->webserver.paths.webroot.h = "Server root on the host";
conf->webserver.paths.webroot.a = cJSON_CreateStringReference("<valid path>");
conf->webserver.paths.webroot.t = CONF_STRING;
conf->webserver.paths.webroot.f = FLAG_ADVANCED_SETTING;
conf->webserver.paths.webroot.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->webserver.paths.webroot.d.s = (char*)"/var/www/html";
conf->webserver.paths.webhome.k = "webserver.paths.webhome";
conf->webserver.paths.webhome.h = "Sub-directory of the root containing the web interface";
conf->webserver.paths.webhome.a = cJSON_CreateStringReference("<valid subpath>, both slashes are needed!");
conf->webserver.paths.webhome.t = CONF_STRING;
conf->webserver.paths.webhome.f = FLAG_ADVANCED_SETTING;
conf->webserver.paths.webhome.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->webserver.paths.webhome.d.s = (char*)"/admin/";
// sub-struct interface
@ -903,6 +911,12 @@ void initConfig(struct config *conf)
conf->webserver.api.localAPIauth.t = CONF_BOOL;
conf->webserver.api.localAPIauth.d.b = true;
conf->webserver.api.max_sessions.k = "webserver.api.max_sessions";
conf->webserver.api.max_sessions.h = "Number of concurrent sessions allowed for the API. If the number of sessions exceeds this value, no new sessions will be allowed until the number of sessions drops due to session expiration or logout. Note that the number of concurrent sessions is irrelevant if authentication is disabled as no sessions are used in this case.";
conf->webserver.api.max_sessions.t = CONF_UINT16;
conf->webserver.api.max_sessions.d.u16 = 16;
conf->webserver.api.max_sessions.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->webserver.api.prettyJSON.k = "webserver.api.prettyJSON";
conf->webserver.api.prettyJSON.h = "Should FTL prettify the API output (add extra spaces, newlines and indentation)?";
conf->webserver.api.prettyJSON.t = CONF_BOOL;
@ -985,7 +999,7 @@ void initConfig(struct config *conf)
conf->files.pid.h = "The file which contains the PID of FTL's main process.";
conf->files.pid.a = cJSON_CreateStringReference("<any writable file>");
conf->files.pid.t = CONF_STRING;
conf->files.pid.f = FLAG_ADVANCED_SETTING;
conf->files.pid.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->files.pid.d.s = (char*)"/run/pihole-FTL.pid";
conf->files.database.k = "files.database";
@ -999,7 +1013,7 @@ void initConfig(struct config *conf)
conf->files.gravity.h = "The location of Pi-hole's gravity database";
conf->files.gravity.a = cJSON_CreateStringReference("<any Pi-hole gravity database>");
conf->files.gravity.t = CONF_STRING;
conf->files.gravity.f = FLAG_ADVANCED_SETTING;
conf->files.gravity.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->files.gravity.d.s = (char*)"/etc/pihole/gravity.db";
conf->files.macvendor.k = "files.macvendor";
@ -1020,14 +1034,14 @@ void initConfig(struct config *conf)
conf->files.pcap.h = "An optional file containing a pcap capture of the network traffic. This file is used for debugging purposes only. If you don't know what this is, you don't need it.\n Setting this to an empty string disables pcap recording. The file must be writable by the user running FTL (typically pihole). Failure to write to this file will prevent the DNS resolver from starting. The file is appended to if it already exists.";
conf->files.pcap.a = cJSON_CreateStringReference("<any writable pcap file>");
conf->files.pcap.t = CONF_STRING;
conf->files.pcap.f = FLAG_ADVANCED_SETTING;
conf->files.pcap.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->files.pcap.d.s = (char*)"";
conf->files.log.webserver.k = "files.log.webserver";
conf->files.log.webserver.h = "The log file used by the webserver";
conf->files.log.webserver.a = cJSON_CreateStringReference("<any writable file>");
conf->files.log.webserver.t = CONF_STRING;
conf->files.log.webserver.f = FLAG_ADVANCED_SETTING;
conf->files.log.webserver.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->files.log.webserver.d.s = (char*)"/var/log/pihole/webserver.log";
// sub-struct files.log
@ -1037,7 +1051,7 @@ void initConfig(struct config *conf)
conf->files.log.dnsmasq.h = "The log file used by the embedded dnsmasq DNS server";
conf->files.log.dnsmasq.a = cJSON_CreateStringReference("<any writable file>");
conf->files.log.dnsmasq.t = CONF_STRING;
conf->files.log.dnsmasq.f = FLAG_ADVANCED_SETTING;
conf->files.log.dnsmasq.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->files.log.dnsmasq.d.s = (char*)"/var/log/pihole/pihole.log";
@ -1065,7 +1079,7 @@ void initConfig(struct config *conf)
conf->misc.nice.k = "misc.nice";
conf->misc.nice.h = "Set niceness of pihole-FTL. Defaults to -10 and can be disabled altogether by setting a value of -999. The nice value is an attribute that can be used to influence the CPU scheduler to favor or disfavor a process in scheduling decisions. The range of the nice value varies across UNIX systems. On modern Linux, the range is -20 (high priority = not very nice to other processes) to +19 (low priority).";
conf->misc.nice.t = CONF_INT;
conf->misc.nice.f = FLAG_ADVANCED_SETTING;
conf->misc.nice.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->misc.nice.d.i = -10;
conf->misc.addr2line.k = "misc.addr2line";
@ -1074,11 +1088,17 @@ void initConfig(struct config *conf)
conf->misc.addr2line.f = FLAG_ADVANCED_SETTING;
conf->misc.addr2line.d.b = true;
conf->misc.etc_dnsmasq_d.k = "misc.etc_dnsmasq_d";
conf->misc.etc_dnsmasq_d.h = "Should FTL load additional dnsmasq configuration files from /etc/dnsmasq.d/?";
conf->misc.etc_dnsmasq_d.t = CONF_BOOL;
conf->misc.etc_dnsmasq_d.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->misc.etc_dnsmasq_d.d.b = false;
conf->misc.dnsmasq_lines.k = "misc.dnsmasq_lines";
conf->misc.dnsmasq_lines.h = "Additional lines to inject into the generated dnsmasq configuration.\n Warning: This is an advanced setting and should only be used with care. Incorrectly formatted or duplicated lines as well as lines conflicting with the automatic configuration of Pi-hole can break the embedded dnsmasq and will stop DNS resolution from working.\n Use this option with extra care.";
conf->misc.dnsmasq_lines.a = cJSON_CreateStringReference("array of valid dnsmasq config line options");
conf->misc.dnsmasq_lines.t = CONF_JSON_STRING_ARRAY;
conf->misc.dnsmasq_lines.f = FLAG_RESTART_FTL;
conf->misc.dnsmasq_lines.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->misc.dnsmasq_lines.d.json = cJSON_CreateArray();
// sub-struct misc.check

View File

@ -125,6 +125,7 @@ struct config {
struct conf_item hosts;
struct conf_item domainNeeded;
struct conf_item expandHosts;
struct conf_item domain;
struct conf_item bogusPriv;
struct conf_item dnssec;
struct conf_item interface;
@ -226,6 +227,7 @@ struct config {
struct {
struct conf_item localAPIauth;
struct conf_item searchAPIauth;
struct conf_item max_sessions;
struct conf_item prettyJSON;
struct conf_item pwhash;
struct conf_item password; // This is a pseudo-item
@ -261,6 +263,7 @@ struct config {
struct conf_item delay_startup;
struct conf_item nice;
struct conf_item addr2line;
struct conf_item etc_dnsmasq_d;
struct conf_item dnsmasq_lines;
struct {
struct conf_item load;

View File

@ -207,11 +207,12 @@ static void write_config_header(FILE *fp, const char *description)
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "ANY CHANGES MADE TO THIS FILE WILL BE LOST WHEN THE CONFIGURATION CHANGES");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "IF YOU WISH TO CHANGE ANY OF THESE VALUES, CHANGE THEM IN");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "etc/pihole/pihole.toml");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "/etc/pihole/pihole.toml");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "and restart pihole-FTL");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "ANY OTHER CHANGES SHOULD BE MADE IN A SEPARATE CONFIG FILE");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "WITHIN /etc/dnsmasq.d/yourname.conf");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "(make sure misc.etc_dnsmasq_d is set to true in /etc/pihole/pihole.toml)");
CONFIG_CENTER(fp, HEADER_WIDTH, "%s", "");
CONFIG_CENTER(fp, HEADER_WIDTH, "Last updated: %s", timestring);
CONFIG_CENTER(fp, HEADER_WIDTH, "by FTL version %s", get_FTL_version());
@ -240,13 +241,14 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
write_config_header(pihole_conf, "Dnsmasq config for Pi-hole's FTLDNS");
fputs("addn-hosts=/etc/pihole/local.list\n", pihole_conf);
fputs("addn-hosts="DNSMASQ_CUSTOM_LIST"\n", pihole_conf);
fputs("hostsdir="DNSMASQ_HOSTSDIR"\n", pihole_conf);
fputs("\n", pihole_conf);
fputs("# Don't read /etc/resolv.conf. Get upstream servers only from the configuration\n", pihole_conf);
fputs("no-resolv\n", pihole_conf);
fputs("\n", pihole_conf);
fputs("# DNS port to be used\n", pihole_conf);
fprintf(pihole_conf, "port=%u\n", conf->dns.port.v.u16);
fputs("\n", pihole_conf);
if(cJSON_GetArraySize(conf->dns.upstreams.v.json) > 0)
{
fputs("# List of upstream DNS server\n", pihole_conf);
@ -278,12 +280,14 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
fputs("# Enable query logging\n", pihole_conf);
fputs("log-queries\n", pihole_conf);
fputs("log-async\n", pihole_conf);
fputs("\n", pihole_conf);
}
else
{
fputs("# Disable query logging\n", pihole_conf);
fputs("#log-queries\n", pihole_conf);
fputs("#log-async\n", pihole_conf);
fputs("\n", pihole_conf);
}
if(strlen(conf->files.log.dnsmasq.v.s) > 0)
@ -334,12 +338,14 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
{
fputs("# Add A, AAAA and PTR records to the DNS\n", pihole_conf);
fprintf(pihole_conf, "host-record=%s\n", conf->dns.hostRecord.v.s);
fputs("\n", pihole_conf);
}
if(conf->dns.cache.optimizer.v.ui > 0u)
{
fputs("# Use stale cache entries for a given number of seconds to optimize cache utilization\n", pihole_conf);
fprintf(pihole_conf, "use-stale-cache=%u\n", conf->dns.cache.optimizer.v.ui);
fputs("\n", pihole_conf);
}
const char *interface = conf->dns.interface.v.s;
@ -402,16 +408,18 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
fputs("# Never forward A or AAAA queries for plain names, without\n",pihole_conf);
fputs("# dots or domain parts, to upstream nameservers. If the name\n", pihole_conf);
fputs("# is not known from /etc/hosts or DHCP a NXDOMAIN is returned\n", pihole_conf);
fprintf(pihole_conf, "local=/%s/\n",
conf->dhcp.domain.v.s);
fputs("\n", pihole_conf);
if(strlen(conf->dns.domain.v.s))
fprintf(pihole_conf, "local=/%s/\n\n", conf->dns.domain.v.s);
else
fputs("\n", pihole_conf);
}
if(strlen(conf->dhcp.domain.v.s) > 0 && strcasecmp("none", conf->dhcp.domain.v.s) != 0)
// Add domain to DNS server. It will also be used for DHCP if the DHCP
// server is enabled below
if(strlen(conf->dns.domain.v.s) > 0)
{
fputs("# DNS domain for the DHCP server\n", pihole_conf);
fprintf(pihole_conf, "domain=%s\n", conf->dhcp.domain.v.s);
fputs("\n", pihole_conf);
fputs("# DNS domain for both the DNS and DHCP server\n", pihole_conf);
fprintf(pihole_conf, "domain=%s\n\n", conf->dns.domain.v.s);
}
if(conf->dhcp.active.v.b)
@ -501,25 +509,27 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
fputs("# Pi-hole implements this via the dnsmasq option \"bogus-priv\" above\n", pihole_conf);
fputs("# (if enabled!) as this option also covers IPv6.\n", pihole_conf);
fputs("\n", pihole_conf);
fputs("# OpenWRT furthermore blocks bind, local, onion domains\n", pihole_conf);
fputs("# OpenWRT furthermore blocks bind, local, onion domains\n", pihole_conf);
fputs("# see https://git.openwrt.org/?p=openwrt/openwrt.git;a=blob_plain;f=package/network/services/dnsmasq/files/rfc6761.conf;hb=HEAD\n", pihole_conf);
fputs("# and https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml\n", pihole_conf);
fputs("# We do not include the \".local\" rule ourselves, see https://github.com/pi-hole/pi-hole/pull/4282#discussion_r689112972\n", pihole_conf);
fputs("server=/bind/\n", pihole_conf);
fputs("server=/onion/\n", pihole_conf);
fputs("\n", pihole_conf);
if(directory_exists("/etc/dnsmasq.d"))
if(directory_exists("/etc/dnsmasq.d") && conf->misc.etc_dnsmasq_d.v.b)
{
// Load possible additional user scripts from /etc/dnsmasq.d if
// the directory exists (it may not, e.g., in a container)
fputs("# Load possible additional user scripts\n", pihole_conf);
// Load additional user scripts from /etc/dnsmasq.d if the
// directory exists (it may not, e.g., in a container)
fputs("# Load additional user scripts\n", pihole_conf);
fputs("conf-dir=/etc/dnsmasq.d\n", pihole_conf);
fputs("\n", pihole_conf);
}
// Add option for caching all DNS records
fputs("# Cache all DNS records\n", pihole_conf);
fputs("cache-rr=ANY\n\n", pihole_conf);
fputs("cache-rr=ANY\n", pihole_conf);
fputs("\n", pihole_conf);
// Add option for PCAP file recording
if(strlen(conf->files.pcap.v.s) > 0)
@ -583,9 +593,6 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
// Skip the first 24 lines as they contain the header
if(files_different(DNSMASQ_TEMP_CONF, DNSMASQ_PH_CONFIG, 24))
{
// Rotate old config files
rotate_files(DNSMASQ_PH_CONFIG, NULL);
if(rename(DNSMASQ_TEMP_CONF, DNSMASQ_PH_CONFIG) != 0)
{
log_err("Cannot install dnsmasq config file: %s", strerror(errno));
@ -734,8 +741,8 @@ bool read_legacy_cnames_config(void)
bool read_legacy_custom_hosts_config(void)
{
// Check if file exists, if not, there is nothing to do
const char *path = DNSMASQ_CUSTOM_LIST;
const char *target = DNSMASQ_CUSTOM_LIST".bck";
const char *path = DNSMASQ_CUSTOM_LIST_LEGACY;
const char *target = DNSMASQ_CUSTOM_LIST_LEGACY".bck";
if(!file_exists(path))
return true;
@ -797,19 +804,19 @@ bool read_legacy_custom_hosts_config(void)
bool write_custom_list(void)
{
log_debug(DEBUG_CONFIG, "Opening "DNSMASQ_CUSTOM_LIST".tmp for writing");
FILE *custom_list = fopen(DNSMASQ_CUSTOM_LIST".tmp", "w");
log_debug(DEBUG_CONFIG, "Opening "DNSMASQ_CUSTOM_LIST_LEGACY".tmp for writing");
FILE *custom_list = fopen(DNSMASQ_CUSTOM_LIST_LEGACY".tmp", "w");
// Return early if opening failed
if(!custom_list)
{
log_err("Cannot open "DNSMASQ_CUSTOM_LIST" for writing, unable to update custom.list: %s", strerror(errno));
log_err("Cannot open "DNSMASQ_CUSTOM_LIST_LEGACY".tmp for writing, unable to update custom.list: %s", strerror(errno));
return false;
}
// Lock file, may block if the file is currently opened
if(flock(fileno(custom_list), LOCK_EX) != 0)
{
log_err("Cannot open "DNSMASQ_CUSTOM_LIST" in exclusive mode: %s", strerror(errno));
log_err("Cannot open "DNSMASQ_CUSTOM_LIST_LEGACY".tmp in exclusive mode: %s", strerror(errno));
fclose(custom_list);
return false;
}
@ -853,12 +860,9 @@ bool write_custom_list(void)
// Check if the new config file is different from the old one
// Skip the first 24 lines as they contain the header
if(files_different(DNSMASQ_CUSTOM_LIST".tmp", DNSMASQ_CUSTOM_LIST, 24))
if(files_different(DNSMASQ_CUSTOM_LIST_LEGACY".tmp", DNSMASQ_CUSTOM_LIST, 24))
{
// Rotate old hosts files
rotate_files(DNSMASQ_CUSTOM_LIST, NULL);
if(rename(DNSMASQ_CUSTOM_LIST".tmp", DNSMASQ_CUSTOM_LIST) != 0)
if(rename(DNSMASQ_CUSTOM_LIST_LEGACY".tmp", DNSMASQ_CUSTOM_LIST) != 0)
{
log_err("Cannot install custom.list: %s", strerror(errno));
return false;
@ -869,7 +873,7 @@ bool write_custom_list(void)
{
log_debug(DEBUG_CONFIG, "custom.list unchanged");
// Remove temporary config file
if(remove(DNSMASQ_CUSTOM_LIST".tmp") != 0)
if(remove(DNSMASQ_CUSTOM_LIST_LEGACY".tmp") != 0)
{
log_err("Cannot remove temporary custom.list: %s", strerror(errno));
return false;

View File

@ -26,7 +26,9 @@ bool write_custom_list(void);
#define DNSMASQ_TEMP_CONF "/etc/pihole/dnsmasq.conf.temp"
#define DNSMASQ_STATIC_LEASES "/etc/pihole/04-pihole-static-dhcp.conf"
#define DNSMASQ_CNAMES "/etc/pihole/05-pihole-custom-cname.conf"
#define DNSMASQ_CUSTOM_LIST "/etc/pihole/custom.list"
#define DNSMASQ_HOSTSDIR "/etc/pihole/hosts"
#define DNSMASQ_CUSTOM_LIST DNSMASQ_HOSTSDIR"/custom.list"
#define DNSMASQ_CUSTOM_LIST_LEGACY "/etc/pihole/custom.list"
#define DHCPLEASESFILE "/etc/pihole/dhcp.leases"
#endif //DNSMASQ_CONFIG_H

View File

@ -22,6 +22,25 @@
// files_different()
#include "files.h"
static void migrate_config(void)
{
// Migrating dhcp.domain -> dns.domain
if(strcmp(config.dns.domain.v.s, config.dns.domain.d.s) == 0)
{
// If the domain is the same as the default, check if the dhcp domain
// is different from the default. If so, migrate it
if(strcmp(config.dhcp.domain.v.s, config.dhcp.domain.d.s) != 0)
{
// Migrate dhcp.domain -> dns.domain
log_info("Migrating dhcp.domain = \"%s\" -> dns.domain", config.dhcp.domain.v.s);
if(config.dns.domain.t == CONF_STRING_ALLOCATED)
free(config.dns.domain.v.s);
config.dns.domain.v.s = strdup(config.dhcp.domain.v.s);
config.dns.domain.t = CONF_STRING_ALLOCATED;
}
}
}
bool writeFTLtoml(const bool verbose)
{
// Try to open a temporary config file for writing
@ -44,6 +63,9 @@ bool writeFTLtoml(const bool verbose)
fputs(get_FTL_version(), fp);
fputs("\n\n", fp);
// Perform possible config migration
migrate_config();
// Iterate over configuration and store it into the file
char *last_path = (char*)"";
for(unsigned int i = 0; i < CONFIG_ELEMENTS; i++)

View File

@ -1663,8 +1663,8 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
querystr = "INSERT INTO client (ip,comment) VALUES (:item,:comment) "\
"ON CONFLICT(ip) DO UPDATE SET comment = :comment;";
else // domainlist
querystr = "INSERT INTO domainlist (domain,type,enabled,comment) VALUES (:item,:type,:enabled,:comment) "\
"ON CONFLICT(domain) DO UPDATE SET type = :type, enabled = :enabled, comment = :comment;";
querystr = "INSERT INTO domainlist (domain,type,enabled,comment) VALUES (:item,:oldtype,:enabled,:comment) "\
"ON CONFLICT(domain,type) DO UPDATE SET type = :type, enabled = :enabled, comment = :comment;";
}
int rc = sqlite3_prepare_v2(gravity_db, querystr, -1, &stmt, NULL);
@ -1672,7 +1672,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
{
*message = sqlite3_errmsg(gravity_db);
log_err("gravityDB_addToTable(%d, %s) - SQL error prepare (%i): %s",
row->type_int, row->domain, rc, *message);
row->type_int, row->item, rc, *message);
return false;
}
@ -1694,7 +1694,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
{
*message = sqlite3_errmsg(gravity_db);
log_err("gravityDB_addToTable(%d, %s): Failed to bind name (error %d) - %s",
row->type_int, row->name, rc, *message);
row->type_int, row->item, rc, *message);
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
return false;
@ -1706,7 +1706,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
{
*message = sqlite3_errmsg(gravity_db);
log_err("gravityDB_addToTable(%d, %s): Failed to bind type (error %d) - %s",
row->type_int, row->domain, rc, *message);
row->type_int, row->item, rc, *message);
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
return false;
@ -1727,7 +1727,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
// Error, one is not meaningful without the other
*message = "Field type missing from request";
log_err("gravityDB_addToTable(%d, %s): type missing",
row->type_int, row->domain);
row->type_int, row->item);
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
return false;
@ -1737,7 +1737,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
// Error, one is not meaningful without the other
*message = "Field oldkind missing from request";
log_err("gravityDB_addToTable(%d, %s): Oldkind missing",
row->type_int, row->domain);
row->type_int, row->item);
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
return false;
@ -1745,7 +1745,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
else
{
if(strcasecmp("allow", row->type) == 0 &&
strcasecmp("exact", row->kind) == 0)
strcasecmp("exact", row->kind) == 0)
oldtype = 0;
else if(strcasecmp("deny", row->type) == 0 &&
strcasecmp("exact", row->kind) == 0)
@ -1760,7 +1760,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
{
*message = "Cannot interpret type/kind";
log_err("gravityDB_addToTable(%d, %s): Failed to identify type=\"%s\", kind=\"%s\"",
row->type_int, row->domain, row->type, row->kind);
row->type_int, row->item, row->type, row->kind);
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
return false;
@ -1772,7 +1772,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
{
*message = sqlite3_errmsg(gravity_db);
log_err("gravityDB_addToTable(%d, %s): Failed to bind oldtype (error %d) - %s",
row->type_int, row->domain, rc, *message);
row->type_int, row->item, rc, *message);
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
return false;
@ -1785,7 +1785,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
{
*message = sqlite3_errmsg(gravity_db);
log_err("gravityDB_addToTable(%d, %s): Failed to bind enabled (error %d) - %s",
row->type_int, row->domain, rc, *message);
row->type_int, row->item, rc, *message);
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
return false;
@ -1797,7 +1797,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
{
*message = sqlite3_errmsg(gravity_db);
log_err("gravityDB_addToTable(%d, %s): Failed to bind comment (error %d) - %s",
row->type_int, row->domain, rc, *message);
row->type_int, row->item, rc, *message);
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
return false;

View File

@ -54,6 +54,8 @@ static const char *get_message_type_str(const enum message_type type)
return "LIST";
case DISK_MESSAGE_EXTENDED:
return "DISK_EXTENDED";
case CERTIFICATE_DOMAIN_MISMATCH_MESSAGE:
return "CERTIFICATE_DOMAIN_MISMATCH";
case MAX_MESSAGE:
default:
return "UNKNOWN";
@ -84,6 +86,8 @@ static enum message_type get_message_type_from_string(const char *typestr)
return INACCESSIBLE_ADLIST_MESSAGE;
else if (strcmp(typestr, "DISK_EXTENDED") == 0)
return DISK_MESSAGE_EXTENDED;
else if (strcmp(typestr, "CERTIFICATE_DOMAIN_MISMATCH") == 0)
return CERTIFICATE_DOMAIN_MISMATCH_MESSAGE;
else
return MAX_MESSAGE;
}
@ -167,6 +171,14 @@ static unsigned char message_blob_types[MAX_MESSAGE][5] =
SQLITE_TEXT, // File system type
SQLITE_TEXT, // Directory mounted on
SQLITE_NULL // not used
},
{
// CERTIFICATE_DOMAIN_MISMATCH_MESSAGE: The message column contains the certificate file
SQLITE_TEXT, // domain
SQLITE_NULL, // not used
SQLITE_NULL, // not used
SQLITE_NULL, // not used
SQLITE_NULL // not used
}
};
// Create message table in the database
@ -333,6 +345,8 @@ static int add_message(const enum message_type type,
case SQLITE_NULL: /* Fall through */
default:
log_warn("add_message(type=%s, message=%s) - Excess property, binding NULL",
get_message_type_str(type), message);
rc = sqlite3_bind_null(stmt, 3 + j);
break;
}
@ -653,6 +667,28 @@ static void format_inaccessible_adlist_message(char *plain, const int sizeof_pla
free(escaped_address);
}
static void format_certificate_domain_mismatch(char *plain, const int sizeof_plain, char *html, const int sizeof_html,
const char *certfile, const char*domain)
{
if(snprintf(plain, sizeof_plain, "SSL/TLS certificate %s does not match domain %s!", certfile, domain) > sizeof_plain)
log_warn("format_certificate_domain_mismatch(): Buffer too small to hold plain message, warning truncated");
// Return early if HTML text is not required
if(sizeof_html < 1 || html == NULL)
return;
char *escaped_certfile = escape_html(certfile);
char *escaped_domain = escape_html(domain);
if(snprintf(html, sizeof_html, "SSL/TLS certificate %s does not match domain <strong>%s</strong>!", escaped_certfile, escaped_domain) > sizeof_html)
log_warn("format_certificate_domain_mismatch(): Buffer too small to hold HTML message, warning truncated");
if(escaped_certfile != NULL)
free(escaped_certfile);
if(escaped_domain != NULL)
free(escaped_domain);
}
int count_messages(const bool filter_dnsmasq_warnings)
{
int count = 0;
@ -876,6 +912,17 @@ bool format_messages(cJSON *array)
break;
}
case CERTIFICATE_DOMAIN_MISMATCH_MESSAGE:
{
const char *certfile = (const char*)sqlite3_column_text(stmt, 3);
const char *domain = (const char*)sqlite3_column_text(stmt, 4);
format_certificate_domain_mismatch(plain, sizeof(plain), html, sizeof(html),
certfile, domain);
break;
}
}
// Add the plain message
@ -1095,3 +1142,19 @@ void logg_inaccessible_adlist(const int dbindex, const char *address)
if(rowid == -1)
log_err("logg_inaccessible_adlist(): Failed to add message to database");
}
void log_certificate_domain_mismatch(const char *certfile, const char *domain)
{
// Create message
char buf[2048];
format_certificate_domain_mismatch(buf, sizeof(buf), NULL, 0, certfile, domain);
// Log to FTL.log
log_warn("%s", buf);
// Log to database
const int rowid = add_message(CERTIFICATE_DOMAIN_MISMATCH_MESSAGE, certfile, 1, domain);
if(rowid == -1)
log_err("log_certificate_domain_mismatch(): Failed to add message to database");
}

View File

@ -28,5 +28,6 @@ void logg_rate_limit_message(const char *clientIP, const unsigned int rate_limit
void logg_warn_dnsmasq_message(char *message);
void log_resource_shortage(const double load, const int nprocs, const int shmem, const int disk, const char *path, const char *msg);
void logg_inaccessible_adlist(const int dbindex, const char *address);
void log_certificate_domain_mismatch(const char *certfile, const char *domain);
#endif //MESSAGETABLE_H

View File

@ -64,7 +64,7 @@ bool add_session_app_column(sqlite3 *db)
}
// Store all session in database
bool backup_db_sessions(struct session *sessions)
bool backup_db_sessions(struct session *sessions, const uint16_t max_sessions)
{
if(!config.webserver.session.restore.v.b)
{
@ -89,7 +89,7 @@ bool backup_db_sessions(struct session *sessions)
}
unsigned int api_sessions = 0;
for(unsigned int i = 0; i < API_MAX_CLIENTS; i++)
for(unsigned int i = 0; i < max_sessions; i++)
{
// Get session
struct session *sess = &sessions[i];
@ -198,8 +198,8 @@ bool backup_db_sessions(struct session *sessions)
return false;
}
log_info("Stored %u API session%s in the database",
api_sessions, api_sessions == 1 ? "" : "s");
log_info("Stored %u/%u API session%s in the database",
api_sessions, max_sessions, max_sessions == 1 ? "" : "s");
// Close database connection
dbclose(&db);
@ -208,7 +208,7 @@ bool backup_db_sessions(struct session *sessions)
}
// Restore all sessions found in the database
bool restore_db_sessions(struct session *sessions)
bool restore_db_sessions(struct session *sessions, const uint16_t max_sessions)
{
if(!config.webserver.session.restore.v.b)
{
@ -237,7 +237,7 @@ bool restore_db_sessions(struct session *sessions)
// Iterate over all still valid sessions
unsigned int i = 0;
while(sqlite3_step(stmt) == SQLITE_ROW && i++ < API_MAX_CLIENTS)
while(sqlite3_step(stmt) == SQLITE_ROW && i < max_sessions)
{
// Allocate memory for new session
struct session *sess = &sessions[i];
@ -292,10 +292,12 @@ bool restore_db_sessions(struct session *sessions)
// Mark session as used
sess->used = true;
i++;
}
log_info("Restored %u API session%s from the database",
i, i == 1 ? "" : "s");
log_info("Restored %u/%u API session%s from the database",
i, max_sessions, max_sessions == 1 ? "" : "s");
// Finalize statement
if(sqlite3_finalize(stmt) != SQLITE_OK)

View File

@ -16,7 +16,7 @@
bool create_session_table(sqlite3 *db);
bool add_session_app_column(sqlite3 *db);
bool backup_db_sessions(struct session *sessions);
bool restore_db_sessions(struct session *sessions);
bool backup_db_sessions(struct session *sessions, const uint16_t max_sessions);
bool restore_db_sessions(struct session *sessions, const uint16_t max_sessions);
#endif // SESSION_TABLE_PRIVATE_H

View File

@ -270,6 +270,7 @@ enum message_type {
DISK_MESSAGE,
INACCESSIBLE_ADLIST_MESSAGE,
DISK_MESSAGE_EXTENDED,
CERTIFICATE_DOMAIN_MISMATCH_MESSAGE,
MAX_MESSAGE,
} __attribute__ ((packed));
@ -311,4 +312,13 @@ enum adlist_type {
ADLIST_ALLOW
} __attribute__ ((packed));
enum cert_check {
CERT_FILE_NOT_FOUND,
CERT_CANNOT_PARSE_CERT,
CERT_CANNOT_PARSE_KEY,
CERT_DOMAIN_MISMATCH,
CERT_DOMAIN_MATCH,
CERT_OKAY
} __attribute__ ((packed));
#endif // ENUMS_H

View File

@ -339,7 +339,7 @@ void importsetupVarsConf(void)
get_conf_upstream_servers_from_setupVars(&config.dns.upstreams);
// Try to get Pi-hole domain
get_conf_string_from_setupVars("PIHOLE_DOMAIN", &config.dhcp.domain);
get_conf_string_from_setupVars("PIHOLE_DOMAIN", &config.dns.domain);
// Try to get bool properties (the first two are intentionally set from the same key)
get_conf_bool_from_setupVars("DNS_FQDN_REQUIRED", &config.dns.domainNeeded);

View File

@ -524,6 +524,10 @@ void read_and_parse_payload(struct ftl_conn *api)
// See https://www.w3.org/International/questions/qa-escapes#use
char *__attribute__((malloc)) escape_html(const char *string)
{
// If the string is NULL, return NULL
if(string == NULL)
return NULL;
// Allocate memory for escaped string
char *escaped = calloc(strlen(string) * 6 + 1, sizeof(char));
if(!escaped)

View File

@ -8,24 +8,26 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
#include "../FTL.h"
#include "webserver.h"
#include "FTL.h"
#include "webserver/webserver.h"
// api_handler()
#include "../api/api.h"
#include "api/api.h"
// send_http()
#include "http-common.h"
// struct config
#include "../config/config.h"
#include "config/config.h"
// log_web()
#include "../log.h"
#include "log.h"
// get_nprocs()
#include <sys/sysinfo.h>
// file_readable()
#include "../files.h"
#include "files.h"
// generate_certificate()
#include "x509.h"
#include "webserver/x509.h"
// allocate_lua(), free_lua(), init_lua(), request_handler()
#include "lua_web.h"
#include "webserver/lua_web.h"
// log_certificate_domain_mismatch()
#include "database/message-table.h"
// Server context handle
static struct mg_context *ctx = NULL;
@ -341,6 +343,10 @@ void http_init(void)
if(file_readable(config.webserver.tls.cert.v.s))
{
if(read_certificate(config.webserver.tls.cert.v.s, config.webserver.domain.v.s, false) != CERT_DOMAIN_MATCH)
{
log_certificate_domain_mismatch(config.webserver.tls.cert.v.s, config.webserver.domain.v.s);
}
options[++next_option] = "ssl_certificate";
options[++next_option] = config.webserver.tls.cert.v.s;

View File

@ -145,6 +145,17 @@ bool generate_certificate(const char* certfile, bool rsa, const char *domain)
serial[i] = '0' + (serial[i] % 10);
serial[sizeof(serial) - 1] = '\0';
// Create validity period
// Use YYYYMMDDHHMMSS as required by RFC 5280
const time_t now = time(NULL);
struct tm tms = { 0 };
struct tm *tm = localtime_r(&now, &tms);
char not_before[16] = { 0 };
char not_after[16] = { 0 };
strftime(not_before, sizeof(not_before), "%Y%m%d%H%M%S", tm);
tm->tm_year += 30; // 30 years from now
strftime(not_after, sizeof(not_after), "%Y%m%d%H%M%S", tm);
// Generate certificate
printf("Generating new certificate with serial number %s...\n", serial);
mbedtls_x509write_crt_set_version(&crt, MBEDTLS_X509_CRT_VERSION_3);
@ -154,7 +165,7 @@ bool generate_certificate(const char* certfile, bool rsa, const char *domain)
mbedtls_x509write_crt_set_subject_key(&crt, &key);
mbedtls_x509write_crt_set_issuer_key(&crt, &key);
mbedtls_x509write_crt_set_issuer_name(&crt, "CN=pi.hole");
mbedtls_x509write_crt_set_validity(&crt, "20010101000000", "20301231235959");
mbedtls_x509write_crt_set_validity(&crt, not_before, not_after);
mbedtls_x509write_crt_set_basic_constraints(&crt, 0, -1);
mbedtls_x509write_crt_set_subject_key_identifier(&crt);
mbedtls_x509write_crt_set_authority_key_identifier(&crt);
@ -282,3 +293,239 @@ bool generate_certificate(const char* certfile, bool rsa, const char *domain)
return true;
}
// This function reads a X.509 certificate from a file and prints a
// human-readable representation of the certificate to stdout. If a domain is
// specified, we only check if this domain is present in the certificate.
// Otherwise, we print verbose human-readable information about the certificate
// and about the private key (if requested).
enum cert_check read_certificate(const char* certfile, const char *domain, const bool private_key)
{
if(certfile == NULL && domain == NULL)
{
log_err("No certificate file specified\n");
return CERT_FILE_NOT_FOUND;
}
mbedtls_x509_crt crt;
mbedtls_pk_context key;
mbedtls_entropy_context entropy;
mbedtls_ctr_drbg_context ctr_drbg;
mbedtls_x509_crt_init(&crt);
mbedtls_pk_init(&key);
mbedtls_entropy_init(&entropy);
mbedtls_ctr_drbg_init(&ctr_drbg);
printf("Reading certificate from %s ...\n\n", certfile);
// Check if the file exists and is readable
if(access(certfile, R_OK) != 0)
{
log_err("Could not read certificate file: %s\n", strerror(errno));
return CERT_FILE_NOT_FOUND;
}
int rc = mbedtls_pk_parse_keyfile(&key, certfile, NULL, mbedtls_ctr_drbg_random, &ctr_drbg);
if (rc != 0)
{
log_err("Cannot parse key: Error code %d\n", rc);
return CERT_CANNOT_PARSE_KEY;
}
rc = mbedtls_x509_crt_parse_file(&crt, certfile);
if (rc != 0)
{
log_err("Cannot parse certificate: Error code %d\n", rc);
return CERT_CANNOT_PARSE_CERT;
}
// Parse mbedtls_x509_parse_subject_alt_names()
mbedtls_x509_sequence *sans = &crt.subject_alt_names;
bool found = false;
if(domain != NULL)
{
// Loop over all SANs
while(sans != NULL)
{
// Parse the SAN
mbedtls_x509_subject_alternative_name san = { 0 };
const int ret = mbedtls_x509_parse_subject_alt_name(&sans->buf, &san);
// Check if SAN is used (otherwise ret < 0, e.g.,
// MBEDTLS_ERR_X509_FEATURE_UNAVAILABLE) and if it is a
// DNS name, skip otherwise
if(ret < 0 || san.type != MBEDTLS_X509_SAN_DNS_NAME)
goto next_san;
// Check if the SAN matches the domain
if(strncasecmp(domain, (char*)san.san.unstructured_name.p, san.san.unstructured_name.len) == 0)
{
found = true;
break;
}
next_san:
// Go to next SAN
sans = sans->next;
}
// Also check against the common name (CN) field
char subject[MBEDTLS_X509_MAX_DN_NAME_SIZE];
if(mbedtls_x509_dn_gets(subject, sizeof(subject), &crt.subject) > 0)
{
// Check subject == "CN=<domain>"
if(strlen(subject) > 3 && strncasecmp(subject, "CN=", 3) == 0 && strcasecmp(domain, subject + 3) == 0)
found = true;
// Check subject == "<domain>"
else if(strcasecmp(domain, subject) == 0)
found = true;
}
// Free resources
mbedtls_x509_crt_free(&crt);
mbedtls_pk_free(&key);
mbedtls_entropy_free(&entropy);
mbedtls_ctr_drbg_free(&ctr_drbg);
return found ? CERT_DOMAIN_MATCH : CERT_DOMAIN_MISMATCH;
}
// else: Print verbose information about the certificate
char certinfo[BUFFER_SIZE] = { 0 };
mbedtls_x509_crt_info(certinfo, BUFFER_SIZE, " ", &crt);
puts("Certificate (X.509):\n");
puts(certinfo);
if(!private_key)
goto end;
puts("Private key:");
const char *keytype = mbedtls_pk_get_name(&key);
printf(" Type: %s\n", keytype);
mbedtls_pk_type_t pk_type = mbedtls_pk_get_type(&key);
if(pk_type == MBEDTLS_PK_RSA)
{
mbedtls_rsa_context *rsa = mbedtls_pk_rsa(key);
printf(" RSA modulus: %zu bit\n", 8*mbedtls_rsa_get_len(rsa));
mbedtls_mpi E, N, P, Q, D;
mbedtls_mpi_init(&E); // E = public exponent (public)
mbedtls_mpi_init(&N); // N = P * Q (public)
mbedtls_mpi_init(&P); // P = prime factor 1 (private)
mbedtls_mpi_init(&Q); // Q = prime factor 2 (private)
mbedtls_mpi_init(&D); // D = private exponent (private)
mbedtls_mpi DP, DQ, QP;
mbedtls_mpi_init(&DP);
mbedtls_mpi_init(&DQ);
mbedtls_mpi_init(&QP);
if(mbedtls_rsa_export(rsa, &N, &P, &Q, &D, &E) != 0 ||
mbedtls_rsa_export_crt(rsa, &DP, &DQ, &QP) != 0)
{
puts(" could not export RSA parameters\n");
return EXIT_FAILURE;
}
puts(" Core parameters:");
if(mbedtls_mpi_write_file(" Exponent:\n E = 0x", &E, 16, NULL) != 0)
{
puts(" could not write MPI\n");
return EXIT_FAILURE;
}
if(mbedtls_mpi_write_file(" Modulus:\n N = 0x", &N, 16, NULL) != 0)
{
puts(" could not write MPI\n");
return EXIT_FAILURE;
}
if(mbedtls_mpi_cmp_mpi(&P, &Q) >= 0)
{
if(mbedtls_mpi_write_file(" Prime factors:\n P = 0x", &P, 16, NULL) != 0 ||
mbedtls_mpi_write_file(" Q = 0x", &Q, 16, NULL) != 0)
{
puts(" could not write MPIs\n");
return EXIT_FAILURE;
}
}
else
{
if(mbedtls_mpi_write_file(" Prime factors:\n Q = 0x", &Q, 16, NULL) != 0 ||
mbedtls_mpi_write_file("\n P = 0x", &P, 16, NULL) != 0)
{
puts(" could not write MPIs\n");
return EXIT_FAILURE;
}
}
if(mbedtls_mpi_write_file(" Private exponent:\n D = 0x", &D, 16, NULL) != 0)
{
puts(" could not write MPI\n");
return EXIT_FAILURE;
}
mbedtls_mpi_free(&N);
mbedtls_mpi_free(&P);
mbedtls_mpi_free(&Q);
mbedtls_mpi_free(&D);
mbedtls_mpi_free(&E);
puts(" CRT parameters:");
if(mbedtls_mpi_write_file(" D mod (P-1):\n DP = 0x", &DP, 16, NULL) != 0 ||
mbedtls_mpi_write_file(" D mod (Q-1):\n DQ = 0x", &DQ, 16, NULL) != 0 ||
mbedtls_mpi_write_file(" Q^-1 mod P:\n QP = 0x", &QP, 16, NULL) != 0)
{
puts(" could not write MPIs\n");
return EXIT_FAILURE;
}
mbedtls_mpi_free(&DP);
mbedtls_mpi_free(&DQ);
mbedtls_mpi_free(&QP);
}
else if(pk_type == MBEDTLS_PK_ECKEY)
{
mbedtls_ecp_keypair *ec = mbedtls_pk_ec(key);
mbedtls_ecp_curve_type ec_type = mbedtls_ecp_get_type(&ec->private_grp);
switch (ec_type)
{
case MBEDTLS_ECP_TYPE_NONE:
puts(" Curve type: Unknown");
break;
case MBEDTLS_ECP_TYPE_SHORT_WEIERSTRASS:
puts(" Curve type: Short Weierstrass (y^2 = x^3 + a x + b)");
break;
case MBEDTLS_ECP_TYPE_MONTGOMERY:
puts(" Curve type: Montgomery (y^2 = x^3 + a x^2 + x)");
break;
}
const size_t bitlen = mbedtls_mpi_bitlen(&ec->private_d);
printf(" Bitlen: %zu bit\n", bitlen);
mbedtls_mpi_write_file(" Private key:\n D = 0x", &ec->private_d, 16, NULL);
mbedtls_mpi_write_file(" Public key:\n X = 0x", &ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(X), 16, NULL);
mbedtls_mpi_write_file(" Y = 0x", &ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(Y), 16, NULL);
mbedtls_mpi_write_file(" Z = 0x", &ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(Z), 16, NULL);
}
else
{
puts("Sorry, but FTL does not know how to print key information for this type\n");
goto end;
}
// Print private key in PEM format
mbedtls_pk_write_key_pem(&key, (unsigned char*)certinfo, BUFFER_SIZE);
puts("Private key (PEM):");
puts(certinfo);
end:
// Print public key in PEM format
mbedtls_pk_write_pubkey_pem(&key, (unsigned char*)certinfo, BUFFER_SIZE);
puts("Public key (PEM):");
puts(certinfo);
// Free resources
mbedtls_x509_crt_free(&crt);
mbedtls_pk_free(&key);
mbedtls_entropy_free(&entropy);
mbedtls_ctr_drbg_free(&ctr_drbg);
return CERT_OKAY;
}

View File

@ -13,6 +13,9 @@
#include <mbedtls/entropy.h>
#include <mbedtls/ctr_drbg.h>
#include "enums.h"
bool generate_certificate(const char* certfile, bool rsa, const char *domain);
enum cert_check read_certificate(const char* certfile, const char *domain, const bool private_key);
#endif // X509_H

View File

@ -152,8 +152,8 @@ class ResponseVerifyer():
# Check for properties in FTL that are not in the API specs
for property in FTLflat.keys():
if property not in YAMLflat.keys() and len([p.startswith(property + ".") for p in YAMLflat.keys()]) == 0:
self.errors.append("Property '" + property + "' missing in the API specs (have " + ",".join(YAMLflat.keys()) + ")")
if property not in YAMLflat.keys():
self.errors.append("Property '" + property + "' missing in the API specs")
elif expected_mimetype == "application/zip":
file_like_object = io.BytesIO(FTLresponse)
@ -296,6 +296,9 @@ class ResponseVerifyer():
for j in FTLprop[i]:
if not self.verify_property(YAMLprop['items']['properties'], YAMLexamples, FTLprop[i], props + [i, str(j)]):
all_okay = False
# Add this property to the YAML response
self.YAMLresponse[flat_path] = []
else:
# Check this property

View File

@ -110,6 +110,27 @@
# same way as for DHCP-derived names
expandHosts = false
# The DNS domain used by your Pi-hole to expand hosts and for DHCP.
#
# Only if DHCP is enabled below: For DHCP, this has two effects; firstly it causes the
# DHCP server to return the domain to any hosts which request it, and secondly it sets
# the domain which it is legal for DHCP-configured hosts to claim. The intention is to
# constrain hostnames so that an untrusted host on the LAN cannot advertise its name
# via DHCP as e.g. "google.com" and capture traffic not meant for it. If no domain
# suffix is specified, then any DHCP hostname with a domain part (ie with a period)
# will be disallowed and logged. If a domain is specified, then hostnames with a
# domain part are allowed, provided the domain part matches the suffix. In addition,
# when a suffix is set then hostnames without a domain part have the suffix added as
# an optional domain part. For instance, we can set domain=mylab.com and have a
# machine whose DHCP hostname is "laptop". The IP address for that machine is
# available both as "laptop" and "laptop.mylab.com".
#
# You can disable setting a domain by setting this option to an empty string.
#
# Possible values are:
# <any valid domain>
domain = "lan"
# Should all reverse lookups for private IP ranges (i.e., 192.168.x.y, etc) which are
# not found in /etc/hosts or the DHCP leases file be answered with "no such domain"
# rather than being forwarded upstream?
@ -370,7 +391,10 @@
# <ip-addr>, e.g., "192.168.0.1"
router = ""
# The DNS domain used by your Pi-hole
# The DNS domain used by your Pi-hole (*** DEPRECATED ***)
# This setting is deprecated and will be removed in a future version. Please use
# dns.domain instead. Setting it to any non-default value will overwrite the value of
# dns.domain if it is still set to its default value.
#
# Possible values are:
# <any valid domain>
@ -604,6 +628,12 @@
# sense of the option means only 127.0.0.1 and [::1]
searchAPIauth = false
# Number of concurrent sessions allowed for the API. If the number of sessions exceeds
# this value, no new sessions will be allowed until the number of sessions drops due
# to session expiration or logout. Note that the number of concurrent sessions is
# irrelevant if authentication is disabled as no sessions are used in this case.
max_sessions = 16
# Should FTL prettify the API output (add extra spaces, newlines and indentation)?
prettyJSON = false
@ -775,6 +805,9 @@
# malfunctioning addr2line can prevent from generating any backtrace at all.
addr2line = true
# Should FTL load additional dnsmasq configuration files from /etc/dnsmasq.d/?
etc_dnsmasq_d = true ### CHANGED, default = false
# Additional lines to inject into the generated dnsmasq configuration. Warning: This is
# an advanced setting and should only be used with care. Incorrectly formatted or
# duplicated lines as well as lines conflicting with the automatic configuration of

View File

@ -1368,6 +1368,89 @@
run bash -c 'curl -I --cacert /etc/pihole/test.crt --resolve pi.hole:443:127.0.0.1 https://pi.hole/'
}
@test "X.509 certificate parser returns expected result" {
# We are getting the certificate from the config
run bash -c './pihole-FTL --read-x509'
printf "%s\n" "${lines[@]}"
[[ "${lines[0]}" == "Reading certificate from /etc/pihole/test.pem ..." ]]
[[ "${lines[1]}" == "Certificate (X.509):" ]]
[[ "${lines[2]}" == " cert. version : 3" ]]
[[ "${lines[3]}" == " serial number : 30:36:35:35:38:30:34:30:38:32:39:39:39:31:36" ]]
[[ "${lines[4]}" == " issuer name : CN=pi.hole" ]]
[[ "${lines[5]}" == " subject name : CN=pi.hole" ]]
[[ "${lines[6]}" == " issued on : 2001-01-01 00:00:00" ]]
[[ "${lines[7]}" == " expires on : 2030-12-31 23:59:59" ]]
[[ "${lines[8]}" == " signed using : ECDSA with SHA256" ]]
[[ "${lines[9]}" == " EC key size : 521 bits" ]]
[[ "${lines[10]}" == " basic constraints : CA=false" ]]
[[ "${lines[11]}" == "Public key (PEM):" ]]
[[ "${lines[12]}" == "-----BEGIN PUBLIC KEY-----" ]]
[[ "${lines[13]}" == "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBQ51HeOLjSap1Xr+pnFQJqvBZc92T" ]]
[[ "${lines[14]}" == "XyL4KwIZdpsHl95Pc0Xcn8Xzyox0cWhMyycQgcGbIw3nuefCZaXfc3CuU30BPDdb" ]]
[[ "${lines[15]}" == "91h+rDhV4+VkEkANPBbgKQ6kCiHNtMAdugyaeHxzFpqegGGvgQ2l4Vp98l4M7zBC" ]]
[[ "${lines[16]}" == "G6K/RbZDlDvNUCgwElE=" ]]
[[ "${lines[17]}" == "-----END PUBLIC KEY-----" ]]
[[ "${lines[18]}" == "" ]]
}
@test "X.509 certificate parser returns expected result (with private key)" {
# We are explicitly specifying the certificate file here
run bash -c './pihole-FTL --read-x509-key /etc/pihole/test.pem'
printf "%s\n" "${lines[@]}"
[[ "${lines[0]}" == "Reading certificate from /etc/pihole/test.pem ..." ]]
[[ "${lines[1]}" == "Certificate (X.509):" ]]
[[ "${lines[2]}" == " cert. version : 3" ]]
[[ "${lines[3]}" == " serial number : 30:36:35:35:38:30:34:30:38:32:39:39:39:31:36" ]]
[[ "${lines[4]}" == " issuer name : CN=pi.hole" ]]
[[ "${lines[5]}" == " subject name : CN=pi.hole" ]]
[[ "${lines[6]}" == " issued on : 2001-01-01 00:00:00" ]]
[[ "${lines[7]}" == " expires on : 2030-12-31 23:59:59" ]]
[[ "${lines[8]}" == " signed using : ECDSA with SHA256" ]]
[[ "${lines[9]}" == " EC key size : 521 bits" ]]
[[ "${lines[10]}" == " basic constraints : CA=false" ]]
[[ "${lines[11]}" == "Private key:" ]]
[[ "${lines[12]}" == " Type: EC" ]]
[[ "${lines[13]}" == " Curve type: Short Weierstrass (y^2 = x^3 + a x + b)" ]]
[[ "${lines[14]}" == " Bitlen: 518 bit" ]]
[[ "${lines[15]}" == " Private key:" ]]
[[ "${lines[16]}" == " D = 0x2CBE6CF8A913B445F211165B0473B7037B5B06187C8685AEF4A58354C7061C388173E0B00374A55CEAC7BB5886159C9D54B3C020564355A0FA71A55559304156D8"* ]]
[[ "${lines[17]}" == " Public key:" ]]
[[ "${lines[18]}" == " X = 0x01439D4778E2E349AA755EBFA99C5409AAF05973DD935F22F82B0219769B0797DE4F7345DC9FC5F3CA8C7471684CCB271081C19B230DE7B9E7C265A5DF7370AE537D"* ]]
[[ "${lines[19]}" == " Y = 0x013C375BF7587EAC3855E3E56412400D3C16E0290EA40A21CDB4C01DBA0C9A787C73169A9E8061AF810DA5E15A7DF25E0CEF30421BA2BF45B643943BCD5028301251"* ]]
[[ "${lines[20]}" == " Z = 0x01"* ]]
[[ "${lines[21]}" == "Private key (PEM):" ]]
[[ "${lines[22]}" == "-----BEGIN EC PRIVATE KEY-----" ]]
[[ "${lines[23]}" == "MIHcAgEBBEIALL5s+KkTtEXyERZbBHO3A3tbBhh8hoWu9KWDVMcGHDiBc+CwA3Sl" ]]
[[ "${lines[24]}" == "XOrHu1iGFZydVLPAIFZDVaD6caVVWTBBVtigBwYFK4EEACOhgYkDgYYABAFDnUd4" ]]
[[ "${lines[25]}" == "4uNJqnVev6mcVAmq8Flz3ZNfIvgrAhl2mweX3k9zRdyfxfPKjHRxaEzLJxCBwZsj" ]]
[[ "${lines[26]}" == "Dee558Jlpd9zcK5TfQE8N1v3WH6sOFXj5WQSQA08FuApDqQKIc20wB26DJp4fHMW" ]]
[[ "${lines[27]}" == "mp6AYa+BDaXhWn3yXgzvMEIbor9FtkOUO81QKDASUQ==" ]]
[[ "${lines[28]}" == "-----END EC PRIVATE KEY-----" ]]
[[ "${lines[29]}" == "Public key (PEM):" ]]
[[ "${lines[30]}" == "-----BEGIN PUBLIC KEY-----" ]]
[[ "${lines[31]}" == "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBQ51HeOLjSap1Xr+pnFQJqvBZc92T" ]]
[[ "${lines[32]}" == "XyL4KwIZdpsHl95Pc0Xcn8Xzyox0cWhMyycQgcGbIw3nuefCZaXfc3CuU30BPDdb" ]]
[[ "${lines[33]}" == "91h+rDhV4+VkEkANPBbgKQ6kCiHNtMAdugyaeHxzFpqegGGvgQ2l4Vp98l4M7zBC" ]]
[[ "${lines[34]}" == "G6K/RbZDlDvNUCgwElE=" ]]
[[ "${lines[35]}" == "-----END PUBLIC KEY-----" ]]
[[ "${lines[36]}" == "" ]]
}
@test "X.509 certificate parser can check if domain is included" {
run bash -c './pihole-FTL --read-x509-key /etc/pihole/test.pem pi.hole'
printf "%s\n" "${lines[@]}"
[[ "${lines[0]}" == "Reading certificate from /etc/pihole/test.pem ..." ]]
[[ "${lines[1]}" == "Certificate matches domain pi.hole" ]]
[[ "${lines[2]}" == "" ]]
[[ $status == 0 ]]
run bash -c './pihole-FTL --read-x509-key /etc/pihole/test.pem pi-hole.net'
printf "%s\n" "${lines[@]}"
[[ "${lines[0]}" == "Reading certificate from /etc/pihole/test.pem ..." ]]
[[ "${lines[1]}" == "Certificate does not match domain pi-hole.net" ]]
[[ "${lines[2]}" == "" ]]
[[ $status == 1 ]]
}
@test "Test embedded GZIP compressor" {
run bash -c './pihole-FTL gzip test/pihole-FTL.db.sql'
printf "Compression output:\n"