Merge branch 'development-v6' into new/validator

Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
DL6ER 2024-01-21 20:01:11 +01:00
commit b9fc7da559
No known key found for this signature in database
GPG Key ID: 00135ACBD90B28DD
50 changed files with 6795 additions and 2982 deletions

View File

@ -1,6 +1,6 @@
{
"name": "FTL x86_64 Build Env",
"image": "ghcr.io/pi-hole/ftl-build:v2.4.1",
"image": "ghcr.io/pi-hole/ftl-build:v2.5",
"runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ],
"customizations": {
"vscode": {

2
.github/Dockerfile vendored
View File

@ -1,4 +1,4 @@
FROM ghcr.io/pi-hole/ftl-build:v2.4.1 AS builder
FROM ghcr.io/pi-hole/ftl-build:v2.5 AS builder
WORKDIR /app

View File

@ -119,7 +119,7 @@ jobs:
-
name: Store binary artifacts for later deployoment
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4.0.0
uses: actions/upload-artifact@v4.2.0
with:
name: ${{ matrix.bin_name }}-binary
path: '${{ matrix.bin_name }}*'
@ -131,7 +131,7 @@ jobs:
-
name: Upload documentation artifacts for deployoment
if: github.event_name != 'pull_request' && matrix.platform == 'linux/amd64'
uses: actions/upload-artifact@v4.0.0
uses: actions/upload-artifact@v4.2.0
with:
name: pihole-api-docs
path: 'api-docs.tar.gz'
@ -146,7 +146,7 @@ jobs:
uses: actions/checkout@v4.1.1
-
name: Get Binaries and documentation built in previous jobs
uses: actions/download-artifact@v4.1.0
uses: actions/download-artifact@v4.1.1
id: download
with:
path: ftl_builds/

View File

@ -15,6 +15,8 @@
// type cJSON
#include "webserver/cJSON/cJSON.h"
#include "webserver/http-common.h"
// regex_t
#include "regex_r.h"
// Common definitions
#define LOCALHOSTv4 "127.0.0.1"
@ -24,6 +26,7 @@
int api_handler(struct mg_connection *conn, void *ignored);
// Statistic methods
int __attribute__((pure)) cmpdesc(const void *a, const void *b);
int api_stats_summary(struct ftl_conn *api);
int api_stats_query_types(struct ftl_conn *api);
int api_stats_upstreams(struct ftl_conn *api);
@ -42,6 +45,7 @@ int api_history_database_clients(struct ftl_conn *api);
// Query methods
int api_queries(struct ftl_conn *api);
int api_queries_suggestions(struct ftl_conn *api);
bool compile_filter_regex(struct ftl_conn *api, const char *path, cJSON *json, regex_t **regex, unsigned int *N_regex);
// Statistics methods (database)
int api_stats_database_top_items(struct ftl_conn *api);

View File

@ -56,6 +56,9 @@ void init_api(void)
void free_api(void)
{
if(auth_data == NULL)
return;
// Store sessions in database
backup_db_sessions(auth_data, max_sessions);
max_sessions = 0;
@ -151,6 +154,7 @@ int check_client_auth(struct ftl_conn *api, const bool is_api)
}
}
// If not, does the client provide a session ID via COOKIE?
bool cookie_auth = false;
if(!sid_avail)
{
@ -162,7 +166,22 @@ int check_client_auth(struct ftl_conn *api, const bool is_api)
// Mark SID as available
sid_avail = true;
}
}
// If not, does the client provide a session ID via URI?
if(!sid_avail && api->request->query_string && GET_VAR("sid", sid, api->request->query_string) > 0)
{
// "+" may have been replaced by " ", undo this here
for(unsigned int i = 0; i < SID_SIZE; i++)
if(sid[i] == ' ')
sid[i] = '+';
// Zero terminate SID string
sid[SID_SIZE-1] = '\0';
// Mention source of SID
sid_source = "URI";
// Mark SID as available
sid_avail = true;
}
if(!sid_avail)
@ -320,14 +339,18 @@ static int get_session_object(struct ftl_conn *api, cJSON *json, const int user_
return 0;
}
static void delete_session(const int user_id)
static bool delete_session(const int user_id)
{
// Skip if nothing to be done here
if(user_id < 0 || user_id >= max_sessions)
return;
return false;
const bool was_valid = auth_data[user_id].used;
// Zero out this session (also sets valid to false == 0)
memset(&auth_data[user_id], 0, sizeof(auth_data[user_id]));
return was_valid;
}
void delete_all_sessions(void)
@ -338,24 +361,6 @@ void delete_all_sessions(void)
static int send_api_auth_status(struct ftl_conn *api, const int user_id, const time_t now)
{
if(user_id == API_AUTH_LOCALHOST)
{
log_debug(DEBUG_API, "API Auth status: OK (localhost does not need auth)");
cJSON *json = JSON_NEW_OBJECT();
get_session_object(api, json, user_id, now);
JSON_SEND_OBJECT(json);
}
if(user_id == API_AUTH_EMPTYPASS)
{
log_debug(DEBUG_API, "API Auth status: OK (empty password)");
cJSON *json = JSON_NEW_OBJECT();
get_session_object(api, json, user_id, now);
JSON_SEND_OBJECT(json);
}
if(user_id > API_AUTH_UNAUTHORIZED && (api->method == HTTP_GET || api->method == HTTP_POST))
{
log_debug(DEBUG_API, "API Auth status: OK");
@ -372,17 +377,45 @@ static int send_api_auth_status(struct ftl_conn *api, const int user_id, const t
get_session_object(api, json, user_id, now);
JSON_SEND_OBJECT(json);
}
else if(user_id > API_AUTH_UNAUTHORIZED && api->method == HTTP_DELETE)
else if(api->method == HTTP_DELETE)
{
log_debug(DEBUG_API, "API Auth status: Logout, asking to delete cookie");
if(user_id > API_AUTH_UNAUTHORIZED)
{
log_debug(DEBUG_API, "API Auth status: Logout, asking to delete cookie");
// Revoke client authentication. This slot can be used by a new client afterwards.
delete_session(user_id);
strncpy(pi_hole_extra_headers, FTL_DELETE_COOKIE, sizeof(pi_hole_extra_headers));
// Revoke client authentication. This slot can be used by a new client afterwards.
const int code = delete_session(user_id) ? 204 : 404;
// Send empty reply with appropriate HTTP status code
send_http_code(api, "application/json; charset=utf-8", code, "");
return code;
}
else
{
log_debug(DEBUG_API, "API Auth status: Logout, but not authenticated");
cJSON *json = JSON_NEW_OBJECT();
get_session_object(api, json, user_id, now);
JSON_SEND_OBJECT_CODE(json, 401); // 401 Unauthorized
}
}
else if(user_id == API_AUTH_LOCALHOST)
{
log_debug(DEBUG_API, "API Auth status: OK (localhost does not need auth)");
strncpy(pi_hole_extra_headers, FTL_DELETE_COOKIE, sizeof(pi_hole_extra_headers));
cJSON *json = JSON_NEW_OBJECT();
get_session_object(api, json, user_id, now);
JSON_SEND_OBJECT_CODE(json, 410); // 410 Gone
JSON_SEND_OBJECT(json);
}
else if(user_id == API_AUTH_EMPTYPASS)
{
log_debug(DEBUG_API, "API Auth status: OK (empty password)");
cJSON *json = JSON_NEW_OBJECT();
get_session_object(api, json, user_id, now);
JSON_SEND_OBJECT(json);
}
else
{
@ -547,7 +580,7 @@ int api_auth(struct ftl_conn *api)
{
// Expired slow, mark as unused
if(auth_data[i].used &&
auth_data[i].valid_until < now)
auth_data[i].valid_until < now)
{
log_debug(DEBUG_API, "API: Session of client %u (%s) expired, freeing...",
i, auth_data[i].remote_addr);
@ -618,6 +651,11 @@ int api_auth(struct ftl_conn *api)
"Rate-limiting login attempts",
NULL);
}
else if(result == NO_PASSWORD_SET)
{
// No password set
log_debug(DEBUG_API, "API: Trying to auth with password but none set: '%s'", password);
}
else
{
log_debug(DEBUG_API, "API: Password incorrect: '%s'", password);
@ -651,9 +689,9 @@ int api_auth_session_delete(struct ftl_conn *api)
return send_json_error(api, 400, "bad_request", "Session ID not in use", NULL);
// Delete session
delete_session(uid);
const int code = delete_session(uid) ? 204 : 404;
// Send empty reply with code 204 No Content
send_http_code(api, "application/json; charset=utf-8", 204, "");
return 204;
// Send empty reply with appropriate HTTP status code
send_http_code(api, "application/json; charset=utf-8", code, "");
return code;
}

View File

@ -294,7 +294,7 @@ static const char *getJSONvalue(struct conf_item *conf_item, cJSON *elem, struct
}
if(!set_and_check_password(conf_item, elem->valuestring))
return "Failed to create password hash (verification failed), password remains unchanged";
return "password hash verification failed";
break;
}
@ -918,7 +918,7 @@ static int api_config_put_delete(struct ftl_conn *api)
key, true);
}
// Check if this entry does already exist in the array
// Check if this entry exists in the array
int idx = 0;
for(; idx < cJSON_GetArraySize(new_item->v.json); idx++)
{
@ -952,13 +952,12 @@ static int api_config_put_delete(struct ftl_conn *api)
if(found)
{
// Remove item from array
found = true;
cJSON_DeleteItemFromArray(new_item->v.json, idx);
}
else
{
// Item not found
message = "Item not found";
hint = "Can only delete existing items";
break;
}
}
@ -993,13 +992,16 @@ static int api_config_put_delete(struct ftl_conn *api)
// Release allocated memory
free_config_path(requested_path);
// Error 404 if not found
if(!found || message != NULL)
// Error 404 if config element not found
if(!found)
{
cJSON *json = JSON_NEW_OBJECT();
JSON_SEND_OBJECT_CODE(json, 404);
}
// Error 400 if unique item already present
if(message != NULL)
{
// For any other error, a more specific message will have been added
// above
if(!message)
message = "No item specified";
return send_json_error(api, 400,
"bad_request",
message,

View File

@ -73,7 +73,7 @@ int api_dhcp_leases_GET(struct ftl_conn *api)
}
// defined in dnsmasq_interface.c
extern bool FTL_unlink_DHCP_lease(const char *ipaddr);
extern bool FTL_unlink_DHCP_lease(const char *ipaddr, const char **hint);
// Delete DHCP leases
int api_dhcp_leases_DELETE(struct ftl_conn *api)
@ -85,16 +85,29 @@ int api_dhcp_leases_DELETE(struct ftl_conn *api)
// Send empty reply with code 204 No Content
return send_json_error(api,
400,
"bad_request",
"bad_request",
"The provided IPv4 address is invalid",
api->item);
api->item);
}
// Delete lease
log_debug(DEBUG_API, "Deleting DHCP lease for address %s", api->item);
FTL_unlink_DHCP_lease(api->item);
// Send empty reply with code 204 No Content
const char *hint = NULL;
const bool found = FTL_unlink_DHCP_lease(api->item, &hint);
if(!found && hint != NULL)
{
// Send error when something went wrong (hint is not NULL)
return send_json_error(api,
400,
"bad_request",
"Failed to delete DHCP lease",
hint);
}
// Send empty reply with codes:
// - 204 No Content (if a lease was deleted)
// - 404 Not Found (if no lease was found)
cJSON *json = JSON_NEW_OBJECT();
JSON_SEND_OBJECT_CODE(json, 204);
JSON_SEND_OBJECT_CODE(json, found ? 204 : 404);
}

View File

@ -118,21 +118,27 @@ components:
- Authentication
operationId: "delete_groups"
description: |
A logout attempt without a valid session will result in a `401 Unauthorized` error.
This endpoint can be used to delete the current session. It will
invalidate the session token and the CSRF token. The session can be
extended before its expiration by performing any authenticated action.
By default, the session lasts for 5 minutes. It can be invalidated by
either logging out or deleting the session. Additionally, the session
becomes invalid when the password is altered or a new application
password is created.
A session that was not created due to a login cannot be deleted (e.g., empty API password).
You can also delete a session by its ID using the `DELETE /auth/session/{id}` endpoint.
Note that you cannot delete the current session if you have not
authenticated (e.g., no password has been set on your Pi-hole).
responses:
'200':
description: OK (session not deletable)
'204':
description: No Content (deleted)
'404':
description: Not Found (no session active)
content:
application/json:
schema:
allOf:
- $ref: 'auth.yaml#/components/schemas/session'
- $ref: 'common.yaml#/components/schemas/took'
examples:
no_login_required:
$ref: 'auth.yaml#/components/examples/no_login_required'
$ref: 'common.yaml#/components/schemas/took'
'401':
description: Unauthorized
content:
@ -141,17 +147,6 @@ components:
allOf:
- $ref: 'common.yaml#/components/errors/unauthorized'
- $ref: 'common.yaml#/components/schemas/took'
'410':
description: Gone
content:
application/json:
schema:
allOf:
- $ref: 'auth.yaml#/components/schemas/session'
- $ref: 'common.yaml#/components/schemas/took'
examples:
login_failed:
$ref: 'auth.yaml#/components/examples/login_failed'
session_list:
get:
summary: List of all current sessions
@ -213,6 +208,12 @@ components:
responses:
'204':
description: No Content (deleted)
'404':
description: Not Found (session not found)
content:
application/json:
schema:
$ref: 'common.yaml#/components/schemas/took'
'400':
description: Bad Request
content:

View File

