Merge branch 'development-v6' into fix/read_rotated_toml_on_error
Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
commit
1a517c7358
|
@ -1,5 +1,8 @@
|
|||
name: Codespell
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
|
|
|
@ -138,7 +138,7 @@
|
|||
// caused by insufficient memory or by code bugs (not properly dealing
|
||||
// with NULL pointers) much easier.
|
||||
#undef strdup // strdup() is a macro in itself, it needs special handling
|
||||
#define free(ptr) FTLfree(ptr, __FILE__, __FUNCTION__, __LINE__)
|
||||
#define free(ptr) FTLfree((void**)&ptr, __FILE__, __FUNCTION__, __LINE__)
|
||||
#define strdup(str_in) FTLstrdup(str_in, __FILE__, __FUNCTION__, __LINE__)
|
||||
#define calloc(numer_of_elements, element_size) FTLcalloc(numer_of_elements, element_size, __FILE__, __FUNCTION__, __LINE__)
|
||||
#define realloc(ptr, new_size) FTLrealloc(ptr, new_size, __FILE__, __FUNCTION__, __LINE__)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
#include "shmem.h"
|
||||
// hash_password()
|
||||
#include "config/password.h"
|
||||
// main_pid()
|
||||
#include "signals.h"
|
||||
|
||||
static struct {
|
||||
const char *name;
|
||||
|
@ -128,12 +130,22 @@ static cJSON *addJSONvalue(const enum conf_type conf_type, union conf_value *val
|
|||
return cJSON_CreateStringReference(get_temp_unit_str(val->temp_unit));
|
||||
case CONF_STRUCT_IN_ADDR:
|
||||
{
|
||||
// Special case 0.0.0.0 -> return empty string
|
||||
if(val->in_addr.s_addr == INADDR_ANY)
|
||||
return cJSON_CreateStringReference("");
|
||||
|
||||
// else: normal address
|
||||
char addr4[INET_ADDRSTRLEN] = { 0 };
|
||||
inet_ntop(AF_INET, &val->in_addr, addr4, INET_ADDRSTRLEN);
|
||||
return cJSON_CreateString(addr4); // Performs a copy
|
||||
}
|
||||
case CONF_STRUCT_IN6_ADDR:
|
||||
{
|
||||
// Special case :: -> return empty string
|
||||
if(memcmp(&val->in6_addr, &in6addr_any, sizeof(in6addr_any)) == 0)
|
||||
return cJSON_CreateStringReference("");
|
||||
|
||||
// else: normal address
|
||||
char addr6[INET6_ADDRSTRLEN] = { 0 };
|
||||
inet_ntop(AF_INET6, &val->in6_addr, addr6, INET6_ADDRSTRLEN);
|
||||
return cJSON_CreateString(addr6); // Performs a copy
|
||||
|
@ -400,11 +412,19 @@ static const char *getJSONvalue(struct conf_item *conf_item, cJSON *elem, struct
|
|||
struct in_addr addr4 = { 0 };
|
||||
if(!cJSON_IsString(elem))
|
||||
return "not of type string";
|
||||
if(!inet_pton(AF_INET, elem->valuestring, &addr4))
|
||||
if(strlen(elem->valuestring) == 0)
|
||||
{
|
||||
// Special case: empty string -> 0.0.0.0
|
||||
conf_item->v.in_addr.s_addr = INADDR_ANY;
|
||||
}
|
||||
else if(inet_pton(AF_INET, elem->valuestring, &addr4))
|
||||
{
|
||||
// Set item
|
||||
memcpy(&conf_item->v.in_addr, &addr4, sizeof(addr4));
|
||||
}
|
||||
else
|
||||
return "not a valid IPv4 address";
|
||||
// Set item
|
||||
memcpy(&conf_item->v.in_addr, &addr4, sizeof(addr4));
|
||||
log_debug(DEBUG_CONFIG, "%s = %s", conf_item->k, elem->valuestring);
|
||||
log_debug(DEBUG_CONFIG, "%s = \"%s\"", conf_item->k, elem->valuestring);
|
||||
break;
|
||||
}
|
||||
case CONF_STRUCT_IN6_ADDR:
|
||||
|
@ -412,11 +432,16 @@ static const char *getJSONvalue(struct conf_item *conf_item, cJSON *elem, struct
|
|||
struct in6_addr addr6 = { 0 };
|
||||
if(!cJSON_IsString(elem))
|
||||
return "not of type string";
|
||||
if(!inet_pton(AF_INET6, elem->valuestring, &addr6))
|
||||
if(strlen(elem->valuestring) == 0)
|
||||
{
|
||||
// Special case: empty string -> ::
|
||||
memcpy(&conf_item->v.in6_addr, &in6addr_any, sizeof(in6addr_any));
|
||||
}
|
||||
else if(!inet_pton(AF_INET6, elem->valuestring, &addr6))
|
||||
return "not a valid IPv6 address";
|
||||
// Set item
|
||||
memcpy(&conf_item->v.in6_addr, &addr6, sizeof(addr6));
|
||||
log_debug(DEBUG_CONFIG, "%s = %s", conf_item->k, elem->valuestring);
|
||||
log_debug(DEBUG_CONFIG, "%s = \"%s\"", conf_item->k, elem->valuestring);
|
||||
break;
|
||||
}
|
||||
case CONF_JSON_STRING_ARRAY:
|
||||
|
@ -659,6 +684,7 @@ static int api_config_patch(struct ftl_conn *api)
|
|||
// Read all known config items
|
||||
bool config_changed = false;
|
||||
bool dnsmasq_changed = false;
|
||||
bool rewrite_hosts = false;
|
||||
struct config newconf;
|
||||
duplicate_config(&newconf, &config);
|
||||
for(unsigned int i = 0; i < CONFIG_ELEMENTS; i++)
|
||||
|
@ -693,8 +719,23 @@ static int api_config_patch(struct ftl_conn *api)
|
|||
const char *response = getJSONvalue(new_item, elem, &newconf);
|
||||
if(response != NULL)
|
||||
{
|
||||
log_err("/api/config: %s invalid: %s", new_item->k, response);
|
||||
continue;
|
||||
char *hint = calloc(strlen(new_item->k) + strlen(response) + 3, sizeof(char));
|
||||
if(hint == NULL)
|
||||
{
|
||||
free_config(&newconf);
|
||||
return send_json_error(api, 500,
|
||||
"internal_error",
|
||||
"Failed to allocate memory for hint",
|
||||
NULL);
|
||||
}
|
||||
strcpy(hint, new_item->k);
|
||||
strcat(hint, ": ");
|
||||
strcat(hint, response);
|
||||
free_config(&newconf);
|
||||
return send_json_error_free(api, 400,
|
||||
"bad_request",
|
||||
"Config item is invalid",
|
||||
hint, true);
|
||||
}
|
||||
|
||||
// Get pointer to memory location of this conf_item (global)
|
||||
|
@ -730,6 +771,10 @@ static int api_config_patch(struct ftl_conn *api)
|
|||
if(conf_item->f & FLAG_RESTART_FTL)
|
||||
dnsmasq_changed = true;
|
||||
|
||||
// Check if this item requires rewriting the HOSTS file
|
||||
if(conf_item == &config.dns.hosts)
|
||||
rewrite_hosts = true;
|
||||
|
||||
// If the privacy level was decreased, we need to restart
|
||||
if(new_item == &newconf.misc.privacylevel &&
|
||||
new_item->v.privacy_level < conf_item->v.privacy_level)
|
||||
|
@ -768,6 +813,10 @@ static int api_config_patch(struct ftl_conn *api)
|
|||
|
||||
// Store changed configuration to disk
|
||||
writeFTLtoml(true);
|
||||
|
||||
// Rewrite HOSTS file if required
|
||||
if(rewrite_hosts)
|
||||
write_custom_list();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -816,6 +865,7 @@ static int api_config_put_delete(struct ftl_conn *api)
|
|||
|
||||
// Read all known config items
|
||||
bool dnsmasq_changed = false;
|
||||
bool rewrite_hosts = false;
|
||||
bool found = false;
|
||||
struct config newconf;
|
||||
duplicate_config(&newconf, &config);
|
||||
|
@ -907,6 +957,10 @@ static int api_config_put_delete(struct ftl_conn *api)
|
|||
if(new_item->f & FLAG_RESTART_FTL)
|
||||
dnsmasq_changed = true;
|
||||
|
||||
// Check if this item requires rewriting the HOSTS file
|
||||
if(new_item == &newconf.dns.hosts)
|
||||
rewrite_hosts = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -954,6 +1008,10 @@ static int api_config_put_delete(struct ftl_conn *api)
|
|||
// Store changed configuration to disk
|
||||
writeFTLtoml(true);
|
||||
|
||||
// Rewrite HOSTS file if required
|
||||
if(rewrite_hosts)
|
||||
write_custom_list();
|
||||
|
||||
// Send empty reply with matching HTTP status code
|
||||
// 201 - Created or 204 - No content
|
||||
cJSON *json = JSON_NEW_OBJECT();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -206,6 +206,8 @@ components:
|
|||
type: boolean
|
||||
expandHosts:
|
||||
type: boolean
|
||||
domain:
|
||||
type: string
|
||||
bogusPriv:
|
||||
type: boolean
|
||||
dnssec:
|
||||
|
@ -268,8 +270,10 @@ components:
|
|||
type: boolean
|
||||
IPv4:
|
||||
type: string
|
||||
x-format: ipv4
|
||||
IPv6:
|
||||
type: string
|
||||
x-format: ipv6
|
||||
blocking:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -279,8 +283,10 @@ components:
|
|||
type: boolean
|
||||
IPv4:
|
||||
type: string
|
||||
x-format: ipv4
|
||||
IPv6:
|
||||
type: string
|
||||
x-format: ipv6
|
||||
rateLimit:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -295,12 +301,20 @@ components:
|
|||
type: boolean
|
||||
start:
|
||||
type: string
|
||||
x-format: ipv4
|
||||
end:
|
||||
type: string
|
||||
x-format: ipv4
|
||||
router:
|
||||
type: string
|
||||
x-format: ipv4
|
||||
netmask:
|
||||
type: string
|
||||
x-format: ipv4
|
||||
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 +400,8 @@ components:
|
|||
type: boolean
|
||||
searchAPIauth:
|
||||
type: boolean
|
||||
max_sessions:
|
||||
type: integer
|
||||
prettyJSON:
|
||||
type: boolean
|
||||
password:
|
||||
|
@ -450,6 +466,8 @@ components:
|
|||
type: integer
|
||||
addr2line:
|
||||
type: boolean
|
||||
etc_dnsmasq_d:
|
||||
type: boolean
|
||||
privacylevel:
|
||||
type: integer
|
||||
dnsmasq_lines:
|
||||
|
@ -583,6 +601,7 @@ components:
|
|||
- "192.168.2.123 mymusicbox"
|
||||
domainNeeded: true
|
||||
expandHosts: true
|
||||
domain: "lan"
|
||||
bogusPriv: true
|
||||
dnssec: true
|
||||
interface: "eth0"
|
||||
|
@ -627,6 +646,7 @@ components:
|
|||
end: "192.168.0.250"
|
||||
router: "192.168.0.1"
|
||||
domain: "lan"
|
||||
netmask: "0.0.0.0"
|
||||
leaseTime: "24h"
|
||||
ipv6: true
|
||||
rapidCommit: true
|
||||
|
@ -666,6 +686,7 @@ components:
|
|||
api:
|
||||
localAPIauth: false
|
||||
searchAPIauth: false
|
||||
max_sessions: 16
|
||||
prettyJSON: false
|
||||
password: "********"
|
||||
pwhash: ''
|
||||
|
@ -694,6 +715,7 @@ components:
|
|||
delay_startup: 10
|
||||
addr2line: true
|
||||
privacylevel: 0
|
||||
etc_dnsmasq_d: false
|
||||
dnsmasq_lines: [ ]
|
||||
check:
|
||||
load: true
|
||||
|
|
|
@ -25,51 +25,11 @@
|
|||
|
||||
int api_history(struct ftl_conn *api)
|
||||
{
|
||||
unsigned int from = 0, until = OVERTIME_SLOTS;
|
||||
const time_t now = time(NULL);
|
||||
bool found = false;
|
||||
|
||||
lock_shm();
|
||||
time_t mintime = overTime[0].timestamp;
|
||||
|
||||
// Start with the first non-empty overTime slot
|
||||
for(unsigned int slot = 0; slot < OVERTIME_SLOTS; slot++)
|
||||
{
|
||||
if((overTime[slot].total > 0 || overTime[slot].blocked > 0) &&
|
||||
overTime[slot].timestamp >= mintime)
|
||||
{
|
||||
from = slot;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// End with last non-empty overTime slot or the last slot that is not
|
||||
// older than the maximum history to be sent
|
||||
for(unsigned int slot = 0; slot < OVERTIME_SLOTS; slot++)
|
||||
{
|
||||
if(overTime[slot].timestamp >= now ||
|
||||
overTime[slot].timestamp - now > (time_t)config.webserver.api.maxHistory.v.ui)
|
||||
{
|
||||
until = slot;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no data to be sent, we send back an empty array
|
||||
// and thereby return early
|
||||
if(!found)
|
||||
{
|
||||
cJSON *json = JSON_NEW_ARRAY();
|
||||
cJSON *item = JSON_NEW_OBJECT();
|
||||
JSON_ADD_ITEM_TO_ARRAY(json, item);
|
||||
JSON_SEND_OBJECT_UNLOCK(json);
|
||||
}
|
||||
|
||||
// Minimum structure is
|
||||
// {"history":[]}
|
||||
// Loop over all overTime slots and add them to the array
|
||||
cJSON *history = JSON_NEW_ARRAY();
|
||||
for(unsigned int slot = from; slot < until; slot++)
|
||||
for(unsigned int slot = 0; slot < OVERTIME_SLOTS; slot++)
|
||||
{
|
||||
cJSON *item = JSON_NEW_OBJECT();
|
||||
JSON_ADD_NUMBER_TO_OBJECT(item, "timestamp", overTime[slot].timestamp);
|
||||
|
@ -79,33 +39,22 @@ int api_history(struct ftl_conn *api)
|
|||
JSON_ADD_ITEM_TO_ARRAY(history, item);
|
||||
}
|
||||
|
||||
// Unlock already here to avoid keeping the lock during JSON generation
|
||||
// This is safe because we don't access any shared memory after this
|
||||
// point. All numbers in the JSON are copied
|
||||
unlock_shm();
|
||||
|
||||
// Minimum structure is
|
||||
// {"history":[]}
|
||||
cJSON *json = JSON_NEW_OBJECT();
|
||||
JSON_ADD_ITEM_TO_OBJECT(json, "history", history);
|
||||
JSON_SEND_OBJECT_UNLOCK(json);
|
||||
JSON_SEND_OBJECT(json);
|
||||
}
|
||||
|
||||
int api_history_clients(struct ftl_conn *api)
|
||||
{
|
||||
int sendit = false;
|
||||
unsigned int from = 0, until = OVERTIME_SLOTS;
|
||||
const time_t now = time(NULL);
|
||||
|
||||
lock_shm();
|
||||
|
||||
// Find minimum ID to send
|
||||
for(unsigned int slot = 0; slot < OVERTIME_SLOTS; slot++)
|
||||
{
|
||||
if((overTime[slot].total > 0 || overTime[slot].blocked > 0) &&
|
||||
overTime[slot].timestamp >= overTime[0].timestamp)
|
||||
{
|
||||
sendit = true;
|
||||
from = slot;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Exit before processing any data if requested via config setting
|
||||
if(config.misc.privacylevel.v.privacy_level >= PRIVACY_HIDE_DOMAINS_CLIENTS || !sendit)
|
||||
if(config.misc.privacylevel.v.privacy_level >= PRIVACY_HIDE_DOMAINS_CLIENTS)
|
||||
{
|
||||
// Minimum structure is
|
||||
// {"history":[], "clients":[]}
|
||||
|
@ -117,17 +66,7 @@ int api_history_clients(struct ftl_conn *api)
|
|||
JSON_SEND_OBJECT_UNLOCK(json);
|
||||
}
|
||||
|
||||
// End with last non-empty overTime slot or the last slot that is not
|
||||
// older than the maximum history to be sent
|
||||
for(unsigned int slot = 0; slot < OVERTIME_SLOTS; slot++)
|
||||
{
|
||||
if(overTime[slot].timestamp >= now ||
|
||||
overTime[slot].timestamp - now > (time_t)config.webserver.api.maxHistory.v.ui)
|
||||
{
|
||||
until = slot;
|
||||
break;
|
||||
}
|
||||
}
|
||||
lock_shm();
|
||||
|
||||
// Get clients which the user doesn't want to see
|
||||
// if skipclient[i] == true then this client should be hidden from
|
||||
|
@ -154,7 +93,7 @@ int api_history_clients(struct ftl_conn *api)
|
|||
}
|
||||
}
|
||||
|
||||
// Also skip alias-clients
|
||||
// Also skip clients included in others (in alias-clients)
|
||||
for(int clientID = 0; clientID < counters->clients; clientID++)
|
||||
{
|
||||
// Get client pointer
|
||||
|
@ -165,9 +104,9 @@ int api_history_clients(struct ftl_conn *api)
|
|||
skipclient[clientID] = true;
|
||||
}
|
||||
|
||||
cJSON *history = JSON_NEW_ARRAY();
|
||||
// Main return loop
|
||||
for(unsigned int slot = from; slot < until; slot++)
|
||||
cJSON *history = JSON_NEW_ARRAY();
|
||||
for(unsigned int slot = 0; slot < OVERTIME_SLOTS; slot++)
|
||||
{
|
||||
cJSON *item = JSON_NEW_OBJECT();
|
||||
JSON_ADD_NUMBER_TO_OBJECT(item, "timestamp", overTime[slot].timestamp);
|
||||
|
@ -196,8 +135,8 @@ int api_history_clients(struct ftl_conn *api)
|
|||
cJSON *json = JSON_NEW_OBJECT();
|
||||
JSON_ADD_ITEM_TO_OBJECT(json, "history", history);
|
||||
|
||||
cJSON *clients = JSON_NEW_ARRAY();
|
||||
// Loop over clients to generate output to be sent to the client
|
||||
cJSON *clients = JSON_NEW_ARRAY();
|
||||
for(int clientID = 0; clientID < counters->clients; clientID++)
|
||||
{
|
||||
if(skipclient[clientID])
|
||||
|
@ -216,10 +155,16 @@ int api_history_clients(struct ftl_conn *api)
|
|||
JSON_REF_STR_IN_OBJECT(item, "ip", client_ip);
|
||||
JSON_ADD_ITEM_TO_ARRAY(clients, item);
|
||||
}
|
||||
JSON_ADD_ITEM_TO_OBJECT(json, "clients", clients);
|
||||
|
||||
// Unlock already here to avoid keeping the lock during JSON generation
|
||||
// This is safe because we don't access any shared memory after this
|
||||
// point and all strings in the JSON are references to idempotent shared
|
||||
// memory and can, thus, be accessed at any time without locking
|
||||
unlock_shm();
|
||||
|
||||
// Free memory
|
||||
free(skipclient);
|
||||
|
||||
JSON_SEND_OBJECT_UNLOCK(json);
|
||||
JSON_ADD_ITEM_TO_OBJECT(json, "clients", clients);
|
||||
JSON_SEND_OBJECT(json);
|
||||
}
|
||||
|
|
|
@ -393,7 +393,8 @@ static int api_list_write(struct ftl_conn *api,
|
|||
strchr(it->valuestring, '\t') != NULL ||
|
||||
strchr(it->valuestring, '\n') != NULL)
|
||||
{
|
||||
cJSON_free(row.items);
|
||||
if(allocated_json)
|
||||
cJSON_free(row.items);
|
||||
return send_json_error(api, 400, // 400 Bad Request
|
||||
"bad_request",
|
||||
"Spaces, newlines and tabs are not allowed in domains and URLs",
|
||||
|
@ -406,11 +407,12 @@ static int api_list_write(struct ftl_conn *api,
|
|||
if(!okay)
|
||||
{
|
||||
// Send error reply
|
||||
cJSON_free(row.items);
|
||||
return send_json_error(api, 400, // 400 Bad Request
|
||||
"regex_error",
|
||||
"Regex validation failed",
|
||||
regex_msg);
|
||||
if(allocated_json)
|
||||
cJSON_free(row.items);
|
||||
return send_json_error_free(api, 400, // 400 Bad Request
|
||||
"regex_error",
|
||||
"Regex validation failed",
|
||||
regex_msg, true);
|
||||
}
|
||||
|
||||
// Try to add item(s) to table
|
||||
|
|
|
@ -244,9 +244,11 @@ int api_search(struct ftl_conn *api)
|
|||
char *allow_list = cJSON_PrintUnformatted(allow_ids);
|
||||
ret = search_table(api,punycode, GRAVITY_DOMAINLIST_ALLOW_REGEX, allow_list, limit, &Nregex, false, domains);
|
||||
free(allow_list);
|
||||
free(punycode);
|
||||
if(ret != 200)
|
||||
{
|
||||
free(punycode);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
if(cJSON_GetArraySize(deny_ids) > 0)
|
||||
|
@ -254,9 +256,11 @@ int api_search(struct ftl_conn *api)
|
|||
char *deny_list = cJSON_PrintUnformatted(deny_ids);
|
||||
ret = search_table(api, punycode, GRAVITY_DOMAINLIST_DENY_REGEX, deny_list, limit, &Nregex, false, domains);
|
||||
free(deny_list);
|
||||
free(punycode);
|
||||
if(ret != 200)
|
||||
{
|
||||
free(punycode);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON *search = JSON_NEW_OBJECT();
|
||||
|
|
60
src/args.c
60
src/args.c
|
@ -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");
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
#ifndef CAPABILITIES_H
|
||||
#define CAPABILITIES_H
|
||||
|
||||
#include <linux/capability.h>
|
||||
|
||||
bool check_capability(const unsigned int cap);
|
||||
bool check_capabilities(void);
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
#include "tomlc99/toml.h"
|
||||
// hash_password()
|
||||
#include "config/password.h"
|
||||
// check_capability()
|
||||
#include "capabilities.h"
|
||||
|
||||
// Read a TOML value from a table depending on its type
|
||||
static bool readStringValue(struct conf_item *conf_item, const char *value, struct config *newconf)
|
||||
|
@ -295,7 +297,12 @@ static bool readStringValue(struct conf_item *conf_item, const char *value, stru
|
|||
case CONF_STRUCT_IN_ADDR:
|
||||
{
|
||||
struct in_addr addr4 = { 0 };
|
||||
if(inet_pton(AF_INET, value, &addr4))
|
||||
if(strlen(value) == 0)
|
||||
{
|
||||
// Special case: empty string -> 0.0.0.0
|
||||
conf_item->v.in_addr.s_addr = INADDR_ANY;
|
||||
}
|
||||
else if(inet_pton(AF_INET, value, &addr4))
|
||||
memcpy(&conf_item->v.in_addr, &addr4, sizeof(addr4));
|
||||
else
|
||||
{
|
||||
|
@ -307,7 +314,12 @@ static bool readStringValue(struct conf_item *conf_item, const char *value, stru
|
|||
case CONF_STRUCT_IN6_ADDR:
|
||||
{
|
||||
struct in6_addr addr6 = { 0 };
|
||||
if(inet_pton(AF_INET6, value, &addr6))
|
||||
if(strlen(value) == 0)
|
||||
{
|
||||
// Special case: empty string -> ::
|
||||
memcpy(&conf_item->v.in6_addr, &in6addr_any, sizeof(in6addr_any));
|
||||
}
|
||||
else if(inet_pton(AF_INET6, value, &addr6))
|
||||
memcpy(&conf_item->v.in6_addr, &addr6, sizeof(addr6));
|
||||
else
|
||||
{
|
||||
|
@ -353,6 +365,25 @@ static bool readStringValue(struct conf_item *conf_item, const char *value, stru
|
|||
|
||||
int set_config_from_CLI(const char *key, const char *value)
|
||||
{
|
||||
// Check if we are either
|
||||
// - root, or
|
||||
// - pihole with CAP_CHOWN capability on the pihole-FTL binary
|
||||
const uid_t euid = geteuid();
|
||||
const struct passwd *current_user = getpwuid(euid);
|
||||
const bool is_root = euid == 0;
|
||||
const bool is_pihole = current_user != NULL && strcmp(current_user->pw_name, "pihole") == 0;
|
||||
const bool have_chown_cap = check_capability(CAP_CHOWN);
|
||||
if(!is_root && !(is_pihole && have_chown_cap))
|
||||
{
|
||||
if(is_pihole)
|
||||
printf("Permission error: CAP_CHOWN is missing on the binary\n");
|
||||
else
|
||||
printf("Permission error: User %s is not allowed to edit Pi-hole's config\n", current_user->pw_name);
|
||||
|
||||
printf("Please run this command using sudo\n\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Identify config option
|
||||
struct config newconf;
|
||||
duplicate_config(&newconf, &config);
|
||||
|
@ -420,10 +451,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();
|
||||
}
|
||||
|
||||
|
|
|
@ -462,7 +462,7 @@ void initConfig(struct config *conf)
|
|||
conf->dns.hosts.h = "Array of custom DNS records\n Example: hosts = [ \"127.0.0.1 mylocal\", \"192.168.0.1 therouter\" ]";
|
||||
conf->dns.hosts.a = cJSON_CreateStringReference("Array of custom DNS records each one in HOSTS form: \"IP HOSTNAME\"");
|
||||
conf->dns.hosts.t = CONF_JSON_STRING_ARRAY;
|
||||
conf->dns.hosts.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
|
||||
conf->dns.hosts.f = FLAG_ADVANCED_SETTING;
|
||||
conf->dns.hosts.d.json = cJSON_CreateArray();
|
||||
|
||||
conf->dns.domainNeeded.k = "dns.domainNeeded";
|
||||
|
@ -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();
|
||||
|
@ -686,32 +693,39 @@ void initConfig(struct config *conf)
|
|||
|
||||
conf->dhcp.start.k = "dhcp.start";
|
||||
conf->dhcp.start.h = "Start address of the DHCP address pool";
|
||||
conf->dhcp.start.a = cJSON_CreateStringReference("<ip-addr>, e.g., \"192.168.0.10\"");
|
||||
conf->dhcp.start.t = CONF_STRING;
|
||||
conf->dhcp.start.a = cJSON_CreateStringReference("<valid IPv4 address> or empty string (\"\"), e.g., \"192.168.0.10\"");
|
||||
conf->dhcp.start.t = CONF_STRUCT_IN_ADDR;
|
||||
conf->dhcp.start.f = FLAG_RESTART_FTL;
|
||||
conf->dhcp.start.d.s = (char*)"";
|
||||
memset(&conf->dhcp.start.d.in_addr, 0, sizeof(struct in_addr));
|
||||
|
||||
conf->dhcp.end.k = "dhcp.end";
|
||||
conf->dhcp.end.h = "End address of the DHCP address pool";
|
||||
conf->dhcp.end.a = cJSON_CreateStringReference("<ip-addr>, e.g., \"192.168.0.250\"");
|
||||
conf->dhcp.end.t = CONF_STRING;
|
||||
conf->dhcp.end.a = cJSON_CreateStringReference("<valid IPv4 address> or empty string (\"\"), e.g., \"192.168.0.250\"");
|
||||
conf->dhcp.end.t = CONF_STRUCT_IN_ADDR;
|
||||
conf->dhcp.end.f = FLAG_RESTART_FTL;
|
||||
conf->dhcp.end.d.s = (char*)"";
|
||||
memset(&conf->dhcp.end.d.in_addr, 0, sizeof(struct in_addr));
|
||||
|
||||
conf->dhcp.router.k = "dhcp.router";
|
||||
conf->dhcp.router.h = "Address of the gateway to be used (typically the address of your router in a home installation)";
|
||||
conf->dhcp.router.a = cJSON_CreateStringReference("<ip-addr>, e.g., \"192.168.0.1\"");
|
||||
conf->dhcp.router.t = CONF_STRING;
|
||||
conf->dhcp.router.a = cJSON_CreateStringReference("<valid IPv4 address> or empty string (\"\"), e.g., \"192.168.0.1\"");
|
||||
conf->dhcp.router.t = CONF_STRUCT_IN_ADDR;
|
||||
conf->dhcp.router.f = FLAG_RESTART_FTL;
|
||||
conf->dhcp.router.d.s = (char*)"";
|
||||
memset(&conf->dhcp.router.d.in_addr, 0, sizeof(struct in_addr));
|
||||
|
||||
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;
|
||||
conf->dhcp.domain.d.s = (char*)"lan";
|
||||
|
||||
conf->dhcp.netmask.k = "dhcp.netmask";
|
||||
conf->dhcp.netmask.h = "The netmask used by your Pi-hole. For directly connected networks (i.e., networks on which the machine running Pi-hole has an interface) the netmask is optional and may be set to an empty string (\"\"): it will then be determined from the interface configuration itself. For networks which receive DHCP service via a relay agent, we cannot determine the netmask itself, so it should explicitly be specified, otherwise Pi-hole guesses based on the class (A, B or C) of the network address.";
|
||||
conf->dhcp.netmask.a = cJSON_CreateStringReference("<any valid netmask> (e.g., \"255.255.255.0\") or empty string (\"\") for auto-discovery");
|
||||
conf->dhcp.netmask.t = CONF_STRUCT_IN_ADDR;
|
||||
conf->dhcp.netmask.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
|
||||
memset(&conf->dhcp.netmask.d.in_addr, 0, sizeof(struct in_addr));
|
||||
|
||||
conf->dhcp.leaseTime.k = "dhcp.leaseTime";
|
||||
conf->dhcp.leaseTime.h = "If the lease time is given, then leases will be given for that length of time. If not given, the default lease time is one hour for IPv4 and one day for IPv6.";
|
||||
conf->dhcp.leaseTime.a = cJSON_CreateStringReference("The lease time can be in seconds, or minutes (e.g., \"45m\") or hours (e.g., \"1h\") or days (like \"2d\") or even weeks (\"1w\"). You may also use \"infinite\" as string but be aware of the drawbacks");
|
||||
|
@ -824,13 +838,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 +877,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 +918,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 +1006,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 +1020,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 +1041,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 +1058,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 +1086,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 +1095,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
|
||||
|
|
|
@ -33,6 +33,11 @@
|
|||
// This static string represents an unchanged password
|
||||
#define PASSWORD_VALUE "********"
|
||||
|
||||
// Remove the following line to disable the use of UTF-8 in the config file
|
||||
// As consequence, the config file will be written in ASCII and all non-ASCII
|
||||
// characters will be replaced by their UTF-8 escape sequences (UCS-2)
|
||||
#define TOML_UTF8
|
||||
|
||||
union conf_value {
|
||||
bool b; // boolean value
|
||||
int i; // integer value
|
||||
|
@ -88,7 +93,7 @@ enum conf_type {
|
|||
#define FLAG_PSEUDO_ITEM (1 << 2)
|
||||
#define FLAG_INVALIDATE_SESSIONS (1 << 3)
|
||||
#define FLAG_WRITE_ONLY (1 << 4)
|
||||
#define FLAG_ENV_VAR (1 << 5)
|
||||
#define FLAG_ENV_VAR (1 << 5)
|
||||
|
||||
struct conf_item {
|
||||
const char *k; // item Key
|
||||
|
@ -125,6 +130,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;
|
||||
|
@ -178,6 +184,7 @@ struct config {
|
|||
struct conf_item end;
|
||||
struct conf_item router;
|
||||
struct conf_item domain;
|
||||
struct conf_item netmask;
|
||||
struct conf_item leaseTime;
|
||||
struct conf_item ipv6;
|
||||
struct conf_item rapidCommit;
|
||||
|
@ -226,6 +233,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 +269,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;
|
||||
|
|
|
@ -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());
|
||||
|
@ -221,6 +222,73 @@ static void write_config_header(FILE *fp, const char *description)
|
|||
|
||||
bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_config, char errbuf[ERRBUF_SIZE])
|
||||
{
|
||||
// Early config checks
|
||||
if(conf->dhcp.active.v.b)
|
||||
{
|
||||
// Check if the addresses are valid
|
||||
// The addresses should neither be 0.0.0.0 nor 255.255.255.255
|
||||
if((ntohl(conf->dhcp.start.v.in_addr.s_addr) == 0) ||
|
||||
(ntohl(conf->dhcp.start.v.in_addr.s_addr) == 0xFFFFFFFF))
|
||||
{
|
||||
strncpy(errbuf, "DHCP start address is not valid", ERRBUF_SIZE);
|
||||
log_err("Unable to update dnsmasq configuration: %s", errbuf);
|
||||
return false;
|
||||
}
|
||||
if((ntohl(conf->dhcp.end.v.in_addr.s_addr) == 0) ||
|
||||
(ntohl(conf->dhcp.end.v.in_addr.s_addr) == 0xFFFFFFFF))
|
||||
{
|
||||
strncpy(errbuf, "DHCP end address is not valid", ERRBUF_SIZE);
|
||||
log_err("Unable to update dnsmasq configuration: %s", errbuf);
|
||||
return false;
|
||||
}
|
||||
if((ntohl(conf->dhcp.router.v.in_addr.s_addr) == 0) ||
|
||||
(ntohl(conf->dhcp.router.v.in_addr.s_addr) == 0xFFFFFFFF))
|
||||
{
|
||||
strncpy(errbuf, "DHCP router address is not valid", ERRBUF_SIZE);
|
||||
log_err("Unable to update dnsmasq configuration: %s", errbuf);
|
||||
return false;
|
||||
}
|
||||
// The addresses should neither end in .0 or .255 in the last octet
|
||||
if((ntohl(conf->dhcp.start.v.in_addr.s_addr) & 0xFF) == 0 ||
|
||||
(ntohl(conf->dhcp.start.v.in_addr.s_addr) & 0xFF) == 0xFF)
|
||||
{
|
||||
strncpy(errbuf, "DHCP start address is not valid", ERRBUF_SIZE);
|
||||
log_err("Unable to update dnsmasq configuration: %s", errbuf);
|
||||
return false;
|
||||
}
|
||||
if((ntohl(conf->dhcp.end.v.in_addr.s_addr) & 0xFF) == 0 ||
|
||||
(ntohl(conf->dhcp.end.v.in_addr.s_addr) & 0xFF) == 0xFF)
|
||||
{
|
||||
strncpy(errbuf, "DHCP end address is not valid", ERRBUF_SIZE);
|
||||
log_err("Unable to update dnsmasq configuration: %s", errbuf);
|
||||
return false;
|
||||
}
|
||||
if((ntohl(conf->dhcp.router.v.in_addr.s_addr) & 0xFF) == 0 ||
|
||||
(ntohl(conf->dhcp.router.v.in_addr.s_addr) & 0xFF) == 0xFF)
|
||||
{
|
||||
strncpy(errbuf, "DHCP router address is not valid", ERRBUF_SIZE);
|
||||
log_err("Unable to update dnsmasq configuration: %s", errbuf);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the DHCP range is valid (start needs to be smaller than end)
|
||||
if(ntohl(conf->dhcp.start.v.in_addr.s_addr) >= ntohl(conf->dhcp.end.v.in_addr.s_addr))
|
||||
{
|
||||
strncpy(errbuf, "DHCP range start address is larger than or equal to the end address", ERRBUF_SIZE);
|
||||
log_err("Unable to update dnsmasq configuration: %s", errbuf);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the router address is within the DHCP range
|
||||
if(ntohl(conf->dhcp.router.v.in_addr.s_addr) >= ntohl(conf->dhcp.start.v.in_addr.s_addr) &&
|
||||
ntohl(conf->dhcp.router.v.in_addr.s_addr) <= ntohl(conf->dhcp.end.v.in_addr.s_addr))
|
||||
{
|
||||
strncpy(errbuf, "DHCP router address should not be within DHCP range", ERRBUF_SIZE);
|
||||
log_err("Unable to update dnsmasq configuration: %s", errbuf);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
log_debug(DEBUG_CONFIG, "Opening "DNSMASQ_TEMP_CONF" for writing");
|
||||
FILE *pihole_conf = fopen(DNSMASQ_TEMP_CONF, "w");
|
||||
// Return early if opening failed
|
||||
|
@ -240,13 +308,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 +347,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 +405,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 +475,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)
|
||||
|
@ -419,12 +494,25 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
|
|||
fputs("# DHCP server setting\n", pihole_conf);
|
||||
fputs("dhcp-authoritative\n", pihole_conf);
|
||||
fputs("dhcp-leasefile="DHCPLEASESFILE"\n", pihole_conf);
|
||||
fprintf(pihole_conf, "dhcp-range=%s,%s,%s\n",
|
||||
conf->dhcp.start.v.s,
|
||||
conf->dhcp.end.v.s,
|
||||
conf->dhcp.leaseTime.v.s);
|
||||
fprintf(pihole_conf, "dhcp-option=option:router,%s\n",
|
||||
conf->dhcp.router.v.s);
|
||||
char start[INET_ADDRSTRLEN] = { 0 },
|
||||
end[INET_ADDRSTRLEN] = { 0 },
|
||||
router[INET_ADDRSTRLEN] = { 0 };
|
||||
inet_ntop(AF_INET, &conf->dhcp.start.v.in_addr, start, INET_ADDRSTRLEN);
|
||||
inet_ntop(AF_INET, &conf->dhcp.end.v.in_addr, end, INET_ADDRSTRLEN);
|
||||
inet_ntop(AF_INET, &conf->dhcp.router.v.in_addr, router, INET_ADDRSTRLEN);
|
||||
fprintf(pihole_conf, "dhcp-range=%s,%s", start, end);
|
||||
// Net mask is optional, only add if it is not 0.0.0.0
|
||||
const struct in_addr inaddr_empty = {0};
|
||||
if(memcmp(&conf->dhcp.netmask.v.in_addr, &inaddr_empty, sizeof(inaddr_empty)) != 0)
|
||||
{
|
||||
char netmask[INET_ADDRSTRLEN] = { 0 };
|
||||
inet_ntop(AF_INET, &conf->dhcp.netmask.v.in_addr, netmask, INET_ADDRSTRLEN);
|
||||
fprintf(pihole_conf, ",%s", netmask);
|
||||
}
|
||||
// Lease time is optional, only add it if it is set
|
||||
if(strlen(conf->dhcp.leaseTime.v.s) > 0)
|
||||
fprintf(pihole_conf, ",%s", conf->dhcp.leaseTime.v.s);
|
||||
fprintf(pihole_conf, "\ndhcp-option=option:router,%s\n", router);
|
||||
|
||||
if(conf->dhcp.rapidCommit.v.b)
|
||||
fputs("dhcp-rapid-commit\n", pihole_conf);
|
||||
|
@ -501,25 +589,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)
|
||||
|
@ -565,6 +655,13 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
|
|||
return false;
|
||||
}
|
||||
|
||||
// Close file
|
||||
if(fclose(pihole_conf) != 0)
|
||||
{
|
||||
log_err("Cannot close dnsmasq config file: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
log_debug(DEBUG_CONFIG, "Testing "DNSMASQ_TEMP_CONF);
|
||||
if(test_config && !test_dnsmasq_config(errbuf))
|
||||
{
|
||||
|
@ -572,21 +669,26 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
|
|||
return false;
|
||||
}
|
||||
|
||||
// Rotate old config files
|
||||
rotate_files(DNSMASQ_PH_CONFIG, NULL);
|
||||
|
||||
log_debug(DEBUG_CONFIG, "Installing "DNSMASQ_TEMP_CONF" to "DNSMASQ_PH_CONFIG);
|
||||
if(rename(DNSMASQ_TEMP_CONF, DNSMASQ_PH_CONFIG) != 0)
|
||||
// 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_TEMP_CONF, DNSMASQ_PH_CONFIG, 24))
|
||||
{
|
||||
log_err("Cannot install dnsmasq config file: %s", strerror(errno));
|
||||
return false;
|
||||
if(rename(DNSMASQ_TEMP_CONF, DNSMASQ_PH_CONFIG) != 0)
|
||||
{
|
||||
log_err("Cannot install dnsmasq config file: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
log_debug(DEBUG_CONFIG, "Config file written to "DNSMASQ_PH_CONFIG);
|
||||
}
|
||||
|
||||
// Close file
|
||||
if(fclose(pihole_conf) != 0)
|
||||
else
|
||||
{
|
||||
log_err("Cannot close dnsmasq config file: %s", strerror(errno));
|
||||
return false;
|
||||
log_debug(DEBUG_CONFIG, "dnsmasq.conf unchanged");
|
||||
// Remove temporary config file
|
||||
if(remove(DNSMASQ_TEMP_CONF) != 0)
|
||||
{
|
||||
log_err("Cannot remove temporary dnsmasq config file: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -719,8 +821,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;
|
||||
|
||||
|
@ -782,22 +884,30 @@ bool read_legacy_custom_hosts_config(void)
|
|||
|
||||
bool write_custom_list(void)
|
||||
{
|
||||
// Rotate old hosts files
|
||||
rotate_files(DNSMASQ_CUSTOM_LIST, NULL);
|
||||
// Ensure that the directory exists
|
||||
if(!directory_exists(DNSMASQ_HOSTSDIR))
|
||||
{
|
||||
log_debug(DEBUG_CONFIG, "Creating directory "DNSMASQ_HOSTSDIR);
|
||||
if(mkdir(DNSMASQ_HOSTSDIR, 0755) != 0)
|
||||
{
|
||||
log_err("Cannot create directory "DNSMASQ_HOSTSDIR": %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
log_debug(DEBUG_CONFIG, "Opening "DNSMASQ_CUSTOM_LIST" for writing");
|
||||
FILE *custom_list = fopen(DNSMASQ_CUSTOM_LIST, "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;
|
||||
}
|
||||
|
@ -838,5 +948,28 @@ bool write_custom_list(void)
|
|||
log_err("Cannot close custom.list: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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_LEGACY".tmp", DNSMASQ_CUSTOM_LIST, 24))
|
||||
{
|
||||
if(rename(DNSMASQ_CUSTOM_LIST_LEGACY".tmp", DNSMASQ_CUSTOM_LIST) != 0)
|
||||
{
|
||||
log_err("Cannot install custom.list: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
log_debug(DEBUG_CONFIG, "HOSTS file written to "DNSMASQ_CUSTOM_LIST);
|
||||
}
|
||||
else
|
||||
{
|
||||
log_debug(DEBUG_CONFIG, "custom.list unchanged");
|
||||
// Remove temporary config file
|
||||
if(remove(DNSMASQ_CUSTOM_LIST_LEGACY".tmp") != 0)
|
||||
{
|
||||
log_err("Cannot remove temporary custom.list: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -47,6 +47,10 @@ FILE * __attribute((malloc)) __attribute((nonnull(1))) openFTLtoml(const char *m
|
|||
{
|
||||
// Use global config file
|
||||
strncpy(filename, GLOBALTOMLPATH, sizeof(filename));
|
||||
|
||||
// Append ".tmp" if we are writing
|
||||
if(mode[0] == 'w')
|
||||
strncat(filename, ".tmp", sizeof(filename));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -148,17 +152,51 @@ static void printTOMLstring(FILE *fp, const char *s, const bool toml)
|
|||
continue;
|
||||
}
|
||||
|
||||
// Escape special characters
|
||||
// Escape special characters with simple escape sequences
|
||||
switch (ch) {
|
||||
case 0x08: fprintf(fp, "\\b"); continue;
|
||||
case 0x09: fprintf(fp, "\\t"); continue;
|
||||
case 0x0a: fprintf(fp, "\\n"); continue;
|
||||
case 0x0c: fprintf(fp, "\\f"); continue;
|
||||
case 0x0d: fprintf(fp, "\\r"); continue;
|
||||
case '"': fprintf(fp, "\\\""); continue;
|
||||
case '\\': fprintf(fp, "\\\\"); continue;
|
||||
default: fprintf(fp, "\\0x%02x", ch & 0xff); continue;
|
||||
case '\b': fputs("\\b", fp); continue;
|
||||
case '\t': fputs("\\t", fp); continue;
|
||||
case '\n': fputs("\\n", fp); continue;
|
||||
case '\f': fputs("\\f", fp); continue;
|
||||
case '\r': fputs("\\r", fp); continue;
|
||||
case '"': fputs("\\\"", fp); continue;
|
||||
case '\\': fputs("\\\\", fp); continue;
|
||||
}
|
||||
|
||||
#ifndef TOML_UTF8
|
||||
// The Universal Coded Character Set (UCS, Unicode) is a
|
||||
// standard set of characters defined by the international
|
||||
// standard ISO/IEC 10646, Information technology — Universal
|
||||
// Coded Character Set (UCS) (plus amendments to that standard),
|
||||
// which is the basis of many character encodings, improving as
|
||||
// characters from previously unrepresented typing systems are
|
||||
// added.
|
||||
// The following code converts a UTF-8 character to UCS and
|
||||
// prints it as \UXXXXXXXX
|
||||
int64_t ucs;
|
||||
int bytes = toml_utf8_to_ucs(s, len, &ucs);
|
||||
if(bytes > 0)
|
||||
{
|
||||
// Print 4-byte UCS as \UXXXXXXXX
|
||||
fprintf(fp, "\\U%08X", (uint32_t)ucs);
|
||||
// Advance string pointer
|
||||
s += bytes - 1;
|
||||
// Decrease remaining string length
|
||||
len -= bytes - 1;
|
||||
continue;
|
||||
}
|
||||
#else
|
||||
// Escape all other control characters as short 2-byte
|
||||
// UCS sequences
|
||||
if(iscntrl(ch))
|
||||
{
|
||||
fprintf(fp, "\\u%04X", ch);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Print remaining characters as is
|
||||
putc(ch, fp);
|
||||
#endif
|
||||
}
|
||||
if(toml) fprintf(fp, "\"");
|
||||
}
|
||||
|
@ -389,6 +427,13 @@ void writeTOMLvalue(FILE * fp, const int indent, const enum conf_type t, union c
|
|||
break;
|
||||
case CONF_STRUCT_IN_ADDR:
|
||||
{
|
||||
// Special case: 0.0.0.0 -> return empty string
|
||||
if(v->in_addr.s_addr == INADDR_ANY)
|
||||
{
|
||||
printTOMLstring(fp, "", toml);
|
||||
break;
|
||||
}
|
||||
// else: normal address
|
||||
char addr4[INET_ADDRSTRLEN] = { 0 };
|
||||
inet_ntop(AF_INET, &v->in_addr, addr4, INET_ADDRSTRLEN);
|
||||
printTOMLstring(fp, addr4, toml);
|
||||
|
@ -396,6 +441,13 @@ void writeTOMLvalue(FILE * fp, const int indent, const enum conf_type t, union c
|
|||
}
|
||||
case CONF_STRUCT_IN6_ADDR:
|
||||
{
|
||||
// Special case: :: -> return empty string
|
||||
if(memcmp(&v->in6_addr, &in6addr_any, sizeof(in6addr_any)) == 0)
|
||||
{
|
||||
printTOMLstring(fp, "", toml);
|
||||
break;
|
||||
}
|
||||
// else: normal address
|
||||
char addr6[INET6_ADDRSTRLEN] = { 0 };
|
||||
inet_ntop(AF_INET6, &v->in6_addr, addr6, INET6_ADDRSTRLEN);
|
||||
printTOMLstring(fp, addr6, toml);
|
||||
|
@ -679,7 +731,12 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t
|
|||
const toml_datum_t val = toml_string_in(toml, key);
|
||||
if(val.ok)
|
||||
{
|
||||
if(inet_pton(AF_INET, val.u.s, &addr4))
|
||||
if(strlen(val.u.s) == 0)
|
||||
{
|
||||
// Special case: empty string -> 0.0.0.0
|
||||
conf_item->v.in_addr.s_addr = INADDR_ANY;
|
||||
}
|
||||
else if(inet_pton(AF_INET, val.u.s, &addr4))
|
||||
memcpy(&conf_item->v.in_addr, &addr4, sizeof(addr4));
|
||||
else
|
||||
log_warn("Config %s is invalid (not of type IPv4 address)", conf_item->k);
|
||||
|
@ -695,7 +752,12 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t
|
|||
const toml_datum_t val = toml_string_in(toml, key);
|
||||
if(val.ok)
|
||||
{
|
||||
if(inet_pton(AF_INET6, val.u.s, &addr6))
|
||||
if(strlen(val.u.s) == 0)
|
||||
{
|
||||
// Special case: empty string -> ::
|
||||
memcpy(&conf_item->v.in6_addr, &in6addr_any, sizeof(in6addr_any));
|
||||
}
|
||||
else if(inet_pton(AF_INET6, val.u.s, &addr6))
|
||||
memcpy(&conf_item->v.in6_addr, &addr6, sizeof(addr6));
|
||||
else
|
||||
log_warn("Config %s is invalid (not of type IPv6 address)", conf_item->k);
|
||||
|
@ -935,7 +997,12 @@ bool readEnvValue(struct conf_item *conf_item, struct config *newconf)
|
|||
case CONF_STRUCT_IN_ADDR:
|
||||
{
|
||||
struct in_addr addr4 = { 0 };
|
||||
if(inet_pton(AF_INET, envvar, &addr4))
|
||||
if(strlen(envvar) == 0)
|
||||
{
|
||||
// Special case: empty string -> 0.0.0.0
|
||||
conf_item->v.in_addr.s_addr = INADDR_ANY;
|
||||
}
|
||||
else if(inet_pton(AF_INET, envvar, &addr4))
|
||||
memcpy(&conf_item->v.in_addr, &addr4, sizeof(addr4));
|
||||
else
|
||||
log_warn("ENV %s is invalid (not of type IPv4 address)", envkey);
|
||||
|
@ -944,7 +1011,12 @@ bool readEnvValue(struct conf_item *conf_item, struct config *newconf)
|
|||
case CONF_STRUCT_IN6_ADDR:
|
||||
{
|
||||
struct in6_addr addr6 = { 0 };
|
||||
if(inet_pton(AF_INET6, envvar, &addr6))
|
||||
if(strlen(envvar) == 0)
|
||||
{
|
||||
// Special case: empty string -> ::
|
||||
memcpy(&conf_item->v.in6_addr, &in6addr_any, sizeof(in6addr_any));
|
||||
}
|
||||
else if(inet_pton(AF_INET6, envvar, &addr6))
|
||||
memcpy(&conf_item->v.in6_addr, &addr6, sizeof(addr6));
|
||||
else
|
||||
log_warn("ENV %s is invalid (not of type IPv6 address)", envkey);
|
||||
|
|
|
@ -141,7 +141,7 @@ static toml_table_t *parseTOML(const unsigned int version)
|
|||
FILE *fp;
|
||||
if((fp = openFTLtoml("r", version)) == NULL)
|
||||
{
|
||||
log_warn("No config file available (%s), using defaults",
|
||||
log_info("No config file available (%s), using defaults",
|
||||
strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
|
|
|
@ -19,39 +19,55 @@
|
|||
#include "datastructure.h"
|
||||
// watch_config()
|
||||
#include "config/inotify.h"
|
||||
// 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)
|
||||
{
|
||||
// Stop watching for changes in the config file
|
||||
watch_config(false);
|
||||
|
||||
// Try to open global config file
|
||||
// Try to open a temporary config file for writing
|
||||
FILE *fp;
|
||||
if((fp = openFTLtoml("w", 0)) == NULL)
|
||||
{
|
||||
log_warn("Cannot write to FTL config file (%s), content not updated", strerror(errno));
|
||||
// Restart watching for changes in the config file
|
||||
watch_config(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Log that we are (re-)writing the config file if either in verbose or
|
||||
// debug mode
|
||||
if(verbose || config.debug.config.v.b)
|
||||
log_info("Writing config file");
|
||||
|
||||
// Write header
|
||||
fputs("# This file is managed by pihole-FTL\n#\n", fp);
|
||||
fputs("# Do not edit the file while FTL is\n", fp);
|
||||
fputs("# running or your changes may be overwritten\n#\n", fp);
|
||||
fprintf(fp, "# Pi-hole configuration file (%s)\n", get_FTL_version());
|
||||
#ifdef TOML_UTF8
|
||||
fputs("# Encoding: UTF-8\n", fp);
|
||||
#else
|
||||
fputs("# Encoding: ASCII + UCS\n", fp);
|
||||
#endif
|
||||
fputs("# This file is managed by pihole-FTL\n", fp);
|
||||
char timestring[TIMESTR_SIZE] = "";
|
||||
get_timestr(timestring, time(NULL), false, false);
|
||||
fputs("# Last updated on ", fp);
|
||||
fputs(timestring, fp);
|
||||
fputs("\n# by FTL ", fp);
|
||||
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++)
|
||||
|
@ -119,8 +135,46 @@ bool writeFTLtoml(const bool verbose)
|
|||
// Close file and release exclusive lock
|
||||
closeFTLtoml(fp);
|
||||
|
||||
// Restart watching for changes in the config file
|
||||
watch_config(true);
|
||||
// Move temporary file to the final location if it is different
|
||||
// We skip the first 8 lines as they contain the header and will always
|
||||
// be different
|
||||
if(files_different(GLOBALTOMLPATH".tmp", GLOBALTOMLPATH, 8))
|
||||
{
|
||||
// Stop watching for changes in the config file
|
||||
watch_config(false);
|
||||
|
||||
// Rotate config file
|
||||
rotate_files(GLOBALTOMLPATH, NULL);
|
||||
|
||||
// Move file
|
||||
if(rename(GLOBALTOMLPATH".tmp", GLOBALTOMLPATH) != 0)
|
||||
{
|
||||
log_warn("Cannot move temporary config file to final location (%s), content not updated", strerror(errno));
|
||||
// Restart watching for changes in the config file
|
||||
watch_config(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Restart watching for changes in the config file
|
||||
watch_config(true);
|
||||
|
||||
// Log that we have written the config file if either in verbose or
|
||||
// debug mode
|
||||
if(verbose || config.debug.config.v.b)
|
||||
log_info("Config file written to %s", GLOBALTOMLPATH);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove temporary file
|
||||
if(unlink(GLOBALTOMLPATH".tmp") != 0)
|
||||
{
|
||||
log_warn("Cannot remove temporary config file (%s), content not updated", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Log that the config file has not changed if in debug mode
|
||||
log_debug(DEBUG_CONFIG, "pihole.toml unchanged");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
15
src/daemon.c
15
src/daemon.c
|
@ -35,6 +35,8 @@
|
|||
#include "webserver/webserver.h"
|
||||
// free_api()
|
||||
#include "api/api.h"
|
||||
// setlocale()
|
||||
#include <locale.h>
|
||||
|
||||
pthread_t threads[THREADS_MAX] = { 0 };
|
||||
bool resolver_ready = false;
|
||||
|
@ -444,3 +446,16 @@ bool ipv6_enabled(void)
|
|||
// IPv6-capable interface
|
||||
return true;
|
||||
}
|
||||
|
||||
void init_locale(void)
|
||||
{
|
||||
// Set locale to system default, needed for libidn to work properly
|
||||
// Without this, libidn will not be able to convert UTF-8 to ASCII
|
||||
// (error message "Character encoding conversion error")
|
||||
setlocale(LC_ALL, "");
|
||||
|
||||
// Set locale for numeric values to C to ensure that we always use
|
||||
// the dot as decimal separator (even if the system locale uses a
|
||||
// comma, e.g., in German)
|
||||
setlocale(LC_NUMERIC, "C");
|
||||
}
|
|
@ -24,6 +24,7 @@ void set_nice(void);
|
|||
void calc_cpu_usage(void);
|
||||
float get_cpu_percentage(void) __attribute__((pure));
|
||||
bool ipv6_enabled(void);
|
||||
void init_locale(void);
|
||||
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -81,10 +81,7 @@ int main_dnsmasq (int argc, char **argv)
|
|||
#endif
|
||||
|
||||
#if defined(HAVE_IDN) || defined(HAVE_LIBIDN2) || defined(LOCALEDIR)
|
||||
setlocale(LC_ALL, "");
|
||||
/*** Pi-hole modification ***/
|
||||
setlocale(LC_NUMERIC, "C");
|
||||
/****************************/
|
||||
/*** Pi-hole modification: Locale is already initialized in main.c ***/
|
||||
#endif
|
||||
#ifdef LOCALEDIR
|
||||
bindtextdomain("dnsmasq", LOCALEDIR);
|
||||
|
|
10
src/enums.h
10
src/enums.h
|
@ -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
|
||||
|
|
63
src/files.c
63
src/files.c
|
@ -623,3 +623,66 @@ char * __attribute__((malloc)) get_hwmon_target(const char *path)
|
|||
|
||||
return target;
|
||||
}
|
||||
|
||||
// Returns true if the files have different contents
|
||||
// from specifies from which line number the files should be compared
|
||||
bool files_different(const char *pathA, const char* pathB, unsigned int from)
|
||||
{
|
||||
// Check if both files exist
|
||||
if(!file_exists(pathA) || !file_exists(pathB))
|
||||
return true;
|
||||
|
||||
// Check if both files are identical
|
||||
if(strcmp(pathA, pathB) == 0)
|
||||
return false;
|
||||
|
||||
// Open both files
|
||||
FILE *fpA = fopen(pathA, "r");
|
||||
if(fpA == NULL)
|
||||
{
|
||||
log_warn("files_different(): Failed to open \"%s\" for reading: %s", pathA, strerror(errno));
|
||||
return true;
|
||||
}
|
||||
FILE *fpB = fopen(pathB, "r");
|
||||
if(fpB == NULL)
|
||||
{
|
||||
log_warn("files_different(): Failed to open \"%s\" for reading: %s", pathB, strerror(errno));
|
||||
fclose(fpA);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compare both files line by line
|
||||
char *lineA = NULL;
|
||||
size_t lenA = 0;
|
||||
ssize_t readA;
|
||||
char *lineB = NULL;
|
||||
size_t lenB = 0;
|
||||
ssize_t readB;
|
||||
bool different = false;
|
||||
while((readA = getline(&lineA, &lenA, fpA)) != -1 &&
|
||||
(readB = getline(&lineB, &lenB, fpB)) != -1)
|
||||
{
|
||||
// Skip lines until we reach the requested line number
|
||||
if(from > 0)
|
||||
{
|
||||
from--;
|
||||
continue;
|
||||
}
|
||||
// Compare lines
|
||||
if(strcmp(lineA, lineB) != 0)
|
||||
{
|
||||
different = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Free memory
|
||||
free(lineA);
|
||||
free(lineB);
|
||||
|
||||
// Close files
|
||||
fclose(fpA);
|
||||
fclose(fpB);
|
||||
|
||||
return different;
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ unsigned int get_path_usage(const char *path, char buffer[64]);
|
|||
struct mntent *get_filesystem_details(const char *path);
|
||||
bool directory_exists(const char *path);
|
||||
void rotate_files(const char *path, char **first_file);
|
||||
bool files_different(const char *pathA, const char* pathB, unsigned int from);
|
||||
|
||||
int parse_line(char *line, char **key, char **value);
|
||||
|
||||
|
|
|
@ -45,6 +45,9 @@ jmp_buf exit_jmp;
|
|||
|
||||
int main (int argc, char *argv[])
|
||||
{
|
||||
// Initialize locale (needed for libidn)
|
||||
init_locale();
|
||||
|
||||
// Get user pihole-FTL is running as
|
||||
// We store this in a global variable
|
||||
// such that the log routine can access
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -13,17 +13,26 @@
|
|||
#include "../log.h"
|
||||
|
||||
#undef free
|
||||
void FTLfree(void *ptr, const char *file, const char *func, const int line)
|
||||
void FTLfree(void **ptr, const char *file, const char *func, const int line)
|
||||
{
|
||||
// The free() function frees the memory space pointed to by ptr, which
|
||||
// must have been returned by a previous call to malloc(), calloc(), or
|
||||
// realloc(). Otherwise, or if free(ptr) has already been called before,
|
||||
// undefined behavior occurs. If ptr is NULL, no operation is performed.
|
||||
if(ptr == NULL)
|
||||
{
|
||||
log_warn("Trying to free NULL memory location in %s() (%s:%i)", func, file, line);
|
||||
return;
|
||||
}
|
||||
if(*ptr == NULL)
|
||||
{
|
||||
log_warn("Trying to free NULL pointer in %s() (%s:%i)", func, file, line);
|
||||
return;
|
||||
}
|
||||
|
||||
free(ptr);
|
||||
// Actually free the memory
|
||||
free(*ptr);
|
||||
|
||||
// Set the pointer to NULL
|
||||
*ptr = NULL;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
char *FTLstrdup(const char *src, const char *file, const char *func, const int line) __attribute__((malloc));
|
||||
void *FTLcalloc(size_t n, size_t size, const char *file, const char *func, const int line) __attribute__((malloc)) __attribute__((alloc_size(1,2)));
|
||||
void *FTLrealloc(void *ptr_in, size_t size, const char *file, const char *func, const int line) __attribute__((alloc_size(2)));
|
||||
void FTLfree(void *ptr, const char*file, const char *func, const int line);
|
||||
void FTLfree(void **ptr, const char*file, const char *func, const int line);
|
||||
int FTLfallocate(const int fd, const off_t offset, const off_t len, const char *file, const char *func, const int line);
|
||||
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ static const char *false_positives[] = {
|
|||
#define MAX_INVALID_DOMAINS 5
|
||||
|
||||
// Validate domain name
|
||||
static inline bool __attribute__((pure)) valid_domain(const char *domain, const size_t len)
|
||||
static inline bool __attribute__((pure)) valid_domain(const char *domain, const size_t len, const bool abp)
|
||||
{
|
||||
// Domain must not be NULL or empty, and they should not be longer than
|
||||
// 255 characters
|
||||
|
@ -84,8 +84,10 @@ static inline bool __attribute__((pure)) valid_domain(const char *domain, const
|
|||
// TLD checks
|
||||
|
||||
// There must be at least two labels (i.e. one dot)
|
||||
// e.g., "example.com" but not "localhost"
|
||||
if(last_dot == -1)
|
||||
// e.g., "example.com" but not "localhost" for exact domain
|
||||
// We do not enforce this for ABP domains
|
||||
// (see https://github.com/pi-hole/pi-hole/pull/5240)
|
||||
if(last_dot == -1 && !abp)
|
||||
return false;
|
||||
|
||||
// TLD must not start or end with a hyphen
|
||||
|
@ -121,7 +123,7 @@ static inline bool __attribute__((pure)) valid_abp_domain(const char *line, cons
|
|||
return false;
|
||||
|
||||
// Domain must be valid
|
||||
return valid_domain(line+4, len-5);
|
||||
return valid_domain(line+4, len-5, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -138,7 +140,7 @@ static inline bool __attribute__((pure)) valid_abp_domain(const char *line, cons
|
|||
return false;
|
||||
|
||||
// Domain must be valid
|
||||
return valid_domain(line+2, len-3);
|
||||
return valid_domain(line+2, len-3, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -279,7 +281,7 @@ int gravity_parseList(const char *infile, const char *outfile, const char *adlis
|
|||
|
||||
// Validate line
|
||||
if(line[0] != (antigravity ? '@' : '|') && // <- Not an ABP-style match
|
||||
valid_domain(line, read))
|
||||
valid_domain(line, read, false))
|
||||
{
|
||||
// Exact match found
|
||||
if(checkOnly)
|
||||
|
|
|
@ -85,7 +85,7 @@ int send_json_error_free(struct ftl_conn *api, const int code,
|
|||
const char *key, const char* message,
|
||||
char *hint, bool free_hint)
|
||||
{
|
||||
if(hint)
|
||||
if(hint != NULL)
|
||||
log_warn("API: %s (%s)", message, hint);
|
||||
else
|
||||
log_warn("API: %s", message);
|
||||
|
@ -94,7 +94,7 @@ int send_json_error_free(struct ftl_conn *api, const int code,
|
|||
JSON_REF_STR_IN_OBJECT(error, "key", key);
|
||||
JSON_REF_STR_IN_OBJECT(error, "message", message);
|
||||
JSON_COPY_STR_TO_OBJECT(error, "hint", hint);
|
||||
if(free_hint)
|
||||
if(free_hint && hint != NULL)
|
||||
free(hint);
|
||||
|
||||
cJSON *json = JSON_NEW_OBJECT();
|
||||
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
# Please see LICENSE file for your rights under this license.
|
||||
|
||||
import io
|
||||
import pprint
|
||||
import ipaddress
|
||||
import random
|
||||
import zipfile
|
||||
from libs.openAPI import openApi
|
||||
|
@ -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)
|
||||
|
@ -172,7 +172,7 @@ class ResponseVerifyer():
|
|||
if expected_file not in zipfile_obj.namelist():
|
||||
self.errors.append("File " + expected_file + " is missing in received archive.")
|
||||
pihole_toml = zipfile_obj.read("etc/pihole/pihole.toml")
|
||||
if not pihole_toml.startswith(b"# This file is managed by pihole-FTL"):
|
||||
if not pihole_toml.startswith(b"# Pi-hole configuration file (v"):
|
||||
self.errors.append("Received ZIP file's pihole.toml starts with wrong header")
|
||||
except Exception as err:
|
||||
self.errors.append("Error during ZIP analysis: " + str(err))
|
||||
|
@ -221,8 +221,35 @@ class ResponseVerifyer():
|
|||
return self.errors
|
||||
|
||||
|
||||
# Check if a string is a valid IPv4 address
|
||||
def valid_ipv4(self, addr: str) -> bool:
|
||||
# Empty string is valid (0.0.0.0)
|
||||
if len(addr) == 0:
|
||||
return True
|
||||
try:
|
||||
if type(ipaddress.ip_address(addr)) is ipaddress.IPv4Address:
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
return False
|
||||
|
||||
# Check if a string is a valid IPv6 address
|
||||
def valid_ipv6(self, addr: str) -> bool:
|
||||
# Empty string is valid (::)
|
||||
if len(addr) == 0:
|
||||
return True
|
||||
try:
|
||||
if type(ipaddress.ip_address(addr)) is ipaddress.IPv6Address:
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
# Verify a single property's type
|
||||
def verify_type(self, prop_type: any, yaml_type: str, yaml_nullable: bool):
|
||||
def verify_type(self, prop: any, yaml_type: str, yaml_nullable: bool, yaml_format: str = None):
|
||||
# Get the type of the property
|
||||
prop_type = type(prop)
|
||||
# None is an acceptable reply when this is specified in the API specs
|
||||
if prop_type is type(None) and yaml_nullable:
|
||||
return True
|
||||
|
@ -230,6 +257,14 @@ class ResponseVerifyer():
|
|||
if yaml_type not in self.YAML_TYPES:
|
||||
self.errors.append("Property type \"" + yaml_type + "\" is not valid in OpenAPI specs")
|
||||
return False
|
||||
if yaml_format is not None:
|
||||
# Check if the format is correct
|
||||
if yaml_format == "ipv4" and not self.valid_ipv4(prop):
|
||||
self.errors.append("Property \"" + str(prop) + "\" is not a valid IPv4 address")
|
||||
return False
|
||||
elif yaml_format == "ipv6" and not self.valid_ipv6(prop):
|
||||
self.errors.append("Property \"" + str(prop) + "\" is not a valid IPv6 address")
|
||||
return False
|
||||
return prop_type in self.YAML_TYPES[yaml_type]
|
||||
|
||||
|
||||
|
@ -296,6 +331,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
|
||||
|
||||
|
@ -306,17 +344,19 @@ class ResponseVerifyer():
|
|||
# if not defined as string, integer, etc.)
|
||||
yaml_nullable = 'nullable' in YAMLprop and YAMLprop['nullable'] == True
|
||||
|
||||
# Get format of this property (if defined)
|
||||
yaml_format = YAMLprop['format'] if 'format' in YAMLprop else YAMLprop['x-format'] if 'x-format' in YAMLprop else None
|
||||
|
||||
# Add this property to the YAML response
|
||||
self.YAMLresponse[flat_path] = []
|
||||
|
||||
# Check type of YAML example (if defined)
|
||||
if 'example' in YAMLprop:
|
||||
example_type = type(YAMLprop['example'])
|
||||
# Check if the type of the example matches the
|
||||
# type we defined in the API specs
|
||||
self.YAMLresponse[flat_path].append(YAMLprop['example'])
|
||||
if not self.verify_type(example_type, yaml_type, yaml_nullable):
|
||||
self.errors.append(f"API example ({str(example_type)}) does not match defined type ({yaml_type}) in {flat_path} (nullable: " + ("True" if yaml_nullable else "False") + ")")
|
||||
if not self.verify_type(YAMLprop['example'], yaml_type, yaml_nullable, yaml_format):
|
||||
self.errors.append(f"API example ({str(type(YAMLprop['example']))}) does not match defined type ({yaml_type}) in {flat_path} (nullable: " + ("True" if yaml_nullable else "False") + ")")
|
||||
return False
|
||||
|
||||
# Check type of externally defined YAML examples (next to schema)
|
||||
|
@ -340,16 +380,14 @@ class ResponseVerifyer():
|
|||
if skip_this:
|
||||
continue
|
||||
# Check if the type of the example matches the type we defined in the API specs
|
||||
example_type = type(example)
|
||||
self.YAMLresponse[flat_path].append(example)
|
||||
if not self.verify_type(example_type, yaml_type, yaml_nullable):
|
||||
self.errors.append(f"API example ({str(example_type)}) does not match defined type ({yaml_type}) in {flat_path} (nullable: " + ("True" if yaml_nullable else "False") + ")")
|
||||
if not self.verify_type(example, yaml_type, yaml_nullable, yaml_format):
|
||||
self.errors.append(f"API example ({str(type(example))}) does not match defined type ({yaml_type}) in {flat_path} (nullable: " + ("True" if yaml_nullable else "False") + ")")
|
||||
return False
|
||||
|
||||
# Compare type of FTL's reply against what we defined in the API specs
|
||||
ftl_type = type(FTLprop)
|
||||
if not self.verify_type(ftl_type, yaml_type, yaml_nullable):
|
||||
self.errors.append(f"FTL's reply ({str(ftl_type)}) does not match defined type ({yaml_type}) in {flat_path}")
|
||||
if not self.verify_type(FTLprop, yaml_type, yaml_nullable, yaml_format):
|
||||
self.errors.append(f"FTL's reply ({str(type(FTLprop))}) does not match defined type ({yaml_type}) in {flat_path}")
|
||||
return False
|
||||
return all_okay
|
||||
|
||||
|
|
|
@ -100,7 +100,10 @@
|
|||
#
|
||||
# Possible values are:
|
||||
# Array of custom DNS records each one in HOSTS form: "IP HOSTNAME"
|
||||
hosts = []
|
||||
hosts = [
|
||||
"1.1.1.1 abc-custom.com def-custom.de",
|
||||
"2.2.2.2 äste.com steä.com"
|
||||
] ### CHANGED, default = []
|
||||
|
||||
# If set, A and AAAA queries for plain names, without dots or domain parts, are never
|
||||
# forwarded to upstream nameservers
|
||||
|
@ -110,6 +113,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?
|
||||
|
@ -175,7 +199,9 @@
|
|||
# Possible values are:
|
||||
# Array of static leases each on in one of the following forms:
|
||||
# "<cname>,<target>[,<TTL>]"
|
||||
cnameRecords = []
|
||||
cnameRecords = [
|
||||
"brücke.com,äste.com,2",
|
||||
]
|
||||
|
||||
# Port used by the DNS server
|
||||
port = 53
|
||||
|
@ -370,12 +396,26 @@
|
|||
# <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>
|
||||
domain = "lan"
|
||||
|
||||
# The netmask used by your Pi-hole. For directly connected networks (i.e., networks on
|
||||
# which the machine running Pi-hole has an interface) the netmask is optional and may
|
||||
# be set to "0.0.0.0": it will then be determined from the interface configuration
|
||||
# itself. For networks which receive DHCP service via a relay agent, we cannot
|
||||
# determine the netmask itself, so it should explicitly be specified, otherwise
|
||||
# Pi-hole guesses based on the class (A, B or C) of the network address.
|
||||
#
|
||||
# Possible values are:
|
||||
# <any valid netmask>, e.g., "255.255.255.0" or "0.0.0.0" for auto-discovery
|
||||
netmask = "0.0.0.0"
|
||||
|
||||
# If the lease time is given, then leases will be given for that length of time. If not
|
||||
# given, the default lease time is one hour for IPv4 and one day for IPv6.
|
||||
#
|
||||
|
@ -520,7 +560,7 @@
|
|||
#
|
||||
# Possible values are:
|
||||
# comma-separated list of <[ip_address:]port>
|
||||
port = "80,[::]:80,443s"
|
||||
port = "80,[::]:80,443s,[::]:443s"
|
||||
|
||||
[webserver.session]
|
||||
# Session timeout in seconds. If a session is inactive for more than this time, it will
|
||||
|
@ -604,6 +644,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 +821,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
|
||||
|
|
|
@ -20,7 +20,7 @@ while pidof -s pihole-FTL > /dev/null; do
|
|||
done
|
||||
|
||||
# Clean up possible old files from earlier test runs
|
||||
rm -f /etc/pihole/gravity.db /etc/pihole/pihole-FTL.db /var/log/pihole/pihole.log /var/log/pihole/FTL.log /dev/shm/FTL-*
|
||||
rm -rf /etc/pihole /var/log/pihole /dev/shm/FTL-*
|
||||
|
||||
# Create necessary directories and files
|
||||
mkdir -p /home/pihole /etc/pihole /run/pihole /var/log/pihole
|
||||
|
|
|
@ -493,21 +493,17 @@
|
|||
[[ ${lines[0]} == "The Pi-hole FTL engine - "* ]]
|
||||
}
|
||||
|
||||
#@test "No WARNING messages in FTL.log (besides known capability issues)" {
|
||||
# run bash -c 'grep "WARNING" /var/log/pihole/FTL.log'
|
||||
# printf "%s\n" "${lines[@]}"
|
||||
# run bash -c 'grep "WARNING" /var/log/pihole/FTL.log | grep -c -v -E "CAP_NET_ADMIN|CAP_NET_RAW|CAP_SYS_NICE|CAP_IPC_LOCK|CAP_CHOWN"'
|
||||
# printf "%s\n" "${lines[@]}"
|
||||
# [[ ${lines[0]} == "0" ]]
|
||||
#}
|
||||
@test "No WARNING messages in FTL.log (besides known capability issues)" {
|
||||
run bash -c 'grep "WARNING:" /var/log/pihole/FTL.log | grep -v -E "CAP_NET_ADMIN|CAP_NET_RAW|CAP_SYS_NICE|CAP_IPC_LOCK|CAP_CHOWN|CAP_NET_BIND_SERVICE|(Cannot set process priority)"'
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ "${lines[@]}" == "" ]]
|
||||
}
|
||||
|
||||
#@test "No FATAL messages in FTL.log (besides error due to starting FTL more than once)" {
|
||||
# run bash -c 'grep "FATAL" /var/log/pihole/FTL.log'
|
||||
# printf "%s\n" "${lines[@]}"
|
||||
# run bash -c 'grep "FATAL:" /var/log/pihole/FTL.log | grep -c -v "FATAL: create_shm(): Failed to create shared memory object \"FTL-lock\": File exists"'
|
||||
# printf "%s\n" "${lines[@]}"
|
||||
# [[ ${lines[0]} == "0" ]]
|
||||
#}
|
||||
@test "No CRIT messages in FTL.log (besides error due to starting FTL more than once)" {
|
||||
run bash -c 'grep "CRIT:" /var/log/pihole/FTL.log | grep -v "CRIT: Initialization of shared memory failed"'
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ "${lines[@]}" == "" ]]
|
||||
}
|
||||
|
||||
@test "No \"database not available\" messages in FTL.log" {
|
||||
run bash -c 'grep -c "database not available" /var/log/pihole/FTL.log'
|
||||
|
@ -1253,6 +1249,35 @@
|
|||
[[ "${lines[0]}" == "192.168.1.7" ]]
|
||||
}
|
||||
|
||||
@test "Custom DNS records: Multiple domains per line are accepted" {
|
||||
run bash -c "dig A abc-custom.com +short @127.0.0.1"
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ "${lines[0]}" == "1.1.1.1" ]]
|
||||
run bash -c "dig A def-custom.de +short @127.0.0.1"
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ "${lines[0]}" == "1.1.1.1" ]]
|
||||
}
|
||||
|
||||
@test "Custom DNS records: International domains are converted to IDNA form" {
|
||||
# äste.com ---> xn--ste-pla.com
|
||||
run bash -c "dig A xn--ste-pla.com +short @127.0.0.1"
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ "${lines[0]}" == "2.2.2.2" ]]
|
||||
# steä.com -> xn--ste-sla.com
|
||||
run bash -c "dig A xn--ste-sla.com +short @127.0.0.1"
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ "${lines[0]}" == "2.2.2.2" ]]
|
||||
}
|
||||
|
||||
@test "Local CNAME records: International domains are converted to IDNA form" {
|
||||
# brücke.com ---> xn--brcke-lva.com
|
||||
run bash -c "dig A xn--brcke-lva.com +short @127.0.0.1"
|
||||
printf "%s\n" "${lines[@]}"
|
||||
# xn--ste-pla.com ---> äste.com
|
||||
[[ "${lines[0]}" == "xn--ste-pla.com." ]]
|
||||
[[ "${lines[1]}" == "2.2.2.2" ]]
|
||||
}
|
||||
|
||||
@test "Environmental variable is favored over config file" {
|
||||
# The config file has -10 but we set FTLCONF_misc_nice="-11"
|
||||
run bash -c 'grep -B1 "nice = -11" /etc/pihole/pihole.toml'
|
||||
|
@ -1292,6 +1317,18 @@
|
|||
[[ ${lines[0]} == '"xn--bc-uia.com"' ]]
|
||||
}
|
||||
|
||||
@test "API history: Returns full 24 hours even if only a few queries are made" {
|
||||
run bash -c 'curl -s 127.0.0.1/api/history | jq ".history | length"'
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ ${lines[0]} == "145" ]]
|
||||
}
|
||||
|
||||
@test "API history/clients: Returns full 24 hours even if only a few queries are made" {
|
||||
run bash -c 'curl -s 127.0.0.1/api/history/clients | jq ".history | length"'
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ ${lines[0]} == "145" ]]
|
||||
}
|
||||
|
||||
@test "API authorization (without password): No login required" {
|
||||
run bash -c 'curl -s 127.0.0.1/api/auth'
|
||||
printf "%s\n" "${lines[@]}"
|
||||
|
@ -1356,6 +1393,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"
|
||||
|
@ -1410,10 +1530,10 @@
|
|||
[[ "${lines[0]}" == "PI.HOLE" ]]
|
||||
run bash -c './pihole-FTL --config dns.hosts'
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ "${lines[0]}" == "[]" ]]
|
||||
[[ "${lines[0]}" == "[ 1.1.1.1 abc-custom.com def-custom.de, 2.2.2.2 äste.com steä.com ]" ]]
|
||||
run bash -c './pihole-FTL --config webserver.port'
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ "${lines[0]}" == "80,[::]:80,443s" ]]
|
||||
[[ "${lines[0]}" == "80,[::]:80,443s,[::]:443s" ]]
|
||||
}
|
||||
|
||||
@test "Create, verify and re-import Teleporter file via CLI" {
|
||||
|
@ -1433,3 +1553,24 @@
|
|||
[[ $status == 0 ]]
|
||||
run bash -c "rm ${filename}"
|
||||
}
|
||||
|
||||
@test "Expected number of config file rotations" {
|
||||
run bash -c 'grep -c "INFO: Config file written to /etc/pihole/pihole.toml" /var/log/pihole/FTL.log'
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ ${lines[0]} == "3" ]]
|
||||
run bash -c 'grep -c "DEBUG_CONFIG: pihole.toml unchanged" /var/log/pihole/FTL.log'
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ ${lines[0]} == "3" ]]
|
||||
run bash -c 'grep -c "DEBUG_CONFIG: Config file written to /etc/pihole/dnsmasq.conf" /var/log/pihole/FTL.log'
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ ${lines[0]} == "1" ]]
|
||||
run bash -c 'grep -c "DEBUG_CONFIG: dnsmasq.conf unchanged" /var/log/pihole/FTL.log'
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ ${lines[0]} == "2" ]]
|
||||
run bash -c 'grep -c "DEBUG_CONFIG: HOSTS file written to /etc/pihole/hosts/custom.list" /var/log/pihole/FTL.log'
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ ${lines[0]} == "1" ]]
|
||||
run bash -c 'grep -c "DEBUG_CONFIG: custom.list unchanged" /var/log/pihole/FTL.log'
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ ${lines[0]} == "3" ]]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue