Compare commits

...

39 Commits

Author SHA1 Message Date
f9e157293c json-rpc-test-server: only return regular files
They can be symlinked.
2020-10-17 23:30:22 +02:00
42d1ff064f json-rpc-test-server: comment on some CGI details
There are some unresolved issues in the CGI clients
that needed a more precise description.
2020-10-17 23:09:29 +02:00
710f8e0b2d json-rpc-test-server: fix function names
Very obviously copied and pasted from the shell.
2020-10-16 23:55:15 +02:00
4938ee43bd json-rpc-test-server: try to send a 408
Also send "Connection: close" when we're closing the connection.

With HTTP/1.1 there come some responsibilities.

Surprisingly enough, the forward declaration is desirable
and the invocation a clean-up.
2020-10-15 04:59:01 +02:00
6927d022fb WebSocket: send a User-Agent header 2020-10-15 04:30:48 +02:00
75b2094cdd json-rpc-test-server: add a simple co-process mode
A disgusting copy-paste but it will have to do for now.

Closes #6
2020-10-15 03:20:20 +02:00
b3c377afdb json-rpc-test-server: WS: fix failures to upgrade
Similar to ad1aba9, only here we return 426 to the client.
2020-10-15 00:39:27 +02:00
4236a4943a WebSocket: adapt to common "await" infrastructure 2020-10-14 13:37:10 +02:00
23c728e535 Add a backend for co-processes
Targets language servers.

In this first stage, we don't need to support bi-directionality,
although it's a requirement for finishing this task.

Updates #4
2020-10-14 13:37:08 +02:00
dfe814316f This software is no longer simple 2020-10-14 13:36:35 +02:00
efc663a178 WebSocket: some clean-up 2020-10-14 12:25:22 +02:00
2b8f52ac72 Split out a http-parser wrapper 2020-10-14 12:25:22 +02:00
bb7ffe1da2 Simplify the FAIL macro 2020-10-14 12:25:21 +02:00
ad1aba9d22 WebSocket: fix upgrade processing
When http-parser sets the upgrade field, it checks for status code 101
and even resolves our TODO about checking the entire Connection header.
2020-10-14 09:44:46 +02:00
0107d09abc json-rpc-shell.adoc: document the M-Enter binding 2020-10-14 02:37:50 +02:00
01767198f2 WebSockets -> WebSocket
This is the correct name of the protocol, usage of the word
"WebSockets" should be limited.
2020-10-14 00:03:34 +02:00
5854ed1b32 Support reading OpenRPC documents from a file
Bump liberty, it generated incorrect help messages.
2020-10-13 21:48:24 +02:00
63c8a79479 Factor out init_backend()
The main() function is still way too long.
2020-10-13 21:07:40 +02:00
d489362a28 json-rpc-test-server: implement rpc.discover 2020-10-13 20:54:27 +02:00
c87869bef7 Cleanup
Prevent the last fuck-up from happening again.
2020-10-13 20:30:33 +02:00
fcf65f8377 Add libedit autocompletion back in
I've mistakenly removed it in the M-Enter change.
2020-10-13 20:19:19 +02:00
d820bc2f23 Bump version, update NEWS 2020-10-13 16:03:19 +02:00
b458fc1f99 libedit: bind M-Enter to newline-insert as well 2020-10-13 15:55:37 +02:00
0771c142fe json-rpc-test-server: fix reading the request URI 2020-10-13 04:46:08 +02:00
742632a931 Bump http-parser
Apparently it's reached maturity and there won't be any changes
anytime soon, making this the perfect time for an upgrade.
2020-10-13 04:35:42 +02:00
2221828763 OpenRPC: avoid eating HTTP/transport errors 2020-10-13 04:35:32 +02:00
c2a00511c0 Document OpenRPC tab completion support
Now that it's functional in both frontends, we can flaunt it.

I still don't want to make it the default.