@ -95,6 +95,12 @@ components:
responses:
'204':
description: Item deleted
'404':
description: Item not found
content:
application/json:
schema:
$ref: 'common.yaml#/components/schemas/took'
'400':
description: Bad request
content:
@ -233,6 +239,12 @@ components:
responses:
'204':
description: Items deleted
'404':
description: Item not found
content:
application/json:
schema:
$ref: 'common.yaml#/components/schemas/took'
'400':
description: Bad request
content:
@ -262,7 +274,7 @@ components:
description: Array of clients
items:
allOf:
- $ref: 'clients.yaml#/components/schemas/client'
- $ref: 'clients.yaml#/components/schemas/client_object'
- $ref: 'clients.yaml#/components/schemas/comment'
- $ref: 'clients.yaml#/components/schemas/groups'
- $ref: 'clients.yaml#/components/schemas/readonly'
@ -305,25 +317,27 @@ components:
description: Comma-separated list of hostnames (if available)
example: "localhost,ip6-localhost"
client:
type: object
properties:
client:
description: client IP / MAC / hostname / interface
type: string
example: 127.0.0.1
description: client IP / MAC / hostname / interface
type: string
example: 127.0.0.1
client_array:
description: array of client IPs / MACs / hostnames / interfaces
type: array
items:
type: string
example: ["127.0.0.1", "192.168.2.12"]
client_maybe_array:
type: object
properties:
client:
description: array of client IPs / MACs / hostnames / interfaces
type: array
items:
type: string
example: ["127.0.0.1", "192.168.2.12"]
client_maybe_array:
oneOf:
- $ref: 'clients.yaml#/components/schemas/client'
- $ref: 'clients.yaml#/components/schemas/client_array'
oneOf:
- $ref: 'clients.yaml#/components/schemas/client'
- $ref: 'clients.yaml#/components/schemas/client_array'
client_object:
type: object
properties:
client:
$ref: 'clients.yaml#/components/schemas/client'
comment:
type: object
properties:

View File

@ -121,7 +121,7 @@ components:
examples:
invalid_path_depth:
$ref: 'config.yaml#/components/examples/errors/bad_request/invalid_path_depth'
item_not_found:
item_already_present:
$ref: 'config.yaml#/components/examples/errors/bad_request/item_already_present'
'401':
description: Unauthorized
@ -144,6 +144,12 @@ components:
responses:
'204':
description: Item deleted
'404':
description: Item not found
content:
application/json:
schema:
$ref: 'common.yaml#/components/schemas/took'
'400':
description: Bad request
content:
@ -155,8 +161,8 @@ components:
examples:
invalid_path_depth:
$ref: 'config.yaml#/components/examples/errors/bad_request/invalid_path_depth'
item_not_found:
$ref: 'config.yaml#/components/examples/errors/bad_request/item_not_found'
item_already_present:
$ref: 'config.yaml#/components/examples/errors/bad_request/item_already_present'
'401':
description: Unauthorized
content:
@ -420,6 +426,8 @@ components:
type: string
maxHistory:
type: integer
maxClients:
type: integer
allow_destructive:
type: boolean
temp:
@ -691,9 +699,10 @@ components:
pwhash: ''
totp_secret: ''
app_pwhash: ''
excludeClients: [ '1.2.3.4', 'localhost', 'fe80::345' ]
excludeDomains: [ 'google.de', 'pi-hole.net' ]
excludeClients: [ '1\.2\.3\.4', 'localhost', 'fe80::345' ]
excludeDomains: [ 'google\\.de', 'pi-hole\.net' ]
maxHistory: 86400
maxClients: 10
allow_destructive: true
temp:
limit: 60.0
@ -795,13 +804,6 @@ components:
key: "bad_request"
message: "Invalid path depth"
hint: "Use, e.g., DELETE /config/dnsmasq/upstreams/127.0.0.1 to remove \"127.0.0.1\" from config.dns.upstreams"
item_not_found:
summary: Item to be deleted does not exist
value:
error:
key: "bad_request"
message: "Item not found"
hint: "Can only delete existing items"
item_already_present:
summary: Item to be added exists already
value:

View File

@ -36,10 +36,17 @@ components:
operationId: "delete_dhcp"
description: |
This API hook removes a currently active DHCP lease.
Managing DHCP leases is only possible when the DHCP server is enabled.
*Note:* There will be no content on success.
responses:
'204':
description: Item deleted
'404':
description: Item not found
content:
application/json:
schema:
$ref: 'common.yaml#/components/schemas/took'
'400':
description: Bad request
content:

View File

@ -128,6 +128,12 @@ components:
responses:
'204':
description: Item deleted
'404':
description: Item not found
content:
application/json:
schema:
$ref: 'common.yaml#/components/schemas/took'
'400':
description: Bad request
content:
@ -251,6 +257,12 @@ components:
responses:
'204':
description: Items deleted
'404':
description: Item not found
content:
application/json:
schema:
$ref: 'common.yaml#/components/schemas/took'
'400':
description: Bad request
content:
@ -280,7 +292,7 @@ components:
description: Array of domains
items:
allOf:
- $ref: 'domains.yaml#/components/schemas/domain'
- $ref: 'domains.yaml#/components/schemas/domain_object'
- $ref: 'domains.yaml#/components/schemas/unicode'
- $ref: 'domains.yaml#/components/schemas/type'
- $ref: 'domains.yaml#/components/schemas/kind'
@ -302,12 +314,9 @@ components:
- $ref: 'domains.yaml#/components/schemas/groups'
- $ref: 'domains.yaml#/components/schemas/enabled'
domain:
type: object
properties:
domain:
description: Domain
type: string
example: testdomain.com
description: Domain
type: string
example: testdomain.com
unicode:
type: object
properties:
@ -316,18 +325,23 @@ components:
type: string
example: "äbc.com"
domain_array:
description: array of domains
type: array
items:
type: string
example: ["testdomain.com", "otherdomain.de"]
domain_maybe_array:
type: object
properties:
domain:
description: array of domains
type: array
items:
type: string
example: ["testdomain.com", "otherdomain.de"]
domain_maybe_array:
oneOf:
- $ref: 'domains.yaml#/components/schemas/domain'
- $ref: 'domains.yaml#/components/schemas/domain_array'
oneOf:
- $ref: 'domains.yaml#/components/schemas/domain'
- $ref: 'domains.yaml#/components/schemas/domain_array'
domain_object:
type: object
properties:
domain:
$ref: 'domains.yaml#/components/schemas/domain'
type:
type: object
properties:

View File

@ -63,6 +63,9 @@ components:
- $ref: 'groups.yaml#/components/schemas/groups/get' # identical to GET
- $ref: 'groups.yaml#/components/schemas/lists_processed'
- $ref: 'common.yaml#/components/schemas/took'
headers:
Location:
$ref: 'common.yaml#/components/headers/Location'
'400':
description: Bad request
content:
@ -94,6 +97,12 @@ components:
responses:
'204':
description: Item deleted
'404':
description: Item not found
content:
application/json:
schema:
$ref: 'common.yaml#/components/schemas/took'
'400':
description: Bad request
content:
@ -193,18 +202,14 @@ components:
- "item": "test1"
- "item": "test2"
responses:
'201':
description: Created item
'204':
description: Items deleted
'404':
description: Item not found
content:
application/json:
schema:
allOf:
- $ref: 'groups.yaml#/components/schemas/groups/get' # identical to GET
- $ref: 'groups.yaml#/components/schemas/lists_processed'
- $ref: 'common.yaml#/components/schemas/took'
headers:
Location:
$ref: 'common.yaml#/components/headers/Location'
$ref: 'common.yaml#/components/schemas/took'
'400':
description: Bad request
content:
@ -235,13 +240,14 @@ components:
type: array
items:
allOf:
- $ref: 'groups.yaml#/components/schemas/name'
- $ref: 'groups.yaml#/components/schemas/name_object'
- $ref: 'groups.yaml#/components/schemas/comment'
- $ref: 'groups.yaml#/components/schemas/enabled'
- $ref: 'groups.yaml#/components/schemas/readonly'
put:
allOf:
- $ref: 'groups.yaml#/components/schemas/name'
# Can rename group
- $ref: 'groups.yaml#/components/schemas/name_object'
- $ref: 'groups.yaml#/components/schemas/comment'
- $ref: 'groups.yaml#/components/schemas/enabled'
post:
@ -250,25 +256,27 @@ components:
- $ref: 'groups.yaml#/components/schemas/comment'
- $ref: 'groups.yaml#/components/schemas/enabled'
name:
type: object
properties:
name:
description: Group name
type: string
example: test_group
description: Group name
type: string
example: test_group
name_array:
description: array of group names
type: array
items:
type: string
example: ["test1", "test2", "test3"]
name_maybe_array:
type: object
properties:
name:
description: array of group names
type: array
items:
type: string
example: ["test1", "test2", "test3"]
name_maybe_array:
oneOf:
- $ref: 'groups.yaml#/components/schemas/name'
- $ref: 'groups.yaml#/components/schemas/name_array'
oneOf:
- $ref: 'groups.yaml#/components/schemas/name'
- $ref: 'groups.yaml#/components/schemas/name_array'
name_object:
type: object
properties:
name:
$ref: 'groups.yaml#/components/schemas/name'
comment:
type: object
properties:

View File

@ -62,7 +62,15 @@ components:
- Metrics
operationId: "get_client_metrics"
description: |
Request data needed to generate the \"Client activity over last 24 hours\" graph
Request data needed to generate the \"Client activity over last 24 hours\" graph.
This endpoint returns the top N clients, sorted by total number of queries within 24 hours. If N is set to 0, all clients will be returned.
The client name is only available if the client's IP address can be resolved to a hostname.
The last client returned is a special client that contains the total number of queries that were not sent by any of the other shown clients , i.e. queries that were sent by clients that are not in the top N. This client is always present, even if it has 0 queries and can be identified by the special name "other clients" (mind the space in the hostname) and the IP address "0.0.0.0".
Note that, due to privacy settings, the returned data may also be empty.
parameters:
- $ref: 'history.yaml#/components/parameters/clients/N'
responses:
'200':
description: OK
@ -142,6 +150,38 @@ components:
client_history:
type: object
properties:
clients:
type: array
description: Data array
items:
type: object
properties:
name:
type: string
nullable: true
description: Client name
ip:
type: string
description: Client IP address
total:
type: integer
description: Total number of queries
example:
- name: localhost
ip: "127.0.0.1"
total: 13428
- name: ip6-localnet
ip: "::1"
total: 2100
- name: null
ip: "192.168.1.1"
total: 254
- name: "pi.hole"
ip: "::"
total: 29
- name: "other clients"
ip: "0.0.0.0"
total: 14
history:
type: array
description: Data array
@ -162,28 +202,22 @@ components:
- 12
- 65
- 67
- 9
- 5
- timestamp: 1511820500.583821
data:
- 1
- 35
- 63
clients:
type: array
description: Data array
items:
type: object
properties:
name:
type: string
nullable: true
description: Client name
ip:
type: string
description: Client IP address
example:
- name: localhost
ip: "127.0.0.1"
- name: ip6-localnet
ip: "::1"
- name: null
ip: "192.168.1.1"
- 20
- 9
parameters:
clients:
N:
in: query
description: Maximum number of clients to return, setting this to 0 will return all clients
name: N
schema:
type: integer
required: false
example: 20

View File

@ -218,10 +218,16 @@ components:
parameters:
- $ref: 'info.yaml#/components/parameters/message_id'
description: |
*Note:* There will be no content on success. You may specify multiple IDs to delete multiple messages at once (comma-separated in the path like `1,2,3`)
You may specify multiple IDs to delete multiple messages at once (comma-separated in the path like `1,2,3`)
responses:
'204':
description: Item deleted
'404':
description: Not found
content:
application/json:
schema:
$ref: 'common.yaml#/components/schemas/took'
'400':
description: Bad request
content:
@ -235,6 +241,14 @@ components:
$ref: 'info.yaml#/components/examples/errors/messages/uri_error'
bad_request:
$ref: 'info.yaml#/components/examples/errors/messages/bad_request'
'401':
description: Unauthorized
content:
application/json:
schema:
allOf:
- $ref: 'common.yaml#/components/errors/unauthorized'
- $ref: 'common.yaml#/components/schemas/took'
messages_count:
get:
summary: Get count of Pi-hole diagnosis messages
@ -835,7 +849,7 @@ components:
version:
type: string
nullable: true
description: Remote (Github) Pi-hole Core version
description: Remote (Github) Pi-hole Core version (null if on custom branch)
example: "v6.1"
hash:
type: string
@ -869,7 +883,7 @@ components:
version:
type: string
nullable: true
description: Remote (Github) Pi-hole Web version
description: Remote (Github) Pi-hole Web version (null if on custom branch)
example: "v6.1"
hash:
type: string
@ -908,7 +922,7 @@ components:
version:
type: string
nullable: true
description: Remote (Github) Pi-hole FTL version
description: Remote (Github) Pi-hole FTL version (null if on custom branch)
example: "v6.1"
hash:
type: string

View File

@ -93,6 +93,12 @@ components:
responses:
'204':
description: Item deleted
'404':
description: Item not found
content:
application/json:
schema:
$ref: 'common.yaml#/components/schemas/took'
'400':
description: Bad request
content:
@ -189,18 +195,14 @@ components:
schema:
$ref: 'lists.yaml#/components/schemas/lists/post'
responses:
'201':
description: Created item
'204':
description: Items deleted
'404':
description: Item not found
content:
application/json:
schema:
allOf:
- $ref: 'lists.yaml#/components/schemas/lists/get'
- $ref: 'lists.yaml#/components/schemas/lists_processed'
- $ref: 'common.yaml#/components/schemas/took'
headers:
Location:
$ref: 'common.yaml#/components/headers/Location'
$ref: 'common.yaml#/components/schemas/took'
'400':
description: Bad request
content:
@ -230,7 +232,7 @@ components:
description: Array of lists
items:
allOf:
- $ref: 'lists.yaml#/components/schemas/list'
- $ref: 'lists.yaml#/components/schemas/address_object'
- $ref: 'lists.yaml#/components/schemas/type'
- $ref: 'lists.yaml#/components/schemas/comment'
- $ref: 'lists.yaml#/components/schemas/groups'
@ -244,31 +246,33 @@ components:
- $ref: 'lists.yaml#/components/schemas/enabled'
post:
allOf:
- $ref: 'lists.yaml#/components/schemas/list_maybe_array'
- $ref: 'lists.yaml#/components/schemas/address_maybe_array'
- $ref: 'lists.yaml#/components/schemas/type'
- $ref: 'lists.yaml#/components/schemas/comment'
- $ref: 'lists.yaml#/components/schemas/groups'
- $ref: 'lists.yaml#/components/schemas/enabled'
list:
address:
description: Address of the list
type: string
example: https://hosts-file.net/ad_servers.txt
address_array:
description: array of list addresses
type: array
items:
type: string
example: ["https://hosts-file.net/ad_servers.txt"]
address_maybe_array:
type: object
properties:
address:
description: Address of the list
type: string
example: https://hosts-file.net/ad_servers.txt
list_array:
oneOf:
- $ref: 'lists.yaml#/components/schemas/address'
- $ref: 'lists.yaml#/components/schemas/address_array'
address_object:
type: object
properties:
list:
description: array of list addresses
type: array
items:
type: string
example: ["https://hosts-file.net/ad_servers.txt"]
list_maybe_array:
oneOf:
- $ref: 'lists.yaml#/components/schemas/list'
- $ref: 'lists.yaml#/components/schemas/list_array'
address:
$ref: 'lists.yaml#/components/schemas/address'
type:
type: object
properties:

View File

@ -93,6 +93,20 @@ components:
responses:
'204':
description: No Content (deleted)
'404':
description: Not found
content:
application/json:
schema:
$ref: 'common.yaml#/components/schemas/took'
'400':
description: Bad request
content:
application/json:
schema:
allOf:
- $ref: 'common.yaml#/components/errors/bad_request'
- $ref: 'common.yaml#/components/schemas/took'
'401':
description: Unauthorized
content:

View File

@ -66,46 +66,71 @@ int api_history_clients(struct ftl_conn *api)
JSON_SEND_OBJECT_UNLOCK(json);
}
// Get number of clients to return´
unsigned int Nc = min(counters->clients, config.webserver.api.maxClients.v.u16);
if(api->request->query_string != NULL)
{
// Does the user request a non-default number of clients
get_uint_var(api->request->query_string, "N", &Nc);
// Limit the number of clients to return to the number of
// clients to avoid possible overflows for very large N
// Also allow N=0 to return all clients
if((int)Nc > counters->clients || Nc == 0)
Nc = counters->clients;
}
// Lock shared memory
lock_shm();
// Get clients which the user doesn't want to see
// if skipclient[i] == true then this client should be hidden from
// returned data. We initialize it with false
bool *skipclient = calloc(counters->clients, sizeof(bool));
unsigned int excludeClients = cJSON_GetArraySize(config.webserver.api.excludeClients.v.json);
if(excludeClients > 0)
int *temparray = calloc(2*counters->clients, sizeof(int));
if(skipclient == NULL || temparray == NULL)
{
for(int clientID = 0; clientID < counters->clients; clientID++)
{
// Get client pointer
const clientsData* client = getClient(clientID, true);
if(client == NULL)
continue;
// Check if this client should be skipped
for(unsigned int i = 0; i < excludeClients; i++)
{
cJSON *item = cJSON_GetArrayItem(config.webserver.api.excludeClients.v.json, i);
if(strcmp(getstr(client->ippos), item->valuestring) == 0 ||
strcmp(getstr(client->namepos), item->valuestring) == 0)
skipclient[clientID] = true;
}
}
unlock_shm();
return send_json_error(api, 500,
"internal_error",
"Failed to allocate memory for skipclient array",
NULL);
}
// Also skip clients included in others (in alias-clients)
// Skip clients included in others (in alias-clients)
for(int clientID = 0; clientID < counters->clients; clientID++)
{
// Get client pointer
const clientsData* client = getClient(clientID, true);
if(client == NULL)
continue;
// Check if this client should be skipped
if(!client->flags.aliasclient && client->aliasclient_id > -1)
skipclient[clientID] = true;
}
// Get MAX_CLIENTS clients with the highest number of queries
for(int clientID = 0; clientID < counters->clients; clientID++)
{
// Get client pointer
const clientsData* client = getClient(clientID, true);
// Skip invalid clients
if(client == NULL)
continue;
// Store clientID and number of queries in temporary array
temparray[2*clientID + 0] = clientID;
temparray[2*clientID + 1] = client->count;
}
// Sort temporary array
qsort(temparray, counters->clients, sizeof(int[2]), cmpdesc);
// Main return loop
cJSON *history = JSON_NEW_ARRAY();
int others_total = 0;
for(unsigned int slot = 0; slot < OVERTIME_SLOTS; slot++)
{
cJSON *item = JSON_NEW_OBJECT();
@ -113,22 +138,31 @@ int api_history_clients(struct ftl_conn *api)
// Loop over clients to generate output to be sent to the client
cJSON *data = JSON_NEW_ARRAY();
for(int clientID = 0; clientID < counters->clients; clientID++)
int others = 0;
for(int id = 0; id < counters->clients; id++)
{
if(skipclient[clientID])
continue;
// Get client pointer
const int clientID = temparray[2*id + 0];
const clientsData* client = getClient(clientID, true);
// Skip invalid clients and also those managed by alias clients
if(client == NULL || client->aliasclient_id >= 0)
// Skip invalid (recycled) clients
if(client == NULL)
continue;
const int thisclient = client->overTime[slot];
// Skip clients which should be hidden and add them to the "others" counter.
// Also skip clients when we reached the maximum number of clients to return
if(skipclient[clientID] || id >= (int)Nc)
{
others += client->overTime[slot];
continue;
}
JSON_ADD_NUMBER_TO_ARRAY(data, thisclient);
JSON_ADD_NUMBER_TO_ARRAY(data, client->overTime[slot]);
}
// Add others as last element in the array
others_total += others;
JSON_ADD_NUMBER_TO_ARRAY(data, others);
JSON_ADD_ITEM_TO_OBJECT(item, "data", data);
JSON_ADD_ITEM_TO_ARRAY(history, item);
}
@ -137,25 +171,40 @@ int api_history_clients(struct ftl_conn *api)
// 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++)
for(int id = 0; id < counters->clients; id++)
{
if(skipclient[clientID])
continue;
// Get client pointer
const int clientID = temparray[2*id + 0];
const clientsData* client = getClient(clientID, true);
// Skip invalid (recycled) clients
if(client == NULL)
continue;
// Skip clients which should be hidden. Also skip clients when
// we reached the maximum number of clients to return
if(skipclient[clientID] || id >= (int)Nc)
continue;
// Get client name and IP address
const char *client_ip = getstr(client->ippos);
const char *client_name = client->namepos != 0 ? getstr(client->namepos) : NULL;
// Create JSON object for this client
cJSON *item = JSON_NEW_OBJECT();
JSON_REF_STR_IN_OBJECT(item, "name", client_name);
JSON_REF_STR_IN_OBJECT(item, "ip", client_ip);
JSON_ADD_NUMBER_TO_OBJECT(item, "total", client->count);
JSON_ADD_ITEM_TO_ARRAY(clients, item);
}
// Add "others" client
cJSON *item = JSON_NEW_OBJECT();
JSON_REF_STR_IN_OBJECT(item, "name", "other clients");
JSON_REF_STR_IN_OBJECT(item, "ip", "0.0.0.0");
JSON_ADD_NUMBER_TO_OBJECT(item, "total", others_total);
JSON_ADD_ITEM_TO_ARRAY(clients, 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 and all strings in the JSON are references to idempotent shared
@ -164,6 +213,7 @@ int api_history_clients(struct ftl_conn *api)
// Free memory
free(skipclient);
free(temparray);
JSON_ADD_ITEM_TO_OBJECT(json, "clients", clients);
JSON_SEND_OBJECT(json);

View File

@ -750,11 +750,26 @@ int api_info_version(struct ftl_conn *api)
//else if(strcmp(key, "FTL_VERSION") == 0)
// JSON_COPY_STR_TO_OBJECT(ftl_local, "version", value);
else if(strcmp(key, "GITHUB_CORE_VERSION") == 0)
JSON_COPY_STR_TO_OBJECT(core_remote, "version", value);
{
if(strcmp(value, "null") == 0)
JSON_ADD_NULL_TO_OBJECT(core_remote, "version");
else
JSON_COPY_STR_TO_OBJECT(core_remote, "version", value);
}
else if(strcmp(key, "GITHUB_WEB_VERSION") == 0)
JSON_COPY_STR_TO_OBJECT(web_remote, "version", value);
{
if(strcmp(value, "null") == 0)
JSON_ADD_NULL_TO_OBJECT(web_remote, "version");
else
JSON_COPY_STR_TO_OBJECT(web_remote, "version", value);
}
else if(strcmp(key, "GITHUB_FTL_VERSION") == 0)
JSON_COPY_STR_TO_OBJECT(ftl_remote, "version", value);
{
if(strcmp(value, "null") == 0)
JSON_ADD_NULL_TO_OBJECT(ftl_remote, "version");
else
JSON_COPY_STR_TO_OBJECT(ftl_remote, "version", value);
}
else if(strcmp(key, "CORE_HASH") == 0)
JSON_COPY_STR_TO_OBJECT(core_local, "hash", value);
else if(strcmp(key, "WEB_HASH") == 0)
@ -940,15 +955,18 @@ static int api_info_messages_DELETE(struct ftl_conn *api)
}
// Delete message with this ID from the database
delete_message(ids);
int deleted = 0;
delete_message(ids, &deleted);
// Free memory
free(id);
cJSON_free(ids);
// Send empty reply with code 204 No Content
// Send empty reply with codes:
// - 204 No Content (if any items were deleted)
// - 404 Not Found (if no items were deleted)
cJSON *json = JSON_NEW_OBJECT();
JSON_SEND_OBJECT_CODE(json, 204);
JSON_SEND_OBJECT_CODE(json, deleted > 0 ? 204 : 404);
}
int api_info_messages(struct ftl_conn *api)

View File

