#include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static void json_add_source(struct json_stream *result, const char *fieldname, const struct configvar *cv) { const char *source; if (!cv) { source = "default"; } else { source = NULL; switch (cv->src) { case CONFIGVAR_CMDLINE: case CONFIGVAR_CMDLINE_SHORT: source = "cmdline"; break; case CONFIGVAR_EXPLICIT_CONF: case CONFIGVAR_BASE_CONF: case CONFIGVAR_NETWORK_CONF: source = tal_fmt(tmpctx, "%s:%u", cv->file, cv->linenum); break; case CONFIGVAR_PLUGIN_START: source = "pluginstart"; break; } } json_add_string(result, fieldname, source); } static const char *configval_fieldname(const struct opt_table *ot) { bool multi = (ot->type & OPT_MULTI); if (ot->type & OPT_SHOWBOOL) return multi ? "values_bool" : "value_bool"; if (ot->type & OPT_SHOWINT) return multi ? "values_int" : "value_int"; if (ot->type & OPT_SHOWMSATS) return multi ? "values_msat" : "value_msat"; return multi ? "values_str" : "value_str"; } #define CONFIG_SHOW_BUFSIZE 4096 static const char *get_opt_val(const struct opt_table *ot, char buf[], const struct configvar *cv) { if (ot->show == (void *)opt_show_charp) { /* Don't truncate or quote! */ return *(char **)ot->u.carg; } if (ot->show) { /* Plugins options' show only shows defaults, so show val if * we have it */ if (is_plugin_opt(ot) && cv) return cv->optarg; strcpy(buf + CONFIG_SHOW_BUFSIZE, "..."); if (ot->show(buf, CONFIG_SHOW_BUFSIZE, ot->u.carg)) return buf; return NULL; } /* For everything else we only display if it's set, * BUT we check here to make sure you've handled * everything! */ if (is_known_opt_cb_arg(ot->cb_arg) || is_restricted_print_if_nonnull(ot->cb_arg)) { /* Only if set! */ if (cv) return cv->optarg; else return NULL; } /* Insert more decodes here! */ errx(1, "Unknown decode for %s", ot->names); } static void check_literal(const char *name, const char *val) { if (streq(val, "true") || streq(val, "false")) return; if (!streq(val, "") && strspn(val, "-0123456789.") == strlen(val)) return; errx(1, "Bad literal for %s: %s", name, val); } static void json_add_configval(struct json_stream *result, const char *fieldname, const struct opt_table *ot, const char *str) { if (ot->type & OPT_SHOWBOOL) { json_add_bool(result, fieldname, opt_canon_bool(str)); } else if (ot->type & (OPT_SHOWMSATS|OPT_SHOWINT)) { check_literal(ot->names, str); json_add_primitive(result, fieldname, str); } else json_add_string(result, fieldname, str); } /* Config vars can have multiple names ("--large-channels|--wumbo"), but first * is preferred. * wrap_object means we wrap json in an object of that name, otherwise outputs * raw fields. */ static void json_add_config(struct lightningd *ld, struct json_stream *response, bool always_include, bool wrap_object, const struct opt_table *ot, const char **names) { char buf[CONFIG_SHOW_BUFSIZE + sizeof("...")]; const char *val; struct configvar *cv; /* This tells us if they actually set the option */ cv = configvar_first(ld->configvars, names); /* Ignore dev/hidden options (deprecated) unless they actually used it */ if (!cv && (ot->desc == opt_hidden || (ot->type & OPT_DEV)) && !always_include) { return; } /* Ignore options which simply exit */ if (ot->type & OPT_EXITS) return; if (ot->type & OPT_NOARG) { if (wrap_object) json_object_start(response, names[0]); json_add_bool(response, "set", cv != NULL); json_add_source(response, "source", cv); json_add_config_plugin(response, ld->plugins, "plugin", ot); if (ot->type & OPT_DYNAMIC) json_add_bool(response, "dynamic", true); if (wrap_object) json_object_end(response); return; } assert(ot->type & OPT_HASARG); if (ot->type & OPT_MULTI) { if (wrap_object) json_object_start(response, names[0]); json_array_start(response, configval_fieldname(ot)); while (cv) { val = get_opt_val(ot, buf, cv); json_add_configval(response, NULL, ot, val); cv = configvar_next(ld->configvars, cv, names); } json_array_end(response); /* Iterate again, for sources */ json_array_start(response, "sources"); for (cv = configvar_first(ld->configvars, names); cv; cv = configvar_next(ld->configvars, cv, names)) { json_add_source(response, NULL, cv); } json_array_end(response); json_add_config_plugin(response, ld->plugins, "plugin", ot); if (ot->type & OPT_DYNAMIC) json_add_bool(response, "dynamic", true); if (wrap_object) json_object_end(response); return; } /* Returns NULL if we don't want to print it */ val = get_opt_val(ot, buf, cv); if (!val) return; if (wrap_object) json_object_start(response, names[0]); json_add_configval(response, configval_fieldname(ot), ot, val); json_add_source(response, "source", cv); json_add_config_plugin(response, ld->plugins, "plugin", ot); if (ot->type & OPT_DYNAMIC) json_add_bool(response, "dynamic", true); if (wrap_object) json_object_end(response); } static struct command_result *param_opt_config(struct command *cmd, const char *name, const char *buffer, const jsmntok_t *tok, const struct opt_table **config) { const char *name0 = json_strdup(tmpctx, buffer, tok); *config = opt_find_long(name0, NULL); if (*config) return NULL; return command_fail_badparam(cmd, name, buffer, tok, "Unknown config option"); } /* FIXME: This is a hack! Expose somehow in ccan/opt.*/ /* Returns string after first '-'. */ static const char *first_name(const char *names, unsigned *len) { *len = strcspn(names + 1, "|= "); return names + 1; } static const char *next_name(const char *names, unsigned *len) { names += *len; if (names[0] == ' ' || names[0] == '=' || names[0] == '\0') return NULL; return first_name(names + 1, len); } static const char **opt_names_arr(const tal_t *ctx, const struct opt_table *ot) { const char **names = tal_arr(ctx, const char *, 0); const char *name; unsigned len; for (name = first_name(ot->names, &len); name; name = next_name(name, &len)) { /* Skips over first -, so just need to look for one */ if (name[0] != '-') continue; tal_arr_expand(&names, tal_strndup(names, name+1, len-1)); } return names; } static struct command_result *json_listconfigs(struct command *cmd, const char *buffer, const jsmntok_t *obj UNNEEDED, const jsmntok_t *params) { struct json_stream *response = NULL; const struct opt_table *config; if (!param(cmd, buffer, params, p_opt("config", param_opt_config, &config), NULL)) return command_param_failed(); response = json_stream_success(cmd); if (!cmd->ld->deprecated_apis) goto modern; if (!config) json_add_string(response, "# version", version()); for (size_t i = 0; i < opt_count; i++) { unsigned int len; const char *name; /* FIXME: Print out comment somehow? */ if (opt_table[i].type == OPT_SUBTABLE) continue; for (name = first_name(opt_table[i].names, &len); name; name = next_name(name, &len)) { /* Skips over first -, so just need to look for one */ if (name[0] != '-') continue; if (!config || config == &opt_table[i]) { add_config_deprecated(cmd->ld, response, &opt_table[i], name+1, len-1); } /* If we have more than one long name, first * is preferred */ break; } } modern: json_object_start(response, "configs"); for (size_t i = 0; i < opt_count; i++) { const char **names; /* FIXME: Print out comment somehow? */ if (opt_table[i].type == OPT_SUBTABLE) continue; if (config && config != &opt_table[i]) continue; names = opt_names_arr(tmpctx, &opt_table[i]); /* We don't usually print dev or deprecated options, unless * they explicitly ask, or they're set. */ json_add_config(cmd->ld, response, config != NULL, true, &opt_table[i], names); } json_object_end(response); return command_success(cmd, response); } static const struct json_command listconfigs_command = { "listconfigs", "utility", json_listconfigs, "List all configuration options, or with [config], just that one.", .verbose = "listconfigs [config]\n" "Outputs an object, with each field a config options\n" "(Option names which start with # are comments)\n" "With [config], object only has that field" }; AUTODATA(json_command, &listconfigs_command); static struct command_result *param_opt_dynamic_config(struct command *cmd, const char *name, const char *buffer, const jsmntok_t *tok, const struct opt_table **config) { struct command_result *ret; ret = param_opt_config(cmd, name, buffer, tok, config); if (ret) return ret; if (!((*config)->type & OPT_DYNAMIC)) return command_fail_badparam(cmd, name, buffer, tok, "Not a dynamic config option"); return NULL; } /* FIXME: put in ccan/mem! */ static size_t memcount(const void *mem, size_t len, char c) { size_t count = 0; for (size_t i = 0; i < len; i++) { if (((char *)mem)[i] == c) count++; } return count; } static void configvar_updated(struct lightningd *ld, enum configvar_src src, const char *fname, size_t linenum, const char *confline) { struct configvar *cv; cv = configvar_new(ld->configvars, src, fname, linenum, confline); configvar_unparsed(cv); log_info(ld->log, "setconfig: %s %s (updated %s:%u)", cv->optvar, cv->optarg ? cv->optarg : "SET", cv->file, cv->linenum); tal_arr_expand(&ld->configvars, cv); configvar_finalize_overrides(ld->configvars); } /* Marker for our own insertions */ #define INSERTED_BY_SETCONFIG "# Inserted by setconfig " static void configvar_append_file(struct lightningd *ld, const char *fname, enum configvar_src src, const char *confline, bool must_exist) { int fd; size_t num_lines; const char *buffer, *insert; bool needs_term; time_t now = time(NULL); fd = open(fname, O_RDWR|O_APPEND); if (fd < 0) { if (errno != ENOENT || must_exist) fatal("Could not write to config %s: %s", fname, strerror(errno)); fd = open(fname, O_RDWR|O_APPEND|O_CREAT, 0644); if (fd < 0) fatal("Could not create config file %s: %s", fname, strerror(errno)); } /* Note: always nul terminates */ buffer = grab_fd(tmpctx, fd); if (!buffer) fatal("Error reading %s: %s", fname, strerror(errno)); num_lines = memcount(buffer, tal_bytelen(buffer)-1, '\n'); /* If there's a last character and it's not \n, add one */ if (tal_bytelen(buffer) == 1) needs_term = false; else needs_term = (buffer[tal_bytelen(buffer)-2] != '\n'); /* Note: ctime() contains a \n! */ insert = tal_fmt(tmpctx, "%s"INSERTED_BY_SETCONFIG"%s%s\n", needs_term ? "\n": "", ctime(&now), confline); if (write(fd, insert, strlen(insert)) != strlen(insert)) fatal("Could not write to config file %s: %s", fname, strerror(errno)); configvar_updated(ld, src, fname, num_lines+2, confline); } /* Returns true if it rewrote in place, otherwise it just comments out * if necessary */ static bool configfile_replace_var(struct lightningd *ld, const struct configvar *cv, const char *confline) { char *contents, **lines, *template; int outfd; bool replaced; switch (cv->src) { case CONFIGVAR_CMDLINE: case CONFIGVAR_CMDLINE_SHORT: case CONFIGVAR_PLUGIN_START: /* These can't be commented out */ return false; case CONFIGVAR_EXPLICIT_CONF: case CONFIGVAR_BASE_CONF: case CONFIGVAR_NETWORK_CONF: break; } contents = grab_file(tmpctx, cv->file); if (!contents) fatal("Could not load configfile %s: %s", cv->file, strerror(errno)); lines = tal_strsplit(contents, contents, "\r\n", STR_EMPTY_OK); if (cv->linenum - 1 >= tal_count(lines)) fatal("Configfile %s no longer has %u lines!", cv->file, cv->linenum); if (!streq(lines[cv->linenum - 1], cv->configline)) fatal("Configfile %s line %u changed from %s to %s!", cv->file, cv->linenum, cv->configline, lines[cv->linenum - 1]); /* If we already have # Inserted by setconfig above, just replace * those two! */ if (cv->linenum > 1 && strstarts(lines[cv->linenum - 2], INSERTED_BY_SETCONFIG)) { time_t now = time(NULL); lines[cv->linenum - 2] = tal_fmt(lines, INSERTED_BY_SETCONFIG"%s", ctime(&now)); /* But trim final \n! (thanks ctime!) */ assert(strends(lines[cv->linenum - 2], "\n")); lines[cv->linenum - 2][strlen(lines[cv->linenum - 2])-1] = '\0'; lines[cv->linenum - 1] = cast_const(char *, confline); replaced = true; } else { /* Comment out, in-place */ lines[cv->linenum - 1] = tal_fmt(lines, "# setconfig commented out: %s", lines[cv->linenum - 1]); log_info(ld->log, "setconfig: commented out line %u of %s (%s)", cv->linenum, cv->file, cv->configline); replaced = false; } template = tal_fmt(tmpctx, "%s.setconfig.XXXXXX", cv->file); outfd = mkstemp(template); if (outfd < 0) fatal("Creating %s: %s", template, strerror(errno)); contents = tal_strjoin(tmpctx, take(lines), "\n", STR_TRAIL); if (!write_all(outfd, contents, strlen(contents))) fatal("Writing %s: %s", template, strerror(errno)); fdatasync(outfd); if (rename(template, cv->file) != 0) fatal("Renaming %s over %s: %s", template, cv->file, strerror(errno)); close(outfd); if (replaced) { configvar_updated(ld, cv->src, cv->file, cv->linenum, confline); return true; } return false; } static void configvar_save(struct lightningd *ld, const char **names, const char *confline) { /* Simple case: set in a config file. */ struct configvar *oldcv; oldcv = configvar_first(ld->configvars, names); if (oldcv) { /* At least comment out, maybe replace */ if (configfile_replace_var(ld, oldcv, confline)) return; } /* If they used --conf then append to that */ if (ld->config_filename) configvar_append_file(ld, ld->config_filename, CONFIGVAR_EXPLICIT_CONF, confline, true); else { const char *fname; fname = path_join(tmpctx, ld->config_netdir, "config"); configvar_append_file(ld, fname, CONFIGVAR_NETWORK_CONF, confline, false); } } static struct command_result *setconfig_success(struct command *cmd, const struct opt_table *ot, const char *val) { struct json_stream *response; const char **names, *confline; names = opt_names_arr(tmpctx, ot); if (val) confline = tal_fmt(tmpctx, "%s=%s", names[0], val); else confline = names[0]; configvar_save(cmd->ld, names, confline); response = json_stream_success(cmd); json_object_start(response, "config"); json_add_string(response, "config", names[0]); json_add_config(cmd->ld, response, true, false, ot, names); json_object_end(response); return command_success(cmd, response); } static struct command_result *json_setconfig(struct command *cmd, const char *buffer, const jsmntok_t *obj UNNEEDED, const jsmntok_t *params) { const struct opt_table *ot; const char *val; char *err; if (!param(cmd, buffer, params, p_req("config", param_opt_dynamic_config, &ot), p_opt("val", param_string, &val), NULL)) return command_param_failed(); /* We don't handle DYNAMIC MULTI, at least yet! */ assert(!(ot->type & OPT_MULTI)); if (ot->type & OPT_NOARG) { if (val) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "%s does not take a value", ot->names + 2); if (is_plugin_opt(ot)) return plugin_set_dynamic_opt(cmd, ot, NULL, setconfig_success); err = ot->cb(ot->u.arg); } else { assert(ot->type & OPT_HASARG); if (!val) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "%s requires a value", ot->names + 2); if (is_plugin_opt(ot)) return plugin_set_dynamic_opt(cmd, ot, val, setconfig_success); err = ot->cb_arg(val, ot->u.arg); } if (err) { return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "Error setting %s: %s", ot->names + 2, err); } return setconfig_success(cmd, ot, val); } static const struct json_command setconfig_command = { "setconfig", "utility", json_setconfig, "Set a dynamically-adjustable config." }; AUTODATA(json_command, &setconfig_command);