Closes #1
2020-10-13 04:23:28 +02:00
2b18ebf314 Implement tab completion under libedit
I haven't tested it with real wide characters but it will have to do.
I wasn't even sure if this piece of crap could be coerced into doing
this at first, so it's a win for me.

It uses a variation of the code in degesch where we /don't/ want to
print the list of candidates on partial failure.

Updates #1
2020-10-13 03:58:26 +02:00
5d2cd01db0 json-rpc-test-server: fix a potential memory leak 2020-10-13 02:08:53 +02:00
ee79249d23 json-rpc-shell.adoc: update WebSocket notes
https://github.com/open-rpc/client-js also uses WebSockets,
although they don't seem to support notifications (in general).
2020-10-10 05:20:31 +02:00
160d23018a Bump liberty
resolve_relative_runtime_unique_filename() used to have a bug.
2020-10-10 05:09:11 +02:00
fed2892ee1 Readline: add trivial OpenRPC support
So far hidden under a switch and only for this frontend.
2020-10-10 05:09:10 +02:00
667b01cb73 Reorder help message entries a bit
Should be both more useful and more alphabetic this way.
2020-10-10 02:57:14 +02:00
20c8578084 Fix use of possibly uninitialised memory 2020-10-10 02:57:14 +02:00
57a3b4e990 Split make_json_rpc_call() in half 2020-10-10 02:57:13 +02:00
e4d1529b4d Slightly refactor make_json_rpc_call() 2020-10-10 02:57:13 +02:00
897a263ee7 Readline: make M-Enter insert a newline
Before, it was only possible with C-v C-j but it's too useful
to require such an awkward method.

There is a precedent in, e.g., zsh and fish for the new binding.
2020-10-09 20:41:37 +02:00
84702fa47d Fix handling terminal resizes while the terminal is suspended
GNU Readline has a misfeature.
2020-10-09 20:21:52 +02:00
b315892249 Readline: fix a dormant bug in prompt changes
For details, see a similar change in degesch from uirc3.
2020-10-09 20:17:17 +02:00
8 changed files with 1338 additions and 488 deletions

View File

@@ -14,7 +14,7 @@ endif ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
# Version
set (project_VERSION_MAJOR "1")
set (project_VERSION_MINOR "0")
set (project_VERSION_MINOR "1")
set (project_VERSION_PATCH "0")
set (project_VERSION "${project_VERSION_MAJOR}")
@@ -113,8 +113,7 @@ foreach (page ${project_MAN_PAGES})
endforeach (page)
# CPack
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY
"A shell for running JSON-RPC 2.0 queries")
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "A shell for JSON-RPC 2.0")
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")

11
NEWS
View File

@@ -1,3 +1,14 @@
1.1.0 (2020-10-13)
* Add method name tab completion using OpenRPC information
* Bind M-Enter to insert a newline into the command line
* json-rpc-test-server: fix a memory leak and request URI parsing
* Miscellaneous bug fixes
1.0.0 (2020-09-05)
* Initial release

View File

@@ -2,22 +2,25 @@ json-rpc-shell
==============
:compact-option:
'json-rpc-shell' is a simple shell for running JSON-RPC 2.0 queries.
'json-rpc-shell' is a shell for running JSON-RPC 2.0 queries.
This software has been created as a replacement for the following shell, which
is written in Java: http://software.dzhuvinov.com/json-rpc-2.0-shell.html
This software was originally created as a replacement for
http://software.dzhuvinov.com/json-rpc-2.0-shell.html[a different one] made by
Vladimir Dzhuvinov, in order to avoid Java, but has evolved since.
Features
--------
In addition to most of the features provided by Vladimir Dzhuvinov's shell
you get the following niceties:
In addition to most of the features provided by its predecessor, you will get
the following niceties:
- configurable JSON syntax highlight, which with prettyprinting turned on
helps you make sense of the results significantly
- ability to pipe output through a shell command, so that you can view the
results in your favourite editor or redirect them to a file
- ability to edit the input line in your favourite editor as well with Alt+E
- WebSockets (RFC 6455) can also be used as a transport rather than HTTP
- WebSocket (RFC 6455) can also be used as a transport rather than HTTP
- even Language Server Protocol servers may be launched as a slave command
- support for method name tab completion using OpenRPC discovery or file input
Documentation
-------------
@@ -62,8 +65,8 @@ Test server
-----------
If you install development packages for libmagic, an included test server will
be built but not installed which provides a trivial JSON-RPC 2.0 service with
FastCGI, SCGI, and WebSocket interfaces. It responds to `ping` and `date`
methods and it can serve static files.
FastCGI, SCGI, WebSocket and LSP-like co-process interfaces. It responds to
`ping` and `date`, supports OpenRPC discovery and it can serve static files.
Contributing and Support
------------------------

