mirror of
https://github.com/pi-hole/FTL.git
synced 2024-10-26 16:52:18 +02:00
dc204a41b0
Signed-off-by: DL6ER <dl6er@dl6er.de>
840 lines
26 KiB
C
840 lines
26 KiB
C
/* Pi-hole: A black hole for Internet advertisements
|
|
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
|
|
* Network-wide ad blocking via your own hardware.
|
|
*
|
|
* FTL Engine
|
|
* API database statistics implementation
|
|
*
|
|
* 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"
|
|
// querytypes[]
|
|
#include "datastructure.h"
|
|
// logging routines
|
|
#include "log.h"
|
|
// db
|
|
#include "database/common.h"
|
|
|
|
// SQL Query type filters for the database
|
|
#define FILTER_STATUS_NOT_BLOCKED "status IN (0,2,3,12,13,14,17)"
|
|
#define FILTER_STATUS_BLOCKED "status NOT IN (0,2,3,12,13,14,17)"
|
|
|
|
int api_history_database(struct ftl_conn *api)
|
|
{
|
|
double from = 0, until = 0;
|
|
const int interval = 600;
|
|
if(api->request->query_string != NULL)
|
|
{
|
|
get_double_var(api->request->query_string, "from", &from);
|
|
get_double_var(api->request->query_string, "until", &until);
|
|
}
|
|
|
|
// Check if we received the required information
|
|
if(from < 1.0 || until < 1.0)
|
|
{
|
|
return send_json_error(api, 400,
|
|
"bad_request",
|
|
"You need to specify both \"from\" and \"until\" in the request.",
|
|
NULL);
|
|
}
|
|
|
|
// Open the database
|
|
sqlite3 *db = dbopen(false, false);
|
|
if(db == NULL)
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to open long-term database",
|
|
NULL);
|
|
|
|
// Build SQL string
|
|
const char *querystr = "SELECT (timestamp/:interval)*:interval interval,status,COUNT(*) FROM query_storage "
|
|
"WHERE (status != 0) AND timestamp >= :from AND timestamp <= :until "
|
|
"GROUP by interval,status ORDER by interval";
|
|
|
|
|
|
// Prepare SQLite statement
|
|
sqlite3_stmt *stmt;
|
|
int rc = sqlite3_prepare_v2(db, querystr, -1, &stmt, NULL);
|
|
if( rc != SQLITE_OK ){
|
|
log_err("api_stats_database_history() - SQL error prepare (%i): %s",
|
|
rc, sqlite3_errstr(rc));
|
|
return false;
|
|
}
|
|
|
|
// Bind interval to prepared statement
|
|
if((rc = sqlite3_bind_int(stmt, 1, interval)) != SQLITE_OK)
|
|
{
|
|
log_err("api_stats_database_history(): Failed to bind interval (error %d) - %s",
|
|
rc, sqlite3_errstr(rc));
|
|
sqlite3_reset(stmt);
|
|
sqlite3_finalize(stmt);
|
|
dbclose(&db);
|
|
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to bind interval",
|
|
NULL);
|
|
}
|
|
|
|
// Bind from to prepared statement
|
|
if((rc = sqlite3_bind_double(stmt, 2, from)) != SQLITE_OK)
|
|
{
|
|
log_err("api_stats_database_history(): Failed to bind from (error %d) - %s",
|
|
rc, sqlite3_errstr(rc));
|
|
sqlite3_reset(stmt);
|
|
sqlite3_finalize(stmt);
|
|
dbclose(&db);
|
|
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to bind from",
|
|
NULL);
|
|
}
|
|
|
|
// Bind until to prepared statement
|
|
if((rc = sqlite3_bind_double(stmt, 3, until)) != SQLITE_OK)
|
|
{
|
|
log_err("api_stats_database_history(): Failed to bind until (error %d) - %s",
|
|
rc, sqlite3_errstr(rc));
|
|
sqlite3_reset(stmt);
|
|
sqlite3_finalize(stmt);
|
|
dbclose(&db);
|
|
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to bind until",
|
|
NULL);
|
|
}
|
|
|
|
// Loop over returned data and accumulate results
|
|
cJSON *history = JSON_NEW_ARRAY();
|
|
cJSON *item = NULL;
|
|
unsigned int previous_timeslot = 0u, blocked = 0u, total = 0u, cached = 0u;
|
|
while((rc = sqlite3_step(stmt)) == SQLITE_ROW)
|
|
{
|
|
// Get timestamp and derive timeslot from it
|
|
const unsigned int timestamp = sqlite3_column_int(stmt, 0);
|
|
const unsigned int timeslot = timestamp - timestamp % interval;
|
|
// Begin new array item for each new timeslot
|
|
if(timeslot != previous_timeslot)
|
|
{
|
|
previous_timeslot = timeslot;
|
|
if(item != NULL)
|
|
{
|
|
// Add and reset total counter
|
|
JSON_ADD_NUMBER_TO_OBJECT(item, "total", total);
|
|
total = 0;
|
|
// Add and reset totacachedl counter
|
|
JSON_ADD_NUMBER_TO_OBJECT(item, "cached", cached);
|
|
cached = 0;
|
|
// Add and reset blocked counter
|
|
JSON_ADD_NUMBER_TO_OBJECT(item, "blocked", blocked);
|
|
blocked = 0;
|
|
JSON_ADD_ITEM_TO_ARRAY(history, item);
|
|
}
|
|
|
|
item = JSON_NEW_OBJECT();
|
|
JSON_ADD_NUMBER_TO_OBJECT(item, "timestamp", previous_timeslot);
|
|
}
|
|
|
|
const int status = sqlite3_column_int(stmt, 1);
|
|
const int count = sqlite3_column_int(stmt, 2);
|
|
// Always add to total count
|
|
total += count;
|
|
|
|
// Add to blocked / cached count if applicable
|
|
if(is_blocked(status))
|
|
blocked += count;
|
|
else if(is_cached(status))
|
|
cached += count;
|
|
}
|
|
|
|
// Append final timeslot at the end if applicable
|
|
if(total > 0 && item != NULL)
|
|
{
|
|
// Add total counter
|
|
JSON_ADD_NUMBER_TO_OBJECT(item, "total", total);
|
|
// Add cached counter
|
|
JSON_ADD_NUMBER_TO_OBJECT(item, "cached", cached);
|
|
// Add blocked counter
|
|
JSON_ADD_NUMBER_TO_OBJECT(item, "blocked", blocked);
|
|
JSON_ADD_ITEM_TO_ARRAY(history, item);
|
|
}
|
|
|
|
// Finalize statement and close (= unlock) database connection
|
|
sqlite3_finalize(stmt);
|
|
dbclose(&db);
|
|
|
|
cJSON *json = JSON_NEW_OBJECT();
|
|
JSON_ADD_ITEM_TO_OBJECT(json, "history", history);
|
|
JSON_SEND_OBJECT(json);
|
|
}
|
|
|
|
int api_stats_database_top_items(struct ftl_conn *api)
|
|
{
|
|
unsigned int count = 10;
|
|
double from = 0.0, until = 0.0;
|
|
|
|
// Get options from API struct
|
|
bool blocked = false; // Can be overwritten by query string
|
|
const bool domains = api->opts.flags & API_DOMAINS;
|
|
|
|
// Get parameters from query string
|
|
if(api->request->query_string != NULL)
|
|
{
|
|
// Get time interval from query string
|
|
get_double_var(api->request->query_string, "from", &from);
|
|
get_double_var(api->request->query_string, "until", &until);
|
|
|
|
// Get blocked queries not only for .../top_blocked
|
|
// but also for .../top_domains?blocked=true
|
|
// Note: this may overwrite the blocked property from the URL
|
|
get_bool_var(api->request->query_string, "blocked", &blocked);
|
|
|
|
// Does the user request a non-default number of replies?
|
|
// Note: We do not accept zero query requests here
|
|
get_uint_var(api->request->query_string, "count", &count);
|
|
}
|
|
|
|
// Check if we received the required information
|
|
if(from < 1.0 || until < 1.0)
|
|
{
|
|
return send_json_error(api, 400,
|
|
"bad_request",
|
|
"You need to specify both \"from\" and \"until\" in the request.",
|
|
NULL);
|
|
}
|
|
|
|
// Open the database
|
|
sqlite3 *db = dbopen(false, false);
|
|
if(db == NULL)
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to open long-term database",
|
|
NULL);
|
|
|
|
// Build SQL string
|
|
const char *querystr, *count_total_str, *count_blocked_str;
|
|
if(domains)
|
|
{
|
|
if(blocked)
|
|
{
|
|
// Get domains and count of queries (blocked)
|
|
querystr = "SELECT COUNT(*),d.domain AS cnt FROM query_storage q "
|
|
"JOIN domain_by_id d ON d.id = q.domain "
|
|
"WHERE timestamp >= :from AND timestamp <= :until "
|
|
"AND " FILTER_STATUS_BLOCKED " "
|
|
"GROUP by q.domain";
|
|
}
|
|
else
|
|
{
|
|
// Get domains and count of queries (not blocked)
|
|
querystr = "SELECT COUNT(*),d.domain AS cnt FROM query_storage q "
|
|
"JOIN domain_by_id d ON d.id = q.domain "
|
|
"WHERE timestamp >= :from AND timestamp <= :until "
|
|
"AND " FILTER_STATUS_NOT_BLOCKED " "
|
|
"GROUP by q.domain";
|
|
}
|
|
|
|
// Count total number of queries for domains
|
|
count_total_str = "SELECT COUNT(DISTINCT domain) FROM query_storage "
|
|
"WHERE timestamp >= :from AND timestamp <= :until";
|
|
|
|
// Count total number of blocked queries for domains
|
|
count_blocked_str = "SELECT COUNT(DISTINCT domain) FROM query_storage "
|
|
"WHERE timestamp >= :from AND timestamp <= :until "
|
|
"AND " FILTER_STATUS_BLOCKED;
|
|
}
|
|
else
|
|
{
|
|
if(blocked)
|
|
{
|
|
// Get clients and count of queries (blocked)
|
|
querystr = "SELECT COUNT(*),c.ip,c.name AS cnt FROM query_storage q "
|
|
"JOIN client_by_id c ON c.id = q.client"
|
|
"WHERE timestamp >= :from AND timestamp <= :until "
|
|
"AND " FILTER_STATUS_BLOCKED " "
|
|
"GROUP by q.client";
|
|
}
|
|
else
|
|
{
|
|
// Get clients and count of queries (not blocked)
|
|
querystr = "SELECT COUNT(*),c.ip,c.name AS cnt FROM query_storage q "
|
|
"JOIN client_by_id c ON c.id = q.client "
|
|
"WHERE timestamp >= :from AND timestamp <= :until "
|
|
"AND " FILTER_STATUS_NOT_BLOCKED " "
|
|
"GROUP by q.client";
|
|
}
|
|
|
|
// Count total number of queries for clients
|
|
count_total_str = "SELECT COUNT(DISTINCT client) FROM query_storage "
|
|
"WHERE timestamp >= :from AND timestamp <= :until";
|
|
|
|
// Count number of blocked queries for clients
|
|
count_blocked_str = "SELECT COUNT(DISTINCT client) FROM query_storage "
|
|
"WHERE timestamp >= :from AND timestamp <= :until "
|
|
"AND " FILTER_STATUS_BLOCKED;
|
|
}
|
|
|
|
|
|
// Prepare SQLite statement
|
|
sqlite3_stmt *stmt;
|
|
int rc = sqlite3_prepare_v2(db, querystr, -1, &stmt, NULL);
|
|
if( rc != SQLITE_OK ){
|
|
log_err("api_stats_database_history() - SQL error prepare (%i): %s",
|
|
rc, sqlite3_errstr(rc));
|
|
|
|
dbclose(&db);
|
|
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to prepare query string",
|
|
querystr);
|
|
}
|
|
|
|
// Bind from to prepared statement
|
|
if((rc = sqlite3_bind_double(stmt, 1, from)) != SQLITE_OK)
|
|
{
|
|
log_err("api_stats_database_history(): Failed to bind from (error %d) - %s",
|
|
rc, sqlite3_errstr(rc));
|
|
sqlite3_reset(stmt);
|
|
sqlite3_finalize(stmt);
|
|
dbclose(&db);
|
|
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to bind from",
|
|
NULL);
|
|
}
|
|
|
|
// Bind until to prepared statement
|
|
if((rc = sqlite3_bind_double(stmt, 2, until)) != SQLITE_OK)
|
|
{
|
|
log_err("api_stats_database_history(): Failed to bind until (error %d) - %s",
|
|
rc, sqlite3_errstr(rc));
|
|
sqlite3_reset(stmt);
|
|
sqlite3_finalize(stmt);
|
|
dbclose(&db);
|
|
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to bind until",
|
|
NULL);
|
|
}
|
|
|
|
// Loop over and accumulate results
|
|
cJSON *top_items = JSON_NEW_ARRAY();
|
|
unsigned int total = 0;
|
|
while((rc = sqlite3_step(stmt)) == SQLITE_ROW &&
|
|
++total < count)
|
|
{
|
|
// Get count
|
|
const int cnt = sqlite3_column_int(stmt, 0);
|
|
cJSON *item = JSON_NEW_OBJECT();
|
|
if(domains)
|
|
{
|
|
// Add domain to item
|
|
JSON_COPY_STR_TO_OBJECT(item, "domain", sqlite3_column_text(stmt, 1));
|
|
}
|
|
else
|
|
{
|
|
// Add client to item
|
|
JSON_COPY_STR_TO_OBJECT(item, "ip", sqlite3_column_text(stmt, 1));
|
|
JSON_COPY_STR_TO_OBJECT(item, "name", sqlite3_column_text(stmt, 2));
|
|
}
|
|
JSON_ADD_NUMBER_TO_OBJECT(item, "count", cnt);
|
|
JSON_ADD_ITEM_TO_ARRAY(top_items, item);
|
|
}
|
|
|
|
// Finalize statement and close (= unlock) database connection
|
|
sqlite3_finalize(stmt);
|
|
|
|
cJSON *json = JSON_NEW_OBJECT();
|
|
JSON_ADD_ITEM_TO_OBJECT(json, (domains ? "domains" : "clients"), top_items);
|
|
const int total_num = db_query_int_from_until(db, count_total_str, from, until);
|
|
const int blocked_num = db_query_int_from_until(db, count_blocked_str, from, until);
|
|
JSON_ADD_NUMBER_TO_OBJECT(json, "total_queries", total_num);
|
|
JSON_ADD_NUMBER_TO_OBJECT(json, "blocked_queries", blocked_num);
|
|
|
|
dbclose(&db);
|
|
JSON_SEND_OBJECT(json);
|
|
}
|
|
|
|
int api_stats_database_summary(struct ftl_conn *api)
|
|
{
|
|
double from = 0, until = 0;
|
|
if(api->request->query_string != NULL)
|
|
{
|
|
get_double_var(api->request->query_string, "from", &from);
|
|
get_double_var(api->request->query_string, "until", &until);
|
|
}
|
|
|
|
// Check if we received the required information
|
|
if(from < 1.0 || until < 1.0)
|
|
{
|
|
return send_json_error(api, 400,
|
|
"bad_request",
|
|
"You need to specify both \"from\" and \"until\" in the request.",
|
|
NULL);
|
|
}
|
|
|
|
// Open the database
|
|
sqlite3 *db = dbopen(false, false);
|
|
if(db == NULL)
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to open long-term database",
|
|
NULL);
|
|
|
|
// Perform SQL queries
|
|
const char *querystr;
|
|
querystr = "SELECT COUNT(*) FROM query_storage "
|
|
"WHERE timestamp >= :from AND timestamp <= :until";
|
|
const int sum_queries = db_query_int_from_until(db, querystr, from, until);
|
|
|
|
querystr = "SELECT COUNT(*) FROM query_storage "
|
|
"WHERE timestamp >= :from AND timestamp <= :until "
|
|
"AND " FILTER_STATUS_BLOCKED;
|
|
const int sum_blocked = db_query_int_from_until(db, querystr, from, until);
|
|
|
|
querystr = "SELECT COUNT(DISTINCT client) FROM query_storage "
|
|
"WHERE timestamp >= :from AND timestamp <= :until";
|
|
const int total_clients = db_query_int_from_until(db, querystr, from, until);
|
|
|
|
// Calculate percentage of blocked queries, substituting 0.0 if there
|
|
// are no blocked queries
|
|
float percent_blocked = 0.0;
|
|
if(sum_queries > 0.0)
|
|
percent_blocked = 1e2f*sum_blocked/sum_queries;
|
|
|
|
if(sum_queries < 0 || sum_blocked < 0 || total_clients < 0)
|
|
{
|
|
|
|
// Close (= unlock) database connection
|
|
dbclose(&db);
|
|
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Internal server error",
|
|
NULL);
|
|
}
|
|
|
|
// Loop over and accumulate results
|
|
cJSON *json = JSON_NEW_OBJECT();
|
|
JSON_ADD_NUMBER_TO_OBJECT(json, "sum_queries", sum_queries);
|
|
JSON_ADD_NUMBER_TO_OBJECT(json, "sum_blocked", sum_blocked);
|
|
JSON_ADD_NUMBER_TO_OBJECT(json, "percent_blocked", percent_blocked);
|
|
JSON_ADD_NUMBER_TO_OBJECT(json, "total_clients", total_clients);
|
|
|
|
// Close (= unlock) database connection
|
|
dbclose(&db);
|
|
|
|
// Send JSON object
|
|
JSON_SEND_OBJECT(json);
|
|
}
|
|
|
|
int api_history_database_clients(struct ftl_conn *api)
|
|
{
|
|
double from = 0, until = 0;
|
|
const int interval = 600;
|
|
if(api->request->query_string != NULL)
|
|
{
|
|
get_double_var(api->request->query_string, "from", &from);
|
|
get_double_var(api->request->query_string, "until", &until);
|
|
}
|
|
|
|
// Check if we received the required information
|
|
if(from < 1.0 || until < 1.0)
|
|
{
|
|
return send_json_error(api, 400,
|
|
"bad_request",
|
|
"You need to specify both \"from\" and \"until\" in the request.",
|
|
NULL);
|
|
}
|
|
|
|
// Open the database
|
|
sqlite3 *db = dbopen(false, false);
|
|
if(db == NULL)
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to open long-term database",
|
|
NULL);
|
|
|
|
const char *querystr = "SELECT DISTINCT(client),ip,name FROM query_storage "
|
|
"JOIN client_by_id ON client_by_id.id = client "
|
|
"WHERE timestamp >= :from AND timestamp <= :until "
|
|
"ORDER BY client DESC";
|
|
|
|
// Prepare SQLite statement
|
|
sqlite3_stmt *stmt;
|
|
int rc = sqlite3_prepare_v2(db, querystr, -1, &stmt, NULL);
|
|
if( rc != SQLITE_OK ){
|
|
log_err("api_stats_database_clients() - SQL error prepare outer (%i): %s",
|
|
rc, sqlite3_errstr(rc));
|
|
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to prepare outer statement",
|
|
NULL);
|
|
}
|
|
|
|
// Bind from to prepared statement
|
|
if((rc = sqlite3_bind_double(stmt, 1, from)) != SQLITE_OK)
|
|
{
|
|
log_err("api_stats_database_clients(): Failed to bind from (error %d) - %s",
|
|
rc, sqlite3_errstr(rc));
|
|
sqlite3_reset(stmt);
|
|
sqlite3_finalize(stmt);
|
|
dbclose(&db);
|
|
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to bind from",
|
|
NULL);
|
|
}
|
|
|
|
// Bind until to prepared statement
|
|
if((rc = sqlite3_bind_double(stmt, 2, until)) != SQLITE_OK)
|
|
{
|
|
log_err("api_stats_database_clients(): Failed to bind until (error %d) - %s",
|
|
rc, sqlite3_errstr(rc));
|
|
sqlite3_reset(stmt);
|
|
sqlite3_finalize(stmt);
|
|
dbclose(&db);
|
|
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to bind until",
|
|
NULL);
|
|
}
|
|
|
|
// Loop over clients and accumulate results
|
|
cJSON *clients = JSON_NEW_OBJECT();
|
|
while((rc = sqlite3_step(stmt)) == SQLITE_ROW)
|
|
{
|
|
cJSON *item = JSON_NEW_OBJECT();
|
|
JSON_COPY_STR_TO_OBJECT(item, "name", sqlite3_column_text(stmt, 2));
|
|
JSON_ADD_ITEM_TO_OBJECT(clients, (const char*)sqlite3_column_text(stmt, 1), item);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
// Build SQL string
|
|
querystr = "SELECT (timestamp/:interval)*:interval interval,client,COUNT(*) FROM query_storage "
|
|
"WHERE timestamp >= :from AND timestamp <= :until "
|
|
"GROUP BY interval,client ORDER BY interval DESC, client DESC";
|
|
|
|
// Prepare SQLite statement
|
|
rc = sqlite3_prepare_v2(db, querystr, -1, &stmt, NULL);
|
|
if( rc != SQLITE_OK ){
|
|
log_err("api_stats_database_clients() - SQL error prepare (%i): %s",
|
|
rc, sqlite3_errstr(rc));
|
|
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to prepare inner statement",
|
|
NULL);
|
|
}
|
|
|
|
// Bind interval to prepared statement
|
|
if((rc = sqlite3_bind_int(stmt, 1, interval)) != SQLITE_OK)
|
|
{
|
|
log_err("api_stats_database_clients(): Failed to bind interval (error %d) - %s",
|
|
rc, sqlite3_errstr(rc));
|
|
sqlite3_reset(stmt);
|
|
sqlite3_finalize(stmt);
|
|
dbclose(&db);
|
|
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to bind interval",
|
|
NULL);
|
|
}
|
|
|
|
// Bind from to prepared statement
|
|
if((rc = sqlite3_bind_int(stmt, 2, from)) != SQLITE_OK)
|
|
{
|
|
log_err("api_stats_database_clients(): Failed to bind from (error %d) - %s",
|
|
rc, sqlite3_errstr(rc));
|
|
sqlite3_reset(stmt);
|
|
sqlite3_finalize(stmt);
|
|
dbclose(&db);
|
|
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to bind from",
|
|
NULL);
|
|
}
|
|
|
|
// Bind until to prepared statement
|
|
if((rc = sqlite3_bind_int(stmt, 3, until)) != SQLITE_OK)
|
|
{
|
|
log_err("api_stats_database_clients(): Failed to bind until (error %d) - %s",
|
|
rc, sqlite3_errstr(rc));
|
|
sqlite3_reset(stmt);
|
|
sqlite3_finalize(stmt);
|
|
dbclose(&db);
|
|
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to bind until",
|
|
NULL);
|
|
}
|
|
|
|
cJSON *item = NULL;
|
|
cJSON *data = NULL;
|
|
unsigned int previous_timeslot = 0u;
|
|
cJSON *over_time = JSON_NEW_ARRAY();
|
|
while((rc = sqlite3_step(stmt)) == SQLITE_ROW)
|
|
{
|
|
// Get timestamp and derive timeslot from it
|
|
const unsigned int timestamp = sqlite3_column_int(stmt, 0);
|
|
const unsigned int timeslot = timestamp - timestamp % interval;
|
|
// Begin new array item for each new timeslot
|
|
if(timeslot != previous_timeslot)
|
|
{
|
|
previous_timeslot = timeslot;
|
|
if(item != NULL && data != NULL)
|
|
{
|
|
JSON_ADD_ITEM_TO_OBJECT(item, "data", data);
|
|
JSON_ADD_ITEM_TO_ARRAY(over_time, item);
|
|
}
|
|
|
|
item = JSON_NEW_OBJECT();
|
|
data = JSON_NEW_OBJECT();
|
|
|
|
JSON_ADD_NUMBER_TO_OBJECT(item, "timestamp", previous_timeslot);
|
|
}
|
|
|
|
const char *client = (char*)sqlite3_column_text(stmt, 1);
|
|
const int count = sqlite3_column_int(stmt, 2);
|
|
|
|
JSON_ADD_NUMBER_TO_OBJECT(data, client, count);
|
|
}
|
|
|
|
// Append final timeslot at the end if applicable
|
|
if(item != NULL && data != NULL)
|
|
{
|
|
JSON_ADD_ITEM_TO_OBJECT(item, "data", data);
|
|
JSON_ADD_ITEM_TO_ARRAY(over_time, item);
|
|
}
|
|
|
|
// Finalize statement and close (= unlock) database connection
|
|
sqlite3_finalize(stmt);
|
|
dbclose(&db);
|
|
|
|
cJSON *json = JSON_NEW_OBJECT();
|
|
JSON_ADD_ITEM_TO_OBJECT(json, "history", over_time);
|
|
JSON_ADD_ITEM_TO_OBJECT(json, "clients", clients);
|
|
JSON_SEND_OBJECT(json);
|
|
}
|
|
|
|
int api_stats_database_query_types(struct ftl_conn *api)
|
|
{
|
|
double from = 0, until = 0;
|
|
if(api->request->query_string != NULL)
|
|
{
|
|
get_double_var(api->request->query_string, "from", &from);
|
|
get_double_var(api->request->query_string, "until", &until);
|
|
}
|
|
|
|
// Check if we received the required information
|
|
if(from < 1.0 || until < 1.0)
|
|
{
|
|
return send_json_error(api, 400,
|
|
"bad_request",
|
|
"You need to specify both \"from\" and \"until\" in the request.",
|
|
NULL);
|
|
}
|
|
|
|
// Open the database
|
|
sqlite3 *db = dbopen(false, false);
|
|
if(db == NULL)
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to open long-term database",
|
|
NULL);
|
|
|
|
// Perform SQL queries
|
|
cJSON *types = JSON_NEW_OBJECT();
|
|
for(int i = TYPE_A; i < TYPE_MAX; i++)
|
|
{
|
|
const char *querystr = "SELECT COUNT(*) FROM query_storage "
|
|
"WHERE timestamp >= :from AND timestamp <= :until "
|
|
"AND type = :type";
|
|
// Add 1 as type is stored one-based in the database for historical reasons
|
|
int count = db_query_int_from_until_type(db, querystr, from, until, i+1);
|
|
JSON_ADD_NUMBER_TO_OBJECT(types, get_query_type_str(i, NULL, NULL), count);
|
|
}
|
|
|
|
// Close (= unlock) database connection
|
|
dbclose(&db);
|
|
|
|
// Send JSON object
|
|
cJSON *json = JSON_NEW_OBJECT();
|
|
JSON_ADD_ITEM_TO_OBJECT(json, "types", types);
|
|
JSON_SEND_OBJECT(json);
|
|
}
|
|
|
|
|
|
int api_stats_database_upstreams(struct ftl_conn *api)
|
|
{
|
|
double from = 0, until = 0;
|
|
if(api->request->query_string != NULL)
|
|
{
|
|
get_double_var(api->request->query_string, "from", &from);
|
|
get_double_var(api->request->query_string, "until", &until);
|
|
}
|
|
|
|
// Check if we received the required information
|
|
if(from < 1.0 || until < 1.0)
|
|
{
|
|
return send_json_error(api, 400,
|
|
"bad_request",
|
|
"You need to specify both \"from\" and \"until\" in the request.",
|
|
NULL);
|
|
}
|
|
|
|
// Open the database
|
|
sqlite3 *db = dbopen(false, false);
|
|
if(db == NULL)
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to open long-term database",
|
|
NULL);
|
|
|
|
// Perform simple SQL queries
|
|
unsigned int sum_queries = 0;
|
|
const char *querystr;
|
|
querystr = "SELECT COUNT(*) FROM query_storage "
|
|
"WHERE timestamp >= :from AND timestamp <= :until "
|
|
"AND status = 3";
|
|
int cached_queries = db_query_int_from_until(db, querystr, from, until);
|
|
sum_queries += cached_queries;
|
|
|
|
querystr = "SELECT COUNT(*) FROM query_storage "
|
|
"WHERE timestamp >= :from AND timestamp <= :until "
|
|
"AND status != 0 AND status != 2 AND status != 3";
|
|
int blocked_queries = db_query_int_from_until(db, querystr, from, until);
|
|
sum_queries += blocked_queries;
|
|
|
|
querystr = "SELECT forward,COUNT(*) FROM query_storage "
|
|
"WHERE timestamp >= :from AND timestamp <= :until "
|
|
"AND forward IS NOT NULL "
|
|
"GROUP BY forward ORDER BY forward";
|
|
|
|
// Prepare SQLite statement
|
|
sqlite3_stmt *stmt;
|
|
int rc = sqlite3_prepare_v2(db, querystr, -1, &stmt, NULL);
|
|
if( rc != SQLITE_OK ){
|
|
log_err("api_stats_database_clients() - SQL error prepare (%i): %s",
|
|
rc, sqlite3_errstr(rc));
|
|
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to prepare statement",
|
|
NULL);
|
|
}
|
|
|
|
// Bind from to prepared statement
|
|
if((rc = sqlite3_bind_double(stmt, 1, from)) != SQLITE_OK)
|
|
{
|
|
log_err("api_stats_database_clients(): Failed to bind from (error %d) - %s",
|
|
rc, sqlite3_errstr(rc));
|
|
sqlite3_reset(stmt);
|
|
sqlite3_finalize(stmt);
|
|
dbclose(&db);
|
|
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to bind from",
|
|
NULL);
|
|
}
|
|
|
|
// Bind until to prepared statement
|
|
if((rc = sqlite3_bind_double(stmt, 2, until)) != SQLITE_OK)
|
|
{
|
|
log_err("api_stats_database_clients(): Failed to bind until (error %d) - %s",
|
|
rc, sqlite3_errstr(rc));
|
|
sqlite3_reset(stmt);
|
|
sqlite3_finalize(stmt);
|
|
dbclose(&db);
|
|
|
|
return send_json_error(api, 500,
|
|
"internal_error",
|
|
"Failed to bind until",
|
|
NULL);
|
|
}
|
|
|
|
// Loop over clients and accumulate results
|
|
cJSON *upstreams = JSON_NEW_ARRAY();
|
|
int forwarded_queries = 0;
|
|
while((rc = sqlite3_step(stmt)) == SQLITE_ROW)
|
|
{
|
|
const char *upstream = (char*)sqlite3_column_text(stmt, 0);
|
|
const int count = sqlite3_column_int(stmt, 1);
|
|
|
|
cJSON *item = JSON_NEW_OBJECT();
|
|
unsigned int port = -1;
|
|
char buffer[512] = { 0 };
|
|
if(sscanf(upstream, "%511[^#]#%u", buffer, &port) == 2)
|
|
{
|
|
buffer[sizeof(buffer)-1] = '\0';
|
|
JSON_COPY_STR_TO_OBJECT(item, "ip", buffer);
|
|
}
|
|
else
|
|
JSON_COPY_STR_TO_OBJECT(item, "ip", upstream);
|
|
JSON_REF_STR_IN_OBJECT(item, "name", "");
|
|
JSON_ADD_NUMBER_TO_OBJECT(item, "port", port);
|
|
JSON_ADD_NUMBER_TO_OBJECT(item, "count", count);
|
|
|
|
cJSON *statistics = JSON_NEW_OBJECT();
|
|
JSON_ADD_NUMBER_TO_OBJECT(statistics, "response", 0);
|
|
JSON_ADD_NUMBER_TO_OBJECT(statistics, "variance", 0);
|
|
JSON_ADD_ITEM_TO_OBJECT(item, "statistics", statistics);
|
|
|
|
JSON_ADD_ITEM_TO_ARRAY(upstreams, item);
|
|
forwarded_queries += count;
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
// Add number of forwarded queries to total query count
|
|
sum_queries += forwarded_queries;
|
|
|
|
// Add cache and blocklist as upstreams
|
|
cJSON *cached = JSON_NEW_OBJECT();
|
|
JSON_REF_STR_IN_OBJECT(cached, "ip", "cache");
|
|
JSON_REF_STR_IN_OBJECT(cached, "name", "cache");
|
|
JSON_ADD_NUMBER_TO_OBJECT(cached, "port", -1);
|
|
JSON_ADD_NUMBER_TO_OBJECT(cached, "count", cached_queries);
|
|
cJSON *statistics = JSON_NEW_OBJECT();
|
|
JSON_ADD_NUMBER_TO_OBJECT(statistics, "response", 0);
|
|
JSON_ADD_NUMBER_TO_OBJECT(statistics, "variance", 0);
|
|
JSON_ADD_ITEM_TO_OBJECT(cached, "statistics", statistics);
|
|
JSON_ADD_ITEM_TO_ARRAY(upstreams, cached);
|
|
|
|
cJSON *blocked = JSON_NEW_OBJECT();
|
|
JSON_REF_STR_IN_OBJECT(blocked, "ip", "blocklist");
|
|
JSON_REF_STR_IN_OBJECT(blocked, "name", "blocklist");
|
|
JSON_ADD_NUMBER_TO_OBJECT(blocked, "port", -1);
|
|
JSON_ADD_NUMBER_TO_OBJECT(blocked, "count", blocked_queries);
|
|
statistics = JSON_NEW_OBJECT();
|
|
JSON_ADD_NUMBER_TO_OBJECT(statistics, "response", 0);
|
|
JSON_ADD_NUMBER_TO_OBJECT(statistics, "variance", 0);
|
|
JSON_ADD_ITEM_TO_OBJECT(cached, "statistics", statistics);
|
|
JSON_ADD_ITEM_TO_ARRAY(upstreams, blocked);
|
|
|
|
// Close (= unlock) database connection
|
|
dbclose(&db);
|
|
|
|
// Send JSON object
|
|
cJSON *json = JSON_NEW_OBJECT();
|
|
JSON_ADD_ITEM_TO_OBJECT(json, "upstreams", upstreams);
|
|
JSON_ADD_NUMBER_TO_OBJECT(json, "forwarded_queries", forwarded_queries);
|
|
JSON_ADD_NUMBER_TO_OBJECT(json, "total_queries", sum_queries);
|
|
JSON_SEND_OBJECT(json);
|
|
}
|