diff --git a/src/cmd/CMakeLists.txt b/src/cmd/CMakeLists.txt index 9ad03e42..1af5771d 100644 --- a/src/cmd/CMakeLists.txt +++ b/src/cmd/CMakeLists.txt @@ -27,6 +27,7 @@ add_library(vitastor_cli STATIC cli_pool_ls.cpp cli_pool_modify.cpp cli_pool_rm.cpp + cli_serve.cpp ) target_compile_options(vitastor_cli PUBLIC -fPIC) diff --git a/src/cmd/cli.cpp b/src/cmd/cli.cpp index c49eb298..e1fb578b 100644 --- a/src/cmd/cli.cpp +++ b/src/cmd/cli.cpp @@ -225,6 +225,14 @@ static const char* help_text = " -r|--reverse Sort in descending order\n" " -n|--count N Only list first N items\n" "\n" + "vitastor-cli serve\n" + " Start HTTP server able to handle CLI commands over a REST API. Options:\n" + " --bind_address ADDR Specify server IP address or addresses, separated by space. Default is 127.0.0.1.\n" + " --port 8080 Specify server port.\n" + " --ssl_cert FILE Path to server SSL certificate file (PEM format).\n" + " --ssl_key FILE Path to server SSL private key file.\n" + " --ssl_ca FILE Path to file with SSL CA certificates used to validate client connections.\n" + "\n" "Use vitastor-cli --help for command details or vitastor-cli --help --all for all details.\n" "\n" "GLOBAL OPTIONS:\n" @@ -319,27 +327,24 @@ static json11::Json::object parse_args(int narg, const char *args[]) return cfg; } -static int run(cli_tool_t *p, json11::Json::object cfg) +std::function cli_tool_t::start(json11::Json::object cfg, cli_result_t & result) { - cli_result_t result = {}; - p->is_command_line = true; - p->parse_config(cfg); json11::Json::array cmd = cfg["command"].array_items(); cfg.erase("command"); std::function action_cb; if (!cmd.size()) { - result = { .err = EINVAL, .text = "command is missing" }; + result = { .err = EOPNOTSUPP, .text = "command is missing" }; } else if (cmd[0] == "status") { // Show cluster status - action_cb = p->start_status(cfg); + action_cb = start_status(cfg); } else if (cmd[0] == "df") { // Show pool space stats - action_cb = p->start_pool_ls(cfg); + action_cb = start_pool_ls(cfg); } else if (cmd[0] == "ls") { @@ -349,7 +354,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg) cmd.erase(cmd.begin(), cmd.begin()+1); cfg["names"] = cmd; } - action_cb = p->start_ls(cfg); + action_cb = start_ls(cfg); } else if (cmd[0] == "snap-create") { @@ -364,7 +369,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg) { cfg["image"] = name.substr(0, pos); cfg["snapshot"] = name.substr(pos + 1); - action_cb = p->start_create(cfg); + action_cb = start_create(cfg); } } else if (cmd[0] == "create") @@ -374,7 +379,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg) { cfg["image"] = cmd[1]; } - action_cb = p->start_create(cfg); + action_cb = start_create(cfg); } else if (cmd[0] == "modify") { @@ -383,12 +388,12 @@ static int run(cli_tool_t *p, json11::Json::object cfg) { cfg["image"] = cmd[1]; } - action_cb = p->start_modify(cfg); + action_cb = start_modify(cfg); } else if (cmd[0] == "rm-data") { // Delete inode data - action_cb = p->start_rm_data(cfg); + action_cb = start_rm_data(cfg); } else if (cmd[0] == "rm-osd") { @@ -398,7 +403,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg) cmd.erase(cmd.begin(), cmd.begin()+1); cfg["osd_id"] = cmd; } - action_cb = p->start_rm_osd(cfg); + action_cb = start_rm_osd(cfg); } else if (cmd[0] == "merge-data") { @@ -409,7 +414,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg) if (cmd.size() > 2) cfg["to"] = cmd[2]; } - action_cb = p->start_merge(cfg); + action_cb = start_merge(cfg); } else if (cmd[0] == "flatten") { @@ -418,7 +423,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg) { cfg["image"] = cmd[1]; } - action_cb = p->start_flatten(cfg); + action_cb = start_flatten(cfg); } else if (cmd[0] == "dd") { @@ -432,7 +437,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg) cfg[arg.substr(0, p)] = arg.substr(p+1); } } - action_cb = p->start_dd(cfg); + action_cb = start_dd(cfg); } else if (cmd[0] == "rm") { @@ -441,7 +446,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg) { cmd.erase(cmd.begin(), cmd.begin()+1); cfg["globs"] = cmd; - action_cb = p->start_rm_wildcard(cfg); + action_cb = start_rm_wildcard(cfg); } else { @@ -451,41 +456,41 @@ static int run(cli_tool_t *p, json11::Json::object cfg) if (cmd.size() > 2) cfg["to"] = cmd[2]; } - action_cb = p->start_rm(cfg); + action_cb = start_rm(cfg); } } else if (cmd[0] == "describe") { // Describe unclean objects - action_cb = p->start_describe(cfg); + action_cb = start_describe(cfg); } else if (cmd[0] == "fix") { // Fix inconsistent objects (by deleting some copies) - action_cb = p->start_fix(cfg); + action_cb = start_fix(cfg); } else if (cmd[0] == "alloc-osd") { // Allocate a new OSD number - action_cb = p->start_alloc_osd(cfg); + action_cb = start_alloc_osd(cfg); } else if (cmd[0] == "osd-tree") { // Print OSD tree - action_cb = p->start_osd_tree(cfg); + action_cb = start_osd_tree(cfg); } else if (cmd[0] == "osds" || cmd[0] == "ls-osds" || cmd[0] == "ls-osd" || cmd[0] == "osd-ls") { // Print OSD list cfg["flat"] = true; - action_cb = p->start_osd_tree(cfg); + action_cb = start_osd_tree(cfg); } else if (cmd[0] == "modify-osd") { // Modify OSD configuration if (cmd.size() > 1) cfg["osd_num"] = cmd[1]; - action_cb = p->start_modify_osd(cfg); + action_cb = start_modify_osd(cfg); } else if (cmd[0] == "pg-list" || cmd[0] == "pg-ls" || cmd[0] == "list-pg" || cmd[0] == "ls-pg" || cmd[0] == "ls-pgs" || cmd[0] == "pgs") { @@ -495,7 +500,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg) cmd.erase(cmd.begin(), cmd.begin()+1); cfg["pg_state"] = cmd; } - action_cb = p->start_pg_list(cfg); + action_cb = start_pg_list(cfg); } else if (cmd[0] == "create-pool" || cmd[0] == "pool-create") { @@ -504,7 +509,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg) { cfg["name"] = cmd[1]; } - action_cb = p->start_pool_create(cfg); + action_cb = start_pool_create(cfg); } else if (cmd[0] == "modify-pool" || cmd[0] == "pool-modify") { @@ -513,7 +518,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg) { cfg["old_name"] = cmd[1]; } - action_cb = p->start_pool_modify(cfg); + action_cb = start_pool_modify(cfg); } else if (cmd[0] == "rm-pool" || cmd[0] == "pool-rm") { @@ -522,7 +527,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg) { cfg["pool"] = cmd[1]; } - action_cb = p->start_pool_rm(cfg); + action_cb = start_pool_rm(cfg); } else if (cmd[0] == "ls-pool" || cmd[0] == "pool-ls" || cmd[0] == "ls-pools" || cmd[0] == "pools") { @@ -533,12 +538,26 @@ static int run(cli_tool_t *p, json11::Json::object cfg) cmd.erase(cmd.begin(), cmd.begin()+1); cfg["names"] = cmd; } - action_cb = p->start_pool_ls(cfg); + action_cb = start_pool_ls(cfg); + } + else if (cmd[0] == "serve") + { + // Start HTTP server + action_cb = start_serve(cfg); } else { - result = { .err = EINVAL, .text = "unknown command: "+cmd[0].string_value() }; + result = { .err = EOPNOTSUPP, .text = "unknown command: "+cmd[0].string_value() }; } + return action_cb; +} + +static int run(cli_tool_t *p, json11::Json::object cfg) +{ + cli_result_t result = {}; + p->is_command_line = true; + p->parse_config(cfg); + auto action_cb = p->start(cfg, result); if (action_cb != NULL) { // Create client @@ -550,6 +569,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg) { result = r; action_cb = NULL; + p->ringloop->submit(); }); // Loop until it completes while (action_cb != NULL) diff --git a/src/cmd/cli.h b/src/cmd/cli.h index e0e764fe..6947587b 100644 --- a/src/cmd/cli.h +++ b/src/cmd/cli.h @@ -46,6 +46,7 @@ public: json11::Json etcd_result; void parse_config(json11::Json::object & cfg); + void parse_api_opts(json11::Json::object & cfg); json11::Json parse_tags(std::string tags); void change_parent(inode_t cur, inode_t new_parent, cli_result_t *result); @@ -56,8 +57,10 @@ public: friend struct snap_flattener_t; friend struct snap_remover_t; + std::function start(json11::Json::object cfg, cli_result_t & result); std::function start_alloc_osd(json11::Json); std::function start_create(json11::Json); + std::function start_dd(json11::Json); std::function start_describe(json11::Json); std::function start_fix(json11::Json); std::function start_flatten(json11::Json); @@ -75,8 +78,8 @@ public: std::function start_rm_data(json11::Json); std::function start_rm_osd(json11::Json); std::function start_rm_wildcard(json11::Json); + std::function start_serve(json11::Json); std::function start_status(json11::Json); - std::function start_dd(json11::Json); // Should be called like loop_and_wait(start_status(), ) void loop_and_wait(std::function loop_cb, std::function complete_cb); diff --git a/src/cmd/cli_common.cpp b/src/cmd/cli_common.cpp index c0805e8b..355a381e 100644 --- a/src/cmd/cli_common.cpp +++ b/src/cmd/cli_common.cpp @@ -101,6 +101,16 @@ inode_config_t* cli_tool_t::get_inode_cfg(const std::string & name) return NULL; } +void cli_tool_t::parse_api_opts(json11::Json::object & cfg) +{ + iodepth = cfg["iodepth"].uint64_value(); + if (!iodepth) + iodepth = 32; + parallel_osds = cfg["parallel_osds"].uint64_value(); + if (!parallel_osds) + parallel_osds = 4; +} + void cli_tool_t::parse_config(json11::Json::object & cfg) { for (auto kv_it = cfg.begin(); kv_it != cfg.end();) @@ -121,15 +131,10 @@ void cli_tool_t::parse_config(json11::Json::object & cfg) else color = isatty(1); json_output = cfg["json"].bool_value(); - iodepth = cfg["iodepth"].uint64_value(); - if (!iodepth) - iodepth = 32; - parallel_osds = cfg["parallel_osds"].uint64_value(); - if (!parallel_osds) - parallel_osds = 4; log_level = cfg["log_level"].int64_value(); progress = cfg["progress"].uint64_value() ? true : false; list_first = cfg["wait_list"].uint64_value() ? true : false; + parse_api_opts(cfg); } struct cli_result_looper_t @@ -153,7 +158,6 @@ void cli_tool_t::loop_and_wait(std::function loop_cb, std: ringloop->unregister_consumer(&looper->consumer); looper->loop_cb = NULL; looper->complete_cb(looper->result); - ringloop->submit(); delete looper; return; } diff --git a/src/cmd/cli_serve.cpp b/src/cmd/cli_serve.cpp new file mode 100644 index 00000000..fa466eab --- /dev/null +++ b/src/cmd/cli_serve.cpp @@ -0,0 +1,428 @@ +// Copyright (c) Vitaliy Filippov, 2019+ +// License: VNPL-1.1 (see README.md for details) + +#include +#include +#include +#include +#include "cli.h" +#include "cluster_client.h" +#include "epoll_manager.h" +#include "http_client.h" +#include "str_util.h" +#include "json_util.h" +#include "addr_util.h" + +struct cli_serve_conn_t +{ + int peer_fd = 0; + std::string peer_addr; + http_co_t *co = NULL; + cli_tool_t *p = NULL; + cli_result_t result; + bool keepalive = false; + bool closed = false; + timespec request_time; + std::string request_method; + std::string request_path; + std::string request_body; + std::function action_cb; +}; + +struct cli_serve_path_t +{ + std::string cmd; + bool allow_get; +}; + +// Serve vitastor-cli commands over HTTP in JSON format +struct cli_serve_t +{ + std::map cmd_paths = { + {"data/delete", {"rm-data", false}}, + {"data/describe", {"describe", true}}, + {"data/fix", {"fix", false}}, + {"data/merge", {"merge-data", false}}, + {"image/create", {"create", false}}, + {"image/delete", {"rm", false}}, + {"image/flatten", {"flatten", false}}, + {"image/list", {"ls", true}}, + {"image/modify", {"modify", false}}, + {"osd/alloc", {"alloc-osd", false}}, + {"osd/delete", {"rm-osd", false}}, + {"osd/list", {"ls-osd", true}}, + {"osd/modify", {"modify-osd", false}}, + {"pg/list", {"ls-pgs", true}}, + {"pool/create", {"create-pool", false}}, + {"pool/delete", {"rm-pool", false}}, + {"pool/list", {"pools", true}}, + {"pool/modify", {"modify-pool", false}}, + {"status", {"status", true}}, + }; + + cli_tool_t *parent = NULL; + json11::Json options; + cli_result_t result; + + bool log_body = false; + bool stop = false; + std::vector bind_addresses; + int port = 0; + int listen_backlog = 0; + bool ssl = false; + std::vector listen_fds; + http_context_t *http_ctx = NULL; + std::set connections; + + int state = 0; + + bool is_done() + { + return state == 100; + } + + void loop() + { + if (state == 1) + goto resume_1; + else if (state == 2) + goto resume_2; + else if (state == 100) + return; + if (options["bind_address"].is_string()) + bind_addresses = explode(" ", options["bind_address"].string_value(), true); + else + bind_addresses.push_back("127.0.0.1"); + port = options["port"].uint64_value(); + if (!port) + port = 8080; + else if (port < 0 || port > 65535) + { + result = (cli_result_t){ .err = EINVAL, .text = "HTTP port can't be larger than 65536" }; + state = 100; + return; + } + listen_backlog = options["listen_backlog"].uint64_value(); + if (!listen_backlog) + listen_backlog = 128; + ssl = json_is_true(options["ssl"]); + if (ssl) + { + std::string ssl_cert = options["ssl_cert"].string_value(); + std::string ssl_key = options["ssl_key"].string_value(); + std::string ssl_ca = options["ssl_ca"].string_value(); + std::string error; + http_ctx = http_context_init(ssl_cert, ssl_key, ssl_ca, ssl_ca != "", error); + if (error != "") + { + result = (cli_result_t){ .err = EINVAL, .text = error }; + state = 100; + return; + } + } + for (auto & bind_address: bind_addresses) + { + int listen_fd = create_and_bind_socket(bind_address, port, listen_backlog, NULL); + fcntl(listen_fd, F_SETFL, fcntl(listen_fd, F_GETFL, 0) | O_NONBLOCK); + parent->epmgr->set_fd_handler(listen_fd, false, [this](int fd, int events) + { + accept_connections(fd); + }); + listen_fds.push_back(listen_fd); + } + resume_1: + if (!stop) + { + state = 1; + return; + } + for (auto conn: connections) + { + http_close(conn->co); + } + resume_2: + // Wait for all connections to finish + if (connections.size() > 0) + { + state = 2; + return; + } + if (http_ctx) + { + http_context_destroy(http_ctx); + http_ctx = NULL; + } + state = 100; + } + + void accept_connections(int listen_fd) + { + sockaddr_storage addr; + socklen_t peer_addr_size = sizeof(addr); + int peer_fd; + while ((peer_fd = accept(listen_fd, (sockaddr*)&addr, &peer_addr_size)) >= 0) + { + auto peer_addr_str = addr_to_string(addr); + assert(peer_fd != 0); + timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + printf("[%s.%03ju] New connection %d from %s\n", format_datetime(ts.tv_sec).c_str(), (uint64_t)ts.tv_nsec/1000000, + peer_fd, peer_addr_str.c_str()); + fcntl(peer_fd, F_SETFL, fcntl(peer_fd, F_GETFL, 0) | O_NONBLOCK); + int one = 1; + setsockopt(peer_fd, SOL_TCP, TCP_NODELAY, &one, sizeof(one)); + cli_serve_conn_t *conn = new cli_serve_conn_t; + conn->peer_fd = peer_fd; + conn->peer_addr = peer_addr_str; + conn->co = http_init(parent->epmgr->tfd, http_ctx); + http_serve(conn->co, peer_fd, (http_options_t){ .ssl = ssl }, [this, conn](http_message_t *msg) + { + process_request(conn, msg); + }); + connections.insert(conn); + // Try to accept next connection + peer_addr_size = sizeof(addr); + } + if (peer_fd == -1 && errno != EAGAIN) + { + throw std::runtime_error(std::string("accept: ") + strerror(errno)); + } + } + + int map_to_http(int err, std::string *text) + { + int code = 0; + if (err == EINVAL) + { + code = 400; + if (text) + *text = "Bad Request"; + } + else if (err == EOPNOTSUPP) + { + code = 404; + if (text) + *text = "Not Found"; + } + else if (err == ENOSYS) + { + code = 405; + if (text) + *text = "Method Not Allowed"; + } + else if (err == EAGAIN) + { + code = 409; + if (text) + *text = "Update Conflict"; + } + else if (err == ENOTEMPTY || err == EEXIST || err == ENOENT || err == EBUSY) + { + code = 412; + if (text) + *text = "Precondition Failed"; + } + else /*if (err == EIO || err == EBADF)*/ + { + code = 500; + if (text) + *text = "Internal Server Error"; + } + return code; + } + + std::string cli_http_response(cli_serve_conn_t *conn) + { + timespec now; + clock_gettime(CLOCK_REALTIME, &now); + int code = 200; + std::string response; + if (conn->result.err) + { + std::string status_line; + code = map_to_http(conn->result.err, &status_line); + response = "HTTP/1.1 "+std::to_string(code)+" "+status_line+"\r\n"; + } + else + { + response = "HTTP/1.1 200 OK\r\n"; + } + response += (conn->keepalive + ? "Connection: keep-alive\r\n" + : "Connection: close\r\n"); + std::string body; + if (!conn->result.data.is_null()) + { + response += "Content-Type: application/json\r\n"; + body = conn->result.data.dump(); + } + else + { + response += "Content-Type: text/plain; charset=utf-8\r\n"; + body = conn->result.text; + } + response += "Content-Length: "+std::to_string(body.size())+"\r\n\r\n"; + response += body; + if (conn->request_method.find("\n") != std::string::npos) + conn->request_method = str_replace(conn->request_method, "\n", "%0a"); + if (conn->request_method.find(" ") != std::string::npos) + conn->request_method = str_replace(conn->request_method, " ", "%20"); + if (conn->request_path.find("\n") != std::string::npos) + conn->request_path = str_replace(conn->request_path, "\n", "%0a"); + if (conn->request_path.find(" ") != std::string::npos) + conn->request_path = str_replace(conn->request_path, " ", "%20"); + uint64_t response_time = (now.tv_sec-conn->request_time.tv_sec)*1000 + (now.tv_nsec-conn->request_time.tv_nsec)/1000000; + printf("[%s.%03ju] %s %s %s %d %.03f sec\n", + format_datetime(now.tv_sec).c_str(), (uint64_t)now.tv_nsec/1000000, + conn->peer_addr.c_str(), conn->request_method.c_str(), conn->request_path.c_str(), code, + response_time/1000.0); + if (log_body) + { + if (conn->request_body.find("\n") != std::string::npos) + conn->request_body = str_replace(conn->request_body, "\n", " "); + if (conn->request_body.size()) + printf(" %s\n", conn->request_body.c_str()); + printf(" %s\n", body.c_str()); + } + return response; + } + + void process_request(cli_serve_conn_t *conn, http_message_t *msg) + { + timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + if (!msg->error.empty()) + { + // connection is closed + fprintf(stderr, "[%s.%03ju] Connection %d closed: %s\n", format_datetime(ts.tv_sec).c_str(), + (uint64_t)ts.tv_nsec/1000000, conn->peer_fd, msg->error.c_str()); + if (conn->p) + conn->closed = true; + else + { + connections.erase(conn); + http_destroy(conn->co); + delete conn; + } + return; + } + conn->keepalive = msg->headers.find("connection") != msg->headers.end() && + msg->headers.at("connection") == "keep-alive"; + conn->p = new cli_tool_t; + conn->p->iodepth = parent->iodepth; + conn->p->parallel_osds = parent->parallel_osds; + conn->p->json_output = true; + conn->p->ringloop = parent->ringloop; + conn->p->epmgr = parent->epmgr; + conn->p->cli = parent->cli; + conn->p->is_command_line = false; + // Parse request + auto req_line = explode(" ", msg->status_line, true); + if (req_line.size() < 2) + { + if (req_line[0] == "") + req_line[0] = "-"; + req_line.push_back("-"); + } + conn->request_time = ts; + conn->request_method = std::move(req_line[0]); + conn->request_path = std::move(req_line[1]); + conn->request_body = std::move(msg->body); + auto ctype = msg->headers["content-type"]; + if (conn->request_method != "GET" && conn->request_method != "POST") + { + conn->result = { .err = ENOSYS, .text = "Unsupported request method "+conn->request_method }; + } + else if (ctype != (conn->request_method == "GET" ? "" : "application/json")) + { + conn->result = { .err = EINVAL, .text = "Unsupported Content-Type: "+ctype+" for "+conn->request_method+" requests" }; + } + else + { + auto uri = explode("?", conn->request_path, true); + uri[0] = trim(uri[0], "/"); + auto cmd_it = cmd_paths.find(uri[0]); + if (uri[0] == "") + { + std::string text = "Supported APIs:\n\n"; + for (auto & pp: cmd_paths) + { + text += (pp.second.allow_get ? "- GET" : "- POST") + (" /" + pp.first) + "\n"; + } + conn->result = { .text = text }; + } + else if (cmd_it == cmd_paths.end()) + { + conn->result = { .err = EOPNOTSUPP, .text = "unknown command: "+uri[0] }; + } + else if (conn->request_method == "GET" && !cmd_it->second.allow_get) + { + conn->result = { .err = ENOSYS, .text = "method /"+uri[0]+" only allows POST requests" }; + } + else + { + std::string error; + json11::Json::object cfg; + if (conn->request_method == "POST") + { + cfg = json11::Json::parse(conn->request_body, error).object_items(); + } + else + { + // Parse URI + cfg = parse_uri_params(uri[1]); + } + if (error != "") + { + conn->result = { .err = EINVAL, .text = "Invalid JSON in body: "+error }; + } + else + { + cfg["command"] = json11::Json::array{cmd_it->second.cmd}; + conn->p->parse_api_opts(cfg); + conn->action_cb = conn->p->start(cfg, conn->result); + } + } + } + if (!conn->action_cb) + { + http_reply(conn->co, cli_http_response(conn)); + delete conn->p; + conn->p = NULL; + return; + } + conn->p->loop_and_wait(conn->action_cb, [this, conn](const cli_result_t & r) + { + conn->result = r; + conn->action_cb = NULL; + delete conn->p; + conn->p = NULL; + if (!conn->closed) + http_reply(conn->co, cli_http_response(conn)); + else + { + connections.erase(conn); + http_destroy(conn->co); + delete conn; + } + }); + } +}; + +std::function cli_tool_t::start_serve(json11::Json cfg) +{ + auto server = new cli_serve_t(); + server->parent = this; + server->options = cfg; + return [server](cli_result_t & result) + { + server->loop(); + if (server->is_done()) + { + result = server->result; + delete server; + return true; + } + return false; + }; +} diff --git a/src/util/json_util.cpp b/src/util/json_util.cpp index e8c96d8f..4ffd8d38 100644 --- a/src/util/json_util.cpp +++ b/src/util/json_util.cpp @@ -1,6 +1,7 @@ // Copyright (c) Vitaliy Filippov, 2019+ // License: VNPL-1.1 or GNU GPL-2.0+ (see README.md for details) +#include "str_util.h" #include "json_util.h" std::map json_to_string_map(const json11::Json::object & config) @@ -49,3 +50,36 @@ std::string implode(const std::string & sep, json11::Json array) } return res; } + +static void json_array_add(json11::Json & to, json11::Json item) +{ + if (to.is_null()) + to = item; + else if (to.is_array()) + { + auto a = to.array_items(); + a.push_back(item); + to = a; + } + else + { + json11::Json::array a = {to}; + a.push_back(item); + to = a; + } +} + +json11::Json::object parse_uri_params(const std::string & params) +{ + json11::Json::object obj; + auto list = explode("&", params, true); + for (auto & param: list) + { + auto pos = param.find("="); + if (pos != std::string::npos) + json_array_add(obj[urldecode(param.substr(0, pos))], urldecode(param.substr(pos+1))); + else + json_array_add(obj[urldecode(param)], true); + } + return obj; +} diff --git a/src/util/json_util.h b/src/util/json_util.h index b091df9a..04aa37be 100644 --- a/src/util/json_util.h +++ b/src/util/json_util.h @@ -14,5 +14,6 @@ std::map json_to_string_map(const json11::Json::object bool json_is_true(const json11::Json & val); bool json_is_false(const json11::Json & val); std::string implode(const std::string & sep, json11::Json array); +json11::Json::object parse_uri_params(const std::string & params); #pragma GCC visibility pop diff --git a/src/util/str_util.cpp b/src/util/str_util.cpp index 320cee95..74649665 100644 --- a/src/util/str_util.cpp +++ b/src/util/str_util.cpp @@ -503,3 +503,34 @@ bool is_zero(void *buf, size_t size) } return true; } + +char hextochar(char h) +{ + if (h >= 'a' && h <= 'f') + return h-'a'+10; + else if (h >= 'A' && h <= 'F') + return h-'A'+10; + else if (h >= '0' && h <= '9') + return h-'0'; + return 0; +} + +std::string urldecode(const std::string & orig) +{ + size_t len = orig.size(); + std::string res; + res.reserve(len); + for (size_t i = 0; i < len; i++) + { + if (orig[i] == '+') + res.push_back(' '); + else if (orig[i] == '%' && i < len-2) + { + res.push_back(hextochar(orig[i+1])*16 + hextochar(orig[i+2])); + i += 2; + } + else + res.push_back(orig[i]); + } + return res; +} diff --git a/src/util/str_util.h b/src/util/str_util.h index c601122f..463cc628 100644 --- a/src/util/str_util.h +++ b/src/util/str_util.h @@ -34,5 +34,6 @@ std::string addslashes(const std::string & str, const char *toescape = "\\\""); std::string realpath_str(std::string path, bool nofail = true); std::string format_datetime(uint64_t unixtime); bool is_zero(void *buf, size_t size); +std::string urldecode(const std::string & orig); #pragma GCC visibility pop