View File

@@ -6,11 +6,11 @@ json-rpc-shell(1)
Name
----
json-rpc-shell - a simple JSON-RPC 2.0 shell
json-rpc-shell - a shell for JSON-RPC 2.0
Synopsis
--------
*json-rpc-shell* [_OPTION_]... _ENDPOINT_
*json-rpc-shell* [_OPTION_]... { _ENDPOINT_ | _COMMAND_ [_ARG_]... }
Description
-----------
@@ -76,6 +76,15 @@ Protocol
*-o* _ORIGIN_, *--origin*=_ORIGIN_::
Set the HTTP Origin header to _ORIGIN_. Some servers may need this.
*-O*[__PATH__], *--openrpc*[**=**__PATH__]::
Call "rpc.discover" upon start-up in order to pull in OpenRPC data for
tab completion of method names. If a path is given, it is read from a file.
*-e*, *--execute*::
Rather than an _ENDPOINT_, accept a command line to execute and communicate
with using the JSON-RPC 2.0 protocol variation used in the Language Server
Protocol.
Program information
~~~~~~~~~~~~~~~~~~~
*-h*, *--help*::
@@ -107,14 +116,13 @@ requests, it is often convenient or even necessary to run a full text editor
in order to construct complex objects or arrays, and may even be used to import
data from elsewhere. You can launch an editor for the current request using
the M-e key combination. Both *readline*(3) and *editline*(7) also support
multiline editing natively, though you need to press C-v C-j in order to insert
multiline editing natively, press either M-Enter or C-v C-j in order to insert
newlines.
WebSockets
~~~~~~~~~~
WebSocket
~~~~~~~~~
The JSON-RPC 2.0 specification doesn't say almost anything about underlying
transports. As far as the author is aware, he is the only person combining it
with WebSockets. The way it's implemented here is that every request is sent as
transports. The way it's implemented here is that every request is sent as
a single text message. If it has an "id" field, i.e., it's not just
a notification, the client waits for a message from the server in response.
Should any message arrive unexpectedly, you will receive a warning.

File diff suppressed because it is too large Load Diff

View File

