diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ce6d8bc..632bfa7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,9 @@ set(WEB_RESOURCES www/home.html www/clients_top.html www/coinbaser_top.html + www/config.html + www/config_errors.html + www/config_restart.html www/threads_top.html www/foot.html www/assets/style.css diff --git a/src/datum_api.c b/src/datum_api.c index 66a92bc0..3773a069 100644 --- a/src/datum_api.c +++ b/src/datum_api.c @@ -37,6 +37,7 @@ #include #include +#include #include #include #include @@ -49,6 +50,8 @@ #include "datum_api.h" #include "datum_blocktemplates.h" #include "datum_conf.h" +#include "datum_gateway.h" +#include "datum_jsonrpc.h" #include "datum_utils.h" #include "datum_stratum.h" #include "datum_sockets.h" @@ -230,28 +233,40 @@ DATUM_API_VarEntry var_entries[] = { {NULL, NULL} // Mark the end of the array }; -DATUM_API_VarFunc datum_api_find_var_func(const char *var_name) { +DATUM_API_VarFunc datum_api_find_var_func(const char * const var_start, const size_t var_name_len) { for (int i = 0; var_entries[i].var_name != NULL; i++) { - if (strcmp(var_entries[i].var_name, var_name) == 0) { + if (strncmp(var_entries[i].var_name, var_start, var_name_len) == 0 && !var_entries[i].var_name[var_name_len]) { return var_entries[i].func; } } return NULL; // Variable not found } -void datum_api_fill_vars(const char *input, char *output, size_t max_output_size, const T_DATUM_API_DASH_VARS *vardata) { +size_t datum_api_fill_var(const char * const var_start, const size_t var_name_len, char * const replacement, const size_t replacement_max_len, const T_DATUM_API_DASH_VARS * const vardata) { + DATUM_API_VarFunc func = datum_api_find_var_func(var_start, var_name_len); + if (!func) { + DLOG_ERROR("%s: Unknown variable '%.*s'", __func__, (int)var_name_len, var_start); + return 0; + } + + // Skip running STRATUM_JOB functions if there's no sjob + if (var_start[8] == 'J' && !vardata->sjob) { + // Leave blank for now + return 0; + } + + assert(replacement_max_len > 0); + replacement[0] = 0; + func(replacement, replacement_max_len, vardata); + return strlen(replacement); +} + +size_t datum_api_fill_vars(const char *input, char *output, size_t max_output_size, const DATUM_API_VarFillFunc var_fill_func, const T_DATUM_API_DASH_VARS *vardata) { const char* p = input; size_t output_len = 0; size_t var_name_len = 0; - char var_name[256]; - char replacement[256]; - size_t replacement_len; - size_t remaining; - size_t to_copy; const char *var_start; const char *var_end; - size_t total_var_len; - char temp_var[260]; while (*p && output_len < max_output_size - 1) { if (strncmp(p, "${", 2) == 0) { @@ -259,47 +274,17 @@ void datum_api_fill_vars(const char *input, char *output, size_t max_output_size var_start = p; var_end = strchr(p, '}'); if (!var_end) { - // No closing '}', copy rest of the input to output - remaining = strlen(p); - to_copy = (remaining < max_output_size - output_len - 1) ? remaining : max_output_size - output_len - 1; - strncpy(&output[output_len], p, to_copy); - output_len += to_copy; + DLOG_ERROR("%s: Missing closing } for variable", __func__); break; } var_name_len = var_end - var_start; - if (var_name_len >= sizeof(var_name)-1) { - output[output_len] = 0; - return; - } - strncpy(var_name, var_start, var_name_len); - var_name[var_name_len] = 0; - - DATUM_API_VarFunc func = datum_api_find_var_func(var_name); - if (func) { - // Skip running STRATUM_JOB functions if there's no sjob - if (var_name[8] == 'J' && !vardata->sjob) { - // Leave blank for now - } else { - replacement[0] = 0; - func(replacement, sizeof(replacement), vardata); - replacement_len = strlen(replacement); - if (replacement_len) { - to_copy = (replacement_len < max_output_size - output_len - 1) ? replacement_len : max_output_size - output_len - 1; - strncpy(&output[output_len], replacement, to_copy); - output_len += to_copy; - } - } - output[output_len] = 0; - } else { - // Not sure what this is... so just leave it - total_var_len = var_name_len + 3; - snprintf(temp_var, sizeof(temp_var), "${%s}", var_name); - to_copy = (total_var_len < max_output_size - output_len - 1) ? total_var_len : max_output_size - output_len - 1; - strncpy(&output[output_len], temp_var, to_copy); - output_len += to_copy; - output[output_len] = 0; - } + char * const replacement = &output[output_len]; + size_t replacement_max_len = max_output_size - output_len; + if (replacement_max_len > 256) replacement_max_len = 256; + const size_t replacement_len = var_fill_func(var_start, var_name_len, replacement, replacement_max_len, vardata); + output_len += replacement_len; + output[output_len] = 0; p = var_end + 1; // Move past '}' } else { output[output_len++] = *p++; @@ -308,6 +293,8 @@ void datum_api_fill_vars(const char *input, char *output, size_t max_output_size } output[output_len] = 0; + + return output_len; } size_t strncpy_html_escape(char *dest, const char *src, size_t n) { @@ -869,6 +856,564 @@ int datum_api_client_dashboard(struct MHD_Connection *connection) { return ret; } +size_t datum_api_fill_config_var(const char *var_start, const size_t var_name_len, char * const replacement, const size_t replacement_max_len, const T_DATUM_API_DASH_VARS * const vardata) { + const char *colon_pos = memchr(var_start, ':', var_name_len); + const char *var_start_2 = colon_pos ? &colon_pos[1] : var_start; + const char * const var_end = &var_start[var_name_len]; + const size_t var_name_len_2 = var_end - var_start_2; + const char * const underscore_pos = memchr(var_start_2, '_', var_name_len_2); + int val; + if (var_name_len_2 == 3 && 0 == strncmp(var_start_2, "*ro", 3)) { + val = !datum_config.api_modify_conf; + if (!colon_pos) { + var_start = "readonly:"; + colon_pos = &var_start[8]; + } + } else if (var_name_len_2 == 24 && 0 == strncmp(var_start_2, "*datum_pool_pass_workers", 24)) { + val = datum_config.datum_pool_pass_workers && !datum_config.datum_pool_pass_full_users; + } else if (var_name_len_2 == 16 && 0 == strncmp(var_start_2, "*datum_pool_host", 16)) { + const char *s = NULL; + if (datum_config.datum_pool_host[0]) { + s = datum_config.datum_pool_host; + } else if (datum_config.config_json) { + const json_t * const config = datum_config.config_json; + json_t *j = json_object_get(config, "datum"); + if (j) j = json_is_object(j) ? json_object_get(j, "pool_host(old)") : NULL; + if (j && json_is_string(j) && json_string_length(j) <= 1023) { + s = json_string_value(j); + } + } + if (!s) { + const T_DATUM_CONFIG_ITEM * const cfginfo = datum_config_get_option_info("datum", 5, "pool_host", 9); + s = cfginfo->default_string[0]; + } + size_t copy_sz = strlen(s); + if (copy_sz >= replacement_max_len) copy_sz = replacement_max_len - 1; + memcpy(replacement, s, copy_sz); + return copy_sz; + } else if (var_name_len_2 == 27 && 0 == strncmp(var_start_2, "*username_behaviour_private", 27)) { + val = !(datum_config.datum_pool_pass_workers || datum_config.datum_pool_pass_full_users); + } else if (var_name_len_2 == 22 && 0 == strncmp(var_start_2, "*reward_sharing_prefer", 22)) { + val = (!datum_config.datum_pooled_mining_only) && datum_config.datum_pool_host[0]; + } else if (var_name_len_2 == 21 && 0 == strncmp(var_start_2, "*reward_sharing_never", 21)) { + val = (!datum_config.datum_pooled_mining_only) && !datum_config.datum_pool_host[0]; + } else if (var_name_len_2 == 34 && 0 == strncmp(var_start_2, "*mining_coinbase_tag_secondary_max", 34)) { + val = 88 - strlen(datum_config.mining_coinbase_tag_primary); + if (val > 60) val = 60; + } else if (var_name_len_2 == 11 && 0 == strncmp(var_start_2, "*CSRF_TOKEN", 11)) { + size_t copy_sz = strlen(datum_config.api_csrf_token); + if (copy_sz >= replacement_max_len) copy_sz = replacement_max_len - 1; + memcpy(replacement, datum_config.api_csrf_token, copy_sz); + return copy_sz; + } else if (underscore_pos) { + const T_DATUM_CONFIG_ITEM * const item = datum_config_get_option_info(var_start_2, underscore_pos - var_start_2, &underscore_pos[1], var_end - &underscore_pos[1]); + if (item) { + switch (item->var_type) { + case DATUM_CONF_INT: { + val = *((int *)item->ptr); + break; + } + case DATUM_CONF_BOOL: { + val = *((bool *)item->ptr); + if ((!colon_pos) && replacement_max_len > 5) { + const size_t len = val ? 4 : 5; + memcpy(replacement, val ? "true" : "false", len); + return len; + } + break; + } + case DATUM_CONF_STRING: { + const char * const s = (char *)item->ptr; + if (colon_pos) { + DLOG_ERROR("%s: '%.*s' modifier not implemented for %s", __func__, (int)(colon_pos - var_start), var_start, "DATUM_CONF_STRING"); + break; + } + size_t copy_sz = strlen(s); + if (copy_sz >= replacement_max_len) copy_sz = replacement_max_len - 1; + memcpy(replacement, s, copy_sz); + return copy_sz; + } + case DATUM_CONF_STRING_ARRAY: { + DLOG_ERROR("%s: %s not implemented", __func__, "DATUM_CONF_STRING_ARRAY"); + break; + } + } + } else { + DLOG_ERROR("%s: '%.*s' not implemented", __func__, (int)(var_end - var_start_2), var_start_2); + return 0; + } + } else { + DLOG_ERROR("%s: '%.*s' not implemented", __func__, (int)(var_end - var_start_2), var_start_2); + return 0; + } + + assert(replacement_max_len > 0); + + if (colon_pos) { + if (0 == strncmp(var_start, "readonly:", 9) || 0 == strncmp(var_start, "selected:", 9) || 0 == strncmp(var_start, "checked:", 8) || 0 == strncmp(var_start, "disabled:", 9)) { + size_t attr_len; + if (val) { + attr_len = colon_pos - var_start; + if (attr_len + 2 > replacement_max_len) attr_len = replacement_max_len - 2; + replacement[0] = ' '; + memcpy(&replacement[1], var_start, attr_len); + ++attr_len; + } else { + attr_len = 0; + } + return attr_len; + } else if (0 == strncmp(var_start, "msg:", 4)) { + if (val) { + static const char * const msg = "
Config file disallows editing"; + const size_t len = strlen(msg); + memcpy(replacement, msg, len); + return len; + } else { + return 0; + } + } else { + DLOG_ERROR("%s: '%.*s' modifier not implemented", __func__, (int)(colon_pos - var_start), var_start); + return 0; + } + } + + return snprintf(replacement, replacement_max_len, "%d", val); +} + +int datum_api_config_dashboard(struct MHD_Connection *connection) { + struct MHD_Response *response; + size_t sz = 0, max_sz = 0; + int ret; + char *output = NULL; + + max_sz = www_config_html_sz * 2; + output = malloc(max_sz); + if (!output) { + return MHD_NO; + } + + sz += datum_api_fill_vars(www_config_html, output, max_sz, datum_api_fill_config_var, NULL); + + response = MHD_create_response_from_buffer(sz, output, MHD_RESPMEM_MUST_FREE); + MHD_add_response_header(response, "Content-Type", "text/html"); + http_resp_prevent_caching(response); + ret = MHD_queue_response(connection, MHD_HTTP_OK, response); + MHD_destroy_response(response); + return ret; +} + +#ifndef JSON_PRESERVE_ORDER +#define JSON_PRESERVE_ORDER 0 +#endif + +// Only modifies config_json; writing is done later +void datum_api_json_modify_new(const char * const category, const char * const key, json_t * const val) { + json_t * const config = datum_config.config_json; + assert(config); + + json_t *j = json_object_get(config, category); + if (!j) { + j = json_object(); + json_object_set_new(config, category, j); + } + json_object_set_new(j, key, val); +} + +bool datum_api_json_write() { + json_t * const config = datum_config.config_json; + assert(config); + assert(datum_gateway_config_filename); + + char buf[0x100]; + int rv = snprintf(buf, sizeof(buf) - 4, "%s", datum_gateway_config_filename); + assert(rv > 0); + strcpy(&buf[rv], ".new"); + + if (json_dump_file(config, buf, JSON_PRESERVE_ORDER | JSON_INDENT(4))) { + DLOG_ERROR("Failed to write new config to %s", buf); + return false; + } + if (rename(buf, datum_gateway_config_filename)) { + DLOG_ERROR("Failed to rename new config %s to %s", buf, datum_gateway_config_filename); + return false; + } + DLOG_INFO("Wrote new config to %s", datum_gateway_config_filename); + return true; +} + +struct datum_api_config_set_status { + json_t *errors; + bool modified_config; + bool need_restart; +}; + +// This does several steps: +// - If the value is unchanged, return true without doing anything +// - Validate the value without changing anything +// - Change the runtime dataum_config (and ensure it goes live) +// - Modify the config file +// If anything fails (including validation), errors is appended and false is returned +bool datum_api_config_set(const char * const key, const char * const val, struct datum_api_config_set_status * const status) { + json_t * const errors = status->errors; + if (0 == strcmp(key, "mining_pool_address")) { + if (0 == strcmp(val, datum_config.mining_pool_address)) return true; + unsigned char dummy[64]; + if (!addr_2_output_script(val, &dummy[0], 64)) { + json_array_append_new(errors, json_string_nocheck("Invalid Bitcoin Address")); + return false; + } + strcpy(datum_config.mining_pool_address, val); + datum_api_json_modify_new("mining", "pool_address", json_string(val)); + } else if (0 == strcmp(key, "username_behaviour")) { + if (0 == strcmp(val, "datum_pool_pass_full_users")) { + if (datum_config.datum_pool_pass_full_users) return true; + datum_config.datum_pool_pass_full_users = true; + // datum_pool_pass_workers doesn't matter with datum_pool_pass_full_users enabled + } else if (0 == strcmp(val, "datum_pool_pass_workers")) { + if (datum_config.datum_pool_pass_workers && !datum_config.datum_pool_pass_full_users) return true; + datum_config.datum_pool_pass_full_users = false; + datum_config.datum_pool_pass_workers = true; + } else if (0 == strcmp(val, "private")) { + if (!(datum_config.datum_pool_pass_workers || datum_config.datum_pool_pass_full_users)) return true; + datum_config.datum_pool_pass_full_users = false; + datum_config.datum_pool_pass_workers = false; + } else { + json_array_append_new(errors, json_string_nocheck("Invalid option for \"Send Miner Usernames To Pool\"")); + return false; + } + datum_api_json_modify_new("datum", "pool_pass_full_users", json_boolean(datum_config.datum_pool_pass_full_users)); + if (!datum_config.datum_pool_pass_full_users) { + datum_api_json_modify_new("datum", "pool_pass_workers", json_boolean(datum_config.datum_pool_pass_workers)); + } + } else if (0 == strcmp(key, "mining_coinbase_tag_secondary")) { + if (0 == strcmp(val, datum_config.mining_coinbase_tag_secondary)) return true; + size_t len_limit = 88 - strlen(datum_config.mining_coinbase_tag_primary); + if (len_limit > 60) len_limit = 60; + if (strlen(val) > len_limit) { + json_array_append_new(errors, json_string_nocheck("Coinbase Tag is too long")); + return false; + } + strcpy(datum_config.mining_coinbase_tag_secondary, val); + datum_api_json_modify_new("mining", "coinbase_tag_secondary", json_string(val)); + } else if (0 == strcmp(key, "mining_coinbase_unique_id")) { + const int val_int = datum_atoi_strict(val, strlen(val)); + if (val_int == datum_config.coinbase_unique_id) return true; + if (val_int > 65535 || val_int < 0) { + json_array_append_new(errors, json_string_nocheck("Unique Gateway ID must be between 0 and 65535")); + return false; + } + datum_config.coinbase_unique_id = val_int; + datum_api_json_modify_new("mining", "coinbase_unique_id", json_integer(val_int)); + } else if (0 == strcmp(key, "reward_sharing")) { + json_t * const config = datum_config.config_json; + assert(config); + + bool want_datum_pool_host = false; + if (0 == strcmp(val, "require")) { + if (datum_config.datum_pool_host[0] && datum_config.datum_pooled_mining_only) return true; + datum_config.datum_pooled_mining_only = true; + want_datum_pool_host = true; + } else if (0 == strcmp(val, "prefer")) { + if (datum_config.datum_pool_host[0] && !datum_config.datum_pooled_mining_only) return true; + datum_config.datum_pooled_mining_only = false; + want_datum_pool_host = true; + } else if (0 == strcmp(val, "never")) { + if (!(datum_config.datum_pool_host[0] || datum_config.datum_pooled_mining_only)) return true; + datum_config.datum_pooled_mining_only = false; + datum_config.datum_pool_host[0] = '\0'; + + // Only copy the old value if it's in the config file + json_t *j = json_object_get(config, "datum"); + if (j) j = json_is_object(j) ? json_object_get(j, "pool_host") : NULL; + if (j) { + datum_api_json_modify_new("datum", "pool_host(old)", json_incref(j)); + } + + datum_api_json_modify_new("datum", "pool_host", json_string_nocheck("")); + } else { + json_array_append_new(errors, json_string_nocheck("Invalid option for \"Collaborative reward sharing\"")); + return false; + } + if (want_datum_pool_host && !datum_config.datum_pool_host[0]) { + json_t *j = json_object_get(config, "datum"); + if (j) j = json_is_object(j) ? json_object_get(j, "pool_host(old)") : NULL; + if (j && json_is_string(j) && json_string_length(j) <= 1023) { + strcpy(datum_config.datum_pool_host, json_string_value(j)); + json_object_del(j, "pool_host(old)"); + datum_api_json_modify_new("datum", "pool_host", json_string(datum_config.datum_pool_host)); + } else { + const T_DATUM_CONFIG_ITEM * const cfginfo = datum_config_get_option_info("datum", 5, "pool_host", 9); + strcpy(datum_config.datum_pool_host, cfginfo->default_string[0]); + + // Avoiding using null here because older versions handled it poorly + json_t *j = json_object_get(config, "datum"); + if (j) json_object_del(j, "pool_host"); + } + } + datum_api_json_modify_new("datum", "pooled_mining_only", json_boolean(datum_config.datum_pooled_mining_only)); + // TODO: apply change without restarting + status->need_restart = true; + } else if (0 == strcmp(key, "datum_pool_host")) { + if (0 == strcmp(val, datum_config.datum_pool_host)) return true; + if (strlen(val) > 1023) { + json_array_append_new(errors, json_string_nocheck("Pool Host is too long")); + return false; + } + if (datum_config.datum_pool_host[0]) { + strcpy(datum_config.datum_pool_host, val); + datum_api_json_modify_new("datum", "pool_host", json_string(val)); + // TODO: apply change without restarting + // TODO: switch pools smoother (keep old connection alive for share submissions until those jobs expire) + status->need_restart = true; + } else { + json_t * const config = datum_config.config_json; + assert(config); + json_t *j = json_object_get(config, "datum"); + if (j) j = json_is_object(j) ? json_object_get(j, "pool_host(old)") : NULL; + const T_DATUM_CONFIG_ITEM * const cfginfo = datum_config_get_option_info("datum", 5, "pool_host", 9); + // Avoid setting the default host in the config file, unless something else was already there + if (0 != strcmp(val, cfginfo->default_string[0]) || json_is_string(j)) { + datum_api_json_modify_new("datum", "pool_host(old)", json_string(val)); + } + } + } else if (0 == strcmp(key, "datum_pool_port")) { + const int val_int = datum_atoi_strict(val, strlen(val)); + if (val_int == datum_config.datum_pool_port) return true; + if (val_int > 65535 || val_int < 1) { + json_array_append_new(errors, json_string_nocheck("Pool Port must be between 1 and 65535")); + return false; + } + datum_config.datum_pool_port = val_int; + datum_api_json_modify_new("datum", "pool_port", json_integer(val_int)); + // TODO: apply change without restarting + // TODO: switch pools smoother (keep old connection alive for share submissions until those jobs expire) + status->need_restart = true; + } else if (0 == strcmp(key, "datum_pool_pubkey")) { + if (0 == strcmp(val, datum_config.datum_pool_pubkey)) return true; + if (strlen(val) > 1023) { + json_array_append_new(errors, json_string_nocheck("Pool Pubkey is too long")); + return false; + } + strcpy(datum_config.datum_pool_pubkey, val); + datum_api_json_modify_new("datum", "pool_pubkey", json_string(val)); + // TODO: apply change without restarting + // TODO: switch pools smoother (keep old connection alive for share submissions until those jobs expire) + status->need_restart = true; + } else if (0 == strcmp(key, "stratum_fingerprint_miners")) { + bool val_bool; + if (!datum_str_to_bool_strict(val, &val_bool)) { + json_array_append_new(errors, json_string_nocheck("\"Fingerprint and workaround known miner bugs\" must be 0 or 1")); + return false; + } + if (val_bool == datum_config.stratum_v1_fingerprint_miners) return true; + datum_config.stratum_v1_fingerprint_miners = val_bool; + datum_api_json_modify_new("stratum", "fingerprint_miners", json_boolean(val_bool)); + // TODO: apply change to connected miners? + } else if (0 == strcmp(key, "datum_always_pay_self")) { + bool val_bool; + if (!datum_str_to_bool_strict(val, &val_bool)) { + json_array_append_new(errors, json_string_nocheck("\"Always pay self\" must be 0 or 1")); + return false; + } + if (val_bool == datum_config.datum_always_pay_self) return true; + datum_config.datum_always_pay_self = val_bool; + datum_api_json_modify_new("datum", "always_pay_self", json_boolean(val_bool)); + } else if (0 == strcmp(key, "bitcoind_work_update_seconds")) { + const int val_int = datum_atoi_strict(val, strlen(val)); + if (val_int == datum_config.bitcoind_work_update_seconds) return true; + if (val_int > 120 || val_int < 5) { + json_array_append_new(errors, json_string_nocheck("bitcoind work update interval must be between 5 and 120")); + return false; + } + datum_config.bitcoind_work_update_seconds = val_int; + datum_api_json_modify_new("bitcoind", "work_update_seconds", json_integer(val_int)); + if (datum_config.bitcoind_work_update_seconds >= datum_config.datum_protocol_global_timeout - 5) { + datum_config.datum_protocol_global_timeout = val_int + 5; + datum_api_json_modify_new("datum", "protocol_global_timeout", json_integer(val_int + 5)); + } + // TODO: apply change without restarting (and don't interfere with existing jobs) + status->need_restart = true; + } else if (0 == strcmp(key, "bitcoind_rpcurl")) { + if (0 == strcmp(val, datum_config.bitcoind_rpcurl)) return true; + if (strlen(val) > 128) { + json_array_append_new(errors, json_string_nocheck("bitcoind RPC URL is too long")); + return false; + } + strcpy(datum_config.bitcoind_rpcurl, val); + datum_api_json_modify_new("bitcoind", "rpcurl", json_string(val)); + } else if (0 == strcmp(key, "bitcoind_rpcuser")) { + if (0 == strcmp(val, datum_config.bitcoind_rpcuser)) return true; + if (strlen(val) > 128) { + json_array_append_new(errors, json_string_nocheck("bitcoind RPC user is too long")); + return false; + } + strcpy(datum_config.bitcoind_rpcuser, val); + datum_api_json_modify_new("bitcoind", "rpcuser", json_string(val)); + update_rpc_auth(&datum_config); + } else if (0 == strcmp(key, "bitcoind_rpcpassword")) { + if (0 == strcmp(val, datum_config.bitcoind_rpcpassword)) return true; + if (!val[0]) return true; // no password change + if (strlen(val) > 128) { + json_array_append_new(errors, json_string_nocheck("bitcoind RPC password is too long")); + return false; + } + strcpy(datum_config.bitcoind_rpcpassword, val); + datum_api_json_modify_new("bitcoind", "rpcpassword", json_string(val)); + update_rpc_auth(&datum_config); + } else { + char err[0x100]; + snprintf(err, sizeof(err), "Unknown setting '%s'", key); + json_array_append_new(errors, json_string_nocheck(err)); + DLOG_ERROR("%s: '%s' not implemented", __func__, key); + return false; + } + status->modified_config = true; + return true; +} + +static const char datum_api_config_errors_fmt[] = "
%s
"; + +size_t datum_api_fill_config_errors(const char *var_start, const size_t var_name_len, char * const replacement, const size_t replacement_max_len, const T_DATUM_API_DASH_VARS * const vardata) { + const json_t * const errors = (void*)vardata; + size_t index, sz = 0; + json_t *j_it; + + json_array_foreach(errors, index, j_it) { + sz += snprintf(&replacement[sz], replacement_max_len, datum_api_config_errors_fmt, json_string_value(j_it)); + } + + return sz; +} + +void *datum_restart_thread(void *ptr) { + // Give logger some time + usleep(500000); + + // Wait for the response to actually get delivered + // FIXME: css/svg/etc might fail (we don't support caching them yet) + struct MHD_Daemon * const mhd = ptr; + MHD_quiesce_daemon(mhd); + while (MHD_get_daemon_info(mhd, MHD_DAEMON_INFO_CURRENT_CONNECTIONS)->num_connections > 0) { + usleep(100); + } + MHD_stop_daemon(mhd); + + datum_reexec(); + abort(); // impossible to get here +} + +int datum_api_config_post(struct MHD_Connection * const connection, char * const post, const int len) { + struct MHD_Response *response; + int ret; + const char *key; + json_t *j_it; + + if (!datum_config.api_modify_conf) { + return datum_api_do_error(connection, MHD_HTTP_FORBIDDEN); + } + + json_t * const j = json_object(); + if (!datum_api_formdata_to_json(connection, post, len, j)) { + json_decref(j); + return datum_api_do_error(connection, MHD_HTTP_INTERNAL_SERVER_ERROR); + } + + if (!datum_api_check_admin_password(connection, j)) { + json_decref(j); + return MHD_YES; + } + json_object_del(j, "csrf"); + json_object_del(j, "password"); + + { + // Unchecked checkboxes are simply omitted, so a hidden field is used to convey them + const json_t * const j_checkboxes = json_object_get(j, "checkboxes"); + const char * const checkboxes = json_string_value(j_checkboxes); + const size_t checkboxes_len = json_string_length(j_checkboxes); + const char *p = checkboxes; + char buf[0x100]; + while (p[0] != '\0') { + const char *p2 = strchr(p, ' '); + if (!p2) p2 = &checkboxes[checkboxes_len]; + const size_t i_len = p2 - p; + if (i_len < sizeof(buf)) { + memcpy(buf, p, i_len); + buf[i_len] = '\0'; + + json_t * const j_cb = json_object_get(j, buf); + if ((!j_cb) || json_is_null(j_cb)) { + json_object_set_new_nocheck(j, buf, json_string_nocheck("0")); + } + } + p = *p2 ? &p2[1] : p2; + } + json_object_del(j, "checkboxes"); + } + + json_t * const errors = json_array(); + struct datum_api_config_set_status status = { + .errors = errors, + }; + json_object_foreach(j, key, j_it) { + datum_api_config_set(key, json_string_value(j_it), &status); + } + + json_decref(j); + + if (status.modified_config) { + if (!datum_api_json_write()) { + if (status.need_restart) { + json_array_append_new(errors, json_string_nocheck("Error writing new config file (changes will be lost)")); + } else { + json_array_append_new(errors, json_string_nocheck("Error writing new config file (changes will be lost at restart)")); + } + } + } + + if (json_array_size(errors) > 0) { + if (status.need_restart) { + json_array_insert_new(errors, 0, json_string_nocheck("NOTE: Other changes require a gateway restart. Please wait a few seconds before trying again.")); + } + + size_t index, max_sz; + max_sz = www_config_errors_html_sz; + json_array_foreach(errors, index, j_it) { + max_sz += json_string_length(j_it) + sizeof(datum_api_config_errors_fmt); + } + + char * const output = malloc(max_sz); + if (!output) { + return MHD_NO; + } + const size_t sz = datum_api_fill_vars(www_config_errors_html, output, max_sz, datum_api_fill_config_errors, (void*)errors); + + response = MHD_create_response_from_buffer(sz, output, MHD_RESPMEM_MUST_FREE); + MHD_add_response_header(response, "Content-Type", "text/html"); + http_resp_prevent_caching(response); + } else if (status.need_restart) { + response = MHD_create_response_from_buffer(www_config_restart_html_sz, (void*)www_config_restart_html, MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(response, "Content-Type", "text/html"); + http_resp_prevent_caching(response); + } else { + response = MHD_create_response_from_buffer(0, "", MHD_RESPMEM_PERSISTENT); + http_resp_prevent_caching(response); + MHD_add_response_header(response, "Location", "/config"); + } + json_decref(errors); + + ret = MHD_queue_response(connection, MHD_HTTP_FOUND, response); + MHD_destroy_response(response); + + if (status.need_restart) { + DLOG_INFO("Config change requires restarting gateway, proceeding"); + struct MHD_Daemon * const mhd = MHD_get_connection_info(connection, MHD_CONNECTION_INFO_DAEMON)->daemon; + pthread_t pthread_datum_restart_thread; + pthread_create(&pthread_datum_restart_thread, NULL, datum_restart_thread, mhd); + } + + return ret; +} + int datum_api_homepage(struct MHD_Connection *connection) { struct MHD_Response *response; char output[DATUM_API_HOMEPAGE_MAX_SIZE]; @@ -923,7 +1468,7 @@ int datum_api_homepage(struct MHD_Connection *connection) { } output[0] = 0; - datum_api_fill_vars(www_home_html, output, DATUM_API_HOMEPAGE_MAX_SIZE, &vardata); + datum_api_fill_vars(www_home_html, output, DATUM_API_HOMEPAGE_MAX_SIZE, datum_api_fill_var, &vardata); // return the home page with some data and such response = MHD_create_response_from_buffer (strlen(output), (void *) output, MHD_RESPMEM_MUST_COPY); @@ -1104,6 +1649,13 @@ enum MHD_Result datum_api_answer(void *cls, struct MHD_Connection *connection, c if (!strcmp(url, "/coinbaser")) { return datum_api_coinbaser(connection); } + if (!strcmp(url, "/config")) { + if (int_method == 2 && con_info) { + return datum_api_config_post(connection, con_info->data, con_info->data_size); + } else { + return datum_api_config_dashboard(connection); + } + } if ((int_method==2) && (!strcmp(url, "/cmd"))) { if (con_info) { return datum_api_cmd(connection, con_info->data, con_info->data_size); diff --git a/src/datum_api.h b/src/datum_api.h index 32afa4fa..70486a4f 100644 --- a/src/datum_api.h +++ b/src/datum_api.h @@ -48,6 +48,7 @@ typedef struct { } T_DATUM_API_DASH_VARS; typedef void (*DATUM_API_VarFunc)(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata); +typedef size_t (*DATUM_API_VarFillFunc)(const char *var_start, size_t var_name_len, char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata); typedef struct { const char *var_name; diff --git a/src/datum_blocktemplates.c b/src/datum_blocktemplates.c index 9cf1ea8e..f135215f 100644 --- a/src/datum_blocktemplates.c +++ b/src/datum_blocktemplates.c @@ -401,6 +401,21 @@ void *datum_gateway_template_thread(void *args) { panic_from_thread(__LINE__); } + { + unsigned char dummy[64]; + if (!addr_2_output_script(datum_config.mining_pool_address, &dummy[0], 64)) { + if (datum_config.api_modify_conf) { + DLOG_ERROR("Could not generate output script for pool addr! Perhaps invalid? Configure via API/dashboard."); + } else { + DLOG_FATAL("Could not generate output script for pool addr! Perhaps invalid? This is bad."); + panic_from_thread(__LINE__); + } + } + while (!addr_2_output_script(datum_config.mining_pool_address, &dummy[0], 64)) { + usleep(50000); + } + } + if (datum_config.bitcoind_notify_fallback) { // start getbestblockhash poller thread as a backup for notifications DLOG_DEBUG("Starting fallback block notifier"); diff --git a/src/datum_conf.c b/src/datum_conf.c index 4fc9afe0..38456431 100644 --- a/src/datum_conf.c +++ b/src/datum_conf.c @@ -111,6 +111,8 @@ const T_DATUM_CONFIG_ITEM datum_config_options[] = { .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 }, + { .var_type = DATUM_CONF_BOOL, .category = "api", .name = "modify_conf", .description = "Enable modifying the config file from API/dashboard", + .required = false, .ptr = &datum_config.api_modify_conf, .default_int = 0 }, // extra block submissions list { .var_type = DATUM_CONF_STRING_ARRAY, .category = "extra_block_submissions", .name = "urls", .description = "Array of bitcoind RPC URLs to submit our blocks to directly. Include auth info: http://user:pass@IP", @@ -322,7 +324,9 @@ int datum_read_config(const char *conffile) { } } - if (config) { + if (datum_config.api_modify_conf) { + datum_config.config_json = config; + } else { json_decref(config); } diff --git a/src/datum_conf.h b/src/datum_conf.h index 8e8d662f..44ca1ee0 100644 --- a/src/datum_conf.h +++ b/src/datum_conf.h @@ -43,6 +43,8 @@ #include #include +#include + #define DATUM_CONF_BOOL 1 #define DATUM_CONF_INT 2 #define DATUM_CONF_STRING 3 @@ -55,10 +57,14 @@ typedef struct { char name[64]; char description[512]; int var_type; - int max_string_len; - int default_int; - bool default_bool; - const char *default_string[DATUM_CONFIG_MAX_ARRAY_ENTRIES]; + union { + int default_int; + bool default_bool; + struct { + int max_string_len; + const char *default_string[DATUM_CONFIG_MAX_ARRAY_ENTRIES]; + }; + }; void *ptr; @@ -103,6 +109,8 @@ typedef struct { size_t api_admin_password_len; char api_csrf_token[65]; int api_listen_port; + bool api_modify_conf; + json_t *config_json; int extra_block_submissions_count; char extra_block_submissions_urls[DATUM_MAX_BLOCK_SUBMITS][DATUM_MAX_SUBMIT_URL_LEN]; diff --git a/src/datum_gateway.c b/src/datum_gateway.c index 562faf9e..a35d02e4 100644 --- a/src/datum_gateway.c +++ b/src/datum_gateway.c @@ -61,6 +61,8 @@ #include "datum_coinbaser.h" #include "datum_protocol.h" +const char *datum_gateway_config_filename = NULL; + // ARGP stuff const char *argp_program_version = "datum_gateway " DATUM_PROTOCOL_VERSION; const char *argp_program_bug_address = ""; @@ -102,7 +104,11 @@ void handle_sigusr1(int sig) { datum_blocktemplates_notifynew(NULL, 0); } -int main(int argc, char **argv) { +const char * const *datum_argv; + +int main(const int argc, const char * const * const argv) { + datum_argv = argv; + struct arguments arguments; pthread_t pthread_datum_stratum_v1; pthread_t pthread_datum_gateway_template; @@ -141,7 +147,7 @@ int main(int argc, char **argv) { datum_utils_init(); arguments.config_file = "datum_gateway_config.json"; // Default config file - if (argp_parse(&argp, argc, argv, 0, 0, &arguments) != 0) { + if (argp_parse(&argp, argc, datum_deepcopy_charpp(argv), 0, 0, &arguments) != 0) { DLOG_FATAL("Error parsing arguments. Check --help"); exit(1); } @@ -150,6 +156,7 @@ int main(int argc, char **argv) { DLOG_FATAL("Error reading config file. Check --help"); exit(1); } + datum_gateway_config_filename = arguments.config_file; // Initialize logger thread datum_logger_init(); diff --git a/src/datum_gateway.h b/src/datum_gateway.h index 026ccac5..64e0a145 100644 --- a/src/datum_gateway.h +++ b/src/datum_gateway.h @@ -55,4 +55,8 @@ #define STRATUM_JOB_INDEX_XOR ((uint16_t)0xC0DE) +extern const char *datum_gateway_config_filename; + +extern const char * const *datum_argv; + #endif diff --git a/src/datum_jsonrpc.c b/src/datum_jsonrpc.c index 778db86a..be099f81 100644 --- a/src/datum_jsonrpc.c +++ b/src/datum_jsonrpc.c @@ -250,6 +250,14 @@ bool update_rpc_cookie(global_config_t * const cfg) { return true; } +void update_rpc_auth(global_config_t * const cfg) { + if (datum_config.bitcoind_rpccookiefile[0] && !cfg->bitcoind_rpcuser[0]) { + update_rpc_cookie(cfg); + } else { + snprintf(datum_config.bitcoind_rpcuserpass, sizeof(datum_config.bitcoind_rpcuserpass), "%s:%s", datum_config.bitcoind_rpcuser, datum_config.bitcoind_rpcpassword); + } +} + json_t *bitcoind_json_rpc_call(CURL * const curl, global_config_t * const cfg, const char * const rpc_req) { long http_resp_code = -1; json_t *j = json_rpc_call_full(curl, cfg->bitcoind_rpcurl, cfg->bitcoind_rpcuserpass, rpc_req, NULL, &http_resp_code); diff --git a/src/datum_jsonrpc.h b/src/datum_jsonrpc.h index 73079c90..ef47a463 100644 --- a/src/datum_jsonrpc.h +++ b/src/datum_jsonrpc.h @@ -66,6 +66,7 @@ struct upload_buffer { json_t *json_rpc_call(CURL *curl, const char *url, const char *userpass, const char *rpc_req); char *basic_http_call(CURL *curl, const char *url); bool update_rpc_cookie(global_config_t *cfg); +void update_rpc_auth(global_config_t *cfg); json_t *bitcoind_json_rpc_call(CURL *curl, global_config_t *cfg, const char *rpc_req); #endif diff --git a/src/datum_utils.c b/src/datum_utils.c index 5e9641d9..68b6313e 100644 --- a/src/datum_utils.c +++ b/src/datum_utils.c @@ -34,6 +34,9 @@ */ #include +#include +#include +#include #include #include #include @@ -48,7 +51,10 @@ #include #include #include +#include +#include "datum_gateway.h" +#include "datum_logger.h" #include "datum_utils.h" #include "thirdparty_base58.h" #include "thirdparty_segwit_addr.h" @@ -727,6 +733,60 @@ int datum_atoi_strict(const char * const s, const size_t size) { return (ret == UINT64_MAX || ret > INT_MAX) ? -1 : ret; } +// Currently accepts 0 and 1 only, but may add more later +// Returns true if valid, actual value in *out +bool datum_str_to_bool_strict(const char * const s, bool * const out) { + if (0 == strcmp(s, "0")) { + *out = false; + return true; + } else if (0 == strcmp(s, "1")) { + *out = true; + return true; + } + return false; +} + +char **datum_deepcopy_charpp(const char * const * const p) { + size_t sz = sizeof(char*), n = 0; + for (const char * const *p2 = p; *p2; ++p2) { + sz += sizeof(char*) + strlen(*p2) + 1; + ++n; + } + char **out = malloc(sz); + char *p3 = (void*)(&out[n + 1]); + out[n] = NULL; + for (size_t i = 0; i < n; ++i) { + const size_t sz = strlen(p[i]) + 1; + memcpy(p3, p[i], sz); + out[i] = p3; + p3 += sz; + } + assert(p3 - (char*)out == sz); + return out; +} + +void datum_reexec() { + // FIXME: kill other threads (except logging?) before closing fds + + DIR * const D = opendir("/proc/self/fd"); + if (D) { + for (struct dirent *ent; (ent = readdir(D)) != NULL; ) { + const int fd = datum_atoi_strict(ent->d_name, strlen(ent->d_name)); + if (fd < 3) continue; + fcntl(fd, F_SETFD, FD_CLOEXEC); + } + closedir(D); + } else { + DLOG_ERROR("%s: Failed to close files, this could cause issues! (Is /proc mounted?)", __func__); + } + + execv((void*)datum_argv[0], (void*)datum_argv); + // execv shouldn't return! + + DLOG_FATAL("Failed to restart! We're too deep in to recover!"); + abort(); +} + bool datum_secure_strequals(const char *secret, const size_t secret_len, const char *guess) { const size_t guess_len = strlen(guess); size_t acc = secret_len ^ guess_len; diff --git a/src/datum_utils.h b/src/datum_utils.h index 32a8f40a..9da5bc91 100644 --- a/src/datum_utils.h +++ b/src/datum_utils.h @@ -71,6 +71,9 @@ uint64_t datum_siphash(const void *src, uint64_t sz, const unsigned char key[16] uint64_t datum_siphash_mod8(const void *src, uint64_t sz, const unsigned char key[16]); uint64_t datum_atoi_strict_u64(const char *s, size_t size); int datum_atoi_strict(const char *s, size_t size); +bool datum_str_to_bool_strict(const char *s, bool *out); +char **datum_deepcopy_charpp(const char * const *p); +void datum_reexec(); bool datum_secure_strequals(const char *secret, const size_t secret_len, const char *guess); diff --git a/www/clients_top.html b/www/clients_top.html index 45acdeed..2c1c47b9 100644 --- a/www/clients_top.html +++ b/www/clients_top.html @@ -14,6 +14,7 @@

(DATUM Logo) Status + Config Clients Threads Coinbaser diff --git a/www/coinbaser_top.html b/www/coinbaser_top.html index 932021f1..bebe7553 100644 --- a/www/coinbaser_top.html +++ b/www/coinbaser_top.html @@ -31,6 +31,7 @@

(DATUM Logo) Status + Config Clients Threads Coinbaser diff --git a/www/config.html b/www/config.html new file mode 100644 index 00000000..ca405fb6 --- /dev/null +++ b/www/config.html @@ -0,0 +1,240 @@ + + + + + DATUM Gateway Configuration + + + + + +
+
+

(DATUM Logo) DATUM GATEWAY

+
+ +
+ +
+ +
+
+ Save + ${msg:*ro} +
+ +
+
+

Basic

+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+ +
+
+

Pool

+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+
+

Advanced

+ +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+ Save + ${msg:*ro} +
+
+
+ + + diff --git a/www/config_errors.html b/www/config_errors.html new file mode 100644 index 00000000..cf7b2e6e --- /dev/null +++ b/www/config_errors.html @@ -0,0 +1,72 @@ + + + + + DATUM Gateway Configuration - ERROR + + + + + +
+
+

(DATUM Logo) DATUM GATEWAY

+
+ +
+ +
+ + +
+
+

Errors Occurred

+ ${*errors} +
+
+ + +
+ + diff --git a/www/config_restart.html b/www/config_restart.html new file mode 100644 index 00000000..3a7d052c --- /dev/null +++ b/www/config_restart.html @@ -0,0 +1,74 @@ + + + + + DATUM Gateway Configuration - Restarting + + + + + +
+
+

(DATUM Logo) DATUM GATEWAY

+
+ +
+ +
+
+
+ +
+ +
+
+

Changes Successful

+
+ DATUM Gateway is restarting... Please wait a few seconds before continuing. +
+
+
+ +
+ +
+
+
+ + + diff --git a/www/home.html b/www/home.html index 8042698b..3ed7efb1 100644 --- a/www/home.html +++ b/www/home.html @@ -35,6 +35,7 @@

(DATUM Logo) Status + Config Clients Threads Coinbaser diff --git a/www/threads_top.html b/www/threads_top.html index 49aa07f7..0545e0e0 100644 --- a/www/threads_top.html +++ b/www/threads_top.html @@ -31,6 +31,7 @@

(DATUM Logo) Status + Config Clients Threads Coinbaser