@ -510,6 +510,20 @@ static int api_list_write(struct ftl_conn *api,
if(api->method == HTTP_PUT)
response_code = 200; // 200 - OK
// Add "Location" header to response
if(snprintf(pi_hole_extra_headers, sizeof(pi_hole_extra_headers), "Location: %s/%s", api->action_path, row.item) >= (int)sizeof(pi_hole_extra_headers))
{
// This may happen for *extremely* long URLs but is not issue in
// itself. Merely add a warning to the log file
log_warn("Could not add Location header to response: URL too long");
// Truncate location by replacing the last characters with "...\0"
pi_hole_extra_headers[sizeof(pi_hole_extra_headers)-4] = '.';
pi_hole_extra_headers[sizeof(pi_hole_extra_headers)-3] = '.';
pi_hole_extra_headers[sizeof(pi_hole_extra_headers)-2] = '.';
pi_hole_extra_headers[sizeof(pi_hole_extra_headers)-1] = '\0';
}
// Send GET style reply
const int ret = api_list_read(api, response_code, listtype, row.item, processed);
@ -691,7 +705,8 @@ static int api_list_remove(struct ftl_conn *api,
}
// From here on, we can assume the JSON payload is valid
if(gravityDB_delFromTable(listtype, array, &sql_msg))
unsigned int deleted = 0u;
if(gravityDB_delFromTable(listtype, array, &deleted, &sql_msg))
{
// Inform the resolver that it needs to reload gravity
set_event(RELOAD_GRAVITY);
@ -700,9 +715,11 @@ static int api_list_remove(struct ftl_conn *api,
if(allocated_json)
cJSON_free(array);
// Send empty reply with code 204 No Content
// Send empty reply with codes:
// - 204 No Content (if any items were deleted)
// - 404 Not Found (if no items were deleted)
cJSON *json = JSON_NEW_OBJECT();
JSON_SEND_OBJECT_CODE(json, 204);
JSON_SEND_OBJECT_CODE(json, deleted > 0u ? 204 : 404);
}
else
{

View File

@ -440,7 +440,8 @@ static int api_network_devices_DELETE(struct ftl_conn *api)
// Delete row from network table by ID
const char *sql_msg = NULL;
if(!networkTable_deleteDevice(db, device_id, &sql_msg))
int deleted = 0;
if(!networkTable_deleteDevice(db, device_id, &deleted, &sql_msg))
{
// Add SQL message (may be NULL = not available)
return send_json_error(api, 500,
@ -452,9 +453,11 @@ static int api_network_devices_DELETE(struct ftl_conn *api)
// Close database
dbclose(&db);
// Send empty reply with code 204 No Content
// Send empty reply with codes:
// - 204 No Content (if any items were deleted)
// - 404 Not Found (if no items were deleted)
cJSON *json = JSON_NEW_OBJECT();
JSON_SEND_OBJECT_CODE(json, 204);
JSON_SEND_OBJECT_CODE(json, deleted > 0 ? 204 : 404);
}
int api_network_devices(struct ftl_conn *api)

View File

@ -19,7 +19,6 @@
#include "database/aliasclients.h"
// get_memdb()
#include "database/query-table.h"
// dbopen(false, ), dbclose()
#include "database/common.h"
@ -438,6 +437,26 @@ int api_queries(struct ftl_conn *api)
}
}
// We use this boolean to memorize if we are filtering at all. It is used
// later to decide if we can short-circuit the query counting for
// performance reasons.
bool filtering = false;
// Regex filtering?
regex_t *regex_domains = NULL;
unsigned int N_regex_domains = 0;
if(compile_filter_regex(api, "webserver.api.excludeDomains",
config.webserver.api.excludeDomains.v.json,
&regex_domains, &N_regex_domains))
filtering = true;
regex_t *regex_clients = NULL;
unsigned int N_regex_clients = 0;
if(compile_filter_regex(api, "webserver.api.excludeClients",
config.webserver.api.excludeClients.v.json,
&regex_clients, &N_regex_clients))
filtering = true;
// Finish preparing query string
querystr_finish(querystr, sort_col, sort_dir);
@ -462,10 +481,6 @@ int api_queries(struct ftl_conn *api)
sqlite3_errstr(rc));
}
// We use this boolean to memorize if we are filtering at all. It is used
// later to decide if we can short-circuit the query counting for
// performance reasons.
bool filtering = false;
// Bind items to prepared statement
if(api->request->query_string != NULL)
{
@ -711,13 +726,74 @@ int api_queries(struct ftl_conn *api)
log_debug(DEBUG_API, " with cursor: %lu, start: %u, length: %d", cursor, start, length);
cJSON *queries = JSON_NEW_ARRAY();
unsigned int added = 0, recordsCounted = 0;
unsigned int added = 0, recordsCounted = 0, regex_skipped = 0;
bool skipTheRest = false;
while((rc = sqlite3_step(read_stmt)) == SQLITE_ROW)
{
// Increase number of records from the database
recordsCounted++;
// Apply possible domain regex filters to Query Log
const char *domain = (const char*)sqlite3_column_text(read_stmt, 4); // d.domain
if(N_regex_domains > 0)
{
bool match = false;
// Iterate over all regex filters
for(unsigned int i = 0; i < N_regex_domains; i++)
{
// Check if the domain matches the regex
if(regexec(&regex_domains[i], domain, 0, NULL, 0) == 0)
{
// Domain matches
match = true;
break;
}
}
if(match)
{
// Domain matches, we skip it and adjust the
// counter
recordsCounted--;
regex_skipped++;
continue;
}
}
// Apply possible client regex filters to Query Log
const char *client_ip = (const char*)sqlite3_column_text(read_stmt, 10); // c.ip
const char *client_name = NULL;
if(sqlite3_column_type(read_stmt, 11) == SQLITE_TEXT && sqlite3_column_bytes(read_stmt, 11) > 0)
client_name = (const char*)sqlite3_column_text(read_stmt, 11); // c.name
if(N_regex_clients > 0)
{
bool match = false;
// Iterate over all regex filters
for(unsigned int i = 0; i < N_regex_clients; i++)
{
// Check if the domain matches the regex
if(regexec(&regex_clients[i], client_ip, 0, NULL, 0) == 0)
{
// Client IP matches
match = true;
break;
}
else if(client_name != NULL && regexec(&regex_clients[i], client_name, 0, NULL, 0) == 0)
{
// Client name matches
match = true;
break;
}
}
if(match)
{
// Domain matches, we skip it and adjust the
// counter
recordsCounted--;
regex_skipped++;
continue;
}
}
// Skip all records once we have enough (but still count them)
if(skipTheRest)
continue;
@ -753,7 +829,27 @@ int api_queries(struct ftl_conn *api)
{
// Skip everything AFTER we added the requested number
// of queries if length is > 0.
break;
continue;
}
// Check if we have reached the limit
if(added >= (unsigned int)length)
{
if(filtering)
{
// We are filtering, so we have to continue to
// step over the remaining rows to get the
// correct number of total records
skipTheRest = true;
continue;
}
else
{
// We are not filtering, so we can stop here
// The total number of records is the number
// of records in the database
break;
}
}
// Build item object
@ -770,7 +866,7 @@ int api_queries(struct ftl_conn *api)
JSON_COPY_STR_TO_OBJECT(item, "type", get_query_type_str(query.type, &query, buffer));
JSON_REF_STR_IN_OBJECT(item, "status", get_query_status_str(query.status));
JSON_REF_STR_IN_OBJECT(item, "dnssec", get_query_dnssec_str(query.dnssec));
JSON_COPY_STR_TO_OBJECT(item, "domain", sqlite3_column_text(read_stmt, 4)); // d.domain
JSON_COPY_STR_TO_OBJECT(item, "domain", domain);
if(sqlite3_column_type(read_stmt, 5) == SQLITE_TEXT &&
sqlite3_column_bytes(read_stmt, 5) > 0)
@ -784,11 +880,9 @@ int api_queries(struct ftl_conn *api)
JSON_ADD_ITEM_TO_OBJECT(item, "reply", reply);
cJSON *client = JSON_NEW_OBJECT();
JSON_COPY_STR_TO_OBJECT(client, "ip", sqlite3_column_text(read_stmt, 10)); // c.ip
if(sqlite3_column_type(read_stmt, 11) == SQLITE_TEXT &&
sqlite3_column_bytes(read_stmt, 11) > 0)
JSON_COPY_STR_TO_OBJECT(client, "name", sqlite3_column_text(read_stmt, 11)); // c.name
JSON_COPY_STR_TO_OBJECT(client, "ip", client_ip);
if(client_name != NULL)
JSON_COPY_STR_TO_OBJECT(client, "name", client_name);
else
JSON_ADD_NULL_TO_OBJECT(client, "name");
JSON_ADD_ITEM_TO_OBJECT(item, "client", client);
@ -836,8 +930,8 @@ int api_queries(struct ftl_conn *api)
added++;
}
log_debug(DEBUG_API, "Sending %u of %lu in memory and %lu on disk queries (counted %u)",
added, mem_dbnum, disk_dbnum, recordsCounted);
log_debug(DEBUG_API, "Sending %u of %lu in memory and %lu on disk queries (counted %u, skipped %u)",
added, mem_dbnum, disk_dbnum, recordsCounted, regex_skipped);
cJSON *json = JSON_NEW_OBJECT();
JSON_ADD_ITEM_TO_OBJECT(json, "queries", queries);
@ -866,5 +960,80 @@ int api_queries(struct ftl_conn *api)
// Finalize statements
sqlite3_finalize(read_stmt);
// Free regex memory if allocated
if(N_regex_domains > 0)
{
// Free individual regexes
for(unsigned int i = 0; i < N_regex_domains; i++)
regfree(&regex_domains[i]);
// Free array of regex pointers
free(regex_domains);
}
if(N_regex_clients > 0)
{
// Free individual regexes
for(unsigned int i = 0; i < N_regex_clients; i++)
regfree(&regex_clients[i]);
// Free array of regex po^inters
free(regex_clients);
}
JSON_SEND_OBJECT(json);
}
bool compile_filter_regex(struct ftl_conn *api, const char *path, cJSON *json, regex_t **regex, unsigned int *N_regex)
{
const int N = cJSON_GetArraySize(json);
if(N < 1)
return false;
// Set number of regexes (positive = unsigned integer)
*N_regex = N;
// Allocate memory for regex array
*regex = calloc(N, sizeof(regex_t));
if(*regex == NULL)
{
return send_json_error(api, 500,
"internal_error",
"Internal server error, failed to allocate memory for regex array",
NULL);
}
// Compile regexes
unsigned int i = 0;
cJSON *filter = NULL;
cJSON_ArrayForEach(filter, json)
{
// Skip non-string, invalid and empty values
if(!cJSON_IsString(filter) || filter->valuestring == NULL || strlen(filter->valuestring) == 0)
{
log_warn("Skipping invalid regex at %s.%u", path, i);
continue;
}
// Compile regex
int rc = regcomp(&(*regex)[i], filter->valuestring, REG_EXTENDED);
if(rc != 0)
{
// Failed to compile regex
char errbuf[1024] = { 0 };
regerror(rc, &(*regex)[i], errbuf, sizeof(errbuf));
log_err("Failed to compile regex \"%s\": %s",
filter->valuestring, errbuf);
return send_json_error(api, 400,
"bad_request",
"Failed to compile regex",
filter->valuestring);
}
i++;
}
// We are filtering, so we have to continue to step over the
// remaining rows to get the correct number of total records
return true;
}

View File