@@ -329,6 +329,7 @@ fcgi_muxer_on_get_values
nv_parser.output = &values;
fcgi_nv_parser_push (&nv_parser, parser->content.str, parser->content.len);
fcgi_nv_parser_free (&nv_parser);
const char *key = NULL;
// No real-world servers seem to actually use multiplexing
@@ -524,11 +525,11 @@ fcgi_muxer_push (struct fcgi_muxer *self, const void *data, size_t len)
}
/// @}
// --- WebSockets --------------------------------------------------------------
/// @defgroup WebSockets
// --- WebSocket ---------------------------------------------------------------
/// @defgroup WebSocket
/// @{
// WebSockets aren't CGI-compatible, therefore we must handle the initial HTTP
// WebSocket isn't CGI-compatible, therefore we must handle the initial HTTP
// handshake ourselves. Luckily it's not too much of a bother with http-parser.
// Typically there will be a normal HTTP server in front of us, proxying the
// requests based on the URI.
@@ -536,7 +537,7 @@ fcgi_muxer_push (struct fcgi_muxer *self, const void *data, size_t len)
enum ws_handler_state
{
WS_HANDLER_CONNECTING, ///< Parsing HTTP
WS_HANDLER_OPEN, ///< Parsing WebSockets frames
WS_HANDLER_OPEN, ///< Parsing WebSocket frames
WS_HANDLER_CLOSING, ///< Partial closure by us
WS_HANDLER_FLUSHING, ///< Just waiting for client EOF
WS_HANDLER_CLOSED ///< Dead, both sides closed
@@ -850,6 +851,17 @@ ws_handler_on_close_timeout (EV_P_ ev_timer *watcher, int revents)
self->close_cb (self, false /* half_close */);
}
static bool ws_handler_fail_handshake (struct ws_handler *self,
const char *status, ...) ATTRIBUTE_SENTINEL;
#define HTTP_101_SWITCHING_PROTOCOLS "101 Switching Protocols"
#define HTTP_400_BAD_REQUEST "400 Bad Request"
#define HTTP_405_METHOD_NOT_ALLOWED "405 Method Not Allowed"
#define HTTP_408_REQUEST_TIMEOUT "408 Request Timeout"
#define HTTP_417_EXPECTATION_FAILED "407 Expectation Failed"
#define HTTP_426_UPGRADE_REQUIRED "426 Upgrade Required"
#define HTTP_505_VERSION_NOT_SUPPORTED "505 HTTP Version Not Supported"
static void
ws_handler_on_handshake_timeout (EV_P_ ev_timer *watcher, int revents)
{
@@ -857,13 +869,7 @@ ws_handler_on_handshake_timeout (EV_P_ ev_timer *watcher, int revents)
(void) revents;
struct ws_handler *self = watcher->data;
// XXX: this is a no-op, since this currently doesn't even call shutdown
// immediately but postpones it until later
self->close_cb (self, true /* half_close */);
self->state = WS_HANDLER_FLUSHING;
if (self->on_close)
self->on_close (self, WS_STATUS_ABNORMAL_CLOSURE, "handshake timeout");
ws_handler_fail_handshake (self, HTTP_408_REQUEST_TIMEOUT, NULL);
self->state = WS_HANDLER_CLOSED;
self->close_cb (self, false /* half_close */);
@@ -1002,9 +1008,10 @@ ws_handler_on_headers_complete (http_parser *parser)
if (self->have_header_value)
ws_handler_on_header_read (self);
// We strictly require a protocol upgrade
// We require a protocol upgrade. 1 is for "skip body", 2 is the same
// + "stop processing", return another number to indicate a problem here.
if (!parser->upgrade)
return 2;
return 3;
return 0;
}
@@ -1013,17 +1020,10 @@ static int
ws_handler_on_url (http_parser *parser, const char *at, size_t len)
{
struct ws_handler *self = parser->data;
str_append_data (&self->value, at, len);
str_append_data (&self->url, at, len);
return 0;
}
#define HTTP_101_SWITCHING_PROTOCOLS "101 Switching Protocols"
#define HTTP_400_BAD_REQUEST "400 Bad Request"
#define HTTP_405_METHOD_NOT_ALLOWED "405 Method Not Allowed"
#define HTTP_417_EXPECTATION_FAILED "407 Expectation Failed"
#define HTTP_426_UPGRADE_REQUIRED "426 Upgrade Required"
#define HTTP_505_VERSION_NOT_SUPPORTED "505 HTTP Version Not Supported"
static void
ws_handler_http_responsev (struct ws_handler *self,
const char *status, char *const *fields)
@@ -1065,6 +1065,7 @@ ws_handler_fail_handshake (struct ws_handler *self, const char *status, ...)
struct strv v = strv_make ();
while ((s = va_arg (ap, const char *)))
strv_append (&v, s);
strv_append (&v, "Connection: close");
va_end (ap);
ws_handler_http_responsev (self, status, v.vector);
@@ -1109,7 +1110,7 @@ ws_handler_finish_handshake (struct ws_handler *self)
if (!connection || strcasecmp_ascii (connection, "Upgrade"))
FAIL_HANDSHAKE (HTTP_400_BAD_REQUEST);
// Check if we can actually upgrade the protocol to WebSockets
// Check if we can actually upgrade the protocol to WebSocket
const char *upgrade = str_map_find (&self->headers, "Upgrade");
struct http_protocol *offered_upgrades = NULL;
bool can_upgrade = false;
@@ -1267,11 +1268,13 @@ ws_handler_push (struct ws_handler *self, const void *data, size_t len)
ev_timer_stop (EV_DEFAULT_ &self->handshake_timeout_watcher);
if (err == HPE_CB_headers_complete)
{
print_debug ("WS handshake failed: %s", "missing `Upgrade' field");
else
print_debug ("WS handshake failed: %s",
http_errno_description (err));
FAIL_HANDSHAKE (HTTP_426_UPGRADE_REQUIRED,
"Upgrade: websocket", SEC_WS_VERSION ": 13");
}
print_debug ("WS handshake failed: %s", http_errno_description (err));
FAIL_HANDSHAKE (HTTP_400_BAD_REQUEST);
}
return true;
@@ -1285,7 +1288,7 @@ static struct simple_config_item g_config_table[] =
{ "bind_host", NULL, "Address of the server" },
{ "port_fastcgi", "9000", "Port to bind for FastCGI" },
{ "port_scgi", NULL, "Port to bind for SCGI" },
{ "port_ws", NULL, "Port to bind for WebSockets" },
{ "port_ws", NULL, "Port to bind for WebSocket" },
{ "pid_file", NULL, "Full path for the PID file" },
// XXX: here belongs something like a web SPA that interfaces with us
{ "static_root", NULL, "The root for static content" },
@@ -1445,6 +1448,38 @@ json_rpc_handler_info_cmp (const void *first, const void *second)
((struct json_rpc_handler_info *) second)->method_name);
}
static json_t *
open_rpc_describe (const char *method, json_t *result)
{
return json_pack ("{sssoso}", "name", method, "params", json_pack ("[]"),
"result", json_pack ("{ssso}", "name", method, "schema", result));
}
// This server rarely sees changes and we can afford to hardcode the schema
static json_t *
json_rpc_discover (struct server_context *ctx, json_t *params)
{
(void) ctx;
(void) params;
json_t *info = json_pack ("{ssss}",
"title", PROGRAM_NAME, "version", PROGRAM_VERSION);
json_t *methods = json_pack ("[ooo]",
open_rpc_describe ("date", json_pack ("{ssso}", "type", "object",
"properties", json_pack ("{s{ss}s{ss}s{ss}s{ss}s{ss}s{ss}}",
"year", "type", "number",
"month", "type", "number",
"day", "type", "number",
"hours", "type", "number",
"minutes", "type", "number",
"seconds", "type", "number"))),
open_rpc_describe ("ping", json_pack ("{ss}", "type", "string")),
open_rpc_describe ("rpc.discover", json_pack ("{ss}", "$ref",
"https://github.com/open-rpc/meta-schema/raw/master/schema.json")));
return json_rpc_response (NULL, json_pack ("{sssoso}",
"openrpc", "1.2.6", "info", info, "methods", methods), NULL);
}
static json_t *
json_rpc_ping (struct server_context *ctx, json_t *params)
{
@@ -1486,8 +1521,9 @@ process_json_rpc_request (struct server_context *ctx, json_t *request)
// Eventually it might be better to move this into a map in the context.
static struct json_rpc_handler_info handlers[] =
{
{ "date", json_rpc_date },
{ "ping", json_rpc_ping },
{ "date", json_rpc_date },
{ "ping", json_rpc_ping },
{ "rpc.discover", json_rpc_discover },
};
if (!json_is_object (request))
@@ -1544,7 +1580,6 @@ static void
process_json_rpc (struct server_context *ctx,
const void *data, size_t len, struct str *output)
{
json_error_t e;
json_t *request;
if (!(request = json_loadb (data, len, JSON_DECODE_ANY, &e)))
@@ -1619,15 +1654,37 @@ struct request_handler
LIST_HEADER (struct request_handler)
/// Install ourselves as the handler for the request, if applicable.
/// If the request contains data, check it against CONTENT_LENGTH.
/// ("Transfer-Encoding: chunked" should be dechunked by the HTTP server,
/// however it is possible that it mishandles this situation.)
/// Sets @a continue_ to false if further processing should be stopped,
/// meaning the request has already been handled.
/// Note that starting the response before receiving all data denies you
/// the option of returning error status codes based on the data.
bool (*try_handle) (struct request *request,
struct str_map *headers, bool *continue_);
/// Handle incoming data. "len == 0" means EOF.
/// Returns false if there is no more processing to be done.
// FIXME: the EOF may or may not be delivered when request is cut short,
// we should fix FastCGI not to deliver it on CONTENT_LENGTH mismatch
/// EOF is never delivered on a network error (see client_read_loop()).
// XXX: the EOF may or may not be delivered when the request is cut short:
// - client_scgi delivers an EOF when it itself receives an EOF without
// considering any mismatch, and it can deliver another one earlier
// when the counter just goes down to 0... depends on what we return
// from here upon the first occasion (whether we want to close).
// - FCGI_ABORT_REQUEST /might/ not close the stdin and it /might/ cover
// a CONTENT_LENGTH mismatch, since this callback wouldn't get invoked.
// The FastCGI specification explicitly says to compare CONTENT_LENGTH
// against the number of received bytes, which may only be smaller.
//
// We might want to adjust client_scgi and client_fcgi to not invoke
// request_push(EOF) when CONTENT_LENGTH hasn't been reached and remove
// the extra EOF generation from client_scgi (why is it there, does the
// server keep the connection open, or is it just a precaution?)
//
// The finalization callback takes care of any needs to destruct data.
// If we handle this reliably in all clients, try_handle won't have to,
// as it will run in a stricter-than-CGI scenario.
bool (*push_cb) (struct request *request, const void *data, size_t len);
/// Destroy the handler's data stored in the request object
@@ -1749,7 +1806,9 @@ request_handler_json_rpc_push
// TODO: check buf.len against CONTENT_LENGTH; if it's less, then the
// client hasn't been successful in transferring all of its data.
// See also comment on request_handler::push_cb.
// See also comment on request_handler::push_cb. For JSON-RPC, though,
// it shouldn't matter as an incomplete request will be invalid and
// clients have no reason to append unnecessary trailing bytes.
struct str response = str_make ();
str_append (&response, "Status: 200 OK\n");
@@ -1866,8 +1925,13 @@ request_handler_static_try_handle
char *path = xstrdup_printf ("%s%s", root, suffix);
print_debug ("trying to statically serve %s", path);
// TODO: check that this is a regular file
FILE *fp = fopen (path, "rb");
struct stat st = {};
if (fp && !fstat (fileno (fp), &st) && !S_ISREG (st.st_mode))
{
fclose (fp);
fp = NULL;
}
if (!fp)
{
struct str response = str_make ();
@@ -1912,8 +1976,8 @@ request_handler_static_try_handle
request_write (request, buf, len);
fclose (fp);
// TODO: this should rather not be returned all at once but in chunks;
// file read requests never return EAGAIN
// TODO: this should rather not be returned all at once but in chunks
// (consider Transfer-Encoding); file read requests never return EAGAIN
// TODO: actual file data should really be returned by a callback when
// the socket is writable with nothing to be sent (pumping the entire
// file all at once won't really work if it's huge).
@@ -2047,6 +2111,8 @@ static void
client_shutdown (struct client *self)
{
self->flushing = true;
// In case this shutdown is immediately followed by a close, try our best
(void) flush_queue (&self->write_queue, self->socket_fd);
ev_feed_event (EV_DEFAULT_ &self->write_watcher, EV_WRITE);
}
@@ -2358,14 +2424,15 @@ client_scgi_on_content (void *user_data, const void *data, size_t len)
print_debug ("SCGI request got more data than CONTENT_LENGTH");
return false;
}
// We're in a slight disagreement with the specification since
// We're in a slight disagreement with the SCGI specification since
// this tries to write output before it has read all the input
if (!request_push (&self->request, data, len))
return false;
if ((self->remaining_content -= len))
return true;
// Signalise end of input to the request handler
return (self->remaining_content -= len) != 0
|| request_push (&self->request, NULL, 0);
return request_push (&self->request, NULL, 0);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -2418,12 +2485,12 @@ client_scgi_create (EV_P_ int sock_fd)
return &self->client;
}
// --- WebSockets client handler -----------------------------------------------
// --- WebSocket client handler ------------------------------------------------
struct client_ws
{
struct client client; ///< Parent class
struct ws_handler handler; ///< WebSockets connection handler
struct ws_handler handler; ///< WebSocket connection handler
};
static bool
@@ -2514,6 +2581,165 @@ client_ws_create (EV_P_ int sock_fd)
return &self->client;
}
// --- Co-process client -------------------------------------------------------
// This is mostly copied over from json-rpc-shell.c, only a bit simplified.
// We're giving up on header parsing in order to keep this small.
struct co_context
{
struct server_context *ctx; ///< Server context
struct str message; ///< Message data
struct http_parser parser; ///< HTTP parser
bool pending_fake_starter; ///< Start of message?
};
static int
client_co_on_message_begin (http_parser *parser)
{
struct co_context *self = parser->data;
str_reset (&self->message);
return 0;
}
static int
client_co_on_body (http_parser *parser, const char *at, size_t len)
{
struct co_context *self = parser->data;
str_append_data (&self->message, at, len);
return 0;
}
static int
client_co_on_message_complete (http_parser *parser)
{
struct co_context *self = parser->data;
http_parser_pause (&self->parser, true);
return 0;
}
// The LSP incorporates a very thin subset of RFC 822, and it so happens
// that we may simply reuse the full HTTP parser here, with a small hack.
static const http_parser_settings client_co_http_settings =
{
.on_message_begin = client_co_on_message_begin,
.on_body = client_co_on_body,
.on_message_complete = client_co_on_message_complete,
};
static void
client_co_respond (const struct str *buf)
{
struct str wrapped = str_make();
str_append_printf (&wrapped,
"Content-Length: %zu\r\n"
"Content-Type: application/json; charset=utf-8\r\n"
"\r\n", buf->len);
str_append_data (&wrapped, buf->str, buf->len);
if (write (STDOUT_FILENO, wrapped.str, wrapped.len)
!= (ssize_t) wrapped.len)
exit_fatal ("write: %s", strerror (errno));
str_free (&wrapped);
}
static void
client_co_inject_starter (struct co_context *self)
{
// The default "Connection: keep-alive" maps well here.
// We cannot feed this line into the parser from within callbacks.
static const char starter[] = "POST / HTTP/1.1\r\n";
http_parser_pause (&self->parser, false);
size_t n_parsed = http_parser_execute (&self->parser,
&client_co_http_settings, starter, sizeof starter - 1);
enum http_errno err = HTTP_PARSER_ERRNO (&self->parser);
if (n_parsed != sizeof starter - 1 || err != HPE_OK)
exit_fatal ("protocol failure: %s", http_errno_description (err));
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
client_co_process (struct co_context *self)
{
struct str *message = &self->message;
struct str response = str_make ();
process_json_rpc (self->ctx, message->str, message->len, &response);
if (response.len)
client_co_respond (&response);
str_free (&response);
}
static void
client_co_parse (struct co_context *self, const char *data, size_t len,
size_t *n_parsed)
{
if (self->pending_fake_starter)
{
self->pending_fake_starter = false;
client_co_inject_starter (self);
}
*n_parsed = http_parser_execute
(&self->parser, &client_co_http_settings, data, len);
if (self->parser.upgrade)
exit_fatal ("protocol failure: %s", "unsupported upgrade attempt");
enum http_errno err = HTTP_PARSER_ERRNO (&self->parser);
if (err == HPE_PAUSED)
{
self->pending_fake_starter = true;
client_co_process (self);
}
else if (err != HPE_OK)
exit_fatal ("protocol failure: %s", http_errno_description (err));
}
static void
client_co_on_data (struct co_context *self, const char *data, size_t len)
{
size_t n_parsed = 0;
do
{
client_co_parse (self, data, len, &n_parsed);
data += n_parsed;
}
while ((len -= n_parsed));
}
static void
client_co_run (struct server_context *ctx)
{
struct co_context self = {};
self.ctx = ctx;
self.message = str_make ();
http_parser_init (&self.parser, HTTP_REQUEST);
self.parser.data = &self;
self.pending_fake_starter = true;
hard_assert (set_blocking (STDIN_FILENO, false));
struct str buf = str_make ();
struct pollfd pfd = { .fd = STDIN_FILENO, .events = POLLIN };
while (true)
{
if (poll (&pfd, 1, -1) <= 0)
exit_fatal ("poll: %s", strerror (errno));
str_remove_slice (&buf, 0, buf.len);
enum socket_io_result result = socket_io_try_read (pfd.fd, &buf);
int errno_saved = errno;
if (buf.len)
client_co_on_data (&self, buf.str, buf.len);
if (result == SOCKET_IO_ERROR)
exit_fatal ("read: %s", strerror (errno_saved));
if (result == SOCKET_IO_EOF)
break;
}
str_free (&buf);
str_free (&self.message);
}
// --- Basic server stuff ------------------------------------------------------
typedef struct client *(*client_create_fn) (EV_P_ int sock_fd);
@@ -2877,11 +3103,12 @@ daemonize (struct server_context *ctx)
}
static void
parse_program_arguments (int argc, char **argv)
parse_program_arguments (int argc, char **argv, bool *running_as_slave)
{
static const struct opt opts[] =
{
{ 't', "test", NULL, 0, "self-test" },
{ 's', "slave", NULL, 0, "co-process mode" },
{ 'd', "debug", NULL, 0, "run in debug mode" },
{ 'h', "help", NULL, 0, "display this help and exit" },
{ 'V', "version", NULL, 0, "output version information and exit" },
@@ -2901,6 +3128,9 @@ parse_program_arguments (int argc, char **argv)
case 't':
test_main (argc, argv);
exit (EXIT_SUCCESS);
case 's':
*running_as_slave = true;
break;
case 'd':
g_debug_mode = true;
break;
@@ -2933,7 +3163,8 @@ parse_program_arguments (int argc, char **argv)
int
main (int argc, char *argv[])
{
parse_program_arguments (argc, argv);
bool running_as_a_slave = false;
parse_program_arguments (argc, argv, &running_as_a_slave);
print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting");
@@ -2948,6 +3179,15 @@ main (int argc, char *argv[])
exit (EXIT_FAILURE);
}
// There's a lot of unnecessary left-over scaffolding in this program,
// for testing purposes assume that everything is synchronous
if (running_as_a_slave)
{
client_co_run (&ctx);
server_context_free (&ctx);
return EXIT_SUCCESS;
}
struct ev_loop *loop;
if (!(loop = EV_DEFAULT))
exit_fatal ("libev initialization failed");

Submodule liberty updated: 1a76b2032e...69101eb155