diff --git a/demo-json-rpc-server.c b/demo-json-rpc-server.c index cdad2be..1eebe4d 100644 --- a/demo-json-rpc-server.c +++ b/demo-json-rpc-server.c @@ -114,6 +114,21 @@ str_pack_u64 (struct str *self, uint64_t x) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +static int +tolower_ascii (int c) +{ + return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c; +} + +static size_t +tolower_ascii_strxfrm (char *dest, const char *src, size_t n) +{ + size_t len = strlen (src); + while (n-- && (*dest++ = tolower_ascii (*src++))) + ; + return len; +} + static void base64_encode (const void *data, size_t len, struct str *output) { @@ -548,6 +563,8 @@ struct fcgi_muxer { struct fcgi_parser parser; ///< FastCGI message parser + // TODO: bool quitting; that causes us to reject all requests? + /// Requests assigned to request IDs // TODO: allocate this dynamically struct fcgi_request *requests[1 << 16]; @@ -1143,7 +1160,7 @@ ws_parser_free (struct ws_parser *self) } static void -ws_parser_demask (struct ws_parser *self) +ws_parser_unmask (struct ws_parser *self) { // Yes, this could be made faster. For example by reading the mask in // native byte ordering and applying it directly here. @@ -1249,7 +1266,7 @@ ws_parser_push (struct ws_parser *self, const void *data, size_t len) return true; if (self->is_masked) - ws_parser_demask (self); + ws_parser_unmask (self); if (!self->on_frame (self->user_data, self)) return false; @@ -1289,6 +1306,9 @@ struct ws_handler struct ws_parser parser; ///< Protocol frame parser + // TODO: bool closing; + // TODO: a configurable max_payload_len initialized by _init() + /// Called upon reception of a single full message bool (*on_message) (void *user_data, const void *data, size_t len); @@ -1306,7 +1326,7 @@ ws_handler_on_frame (void *user_data, const struct ws_parser *parser) struct ws_handler *self = user_data; // TODO: handle pings and what not // TODO: validate the message - + // TODO: first concatenate all parts of the message return self->on_message (self->user_data, self->parser.input.str, self->parser.payload_len); } @@ -1323,7 +1343,7 @@ ws_handler_init (struct ws_handler *self) str_init (&self->value); str_map_init (&self->headers); self->headers.free = free; - // TODO: set headers.key_strxfrm? + self->headers.key_xfrm = tolower_ascii_strxfrm; str_init (&self->url); ws_parser_init (&self->parser); @@ -1613,6 +1633,21 @@ validate_json_rpc_content_type (const char *type) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +typedef json_t *(*json_rpc_handler_fn) (struct server_context *, json_t *); + +struct json_rpc_handler_info +{ + const char *method_name; ///< JSON-RPC method name + json_rpc_handler_fn handler; ///< Method handler +}; + +static int +json_rpc_handler_info_cmp (const void *first, const void *second) +{ + return strcmp (((struct json_rpc_handler_info *) first)->method_name, + ((struct json_rpc_handler_info *) second)->method_name); +} + // TODO: a method that queues up a ping over IRC: this has to be owned by the // server context as a background job that removes itself upon completion. @@ -1628,6 +1663,13 @@ json_rpc_ping (struct server_context *ctx, json_t *params) static json_t * process_json_rpc_request (struct server_context *ctx, json_t *request) { + // A list of all available methods; this list has to be ordered. + // Eventually it might be better to move this into a map in the context. + static struct json_rpc_handler_info handlers[] = + { + { "ping", json_rpc_ping }, + }; + if (!json_is_object (request)) return json_rpc_response (NULL, NULL, json_rpc_error (JSON_RPC_ERROR_INVALID_REQUEST, NULL)); @@ -1650,14 +1692,14 @@ process_json_rpc_request (struct server_context *ctx, json_t *request) return json_rpc_response (id, NULL, json_rpc_error (JSON_RPC_ERROR_INVALID_REQUEST, NULL)); - // TODO: add a more extensible mechanism - json_t *response = NULL; - if (!strcmp (method, "ping")) - response = json_rpc_ping (ctx, params); - else + struct json_rpc_handler_info key = { .method_name = method }; + struct json_rpc_handler_info *handler = bsearch (&key, handlers, + N_ELEMENTS (handlers), sizeof key, json_rpc_handler_info_cmp); + if (!handler) return json_rpc_response (id, NULL, json_rpc_error (JSON_RPC_ERROR_METHOD_NOT_FOUND, NULL)); + json_t *response = handler->handler (ctx, params); if (id) return response; @@ -1808,11 +1850,11 @@ request_start (struct request *self, struct str_map *headers) static bool request_push (struct request *self, const void *data, size_t len) { - if (soft_assert (self->handler)) - return self->handler->push_cb (self, data, len); + if (!soft_assert (self->handler)) + // No handler, nothing to do with any data + return false; - // No handler, nothing to do with any data - return false; + return self->handler->push_cb (self, data, len); } // --- Requests handlers ------------------------------------------------------- @@ -1841,16 +1883,19 @@ request_handler_json_rpc_push { struct str *buf = request->handler_data; if (len) - str_append_data (buf, data, len); - else { - struct str response; - str_init (&response); - process_json_rpc (request->ctx, buf->str, buf->len, &response); - request->write_cb (request->user_data, response.str, response.len); - str_free (&response); + str_append_data (buf, data, len); + return true; } - return true; + + struct str response; + str_init (&response); + str_append (&response, "Status: 200 OK\n"); + str_append_printf (&response, "Content-Type: %s\n\n", "application/json"); + process_json_rpc (request->ctx, buf->str, buf->len, &response); + request->write_cb (request->user_data, response.str, response.len); + str_free (&response); + return false; } static void @@ -1872,7 +1917,58 @@ struct request_handler g_request_handler_json_rpc = // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// TODO: refactor this spaghetti-tier code +static char * +canonicalize_url_path (const char *path) +{ + struct str_vector v; + str_vector_init (&v); + split_str_ignore_empty (path, '/', &v); + + struct str_vector canonical; + str_vector_init (&canonical); + + // So that the joined path always begins with a slash + str_vector_add (&canonical, ""); + + for (size_t i = 0; i < v.len; i++) + { + const char *dir = v.vector[i]; + if (!strcmp (dir, ".")) + continue; + + if (strcmp (dir, "..")) + str_vector_add (&canonical, dir); + else if (canonical.len) + // ".." never goes above the root + str_vector_remove (&canonical, canonical.len - 1); + } + str_vector_free (&v); + + char *joined = join_str_vector (&canonical, '/'); + str_vector_free (&canonical); + return joined; +} + +static char * +detect_magic (const void *data, size_t len) +{ + magic_t cookie; + char *mime_type = NULL; + + if (!(cookie = magic_open (MAGIC_MIME))) + return NULL; + + const char *magic = NULL; + if (!magic_load (cookie, NULL) + && (magic = magic_buffer (cookie, data, len))) + mime_type = xstrdup (magic); + else + print_debug ("MIME type detection failed: %s", magic_error (cookie)); + + magic_close (cookie); + return mime_type; +} + static bool request_handler_static_try_handle (struct request *request, struct str_map *headers) @@ -1897,34 +1993,9 @@ request_handler_static_try_handle return false; } - struct str_vector v; - str_vector_init (&v); - split_str_ignore_empty (path_info, '/', &v); - - struct str_vector resolved; - str_vector_init (&resolved); - - // So that the joined path begins with a slash - str_vector_add (&resolved, ""); - // We need to filter the path to stay in our root // Being able to read /etc/passwd would be rather embarrasing - for (size_t i = 0; i < v.len; i++) - { - const char *dir = v.vector[i]; - if (!strcmp (dir, ".")) - continue; - - if (strcmp (dir, "..")) - str_vector_add (&resolved, dir); - else if (resolved.len) - str_vector_remove (&resolved, resolved.len - 1); - } - str_vector_free (&v); - - char *suffix = join_str_vector (&resolved, '/'); - str_vector_free (&resolved); - + char *suffix = canonicalize_url_path (path_info); char *path = xstrdup_printf ("%s%s", root, suffix); FILE *fp = fopen (path, "rb"); @@ -1953,22 +2024,9 @@ request_handler_static_try_handle // Try to detect the Content-Type from the actual contents char *mime_type = NULL; if ((len = fread (buf, 1, sizeof buf, fp))) - { - magic_t cookie; - const char *magic = NULL; - if ((cookie = magic_open (MAGIC_MIME))) - { - if (!magic_load (cookie, NULL) - && (magic = magic_buffer (cookie, buf, len))) - mime_type = xstrdup (magic); - magic_close (cookie); - } - } + mime_type = detect_magic (buf, len); if (!mime_type) - { - print_debug ("MIME type detection failed"); mime_type = xstrdup ("application/octet_stream"); - } struct str response; str_init (&response); @@ -2037,6 +2095,15 @@ struct client_impl /// Initialize the client as needed void (*init) (struct client *client); + // TODO: a method for graceful shutdown which will, in the case of + // WebSockets, actually send a "shutdown" close packet, and in the case + // of FastCGI will FCGI_END_REQUEST everything with FCGI_REQUEST_COMPLETE + // and FCGI_OVERLOADED all incoming requests in the meantime (the FastCGI + // specification isn't very clear about how we should respond to this). + // + // We then should set up a timer for about a second until we kill all + // clients for good. + /// Do any additional cleanup void (*destroy) (struct client *client); @@ -2370,28 +2437,23 @@ client_read_loop (EV_P_ struct client *client, ev_io *watcher) while (true) { ssize_t n_read = recv (watcher->fd, buf, sizeof buf, 0); - if (n_read < 0) + if (n_read >= 0) { - if (errno == EAGAIN) + if (!client->impl->push (client, buf, n_read)) + return false; + if (!n_read) break; - if (errno == EINTR) - continue; - - return false; } - - if (!client->impl->push (client, buf, n_read)) - return false; - - if (!n_read) - { - // Don't receive the EOF condition repeatedly - ev_io_stop (EV_A_ watcher); - - // We can probably still write, so let's just return + else if (errno == EAGAIN) return true; - } + else if (errno != EINTR) + return false; } + + // Don't receive the EOF condition repeatedly + ev_io_stop (EV_A_ watcher); + + // We can probably still write, so let's just return return true; } @@ -2402,15 +2464,18 @@ on_client_ready (EV_P_ ev_io *watcher, int revents) if (revents & EV_READ) if (!client_read_loop (EV_A_ client, watcher)) - goto error; + goto close; if (revents & EV_WRITE) + // TODO: shouldn't we at least provide an option (to be used by a client + // implementation if it so desires) to close the connection once we've + // finished flushing the write queue? This should probably even be + // the default behaviour, as it's fairly uncommon for clients to + // shutdown the socket for writes while leaving it open for reading. if (!flush_queue (&client->write_queue, watcher)) - goto error; + goto close; return; -error: - // The callback also could have just told us to stop reading, - // this is not necessarily an error condition +close: client_remove (client); } @@ -2479,7 +2544,7 @@ parse_config (struct server_context *ctx, struct error **e) } static int -listener_finish (struct addrinfo *gai_iter) +listener_bind (struct addrinfo *gai_iter) { int fd = socket (gai_iter->ai_family, gai_iter->ai_socktype, gai_iter->ai_protocol); @@ -2536,7 +2601,7 @@ listener_add (struct server_context *ctx, const char *host, const char *port, int fd; for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next) { - if ((fd = listener_finish (gai_iter)) == -1) + if ((fd = listener_bind (gai_iter)) == -1) continue; set_blocking (fd, false); @@ -2550,6 +2615,15 @@ listener_add (struct server_context *ctx, const char *host, const char *port, freeaddrinfo (gai_result); } +static void +get_ports_from_config (struct server_context *ctx, + const char *key, struct str_vector *out) +{ + const char *ports; + if ((ports = str_map_find (&ctx->config, key))) + split_str_ignore_empty (ports, ',', out); +} + static bool setup_listen_fds (struct server_context *ctx, struct error **e) { @@ -2559,20 +2633,15 @@ setup_listen_fds (struct server_context *ctx, struct error **e) .ai_flags = AI_PASSIVE, }; - const char *bind_host = str_map_find (&ctx->config, "bind_host"); - - const char *port_fcgi = str_map_find (&ctx->config, "port_fastcgi"); - const char *port_scgi = str_map_find (&ctx->config, "port_scgi"); - const char *port_ws = str_map_find (&ctx->config, "port_ws"); - struct str_vector ports_fcgi; str_vector_init (&ports_fcgi); struct str_vector ports_scgi; str_vector_init (&ports_scgi); struct str_vector ports_ws; str_vector_init (&ports_ws); - if (port_fcgi) split_str_ignore_empty (port_fcgi, ',', &ports_fcgi); - if (port_scgi) split_str_ignore_empty (port_scgi, ',', &ports_scgi); - if (port_ws) split_str_ignore_empty (port_ws, ',', &ports_ws); + get_ports_from_config (ctx, "port_fastcgi", &ports_fcgi); + get_ports_from_config (ctx, "port_scgi", &ports_scgi); + get_ports_from_config (ctx, "port_ws", &ports_ws); + const char *bind_host = str_map_find (&ctx->config, "bind_host"); size_t n_ports = ports_fcgi.len + ports_scgi.len + ports_ws.len; ctx->listeners = xcalloc (n_ports, sizeof *ctx->listeners); @@ -2615,6 +2684,8 @@ static void daemonize (void) { // TODO: create and lock a PID file? + // TODO: add the path for the PID file into "struct server_context", + // see the UNIX bible for more details on how to proceed. print_status ("daemonizing..."); if (chdir ("/"))