@ -8,22 +8,22 @@
* 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/http-common.h"
#include "../webserver/json_macros.h"
#include "api.h"
#include "../shmem.h"
#include "../datastructure.h"
#include "FTL.h"
#include "webserver/http-common.h"
#include "webserver/json_macros.h"
#include "api/api.h"
#include "shmem.h"
#include "datastructure.h"
// read_setupVarsconf()
#include "../config/setupVars.h"
#include "config/setupVars.h"
// logging routines
#include "../log.h"
#include "log.h"
// config struct
#include "../config/config.h"
#include "config/config.h"
// overTime data
#include "../overTime.h"
#include "overTime.h"
// enum REGEX
#include "../regex_r.h"
#include "regex_r.h"
// sqrt()
#include <math.h>
@ -42,7 +42,7 @@ static int __attribute__((pure)) cmpasc(const void *a, const void *b)
} */
// qsort subroutine, sort DESC
static int __attribute__((pure)) cmpdesc(const void *a, const void *b)
int __attribute__((pure)) cmpdesc(const void *a, const void *b)
{
const int *elem1 = (int*)a;
const int *elem2 = (int*)b;
@ -137,15 +137,6 @@ int api_stats_summary(struct ftl_conn *api)
int api_stats_top_domains(struct ftl_conn *api)
{
int count = 10;
const int domains = counters->domains;
int *temparray = calloc(2*domains, sizeof(int*));
if(temparray == NULL)
{
log_err("Memory allocation failed in %s()", __FUNCTION__);
return 0;
}
// Exit before processing any data if requested via config setting
if(config.misc.privacylevel.v.privacy_level >= PRIVACY_HIDE_DOMAINS)
{
@ -157,11 +148,24 @@ int api_stats_top_domains(struct ftl_conn *api)
cJSON *json = JSON_NEW_OBJECT();
cJSON *top_domains = JSON_NEW_ARRAY();
JSON_ADD_ITEM_TO_OBJECT(json, "top_domains", top_domains);
free(temparray);
JSON_SEND_OBJECT(json);
}
// Lock shared memory
lock_shm();
// Allocate memory
const int domains = counters->domains;
int *temparray = calloc(2*domains, sizeof(int));
if(temparray == NULL)
{
log_err("Memory allocation failed in %s()", __FUNCTION__);
return 0;
}
bool blocked = false; // Can be overwritten by query string
int count = 10;
// /api/stats/top_domains?blocked=true
if(api->request->query_string != NULL)
{
@ -173,10 +177,8 @@ int api_stats_top_domains(struct ftl_conn *api)
get_int_var(api->request->query_string, "count", &count);
}
// Lock shared memory
lock_shm();
for(int domainID=0; domainID < domains; domainID++)
unsigned int added_domains = 0u;
for(int domainID = 0; domainID < domains; domainID++)
{
// Get domain pointer
const domainsData* domain = getDomain(domainID, true);
@ -189,21 +191,23 @@ int api_stats_top_domains(struct ftl_conn *api)
else
// Count only permitted queries
temparray[2*domainID + 1] = (domain->count - domain->blockedcount);
added_domains++;
}
// Sort temporary array
qsort(temparray, domains, sizeof(int[2]), cmpdesc);
qsort(temparray, added_domains, sizeof(int[2]), cmpdesc);
// Get filter
const char* filter = read_setupVarsconf("API_QUERY_LOG_SHOW");
const char* log_show = read_setupVarsconf("API_QUERY_LOG_SHOW");
bool showpermitted = true, showblocked = true;
if(filter != NULL)
if(log_show != NULL)
{
if((strcmp(filter, "permittedonly")) == 0)
if((strcmp(log_show, "permittedonly")) == 0)
showblocked = false;
else if((strcmp(filter, "blockedonly")) == 0)
else if((strcmp(log_show, "blockedonly")) == 0)
showpermitted = false;
else if((strcmp(filter, "nothing")) == 0)
else if((strcmp(log_show, "nothing")) == 0)
{
showpermitted = false;
showblocked = false;
@ -212,11 +216,15 @@ int api_stats_top_domains(struct ftl_conn *api)
clearSetupVarsArray();
// Get domains which the user doesn't want to see
unsigned int excludeDomains = cJSON_GetArraySize(config.webserver.api.excludeDomains.v.json);
regex_t *regex_domains = NULL;
unsigned int N_regex_domains = 0;
compile_filter_regex(api, "webserver.api.excludeDomains",
config.webserver.api.excludeDomains.v.json,
&regex_domains, &N_regex_domains);
int n = 0;
cJSON *top_domains = JSON_NEW_ARRAY();
for(int i = 0; i < domains; i++)
for(unsigned int i = 0; i < added_domains; i++)
{
// Get sorted index
const int domainID = temparray[2*i + 0];
@ -225,22 +233,31 @@ int api_stats_top_domains(struct ftl_conn *api)
if(domain == NULL)
continue;
// Skip this domain if there is a filter on it
bool skip_domain = false;
for(unsigned int j = 0; j < excludeDomains; j++)
{
cJSON *item = cJSON_GetArrayItem(config.webserver.api.excludeDomains.v.json, j);
if(strcmp(getstr(domain->domainpos), item->valuestring) == 0)
{
skip_domain = true;
break;
}
}
if(skip_domain)
continue;
// Get domain name
const char *domain_name = getstr(domain->domainpos);
// Hidden domain, probably due to privacy level. Skip this in the top lists
if(strcmp(getstr(domain->domainpos), HIDDEN_DOMAIN) == 0)
if(strcmp(domain_name, HIDDEN_DOMAIN) == 0)
continue;
// Skip this client if there is a filter on it
bool skip_domain = false;
if(N_regex_domains > 0)
{
// Iterate over all regex filters
for(unsigned int j = 0; j < N_regex_domains; j++)
{
// Check if the domain matches the regex
if(regexec(&regex_domains[j], domain_name, 0, NULL, 0) == 0)
{
// Domain matches
skip_domain = true;
break;
}
}
}
if(skip_domain)
continue;
int domain_count = -1;
@ -257,7 +274,7 @@ int api_stats_top_domains(struct ftl_conn *api)
if(domain_count > -1)
{
cJSON *domain_item = JSON_NEW_OBJECT();
JSON_REF_STR_IN_OBJECT(domain_item, "domain", getstr(domain->domainpos));
JSON_REF_STR_IN_OBJECT(domain_item, "domain", domain_name);
JSON_ADD_NUMBER_TO_OBJECT(domain_item, "count", domain_count);
JSON_ADD_ITEM_TO_ARRAY(top_domains, domain_item);
}
@ -268,6 +285,17 @@ int api_stats_top_domains(struct ftl_conn *api)
}
free(temparray);
// Free regexes
if(N_regex_domains > 0)
{
// Free individual regexes
for(unsigned int i = 0; i < N_regex_domains; i++)
regfree(&regex_domains[i]);
// Free array of regex pointers
free(regex_domains);
}
cJSON *json = JSON_NEW_OBJECT();
JSON_ADD_ITEM_TO_OBJECT(json, "domains", top_domains);
@ -282,7 +310,7 @@ int api_stats_top_clients(struct ftl_conn *api)
{
int count = 10;
const int clients = counters->clients;
int *temparray = calloc(2*clients, sizeof(int*));
int *temparray = calloc(2*clients, sizeof(int));
if(temparray == NULL)
{
log_err("Memory allocation failed in api_stats_top_clients()");
@ -336,11 +364,15 @@ int api_stats_top_clients(struct ftl_conn *api)
qsort(temparray, clients, sizeof(int[2]), cmpdesc);
// Get clients which the user doesn't want to see
unsigned int excludeClients = cJSON_GetArraySize(config.webserver.api.excludeClients.v.json);
regex_t *regex_clients = NULL;
unsigned int N_regex_clients = 0;
compile_filter_regex(api, "webserver.api.excludeClients",
config.webserver.api.excludeClients.v.json,
&regex_clients, &N_regex_clients);
int n = 0;
cJSON *top_clients = JSON_NEW_ARRAY();
for(int i=0; i < clients; i++)
for(int i = 0; i < clients; i++)
{
// Get sorted indices and counter values (may be either total or blocked count)
const int clientID = temparray[2*i + 0];
@ -350,29 +382,40 @@ int api_stats_top_clients(struct ftl_conn *api)
if(client == NULL)
continue;
// Skip this client if there is a filter on it
bool skip_client = false;
for(unsigned int j = 0; j < excludeClients; j++)
{
cJSON *item = cJSON_GetArrayItem(config.webserver.api.excludeClients.v.json, j);
if(strcmp(getstr(client->ippos), item->valuestring) == 0 ||
strcmp(getstr(client->namepos), item->valuestring) == 0)
{
skip_client = true;
break;
}
}
if(skip_client)
continue;
// Hidden client, probably due to privacy level. Skip this in the top lists
if(strcmp(getstr(client->ippos), HIDDEN_CLIENT) == 0)
continue;
// Get client IP and name
// Get IP and host name of client
const char *client_ip = getstr(client->ippos);
const char *client_name = getstr(client->namepos);
// Hidden client, probably due to privacy level. Skip this in the top lists
if(strcmp(client_ip, HIDDEN_CLIENT) == 0)
continue;
// Skip this client if there is a filter on it
bool skip_client = false;
if(N_regex_clients > 0)
{
// Iterate over all regex filters
for(unsigned int j = 0; j < N_regex_clients; j++)
{
// Check if the domain matches the regex
if(regexec(&regex_clients[j], client_ip, 0, NULL, 0) == 0)
{
// Client IP matches
skip_client = true;
break;
}
else if(client_name != NULL && regexec(&regex_clients[j], client_name, 0, NULL, 0) == 0)
{
// Client name matches
skip_client = true;
break;
}
}
}
if(skip_client)
continue;
// Return this client if the client made at least one query
// within the most recent 24 hours
if(client_count > 0)
@ -391,6 +434,17 @@ int api_stats_top_clients(struct ftl_conn *api)
// Free temporary array
free(temparray);
// Free regexes
if(N_regex_clients > 0)
{
// Free individual regexes
for(unsigned int i = 0; i < N_regex_clients; i++)
regfree(&regex_clients[i]);
// Free array of regex pointers
free(regex_clients);
}
cJSON *json = JSON_NEW_OBJECT();
JSON_ADD_ITEM_TO_OBJECT(json, "clients", top_clients);
@ -405,7 +459,7 @@ int api_stats_upstreams(struct ftl_conn *api)
{
unsigned int totalcount = 0;
const int upstreams = counters->upstreams;
int *temparray = calloc(2*upstreams, sizeof(int*));
int *temparray = calloc(2*upstreams, sizeof(int));
if(temparray == NULL)
{
log_err("Memory allocation failed in api_stats_upstreams()");

View File

@ -160,8 +160,9 @@ static bool readStringValue(struct conf_item *conf_item, const char *value, stru
// Get password hash as allocated string (an empty string is hashed to an empty string)
char *pwhash = strlen(value) > 0 ? create_password(value) : strdup("");
// Verify that the password hash is valid
if(verify_password(value, pwhash, false) != PASSWORD_CORRECT)
// Verify that the password hash is either valid or empty
const enum password_result status = verify_password(value, pwhash, false);
if(status != PASSWORD_CORRECT && status != NO_PASSWORD_SET)
{
log_err("Failed to create password hash (verification failed), password remains unchanged");
free(pwhash);

View File

@ -968,14 +968,14 @@ void initConfig(struct config *conf)
conf->webserver.api.app_pwhash.d.s = (char*)"";
conf->webserver.api.excludeClients.k = "webserver.api.excludeClients";
conf->webserver.api.excludeClients.h = "Array of clients to be excluded from certain API responses\n Example: [ \"192.168.2.56\", \"fe80::341\", \"localhost\" ]";
conf->webserver.api.excludeClients.a = cJSON_CreateStringReference("array of IP addresses and/or hostnames");
conf->webserver.api.excludeClients.h = "Array of clients to be excluded from certain API responses (regex):\n - Query Log (/api/queries)\n - Top Clients (/api/stats/top_clients)\n This setting accepts both IP addresses (IPv4 and IPv6) as well as hostnames.\n Note that backslashes \"\\\" need to be escaped, i.e. \"\\\\\" in this setting\n\n Example: [ \"^192\\\\.168\\\\.2\\\\.56$\", \"^fe80::341:[0-9a-f]*$\", \"^localhost$\" ]";
conf->webserver.api.excludeClients.a = cJSON_CreateStringReference("array of regular expressions describing clients");
conf->webserver.api.excludeClients.t = CONF_JSON_STRING_ARRAY;
conf->webserver.api.excludeClients.d.json = cJSON_CreateArray();
conf->webserver.api.excludeDomains.k = "webserver.api.excludeDomains";
conf->webserver.api.excludeDomains.h = "Array of domains to be excluded from certain API responses\n Example: [ \"google.de\", \"pi-hole.net\" ]";
conf->webserver.api.excludeDomains.a = cJSON_CreateStringReference("array of domains");
conf->webserver.api.excludeDomains.h = "Array of domains to be excluded from certain API responses (regex):\n - Query Log (/api/queries)\n - Top Clients (/api/stats/top_domains)\n Note that backslashes \"\\\" need to be escaped, i.e. \"\\\\\" in this setting\n\n Example: [ \"(^|\\\\.)\\\\.google\\\\.de$\", \"\\\\.pi-hole\\\\.net$\" ]";
conf->webserver.api.excludeDomains.a = cJSON_CreateStringReference("array of regular expressions describing domains");
conf->webserver.api.excludeDomains.t = CONF_JSON_STRING_ARRAY;
conf->webserver.api.excludeDomains.d.json = cJSON_CreateArray();
@ -984,6 +984,11 @@ void initConfig(struct config *conf)
conf->webserver.api.maxHistory.t = CONF_UINT;
conf->webserver.api.maxHistory.d.ui = MAXLOGAGE*3600;
conf->webserver.api.maxClients.k = "webserver.api.maxClients";
conf->webserver.api.maxClients.h = "Up to how many clients should be returned in the activity graph endpoint (/api/history/clients)?\n This setting can be overwritten at run-time using the parameter N";
conf->webserver.api.maxClients.t = CONF_UINT16;
conf->webserver.api.maxClients.d.u16 = 10;
conf->webserver.api.allow_destructive.k = "webserver.api.allow_destructive";
conf->webserver.api.allow_destructive.h = "Allow destructive API calls (e.g. deleting all queries, powering off the system, ...)";
conf->webserver.api.allow_destructive.t = CONF_BOOL;

View File

@ -249,6 +249,7 @@ struct config {
struct conf_item excludeClients;
struct conf_item excludeDomains;
struct conf_item maxHistory;
struct conf_item maxClients;
struct conf_item allow_destructive;
struct {
struct conf_item limit;

View File

@ -307,7 +307,6 @@ 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("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);

View File

@ -328,6 +328,7 @@ enum password_result verify_login(const char *password)
log_debug(DEBUG_API, "App password correct");
return APPPASSWORD_CORRECT;
}
// Return result
return pw;
}
@ -336,7 +337,7 @@ enum password_result verify_password(const char *password, const char *pwhash, c
{
// No password set
if(pwhash == NULL || pwhash[0] == '\0')
return PASSWORD_CORRECT;
return NO_PASSWORD_SET;
// No password supplied
if(password == NULL || password[0] == '\0')
@ -606,8 +607,9 @@ bool set_and_check_password(struct conf_item *conf_item, const char *password)
// Get password hash as allocated string (an empty string is hashed to an empty string)
char *pwhash = strlen(password) > 0 ? create_password(password) : strdup("");
// Verify that the password hash is valid
if(verify_password(password, pwhash, false) != PASSWORD_CORRECT)
// Verify that the password hash is valid or that no password is set
const enum password_result status = verify_password(password, pwhash, false);
if(status != PASSWORD_CORRECT && status != NO_PASSWORD_SET)
{
free(pwhash);
log_warn("Failed to create password hash (verification failed), password remains unchanged");

View File

@ -26,6 +26,7 @@ enum password_result {
PASSWORD_INCORRECT = 0,
PASSWORD_CORRECT = 1,
APPPASSWORD_CORRECT = 2,
NO_PASSWORD_SET = 3,
PASSWORD_RATE_LIMITED = -1
} __attribute__((packed));

View File

@ -120,7 +120,7 @@ static void get_conf_bool_from_setupVars(const char *key, struct conf_item *conf
key, conf_item->k, conf_item->v.b ? "true" : "false");
}
static void get_conf_string_array_from_setupVars(const char *key, struct conf_item *conf_item)
static void get_conf_string_array_from_setupVars_regex(const char *key, struct conf_item *conf_item)
{
// Verify we are allowed to use this function
if(conf_item->t != CONF_JSON_STRING_ARRAY)
@ -137,12 +137,56 @@ static void get_conf_string_array_from_setupVars(const char *key, struct conf_it
getSetupVarsArray(array);
for (unsigned int i = 0; i < setupVarsElements; ++i)
{
// Convert to regex by adding ^ and $ to the string and replacing . with \.
// We need to allocate memory for this
char *regex = calloc(2*strlen(setupVarsArray[i]), sizeof(char));
if(regex == NULL)
{
log_warn("get_conf_string_array_from_setupVars(%s) failed: Could not allocate memory for regex", key);
continue;
}
// Copy string
strcpy(regex, setupVarsArray[i]);
// Replace . with \.
char *p = regex;
while(*p)
{
if(*p == '.')
{
// Move the rest of the string one character to the right
memmove(p + 1, p, strlen(p) + 1);
// Insert the escape character
*p = '\\';
// Skip the escape character
p++;
}
p++;
}
// Add ^ and $ to the string
char *regex2 = calloc(strlen(regex) + 3, sizeof(char));
if(regex2 == NULL)
{
log_warn("get_conf_string_array_from_setupVars(%s) failed: Could not allocate memory for regex2", key);
free(regex);
continue;
}
sprintf(regex2, "^%s$", regex);
// Free memory
free(regex);
// Add string to our JSON array
cJSON *item = cJSON_CreateString(setupVarsArray[i]);
cJSON *item = cJSON_CreateString(regex2);
cJSON_AddItemToArray(conf_item->v.json, item);
log_debug(DEBUG_CONFIG, "setupVars.conf:%s -> Setting %s[%u] = %s\n",
key, conf_item->k, i, item->valuestring);
key, conf_item->k, i, item->valuestring);
// Free memory
free(regex2);
}
}
@ -384,10 +428,10 @@ void importsetupVarsConf(void)
get_conf_bool_from_setupVars("BLOCKING_ENABLED", &config.dns.blocking.active);
// Get clients which the user doesn't want to see
get_conf_string_array_from_setupVars("API_EXCLUDE_CLIENTS", &config.webserver.api.excludeClients);
get_conf_string_array_from_setupVars_regex("API_EXCLUDE_CLIENTS", &config.webserver.api.excludeClients);
// Get domains which the user doesn't want to see
get_conf_string_array_from_setupVars("API_EXCLUDE_DOMAINS", &config.webserver.api.excludeDomains);
get_conf_string_array_from_setupVars_regex("API_EXCLUDE_DOMAINS", &config.webserver.api.excludeDomains);
// Try to obtain temperature hot value
get_conf_temp_limit_from_setupVars();
@ -583,15 +627,27 @@ void getSetupVarsArray(const char * input)
/* split string and append tokens to 'res' */
while (p) {
setupVarsArray = realloc(setupVarsArray, sizeof(char*) * ++setupVarsElements);
if(setupVarsArray == NULL) return;
char **tmp = realloc(setupVarsArray, sizeof(char*) * ++setupVarsElements);
if(tmp == NULL)
{
free(setupVarsArray);
setupVarsArray = NULL;
return;
}
setupVarsArray = tmp;
setupVarsArray[setupVarsElements-1] = p;
p = strtok(NULL, ",");
}
/* realloc one extra element for the last NULL */
setupVarsArray = realloc(setupVarsArray, sizeof(char*) * (setupVarsElements+1));
if(setupVarsArray == NULL) return;
char **tmp = realloc(setupVarsArray, sizeof(char*) * (setupVarsElements+1));
if(tmp == NULL)
{
free(setupVarsArray);
setupVarsArray = NULL;
return;
}
setupVarsArray = tmp;
setupVarsArray[setupVarsElements] = NULL;
}

View File

@ -298,4 +298,36 @@ bool validate_filepath_empty(union conf_value *val, char err[VALIDATOR_ERRBUF_LE
// else:
return validate_filepath(val, err);
}
}
// Validate array of regexes
bool validate_regex_array(union conf_value *val, char err[VALIDATOR_ERRBUF_LEN])
{
if(!cJSON_IsArray(val->json))
{
strncat(err, "Not an array", VALIDATOR_ERRBUF_LEN);
return false;
}
for(int i = 1; i <= cJSON_GetArraySize(val->json); i++)
{
// Get array item
cJSON *item = cJSON_GetArrayItem(val->json, i-1);
// Check if it's a string
if(!cJSON_IsString(item))
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%d%s element is not a string", i, get_ordinal_suffix(i));
return false;
}
// Check if it's a valid regex
if(!validate_regex(item->valuestring))
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%d%s element is not a valid regex (\"%s\")", i, get_ordinal_suffix(i), item->valuestring);
return false;
}
}
return true;
}

View File

@ -1792,8 +1792,9 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
return okay;
}
bool gravityDB_delFromTable(const enum gravity_list_type listtype, const cJSON* array, const char **message)
bool gravityDB_delFromTable(const enum gravity_list_type listtype, const cJSON* array, unsigned int *deleted, const char **message)
{
// Return early if database is not available
if(gravity_db == NULL)
{
*message = "Database not available";
@ -2004,6 +2005,9 @@ bool gravityDB_delFromTable(const enum gravity_list_type listtype, const cJSON*
break;
}
// Add number of deleted rows
*deleted += sqlite3_changes(gravity_db);
}
// Drop temporary table

View File

@ -69,7 +69,7 @@ bool gravityDB_readTableGetRow(const enum gravity_list_type listtype, tablerow *
void gravityDB_readTableFinalize(void);
bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
const char **message, const enum http_method method);
bool gravityDB_delFromTable(const enum gravity_list_type listtype, const cJSON* array, const char **message);
bool gravityDB_delFromTable(const enum gravity_list_type listtype, const cJSON* array, unsigned int *deleted, const char **message);
bool gravityDB_edit_groups(const enum gravity_list_type listtype, cJSON *groups,
const tablerow *row, const char **message);

View File

@ -378,7 +378,7 @@ end_of_add_message: // Close database connection
return rowid;
}
bool delete_message(cJSON *ids)
bool delete_message(cJSON *ids, int *deleted)
{
// Return early if database is known to be broken
if(FTLDBerror())
@ -413,6 +413,10 @@ bool delete_message(cJSON *ids)
log_err("SQL error (%i): %s", sqlite3_errcode(db), sqlite3_errmsg(db));
return false;
}
// Add to deleted count
*deleted += sqlite3_changes(db);
sqlite3_reset(res);
sqlite3_clear_bindings(res);
}

View File

@ -16,7 +16,7 @@
int count_messages(const bool filter_dnsmasq_warnings);
bool format_messages(cJSON *array);
bool create_message_table(sqlite3 *db);
bool delete_message(cJSON *ids);
bool delete_message(cJSON *ids, int *deleted);
bool flush_message_table(void);
void logg_regex_warning(const char *type, const char *warning, const int dbindex, const char *regex);
void logg_subnet_warning(const char *ip, const int matching_count, const char *matching_ids,

View File

@ -2425,7 +2425,7 @@ void networkTable_readIPsFinalize(sqlite3_stmt *read_stmt)
sqlite3_finalize(read_stmt);
}
bool networkTable_deleteDevice(sqlite3 *db, const int id, const char **message)
bool networkTable_deleteDevice(sqlite3 *db, const int id, int *deleted, const char **message)
{
// First step: Delete all associated IPs of this device
// Prepare SQLite statement
@ -2462,6 +2462,9 @@ bool networkTable_deleteDevice(sqlite3 *db, const int id, const char **message)
return false;
}
// Check if we deleted any rows
*deleted += sqlite3_changes(db);
// Finalize statement
sqlite3_finalize(stmt);
@ -2498,6 +2501,9 @@ bool networkTable_deleteDevice(sqlite3 *db, const int id, const char **message)
return false;
}
// Check if we deleted any rows
*deleted += sqlite3_changes(db);
// Finalize statement
sqlite3_finalize(stmt);

View File

@ -52,6 +52,6 @@ bool networkTable_readIPs(sqlite3 *db, sqlite3_stmt **read_stmt, const int id, c
bool networkTable_readIPsGetRecord(sqlite3_stmt *read_stmt, network_addresses_record *network_addresses, const char **message);
void networkTable_readIPsFinalize(sqlite3_stmt *read_stmt);
bool networkTable_deleteDevice(sqlite3 *db, const int id, const char **message);
bool networkTable_deleteDevice(sqlite3 *db, const int id, int *deleted, const char **message);
#endif //NETWORKTABLE_H

View File

@ -298,7 +298,7 @@ extern LPWSTR sqlite3_win32_utf8_to_unicode(const char *zText);
** CIO_WIN_WC_XLATE is defined as 0 or 1, reflecting whether console I/O
** translation for Windows is effected for the build.
*/
#define HAVE_CONSOLE_IO_H 1
#ifndef SQLITE_INTERNAL_LINKAGE
# define SQLITE_INTERNAL_LINKAGE extern /* external to translation unit */
# include <stdio.h>
@ -436,8 +436,8 @@ SQLITE_INTERNAL_LINKAGE int ePutsUtf8(const char *z);
#ifdef CONSIO_SPUTB
SQLITE_INTERNAL_LINKAGE int
fPutbUtf8(FILE *pfOut, const char *cBuf, int nAccept);
#endif
/* Like fPutbUtf8 except stream is always the designated output. */
#endif
SQLITE_INTERNAL_LINKAGE int
oPutbUtf8(const char *cBuf, int nAccept);
/* Like fPutbUtf8 except stream is always the designated error. */
@ -577,9 +577,11 @@ zSkipValidUtf8(const char *z, int nAccept, long ccm);
# include <stdlib.h>
# include <limits.h>
# include <assert.h>
# include "console_io.h"
/* # include "sqlite3.h" */
#endif
#ifndef HAVE_CONSOLE_IO_H
# include "console_io.h"
#endif
#ifndef SQLITE_CIO_NO_TRANSLATE
# if (defined(_WIN32) || defined(WIN32)) && !SQLITE_OS_WINRT
@ -1102,12 +1104,11 @@ zSkipValidUtf8(const char *z, int nAccept, long ccm){
#endif /*!(defined(SQLITE_CIO_NO_UTF8SCAN)&&defined(SQLITE_CIO_NO_TRANSLATE))*/
#ifndef SQLITE_CIO_NO_TRANSLATE
#ifdef CONSIO_SPUTB
# ifdef CONSIO_SPUTB
SQLITE_INTERNAL_LINKAGE int
fPutbUtf8(FILE *pfO, const char *cBuf, int nAccept){
assert(pfO!=0);
# if CIO_WIN_WC_XLATE
# if CIO_WIN_WC_XLATE
PerStreamTags pst = PST_INITIALIZER; /* for unknown streams */
PerStreamTags *ppst = getEmitStreamInfo(0, &pst, &pfO);
if( pstReachesConsole(ppst) ){
@ -1117,13 +1118,13 @@ fPutbUtf8(FILE *pfO, const char *cBuf, int nAccept){
if( 0 == isKnownWritable(ppst->pf) ) restoreConsoleArb(ppst);
return rv;
}else {
# endif
# endif
return (int)fwrite(cBuf, 1, nAccept, pfO);
# if CIO_WIN_WC_XLATE
# if CIO_WIN_WC_XLATE
}
# endif
# endif
}
#endif /* defined(CONSIO_SPUTB) */
# endif
SQLITE_INTERNAL_LINKAGE int
oPutbUtf8(const char *cBuf, int nAccept){
@ -1234,6 +1235,7 @@ SQLITE_INTERNAL_LINKAGE char* fGetsUtf8(char *cBuf, int ncMax, FILE *pfIn){
/************************* End ../ext/consio/console_io.c ********************/
#ifndef SQLITE_SHELL_FIDDLE
/* From here onward, fgets() is redirected to the console_io library. */
# define fgets(b,n,f) fGetsUtf8(b,n,f)
/*
@ -1258,6 +1260,7 @@ SQLITE_INTERNAL_LINKAGE char* fGetsUtf8(char *cBuf, int ncMax, FILE *pfIn){
# define eputz(z) ePutsUtf8(z)
# define eputf ePrintfUtf8
# define oputb(buf,na) oPutbUtf8(buf,na)
#else
/* For Fiddle, all console handling and emit redirection is omitted. */
# define sputz(fp,z) fputs(z,fp)
@ -1341,7 +1344,7 @@ static void endTimer(void){
sqlite3_int64 iEnd = timeOfDay();
struct rusage sEnd;
getrusage(RUSAGE_SELF, &sEnd);
oputf("Run Time: real %.3f user %f sys %f\n",
sputf(stdout, "Run Time: real %.3f user %f sys %f\n",
(iEnd - iBegin)*0.001,
timeDiff(&sBegin.ru_utime, &sEnd.ru_utime),
timeDiff(&sBegin.ru_stime, &sEnd.ru_stime));
@ -1420,7 +1423,7 @@ static void endTimer(void){
FILETIME ftCreation, ftExit, ftKernelEnd, ftUserEnd;
sqlite3_int64 ftWallEnd = timeOfDay();
getProcessTimesAddr(hProcess,&ftCreation,&ftExit,&ftKernelEnd,&ftUserEnd);
oputf("Run Time: real %.3f user %f sys %f\n",
sputf(stdout, "Run Time: real %.3f user %f sys %f\n",
(ftWallEnd - ftWallBegin)*0.001,
timeDiff(&ftUserBegin, &ftUserEnd),
timeDiff(&ftKernelBegin, &ftKernelEnd));
@ -1717,14 +1720,14 @@ static int strlenChar(const char *z){
*/
static FILE * openChrSource(const char *zFile){
#if defined(_WIN32) || defined(WIN32)
struct _stat x = {0};
struct __stat64 x = {0};
# define STAT_CHR_SRC(mode) ((mode & (_S_IFCHR|_S_IFIFO|_S_IFREG))!=0)
/* On Windows, open first, then check the stream nature. This order
** is necessary because _stat() and sibs, when checking a named pipe,
** effectively break the pipe as its supplier sees it. */
FILE *rv = fopen(zFile, "rb");
if( rv==0 ) return 0;
if( _fstat(_fileno(rv), &x) != 0
if( _fstat64(_fileno(rv), &x) != 0
|| !STAT_CHR_SRC(x.st_mode)){
fclose(rv);
rv = 0;
@ -14829,6 +14832,7 @@ static int dbdataNext(sqlite3_vtab_cursor *pCursor){
bNextPage = 1;
}else{
iOff += dbdataGetVarintU32(&pCsr->aPage[iOff], &nPayload);
if( nPayload>0x7fffff00 ) nPayload &= 0x3fff;
}
/* If this is a leaf intkey cell, load the rowid */
@ -18145,6 +18149,7 @@ struct ShellState {
u8 eTraceType; /* SHELL_TRACE_* value for type of trace */
u8 bSafeMode; /* True to prohibit unsafe operations */
u8 bSafeModePersist; /* The long-term value of bSafeMode */
u8 eRestoreState; /* See comments above doAutoDetectRestore() */
ColModeOpts cmOpts; /* Option values affecting columnar mode output */
unsigned statsOn; /* True to display memory stats before each finalize */
unsigned mEqpLines; /* Mask of vertical lines in the EQP output graph */
@ -22172,7 +22177,6 @@ static void open_db(ShellState *p, int openFlags){
break;
}
}
globalDb = p->db;
if( p->db==0 || SQLITE_OK!=sqlite3_errcode(p->db) ){
eputf("Error: unable to open database \"%s\": %s\n",
zDbFilename, sqlite3_errmsg(p->db));
@ -22189,6 +22193,7 @@ static void open_db(ShellState *p, int openFlags){
zDbFilename);
}
}
globalDb = p->db;
sqlite3_db_config(p->db, SQLITE_DBCONFIG_STMT_SCANSTATUS, (int)0, (int*)0);
/* Reflect the use or absence of --unsafe-testing invocation. */
@ -23584,7 +23589,6 @@ static int lintDotCommand(
return SQLITE_ERROR;
}
#if !defined SQLITE_OMIT_VIRTUALTABLE
static void shellPrepare(
sqlite3 *db,
int *pRc,
@ -23603,12 +23607,8 @@ static void shellPrepare(
/*
** Create a prepared statement using printf-style arguments for the SQL.
**
** This routine is could be marked "static". But it is not always used,
** depending on compile-time options. By omitting the "static", we avoid
** nuisance compiler warnings about "defined but not used".
*/
void shellPreparePrintf(
static void shellPreparePrintf(
sqlite3 *db,
int *pRc,
sqlite3_stmt **ppStmt,
@ -23631,13 +23631,10 @@ void shellPreparePrintf(
}
}
/* Finalize the prepared statement created using shellPreparePrintf().
**
** This routine is could be marked "static". But it is not always used,
** depending on compile-time options. By omitting the "static", we avoid
** nuisance compiler warnings about "defined but not used".
/*
** Finalize the prepared statement created using shellPreparePrintf().
*/
void shellFinalize(
static void shellFinalize(
int *pRc,
sqlite3_stmt *pStmt
){
@ -23653,6 +23650,7 @@ void shellFinalize(
}
}
#if !defined SQLITE_OMIT_VIRTUALTABLE
/* Reset the prepared statement created using shellPreparePrintf().
**
** This routine is could be marked "static". But it is not always used,
@ -24719,6 +24717,30 @@ FROM (\
}
}
/*
** Check if the sqlite_schema table contains one or more virtual tables. If
** parameter zLike is not NULL, then it is an SQL expression that the
** sqlite_schema row must also match. If one or more such rows are found,
** print the following warning to the output:
**
** WARNING: Script requires that SQLITE_DBCONFIG_DEFENSIVE be disabled
*/
static int outputDumpWarning(ShellState *p, const char *zLike){
int rc = SQLITE_OK;
sqlite3_stmt *pStmt = 0;
shellPreparePrintf(p->db, &rc, &pStmt,
"SELECT 1 FROM sqlite_schema o WHERE "
"sql LIKE 'CREATE VIRTUAL TABLE%%' AND %s", zLike ? zLike : "true"
);
if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
oputz("/* WARNING: "
"Script requires that SQLITE_DBCONFIG_DEFENSIVE be disabled */\n"
);
}
shellFinalize(&rc, pStmt);
return rc;
}
/*
** If an input line begins with "." then invoke this routine to
** process that line.
@ -25181,6 +25203,7 @@ static int do_meta_command(char *zLine, ShellState *p){
open_db(p, 0);
outputDumpWarning(p, zLike);
if( (p->shellFlgs & SHFLG_DumpDataOnly)==0 ){
/* When playing back a "dump", the content might appear in an order
** which causes immediate foreign key constraints to be violated.
@ -27609,6 +27632,7 @@ static int do_meta_command(char *zLine, ShellState *p){
{"fk_no_action", SQLITE_TESTCTRL_FK_NO_ACTION, 0, "BOOLEAN" },
{"imposter", SQLITE_TESTCTRL_IMPOSTER,1,"SCHEMA ON/OFF ROOTPAGE"},
{"internal_functions", SQLITE_TESTCTRL_INTERNAL_FUNCTIONS,0,"" },
{"json_selfcheck", SQLITE_TESTCTRL_JSON_SELFCHECK ,0,"BOOLEAN" },
{"localtime_fault", SQLITE_TESTCTRL_LOCALTIME_FAULT,0,"BOOLEAN" },
{"never_corrupt", SQLITE_TESTCTRL_NEVER_CORRUPT,1, "BOOLEAN" },
{"optimizations", SQLITE_TESTCTRL_OPTIMIZATIONS,0,"DISABLE-MASK" },
@ -27827,6 +27851,16 @@ static int do_meta_command(char *zLine, ShellState *p){
isOk = 3;
}
break;
case SQLITE_TESTCTRL_JSON_SELFCHECK:
if( nArg==2 ){
rc2 = -1;
isOk = 1;
}else{
rc2 = booleanValue(azArg[2]);
isOk = 3;
}
sqlite3_test_control(testctrl, &rc2);
break;
}
}
if( isOk==0 && iCtrl>=0 ){
@ -28233,6 +28267,88 @@ static int line_is_complete(char *zSql, int nSql){
return rc;
}
/*
** This function is called after processing each line of SQL in the
** runOneSqlLine() function. Its purpose is to detect scenarios where
** defensive mode should be automatically turned off. Specifically, when
**
** 1. The first line of input is "PRAGMA foreign_keys=OFF;",
** 2. The second line of input is "BEGIN TRANSACTION;",
** 3. The database is empty, and
** 4. The shell is not running in --safe mode.
**
** The implementation uses the ShellState.eRestoreState to maintain state:
**
** 0: Have not seen any SQL.
** 1: Have seen "PRAGMA foreign_keys=OFF;".
** 2-6: Currently running .dump transaction. If the "2" bit is set,
** disable DEFENSIVE when done. If "4" is set, disable DQS_DDL.
** 7: Nothing left to do. This function becomes a no-op.
*/
static int doAutoDetectRestore(ShellState *p, const char *zSql){
int rc = SQLITE_OK;
if( p->eRestoreState<7 ){
switch( p->eRestoreState ){
case 0: {
const char *zExpect = "PRAGMA foreign_keys=OFF;";
assert( strlen(zExpect)==24 );
if( p->bSafeMode==0 && memcmp(zSql, zExpect, 25)==0 ){
p->eRestoreState = 1;
}else{
p->eRestoreState = 7;
}
break;
};
case 1: {
int bIsDump = 0;
const char *zExpect = "BEGIN TRANSACTION;";
assert( strlen(zExpect)==18 );
if( memcmp(zSql, zExpect, 19)==0 ){
/* Now check if the database is empty. */
const char *zQuery = "SELECT 1 FROM sqlite_schema LIMIT 1";
sqlite3_stmt *pStmt = 0;
bIsDump = 1;
shellPrepare(p->db, &rc, zQuery, &pStmt);
if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
bIsDump = 0;
}
shellFinalize(&rc, pStmt);
}
if( bIsDump && rc==SQLITE_OK ){
int bDefense = 0;
int bDqsDdl = 0;
sqlite3_db_config(p->db, SQLITE_DBCONFIG_DEFENSIVE, -1, &bDefense);
sqlite3_db_config(p->db, SQLITE_DBCONFIG_DQS_DDL, -1, &bDqsDdl);
sqlite3_db_config(p->db, SQLITE_DBCONFIG_DEFENSIVE, 0, 0);
sqlite3_db_config(p->db, SQLITE_DBCONFIG_DQS_DDL, 1, 0);
p->eRestoreState = (bDefense ? 2 : 0) + (bDqsDdl ? 4 : 0);
}else{
p->eRestoreState = 7;
}
break;
}
default: {
if( sqlite3_get_autocommit(p->db) ){
if( (p->eRestoreState & 2) ){
sqlite3_db_config(p->db, SQLITE_DBCONFIG_DEFENSIVE, 1, 0);
}
if( (p->eRestoreState & 4) ){
sqlite3_db_config(p->db, SQLITE_DBCONFIG_DQS_DDL, 0, 0);
}
p->eRestoreState = 7;
}
break;
}
}
}
return rc;
}
/*
** Run a single line of SQL. Return the number of errors.
*/
@ -28280,6 +28396,8 @@ static int runOneSqlLine(ShellState *p, char *zSql, FILE *in, int startline){
sqlite3_changes64(p->db), sqlite3_total_changes64(p->db));
oputf("%s\n", zLineBuf);
}
if( doAutoDetectRestore(p, zSql) ) return 1;
return 0;
}
@ -28713,14 +28831,14 @@ static void printBold(const char *zText){
FOREGROUND_RED|FOREGROUND_INTENSITY
);
#endif
oputz(zText);
sputz(stdout, zText);
#if !SQLITE_OS_WINRT
SetConsoleTextAttribute(out, defaultScreenInfo.wAttributes);
#endif
}
#else
static void printBold(const char *zText){
oputf("\033[1m%s\033[0m", zText);
sputf(stdout, "\033[1m%s\033[0m", zText);
}
#endif
@ -28914,10 +29032,6 @@ int SQLITE_CDECL wmain(int argc, wchar_t **wargv){
}else if( cli_strcmp(z,"-init")==0 ){
zInitFile = cmdline_option_value(argc, argv, ++i);
}else if( cli_strcmp(z,"-interactive")==0 ){
/* Need to check for interactive override here to so that it can
** affect console setup (for Windows only) and testing thereof.
*/
stdin_is_interactive = 1;
}else if( cli_strcmp(z,"-batch")==0 ){
/* Need to check for batch mode here to so we can avoid printing
** informational messages (like from process_sqliterc) before
@ -29187,11 +29301,14 @@ int SQLITE_CDECL wmain(int argc, wchar_t **wargv){
}else if( cli_strcmp(z,"-bail")==0 ){
/* No-op. The bail_on_error flag should already be set. */
}else if( cli_strcmp(z,"-version")==0 ){
oputf("%s %s (%d-bit)\n", sqlite3_libversion(), sqlite3_sourceid(),
8*(int)sizeof(char*));
sputf(stdout, "%s %s (%d-bit)\n",
sqlite3_libversion(), sqlite3_sourceid(), 8*(int)sizeof(char*));
return 0;
}else if( cli_strcmp(z,"-interactive")==0 ){
/* already handled */
/* Need to check for interactive override here to so that it can
** affect console setup (for Windows only) and testing thereof.
*/
stdin_is_interactive = 1;
}else if( cli_strcmp(z,"-batch")==0 ){
/* already handled */
}else if( cli_strcmp(z,"-utf8")==0 ){
@ -29321,13 +29438,13 @@ int SQLITE_CDECL wmain(int argc, wchar_t **wargv){
#else
# define SHELL_CIO_CHAR_SET ""
#endif
oputf("SQLite version %s %.19s%s\n" /*extra-version-info*/
sputf(stdout, "SQLite version %s %.19s%s\n" /*extra-version-info*/
"Enter \".help\" for usage hints.\n",
sqlite3_libversion(), sqlite3_sourceid(), SHELL_CIO_CHAR_SET);
if( warnInmemoryDb ){
oputz("Connected to a ");
sputz(stdout, "Connected to a ");
printBold("transient in-memory database");
oputz(".\nUse \".open FILENAME\" to reopen on a"
sputz(stdout, ".\nUse \".open FILENAME\" to reopen on a"
" persistent database.\n");
}
zHistory = getenv("SQLITE_HISTORY");

File diff suppressed because it is too large Load Diff

View File

@ -146,9 +146,9 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION "3.44.2"
#define SQLITE_VERSION_NUMBER 3044002
#define SQLITE_SOURCE_ID "2023-11-24 11:41:44 ebead0e7230cd33bcec9f95d2183069565b9e709bf745c9b5db65cc0cbf92c0f"
#define SQLITE_VERSION "3.45.0"
#define SQLITE_VERSION_NUMBER 3045000
#define SQLITE_SOURCE_ID "2024-01-15 17:01:13 1066602b2b1976fe58b5150777cced894af17c803e068f5918390d6915b46e1d"
/*
** CAPI3REF: Run-Time Library Version Numbers
@ -3954,15 +3954,17 @@ SQLITE_API void sqlite3_free_filename(sqlite3_filename);
** </ul>
**
** ^The sqlite3_errmsg() and sqlite3_errmsg16() return English-language
** text that describes the error, as either UTF-8 or UTF-16 respectively.
** text that describes the error, as either UTF-8 or UTF-16 respectively,
** or NULL if no error message is available.
** (See how SQLite handles [invalid UTF] for exceptions to this rule.)
** ^(Memory to hold the error message string is managed internally.
** The application does not need to worry about freeing the result.
** However, the error string might be overwritten or deallocated by
** subsequent calls to other SQLite interface functions.)^
**
** ^The sqlite3_errstr() interface returns the English-language text
** that describes the [result code], as UTF-8.
** ^The sqlite3_errstr(E) interface returns the English-language text
** that describes the [result code] E, as UTF-8, or NULL if E is not an
** result code for which a text error message is available.
** ^(Memory to hold the error message string is managed internally
** and must not be freed by the application)^.
**
@ -8037,9 +8039,11 @@ SQLITE_API int sqlite3_vfs_unregister(sqlite3_vfs*);
**
** ^(Some systems (for example, Windows 95) do not support the operation
** implemented by sqlite3_mutex_try(). On those systems, sqlite3_mutex_try()
** will always return SQLITE_BUSY. The SQLite core only ever uses
** sqlite3_mutex_try() as an optimization so this is acceptable
** behavior.)^
** will always return SQLITE_BUSY. In most cases the SQLite core only uses
** sqlite3_mutex_try() as an optimization, so this is acceptable
** behavior. The exceptions are unix builds that set the
** SQLITE_ENABLE_SETLK_TIMEOUT build option. In that case a working
** sqlite3_mutex_try() is required.)^
**
** ^The sqlite3_mutex_leave() routine exits a mutex that was
** previously entered by the same thread. The behavior
@ -8298,6 +8302,7 @@ SQLITE_API int sqlite3_test_control(int op, ...);
#define SQLITE_TESTCTRL_ASSERT 12
#define SQLITE_TESTCTRL_ALWAYS 13
#define SQLITE_TESTCTRL_RESERVE 14 /* NOT USED */
#define SQLITE_TESTCTRL_JSON_SELFCHECK 14
#define SQLITE_TESTCTRL_OPTIMIZATIONS 15
#define SQLITE_TESTCTRL_ISKEYWORD 16 /* NOT USED */
#define SQLITE_TESTCTRL_SCRATCHMALLOC 17 /* NOT USED */
@ -12811,8 +12816,11 @@ struct Fts5PhraseIter {
** created with the "columnsize=0" option.
**
** xColumnText:
** This function attempts to retrieve the text of column iCol of the
** current document. If successful, (*pz) is set to point to a buffer
** If parameter iCol is less than zero, or greater than or equal to the
** number of columns in the table, SQLITE_RANGE is returned.
**
** Otherwise, this function attempts to retrieve the text of column iCol of
** the current document. If successful, (*pz) is set to point to a buffer
** containing the text in utf-8 encoding, (*pn) is set to the size in bytes
** (not characters) of the buffer and SQLITE_OK is returned. Otherwise,
** if an error occurs, an SQLite error code is returned and the final values
@ -12822,8 +12830,10 @@ struct Fts5PhraseIter {
** Returns the number of phrases in the current query expression.
**
** xPhraseSize:
** Returns the number of tokens in phrase iPhrase of the query. Phrases
** are numbered starting from zero.
** If parameter iCol is less than zero, or greater than or equal to the
** number of phrases in the current query, as returned by xPhraseCount,
** 0 is returned. Otherwise, this function returns the number of tokens in
** phrase iPhrase of the query. Phrases are numbered starting from zero.
**
** xInstCount:
** Set *pnInst to the total number of occurrences of all phrases within
@ -12839,12 +12849,13 @@ struct Fts5PhraseIter {
** Query for the details of phrase match iIdx within the current row.
** Phrase matches are numbered starting from zero, so the iIdx argument
** should be greater than or equal to zero and smaller than the value
** output by xInstCount().
** output by xInstCount(). If iIdx is less than zero or greater than
** or equal to the value returned by xInstCount(), SQLITE_RANGE is returned.
**
** Usually, output parameter *piPhrase is set to the phrase number, *piCol
** Otherwise, output parameter *piPhrase is set to the phrase number, *piCol
** to the column in which it occurs and *piOff the token offset of the
** first token of the phrase. Returns SQLITE_OK if successful, or an error
** code (i.e. SQLITE_NOMEM) if an error occurs.
** first token of the phrase. SQLITE_OK is returned if successful, or an
** error code (i.e. SQLITE_NOMEM) if an error occurs.
**
** This API can be quite slow if used with an FTS5 table created with the
** "detail=none" or "detail=column" option.
@ -12870,6 +12881,10 @@ struct Fts5PhraseIter {
** Invoking Api.xUserData() returns a copy of the pointer passed as
** the third argument to pUserData.
**
** If parameter iPhrase is less than zero, or greater than or equal to
** the number of phrases in the query, as returned by xPhraseCount(),
** this function returns SQLITE_RANGE.
**
** If the callback function returns any value other than SQLITE_OK, the
** query is abandoned and the xQueryPhrase function returns immediately.
** If the returned value is SQLITE_DONE, xQueryPhrase returns SQLITE_OK.
@ -12984,9 +12999,42 @@ struct Fts5PhraseIter {
**
** xPhraseNextColumn()
** See xPhraseFirstColumn above.
**
** xQueryToken(pFts5, iPhrase, iToken, ppToken, pnToken)
** This is used to access token iToken of phrase iPhrase of the current
** query. Before returning, output parameter *ppToken is set to point
** to a buffer containing the requested token, and *pnToken to the
** size of this buffer in bytes.
**
** If iPhrase or iToken are less than zero, or if iPhrase is greater than
** or equal to the number of phrases in the query as reported by
** xPhraseCount(), or if iToken is equal to or greater than the number of
** tokens in the phrase, SQLITE_RANGE is returned and *ppToken and *pnToken
are both zeroed.
**
** The output text is not a copy of the query text that specified the
** token. It is the output of the tokenizer module. For tokendata=1
** tables, this includes any embedded 0x00 and trailing data.
**
** xInstToken(pFts5, iIdx, iToken, ppToken, pnToken)
** This is used to access token iToken of phrase hit iIdx within the
** current row. If iIdx is less than zero or greater than or equal to the
** value returned by xInstCount(), SQLITE_RANGE is returned. Otherwise,
** output variable (*ppToken) is set to point to a buffer containing the
** matching document token, and (*pnToken) to the size of that buffer in
** bytes. This API is not available if the specified token matches a
** prefix query term. In that case both output variables are always set
** to 0.
**
** The output text is not a copy of the document text that was tokenized.
** It is the output of the tokenizer module. For tokendata=1 tables, this
** includes any embedded 0x00 and trailing data.
**
** This API can be quite slow if used with an FTS5 table created with the
** "detail=none" or "detail=column" option.
*/
struct Fts5ExtensionApi {
int iVersion; /* Currently always set to 2 */
int iVersion; /* Currently always set to 3 */
void *(*xUserData)(Fts5Context*);
@ -13021,6 +13069,13 @@ struct Fts5ExtensionApi {
int (*xPhraseFirstColumn)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*);
void (*xPhraseNextColumn)(Fts5Context*, Fts5PhraseIter*, int *piCol);
/* Below this point are iVersion>=3 only */
int (*xQueryToken)(Fts5Context*,
int iPhrase, int iToken,
const char **ppToken, int *pnToken
);
int (*xInstToken)(Fts5Context*, int iIdx, int iToken, const char**, int*);
};
/*

View File

@ -314,7 +314,7 @@ int _findClientID(const char *clientIP, const bool count, const bool aliasclient
// Set all MAC address bytes to zero
client->hwlen = -1;
memset(client->hwaddr, 0, sizeof(client->hwaddr));
// This may be a alias-client, the ID is set elsewhere
// This may be an alias-client, the ID is set elsewhere
client->flags.aliasclient = aliasclient;
client->aliasclient_id = -1;

View File

@ -94,7 +94,7 @@ int main_dnsmasq (int argc, char **argv)
sigaction(SIGUSR1, &sigact, NULL);
sigaction(SIGUSR2, &sigact, NULL);
sigaction(SIGHUP, &sigact, NULL);
sigaction(SIGTERM, &sigact, NULL);
sigaction(SIGUSR6, &sigact, NULL); // Pi-hole modification
sigaction(SIGALRM, &sigact, NULL);
sigaction(SIGCHLD, &sigact, NULL);
sigaction(SIGINT, &sigact, NULL);
@ -1330,7 +1330,7 @@ static void sig_handler(int sig)
event = EVENT_CHILD;
else if (sig == SIGALRM)
event = EVENT_ALARM;
else if (sig == SIGTERM)
else if (sig == SIGUSR6) // Pi-hole modified
event = EVENT_TERM;
else if (sig == SIGUSR1)
event = EVENT_DUMP;

View File

@ -1915,7 +1915,7 @@ static void FTL_reply(const unsigned int flags, const char *name, const union al
if(!(flags & F_UPSTREAM))
{
cached = true;
if((flags & F_HOSTS) || // local.list, hostname.list, /etc/hosts and others
if((flags & F_HOSTS) || // hostname.list, /etc/hosts and others
((flags & F_NAMEP) && (flags & F_DHCP)) || // DHCP server reply
(flags & F_FORWARD) || // cached answer to previously forwarded request
(flags & F_REVERSE) || // cached answer to reverse request (PTR)
@ -3244,12 +3244,18 @@ void FTL_TCP_worker_created(const int confd)
gravityDB_forked();
}
bool FTL_unlink_DHCP_lease(const char *ipaddr)
bool FTL_unlink_DHCP_lease(const char *ipaddr, const char **hint)
{
struct dhcp_lease *lease;
union all_addr addr;
const time_t now = dnsmasq_time();
if(!daemon->dhcp)
{
*hint = "DHCP is not enabled";
return false;
}
// Try to extract IP address
if (inet_pton(AF_INET, ipaddr, &addr.addr4) > 0)
{
@ -3263,6 +3269,8 @@ bool FTL_unlink_DHCP_lease(const char *ipaddr)
#endif
else
{
// Invalid IP address
*hint = "invalid target address (neither IPv4 nor IPv6)";
return false;
}
@ -3279,6 +3287,11 @@ bool FTL_unlink_DHCP_lease(const char *ipaddr)
// (variable lease.c:dns_dirty is used here)
lease_update_dns(0);
}
else
{
*hint = NULL;
return false;
}
// Return success
return true;

View File

@ -46,7 +46,7 @@ void FTL_dnsmasq_reload(void);
void FTL_TCP_worker_created(const int confd);
void FTL_TCP_worker_terminating(bool finished);
bool FTL_unlink_DHCP_lease(const char *ipaddr);
bool FTL_unlink_DHCP_lease(const char *ipaddr, const char **hint);
// defined in src/dnsmasq/cache.c
extern char *querystr(char *desc, unsigned short type);

View File

@ -311,11 +311,94 @@ static void SIGRT_handler(int signum, siginfo_t *si, void *unused)
// Parse neighbor cache
set_event(PARSE_NEIGHBOR_CACHE);
}
// else if(rtsig == 6)
// {
// // Signal internally used to signal dnsmasq it has to stop
// }
// Restore errno before returning back to previous context
errno = _errno;
}
static void SIGTERM_handler(int signum, siginfo_t *si, void *unused)
{
// Ignore SIGTERM outside of the main process (TCP forks)
if(mpid != getpid())
return;
// Get PID and UID of the process that sent the terminating signal
const pid_t kill_pid = si->si_pid;
const uid_t kill_uid = si->si_uid;
// Get name of the process that sent the terminating signal
char kill_name[256] = { 0 };
char kill_exe [256] = { 0 };
snprintf(kill_exe, sizeof(kill_exe), "/proc/%ld/cmdline", (long int)kill_pid);
FILE *fp = fopen(kill_exe, "r");
if(fp != NULL)
{
// Successfully opened file
size_t read = 0;
// Read line from file
if((read = fread(kill_name, sizeof(char), sizeof(kill_name), fp)) > 0)
{
// Successfully read line
// cmdline contains the command-line arguments as a set
// of strings separated by null bytes ('\0'), with a
// further null byte after the last string. Hence, we
// need to replace all null bytes with spaces for
// displaying it below
for(unsigned int i = 0; i < min((size_t)read, sizeof(kill_name)); i++)
{
if(kill_name[i] == '\0')
kill_name[i] = ' ';
}
// Remove any trailing spaces
for(unsigned int i = read - 1; i > 0; i--)
{
if(kill_name[i] == ' ')
kill_name[i] = '\0';
else
break;
}
}
else
{
// Failed to read line
strcpy(kill_name, "N/A");
}
}
else
{
// Failed to open file
strcpy(kill_name, "N/A");
}
// Get username of the process that sent the terminating signal
char kill_user[256] = { 0 };
struct passwd *pwd = getpwuid(kill_uid);
if(pwd != NULL)
{
// Successfully obtained username
strncpy(kill_user, pwd->pw_name, sizeof(kill_user));
}
else
{
// Failed to obtain username
strcpy(kill_user, "N/A");
}
// Log who sent the signal
log_info("Asked to terminate by \"%s\" (PID %ld, user %s UID %ld)",
kill_name, (long int)kill_pid,
kill_user, (long int)kill_uid);
// Terminate dnsmasq to stop DNS service
raise(SIGUSR6);
}
// Register ordinary signals handler
void handle_signals(void)
{
@ -337,6 +420,13 @@ void handle_signals(void)
}
}
// Also catch SIGTERM
struct sigaction SIGaction = { 0 };
SIGaction.sa_flags = SA_SIGINFO;
sigemptyset(&SIGaction.sa_mask);
SIGaction.sa_sigaction = &SIGTERM_handler;
sigaction(SIGTERM, &SIGaction, NULL);
// Log start time of FTL
FTLstarttime = time(NULL);
}
@ -351,8 +441,12 @@ void handle_realtime_signals(void)
// Catch all real-time signals
for(int signum = SIGRTMIN; signum <= SIGRTMAX; signum++)
{
struct sigaction SIGACTION;
memset(&SIGACTION, 0, sizeof(struct sigaction));
if(signum == SIGUSR6)
// Skip SIGUSR6 as it is used internally to signify
// dnsmasq to stop
continue;
struct sigaction SIGACTION = { 0 };
SIGACTION.sa_flags = SA_SIGINFO;
sigemptyset(&SIGACTION.sa_mask);
SIGACTION.sa_sigaction = &SIGRT_handler;

View File

@ -12,6 +12,8 @@
#include "enums.h"
#define SIGUSR6 (SIGRTMIN + 6)
// defined in dnsmasq/dnsmasq.h
extern volatile char FTL_terminate;

View File

@ -542,8 +542,6 @@ end_of_parseList:
// Print newline
puts("");
}
// Print final newline
puts("");
}
// Free memory

View File

@ -200,7 +200,10 @@
})
#define JSON_SEND_OBJECT_CODE(object, code)({ \
cJSON_AddNumberToObject(object, "took", double_time() - api->now);\
if((code) != 204) \
{ \
cJSON_AddNumberToObject(object, "took", double_time() - api->now); \
} \
char *json_string = json_formatter(object); \
if(json_string == NULL) \
{ \

View File

@ -15,6 +15,7 @@ import requests
from typing import List
import json
from hashlib import sha256
import urllib.parse
url = "http://pi.hole/api/auth"
@ -23,6 +24,7 @@ class AuthenticationMethods(Enum):
HEADER = 1
BODY = 2
COOKIE = 3
QUERY_STR = 4
# Class to query the FTL API
class FTLAPI():
@ -103,13 +105,18 @@ class FTLAPI():
def GET(self, uri: str, params: List[str] = [], expected_mimetype: str = "application/json", authenticate: AuthenticationMethods = AuthenticationMethods.BODY):
self.errors = []
try:
# Get json_data, headers and cookies
json_data, headers, cookies = self.get_jsondata_headers_cookies(authenticate)
# Add session ID to the request if authenticating via query string
if self.auth_method == AuthenticationMethods.QUERY_STR.name:
encoded_sid = urllib.parse.quote(self.session['sid'], safe='')
params.append("sid=" + encoded_sid)
# Add parameters to the URI (if any)
if len(params) > 0:
uri = uri + "?" + "&".join(params)
# Get json_data, headers and cookies
json_data, headers, cookies = self.get_jsondata_headers_cookies(authenticate)
if self.verbose:
print("GET " + self.api_url + uri + " with json_data: " + json.dumps(json_data))

View File

@ -673,26 +673,40 @@
# <valid Pi-hole password hash>
app_pwhash = ""
# Array of clients to be excluded from certain API responses
# Example: [ "192.168.2.56", "fe80::341", "localhost" ]
# Array of clients to be excluded from certain API responses (regex):
# - Query Log (/api/queries)
# - Top Clients (/api/stats/top_clients)
# This setting accepts both IP addresses (IPv4 and IPv6) as well as hostnames.
# Note that backslashes "\" need to be escaped, i.e. "\\" in this setting
#
# Example: [ "^192\\.168\\.2\\.56$", "^fe80::341:[0-9a-f]*$", "^localhost$" ]
#
# Possible values are:
# array of IP addresses and/or hostnames
# array of regular expressions describing clients
excludeClients = [
"1.2.3.4"
"^1\\.2\\.3\\.4$"
] ### CHANGED, default = []
# Array of domains to be excluded from certain API responses
# Example: [ "google.de", "pi-hole.net" ]
# Array of domains to be excluded from certain API responses (regex):
# - Query Log (/api/queries)
# - Top Clients (/api/stats/top_domains)
# Note that backslashes "\" need to be escaped, i.e. "\\" in this setting
#
# Example: [ "(^|\\.)\\.google\\.de$", "\\.pi-hole\\.net$" ]
#
# Possible values are:
# array of domains
# array of regular expressions describing domains
excludeDomains = []
# How much history should be imported from the database [seconds]? (max 24*60*60 =
# 86400)
maxHistory = 86400
# Up to how many clients should be returned in the activity graph endpoint
# (/api/history/clients)?
# This setting can be overwritten at run-time using the parameter N
maxClients = 10
# Allow destructive API calls (e.g. deleting all queries, powering off the system, ...)
allow_destructive = true