diff --git a/Makefile b/Makefile index 0b05e33f..aa20eea8 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ DNSMASQ_OPTS = -DHAVE_DNSSEC -DHAVE_DNSSEC_STATIC -DHAVE_IDN FTL_DEPS = *.h database/*.h api/*.h version.h FTL_DB_OBJ = database/common.o database/query-table.o database/network-table.o database/gravity-db.o database/database-thread.o \ database/sqlite3-ext.o database/message-table.o -FTL_API_OBJ = api/http.o api/ftl.o api/stats.o api/dns.o api/version.o +FTL_API_OBJ = api/http.o api/ftl.o api/stats.o api/dns.o api/version.o api/auth.o api/settings.o FTL_OBJ = $(FTL_DB_OBJ) $(FTL_API_OBJ) main.o memory.o log.o daemon.o datastructure.o signals.o files.o setupVars.o args.o gc.o config.o dnsmasq_interface.o resolve.o regex.o shmem.o capabilities.o overTime.o timers.o vector.o DNSMASQ_DEPS = config.h dhcp-protocol.h dns-protocol.h radv-protocol.h dhcp6-protocol.h dnsmasq.h ip6addr.h metrics.h ../dnsmasq_interface.h diff --git a/src/FTL.h b/src/FTL.h index 491eb8b1..54c272d8 100644 --- a/src/FTL.h +++ b/src/FTL.h @@ -101,6 +101,10 @@ // Default: 1000 (one second) #define DATABASE_BUSY_TIMEOUT 1000 +// After how much time does a valid API session expire? [seconds] +// Default: 300 (five minutes) +#define API_SESSION_EXPIRE 300 + // FTLDNS enums enum { QUERIES, UPSTREAMS, CLIENTS, DOMAINS, OVERTIME, WILDCARD, DNS_CACHE }; enum { DNSSEC_UNSPECIFIED, DNSSEC_SECURE, DNSSEC_INSECURE, DNSSEC_BOGUS, DNSSEC_ABANDONED }; diff --git a/src/api/api.h b/src/api/api.h index e312f4ad..4e762a4f 100644 --- a/src/api/api.h +++ b/src/api/api.h @@ -41,4 +41,10 @@ int api_dns_somelist(struct mg_connection *conn, // Version method int api_version(struct mg_connection *conn); +// Auth method +int api_auth(struct mg_connection *conn); + +// Settings methods +int api_settings_web(struct mg_connection *conn); + #endif // API_H diff --git a/src/api/auth.c b/src/api/auth.c new file mode 100644 index 00000000..d5b81081 --- /dev/null +++ b/src/api/auth.c @@ -0,0 +1,67 @@ +/* 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 Implementation /api/auth +* +* 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 "api.h" +#include "log.h" +#include "config.h" + +int api_auth(struct mg_connection *conn) +{ + bool auth = false; + + // Does the client try to authenticate through a set header? + const char *xHeader = mg_get_header(conn, "X-Pi-hole-Authenticate"); + if(xHeader != NULL && strlen(xHeader) > 0) + { + auth = true; + + if(config.debug & DEBUG_API) + logg("Received X-Pi-hole-Authenticate: %s", xHeader); + } + + // Does the client provide a user_id cookie? + int user_id = 0; + if(http_get_cookie_int(conn, "user_id", &user_id)) + { + auth = true; + + if(config.debug & DEBUG_API) + logg("Read user_id=%i from user-provided cookie", user_id); + } + + cJSON *json = JSON_NEW_OBJ(); + if(auth) + { + if(config.debug & DEBUG_API) + logg("Authentification: OK"); + + JSON_OBJ_REF_STR(json, "status", "success"); + // Ten minutes validity + char *additional_headers = NULL; + if(asprintf(&additional_headers, "Set-Cookie: user_id=%u; Path=/; Max-Age=%u\r\n", 1, API_SESSION_EXPIRE) > 0) + { + JSON_SENT_OBJECT_AND_HEADERS(json, additional_headers); + } + else + { + JSON_SENT_OBJECT(json); + } + } + else + { + if(config.debug & DEBUG_API) + logg("Authentification: FAIL"); + + JSON_OBJ_REF_STR(json, "key", "unauthorized"); + char *additional_headers = strdup("Set-Cookie: user_id=deleted; Path=/; Max-Age=-1\r\n"); + JSON_SENT_OBJECT_AND_HEADERS_CODE(json, 401, additional_headers); + } +} \ No newline at end of file diff --git a/src/api/http.c b/src/api/http.c index d0ac004c..39bac62d 100644 --- a/src/api/http.c +++ b/src/api/http.c @@ -18,12 +18,21 @@ // Server context handle static struct mg_context *ctx = NULL; -int send_http(struct mg_connection *conn, const char *mime_type, const char *msg) +int send_http(struct mg_connection *conn, const char *mime_type, + const char *additional_headers, const char *msg) { - mg_send_http_ok(conn, mime_type, strlen(msg)); + mg_send_http_ok(conn, mime_type, additional_headers, strlen(msg)); return mg_write(conn, msg, strlen(msg)); } +int send_http_unauth(struct mg_connection *conn, + const char *additional_headers, const char *msg) +{ + // MPayload will be sent with text/plain encoding due to + // the first line being "Error 401" by definition + return mg_send_http_error(conn, 401, "%s", msg); +} + int send_http_error(struct mg_connection *conn) { return mg_send_http_error(conn, 500, "Internal server error"); @@ -41,7 +50,7 @@ void __attribute__ ((format (gnu_printf, 3, 4))) http_send(struct mg_connection if(!chunk) { // Send 200 HTTP header with content size - mg_send_http_ok(conn, "application/json", len); + mg_send_http_ok(conn, "application/json", NULL, len); } if(chunk && mg_send_chunk(conn, buffer, len) < 0) { @@ -60,7 +69,7 @@ void __attribute__ ((format (gnu_printf, 3, 4))) http_send(struct mg_connection // Print passed string directly static int print_simple(struct mg_connection *conn, void *input) { - return send_http(conn, "text/plain", input); + return send_http(conn, "text/plain", NULL, input); } static int api_handler(struct mg_connection *conn, void *ignored) @@ -161,6 +170,16 @@ static int api_handler(struct mg_connection *conn, void *ignored) { ret = api_version(conn); } + /******************************** api/auth ****************************/ + else if(strcasecmp("/api/auth", request->local_uri) == 0) + { + ret = api_auth(conn); + } + /******************************** api/settings ****************************/ + else if(strcasecmp("/api/settings/web", request->local_uri) == 0) + { + ret = api_settings_web(conn); + } /******************************** not found ******************************/ /* else { @@ -213,4 +232,27 @@ void http_terminate(void) /* Un-initialize the library */ mg_exit_library(); -} \ No newline at end of file +} + +bool http_get_cookie_int(struct mg_connection *conn, const char *cookieName, int *i) +{ + // Maximum cookie length is 4KB + char cookieValue[4096]; + const char *cookie = mg_get_header(conn, "Cookie"); + if(mg_get_cookie(cookie, cookieName, cookieValue, sizeof(cookieValue)) > 0) + { + *i = atoi(cookieValue); + return true; + } + return false; +} + +bool http_get_cookie_str(struct mg_connection *conn, const char *cookieName, char *str, size_t str_size) +{ + const char *cookie = mg_get_header(conn, "Cookie"); + if(mg_get_cookie(cookie, cookieName, str, str_size) > 0) + { + return true; + } + return false; +} diff --git a/src/api/http.h b/src/api/http.h index 4b3e100a..d5432dc7 100644 --- a/src/api/http.h +++ b/src/api/http.h @@ -24,9 +24,16 @@ void http_init(void); void http_terminate(void); void http_send(struct mg_connection *conn, bool chunk, const char *format, ...) __attribute__ ((format (gnu_printf, 3, 4))); -int send_http(struct mg_connection *conn, const char *mime_type, const char *msg); +int send_http(struct mg_connection *conn, const char *mime_type, + const char *additional_headers, const char *msg); +int send_http_unauth(struct mg_connection *conn, + const char *additional_headers, const char *msg); int send_http_error(struct mg_connection *conn); +// Cookie routines +bool http_get_cookie_int(struct mg_connection *conn, const char *cookieName, int *i); +bool http_get_cookie_str(struct mg_connection *conn, const char *cookieName, char *str, size_t str_size); + // HTTP macros #define GET_VAR(variable, destination, source) mg_get_var(source, strlen(source), variable, destination, sizeof(destination)) diff --git a/src/api/json_macros.h b/src/api/json_macros.h index 0abe8d13..2d76dda8 100644 --- a/src/api/json_macros.h +++ b/src/api/json_macros.h @@ -77,6 +77,17 @@ cJSON_AddItemToObject(object, key, null_item); \ } +#define JSON_OBJ_ADD_BOOL(object, key, value) {\ + cJSON *bool_item = cJSON_CreateBool(value); \ + if(bool_item == NULL) \ + { \ + cJSON_Delete(object); \ + send_http_error(conn); \ + return 500; \ + } \ + cJSON_AddItemToObject(object, key, bool_item); \ +} + #define JSON_ARRAY_ADD_NUMBER(object, number){ \ cJSON *number_item = cJSON_CreateNumber((double)number); \ cJSON_AddItemToArray(object, number_item); \ @@ -119,7 +130,35 @@ send_http_error(conn); \ return 500; \ } \ - send_http(conn, "application/json", msg); \ + send_http(conn, "application/json", NULL, msg); \ cJSON_Delete(object); \ return 200; \ } + +#define JSON_SENT_OBJECT_AND_HEADERS(object, additional_headers){ \ + const char* msg = JSON_FORMATTER(object); \ + if(msg == NULL) \ + { \ + cJSON_Delete(object); \ + send_http_error(conn); \ + return 500; \ + } \ + send_http(conn, "application/json", additional_headers, msg); \ + cJSON_Delete(object); \ + free(additional_headers); \ + return 200; \ +} + +#define JSON_SENT_OBJECT_AND_HEADERS_CODE(object, code, additional_headers){ \ + const char* msg = JSON_FORMATTER(object); \ + if(msg == NULL) \ + { \ + cJSON_Delete(object); \ + send_http_error(conn); \ + return 500; \ + } \ + send_http_unauth(conn, additional_headers, msg); \ + cJSON_Delete(object); \ + free(additional_headers); \ + return code; \ +} diff --git a/src/api/settings.c b/src/api/settings.c new file mode 100644 index 00000000..2172449a --- /dev/null +++ b/src/api/settings.c @@ -0,0 +1,20 @@ +/* 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 Implementation /api/settings +* +* 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 "api.h" + +int api_settings_web(struct mg_connection *conn) +{ + cJSON *json = JSON_NEW_OBJ(); + JSON_OBJ_REF_STR(json, "layout", "boxed"); + JSON_OBJ_REF_STR(json, "language", "en"); + JSON_SENT_OBJECT(json); +} \ No newline at end of file diff --git a/src/civetweb/civetweb.c b/src/civetweb/civetweb.c index 20f24c11..e742cfeb 100644 --- a/src/civetweb/civetweb.c +++ b/src/civetweb/civetweb.c @@ -4802,6 +4802,7 @@ mg_send_http_error(struct mg_connection *conn, int status, const char *fmt, ...) int mg_send_http_ok(struct mg_connection *conn, const char *mime_type, + const char *additional_headers, long long content_length) { char date[64]; @@ -4823,6 +4824,11 @@ mg_send_http_ok(struct mg_connection *conn, date, suggest_connection_header(conn)); + if(additional_headers != NULL && strlen(additional_headers) > 0) + { + mg_write(conn, additional_headers, strlen(additional_headers)); + } + send_no_cache_header(conn); send_additional_header(conn); if (content_length < 0) { diff --git a/src/civetweb/civetweb.h b/src/civetweb/civetweb.h index 59cf2f4e..f3b69ef2 100644 --- a/src/civetweb/civetweb.h +++ b/src/civetweb/civetweb.h @@ -939,6 +939,8 @@ CIVETWEB_API int mg_send_http_error(struct mg_connection *conn, * Parameters: * conn: Current connection handle. * mime_type: Set Content-Type for the following content. + * additional_header: Pi-hole addition for sending custom additional + * information (like settingcookies) * content_length: Size of the following content, if content_length >= 0. * Will set transfer-encoding to chunked, if set to -1. * Return: @@ -946,6 +948,7 @@ CIVETWEB_API int mg_send_http_error(struct mg_connection *conn, */ CIVETWEB_API int mg_send_http_ok(struct mg_connection *conn, const char *mime_type, + const char *additional_headers, long long content_length);