From 297b9beb349a3ae17caa77543352734554ad0110 Mon Sep 17 00:00:00 2001 From: Luke Dashjr Date: Thu, 16 Jan 2025 15:49:30 +0000 Subject: [PATCH] api: Require admin password to perform actions --- src/datum_api.c | 96 +++++++++++++++++++++++++++++++++++++++++++++--- src/datum_conf.c | 16 ++++++++ src/datum_conf.h | 3 ++ 3 files changed, 109 insertions(+), 6 deletions(-) diff --git a/src/datum_api.c b/src/datum_api.c index a18d6d4..70a962d 100644 --- a/src/datum_api.c +++ b/src/datum_api.c @@ -398,6 +398,55 @@ int datum_api_do_error(struct MHD_Connection * const connection, const unsigned return ret; } +bool datum_api_check_admin_password_only(struct MHD_Connection * const connection, const char * const password) { + if (datum_secure_strequals(datum_config.api_admin_password, datum_config.api_admin_password_len, password) && datum_config.api_admin_password_len) { + return true; + } + DLOG_DEBUG("Wrong password in request"); + datum_api_do_error(connection, MHD_HTTP_FORBIDDEN); + return false; +} + +bool datum_api_check_admin_password(struct MHD_Connection * const connection, const json_t * const j) { + int ret; + + const json_t * const j_password = json_object_get(j, "password"); + if (json_is_string(j_password)) { + return datum_api_check_admin_password_only(connection, json_string_value(j_password)); + } + + // Only accept HTTP authentication if there's an anti-CSRF token + const json_t * const j_csrf = json_object_get(j, "csrf"); + if (!json_is_string(j_csrf)) { + DLOG_DEBUG("Missing CSRF token in request"); + datum_api_do_error(connection, MHD_HTTP_FORBIDDEN); + return false; + } + if (!datum_secure_strequals(datum_config.api_csrf_token, sizeof(datum_config.api_csrf_token)-1, json_string_value(j_csrf))) { + DLOG_DEBUG("Wrong CSRF token in request"); + datum_api_do_error(connection, MHD_HTTP_FORBIDDEN); + return false; + } + + char * const username = MHD_digest_auth_get_username(connection); + const char * const realm = "DATUM Gateway"; + if (username) { + ret = MHD_digest_auth_check2(connection, realm, username, datum_config.api_admin_password, 300, MHD_DIGEST_ALG_SHA256); + free(username); + } else { + ret = MHD_NO; + } + if (ret != MHD_YES) { + DLOG_DEBUG("Wrong password in HTTP authentication"); + struct MHD_Response *response = MHD_create_response_from_buffer(0, "", MHD_RESPMEM_PERSISTENT); + ret = MHD_queue_auth_fail_response2(connection, realm, datum_config.api_csrf_token, response, (ret == MHD_INVALID_NONCE) ? MHD_YES : MHD_NO, MHD_DIGEST_ALG_SHA256); + MHD_destroy_response(response); + return false; + } + + return true; +} + static int datum_api_asset(struct MHD_Connection * const connection, const char * const mimetype, const char * const data, const size_t datasz) { struct MHD_Response * const response = MHD_create_response_from_buffer(datasz, (void*)data, MHD_RESPMEM_PERSISTENT); MHD_add_response_header(response, "Content-Type", mimetype); @@ -440,6 +489,11 @@ int datum_api_cmd(struct MHD_Connection *connection, char *post, int len) { root = json_loadb(post, len, 0, &error); if (root) { if (json_is_object(root) && (cmd = json_object_get(root, "cmd"))) { + if (!datum_api_check_admin_password(connection, root)) { + json_decref(root); + return MHD_YES; + } + if (json_is_string(cmd)) { cstr = json_string_value(cmd); DLOG_DEBUG("JSON CMD: %s",cstr); @@ -481,6 +535,11 @@ int datum_api_cmd(struct MHD_Connection *connection, char *post, int len) { return datum_api_do_error(connection, MHD_HTTP_INTERNAL_SERVER_ERROR); } + if (!datum_api_check_admin_password(connection, root)) { + json_decref(root); + return MHD_YES; + } + const char *redirect = "/"; param = json_object_get(root, "empty_thread"); @@ -587,10 +646,12 @@ int datum_api_thread_dashboard(struct MHD_Connection *connection) { return MHD_NO; } + const bool have_admin = datum_config.api_admin_password_len; + tsms = current_time_millis(); sz = snprintf(output, max_sz-1-sz, "%s", www_threads_top_html); - sz += snprintf(&output[sz], max_sz-1-sz, "
"); + sz += snprintf(&output[sz], max_sz-1-sz, "
TID Connection Count Sub Count Approx. Hashrate Command
", datum_config.api_csrf_token); for(j=0;jmax_threads;j++) { thr = 0.0; subs = 0; @@ -614,11 +675,19 @@ int datum_api_thread_dashboard(struct MHD_Connection *connection) { } } if (conns) { - sz += snprintf(&output[sz], max_sz-1-sz, "", j, conns, subs, thr, j, j); + sz += snprintf(&output[sz], max_sz-1-sz, ""); } } sz += snprintf(&output[sz], max_sz-1-sz, "
TID Connection Count Sub Count Approx. Hashrate Command
%d %d %d %.2f Th/s
%d %d %d %.2f Th/s
"); - sz += snprintf(&output[sz], max_sz-1-sz, ""); + if (have_admin) { + sz += snprintf(&output[sz], max_sz-1-sz, "", datum_config.api_csrf_token); + } sz += snprintf(&output[sz], max_sz-1-sz, "%s", www_foot_html); response = MHD_create_response_from_buffer (sz, (void *) output, MHD_RESPMEM_MUST_FREE); @@ -650,10 +719,12 @@ int datum_api_client_dashboard(struct MHD_Connection *connection) { return MHD_NO; } + const bool have_admin = datum_config.api_admin_password_len; + tsms = current_time_millis(); sz = snprintf(output, max_sz-1-sz, "%s", www_clients_top_html); - sz += snprintf(&output[sz], max_sz-1-sz, "
"); + sz += snprintf(&output[sz], max_sz-1-sz, "
TID/CID RemHost Auth Username Subbed Last Accepted VDiff DiffA (A) DiffR (R) Hashrate (age) Coinbase UserAgent Command
", datum_config.api_csrf_token); for(j=0;jmax_threads;j++) { for(ii=0;iimax_clients_thread;ii++) { @@ -712,13 +783,21 @@ int datum_api_client_dashboard(struct MHD_Connection *connection) { sz += snprintf(&output[sz], max_sz-1-sz, ""); } - sz += snprintf(&output[sz], max_sz-1-sz, "", j, ii, j, ii); + sz += snprintf(&output[sz], max_sz-1-sz, ""); } } } sz += snprintf(&output[sz], max_sz-1-sz, "
TID/CID RemHost Auth Username Subbed Last Accepted VDiff DiffA (A) DiffR (R) Hashrate (age) Coinbase UserAgent Command
Not Subscribed

Total active hashrate estimate: %.2f Th/s

", thr); - sz += snprintf(&output[sz], max_sz-1-sz, ""); + if (have_admin) { + sz += snprintf(&output[sz], max_sz-1-sz, "", datum_config.api_csrf_token); + } sz += snprintf(&output[sz], max_sz-1-sz, "%s", www_foot_html); // return the home page with some data and such @@ -810,6 +889,11 @@ int datum_api_OK(struct MHD_Connection *connection) { int datum_api_testnet_fastforward(struct MHD_Connection * const connection) { const char *time_str; + time_str = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, "password"); + if (!datum_api_check_admin_password_only(connection, time_str)) { + return MHD_YES; + } + // Get the time parameter from the URL query time_str = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, "ts"); diff --git a/src/datum_conf.c b/src/datum_conf.c index 2656f41..6ac52e5 100644 --- a/src/datum_conf.c +++ b/src/datum_conf.c @@ -104,6 +104,8 @@ const T_DATUM_CONFIG_ITEM datum_config_options[] = { .required = false, .ptr = datum_config.mining_save_submitblocks_dir, .default_string[0] = "", .max_string_len = 256 }, // API/dashboard + { .var_type = DATUM_CONF_STRING, .category = "api", .name = "admin_password", .description = "API password for actions/changes (username 'admin'; disabled if blank)", + .required = false, .ptr = datum_config.api_admin_password, .default_string[0] = "", .max_string_len = sizeof(datum_config.api_admin_password) }, { .var_type = DATUM_CONF_INT, .category = "api", .name = "listen_port", .description = "Port to listen for API/dashboard requests (0=disabled)", .required = false, .ptr = &datum_config.api_listen_port, .default_int = 0 }, @@ -311,6 +313,20 @@ int datum_read_config(const char *conffile) { datum_config.bitcoind_work_update_seconds = 120; } + datum_config.api_admin_password_len = strlen(datum_config.api_admin_password); + if (datum_config.api_admin_password_len) { + static const char hash_tag[] = "DATUM Anti-CSRF Token"; + const size_t data_max_sz = sizeof(hash_tag) + sizeof(datum_config.api_listen_port) + sizeof(datum_config.api_admin_password); + const size_t data_sz = sizeof(hash_tag) + sizeof(datum_config.api_listen_port) + datum_config.api_admin_password_len; + char data[data_max_sz]; + strcpy(data, hash_tag); + memcpy(&data[sizeof(hash_tag)], &datum_config.api_listen_port, sizeof(datum_config.api_listen_port)); + strcpy(&data[sizeof(hash_tag)+sizeof(datum_config.api_listen_port)], datum_config.api_admin_password); + unsigned char hash[32]; + my_sha256(hash, data, data_sz); + hash2hex(hash, datum_config.api_csrf_token); + } + if (datum_config.stratum_v1_max_threads > MAX_THREADS) { DLOG_FATAL("Maximum threads must be less than %d.", MAX_THREADS); return 0; diff --git a/src/datum_conf.h b/src/datum_conf.h index c4ba3c2..97ee412 100644 --- a/src/datum_conf.h +++ b/src/datum_conf.h @@ -94,6 +94,9 @@ typedef struct { char mining_save_submitblocks_dir[256]; int coinbase_unique_id; + char api_admin_password[64]; + size_t api_admin_password_len; + char api_csrf_token[65]; int api_listen_port; int extra_block_submissions_count;