diff --git a/CMakeLists.txt b/CMakeLists.txt index e00317bd0..c619f3c51 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -264,6 +264,7 @@ option(LWS_WITH_CBOR_FLOAT "Build floating point types if building CBOR LECP" ON option(LWS_WITH_SQLITE3 "Require SQLITE3 support" OFF) option(LWS_WITH_STRUCT_JSON "Generic struct serialization to and from JSON" OFF) option(LWS_WITH_STRUCT_SQLITE3 "Generic struct serialization to and from SQLITE3" OFF) +option(LWS_WITH_JSONRPC "JSON RPC support" ON) # broken atm #option(LWS_WITH_SMTP "Provide SMTP support" OFF) if (LWS_WITH_ESP32) @@ -960,7 +961,6 @@ list(APPEND LIB_LIST ${LIB_LIST_AT_END}) # include_directories("${PROJECT_SOURCE_DIR}/lib") - add_subdirectory(lib) diff --git a/cmake/lws_config.h.in b/cmake/lws_config.h.in index 26b349f5b..bfc591d4c 100644 --- a/cmake/lws_config.h.in +++ b/cmake/lws_config.h.in @@ -176,6 +176,7 @@ #cmakedefine LWS_WITH_JOSE #cmakedefine LWS_WITH_CBOR #cmakedefine LWS_WITH_CBOR_FLOAT +#cmakedefine LWS_WITH_JSONRPC #cmakedefine LWS_WITH_LEJP #cmakedefine LWS_WITH_LIBEV #cmakedefine LWS_WITH_LIBEVENT diff --git a/include/libwebsockets.h b/include/libwebsockets.h index 9f529fbab..793cef574 100644 --- a/include/libwebsockets.h +++ b/include/libwebsockets.h @@ -670,6 +670,7 @@ struct lws; #include #include #include +#include #if !defined(LWS_PLAT_FREERTOS) #include diff --git a/include/libwebsockets/lws-jrpc.h b/include/libwebsockets/lws-jrpc.h new file mode 100644 index 000000000..88f564e96 --- /dev/null +++ b/include/libwebsockets/lws-jrpc.h @@ -0,0 +1,229 @@ +/* + * libwebsockets - small server side websockets and web server implementation + * + * Copyright (C) 2010 - 2020 Andy Green + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + * + * This is a JSON-RPC parser and state management implementation that's: + * + * - Lightweight, it uses lws LEJP JSON stream parser for requests, responses, + * and user-defined parameter objects + * + * - Stateful... you can give it sequential input buffers randomly fragmented + * and it will complete when it has enough + * + * - Asynchronous... response processing can return to the event loop both + * while the RX is still coming and after it's all received before forming + * the response, eg, because it's querying on a remote connection to get the + * response data. Any number of RPCs can be either in flight or waiting for + * response processing to complete before responding. + * + * - Supports "version" extension + * + * - allows binding different method names to different callbacks + * + * - Supports both client and server roles, eg, can parse both requests and + * responses + * + * - No support for batch. Batching is not widely used because it doesn't + * add anything for the vast bulk of cases compared to sending n requests. + * + * This handles client and server RX and transaction state, creating a callback + * when parameters can be parsed and all of the request or notification is + * done. + * + * Producing JSON is usually simpler and more compact than expressing it as an + * object model, ie often a response can be completely formed in a single + * lws_snprintf(). Response JSON must be buffered on heap until the method + * callback is called with NULL / 0 buf len indicating that the incoming request + * has completed parsing. + * + */ + +/* these are opaque */ + +struct lws_jrpc_obj; +struct lws_jrpc; + +typedef enum { + LJRPC_CBRET_CONTINUE, + LJRPC_CBRET_WANT_TO_EMIT, + LJRPC_CBRET_FINISHED, + LJRPC_CBRET_FAILED +} lws_jrpc_cb_return_t; + +/* + * method name to lejp parsing handler map + */ + +typedef struct lws_jrpc_method { + const char *method_name; + const char * const *paths; + lejp_callback cb; + int count_paths; +} lws_jrpc_method_t; + +/* + * Boilerplate for forming correct requests + */ + +/* Boilerplate to start a request */ +#define LWSJRPCBP_REQ_START_S "{\"jsonrpc\":\"2.0\",\"method\":\"%s\"" +/* Boilerplate to start parameters (params are left freeform for user) */ +#define LWSJRPCBP_REQ_VERSION_S ",\"version\":\"%s\"" +/* Boilerplate to start parameters (params are left freeform for user) */ +#define LWSJRPCBP_REQ_PARAMS ",\"params\":" +/* Boilerplate to complete the result object */ +#define LWSJRPCBP_REQ_NOTIF_END "}" +/* Boilerplate to complete the result object */ +#define LWSJRPCBP_REQ_ID_END_S ",\"id\":%s}" + +/* + * Boilerplate for forming correct responses + */ + +/* Boilerplate to start a result */ +#define LWSJRPCBP_RESP_RESULT "{\"jsonrpc\":\"2.0\",\"result\":" +/* Boilerplate to complete the result object */ +#define LWSJRPCBP_RESP_ID_END_S ",\"id\":%s}" + +/* Boilerplate to form an error */ +#define LWSJRPCBP_RESP_ERROR_D "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":%d" +/* optional */ +#define LWSJRPCBP_RESP_ERROR_MSG_S ",\"message\":\"%s\"" +/* optional */ +#define LWSJRPCBP_RESP_ERROR_DATA ",\"data\":" +/* required */ +#define LWSJRPCBP_RESP_ERROR_END "}" + +/* + * JSONRPC Well-known Errors + */ + +enum { + LWSJRPCE__NO_ERROR = 0, + + LWSJRPCWKE__PARSE_ERROR = -32700, /* invalid JSON */ + LWSJRPCWKE__INVALID_REQUEST = -32600, /* not valid JSONRPC object */ + LWSJRPCWKE__METHOD_NOT_FOUND = -32601, /* method not supported */ + LWSJRPCWKE__INVALID_PARAMS = -32602, /* parameters are invalid */ + LWSJRPCWKE__INTERNAL_ERROR = -32603, /* internal JSONRPC error */ + LWSJRPCWKE__SERVER_ERROR_FIRST = -32000, /* implementation-defined...*/ + LWSJRPCWKE__SERVER_ERROR_LAST = -32099, /* ... server errors range */ + + LWSJRPCE__INVALID_MEMBERS = -31000, /* reponse membs in req, vv */ +}; + +enum { + LWSJRPC_PARSE_REQUEST, + LWSJRPC_PARSE_RESPONSE +}; + +/* + * APIs for the opaque JRPC request object + */ + +/** + * lws_jrpc_obj_parse() - parse a request or response + * + * \param jrpc: the jrpc context this belongs to + * \param type: LWSJRPC_PARSE_REQUEST or ..._RESPONSE + * \param opaque: user-defined pointer bound to lws_jrpc, ignored by lws + * \param buf: chunk of JSON-RPC + * \param l: remaining length of JSON (may be under or oversize) + * \param r: NULL to indicate starting new req, already set means continue parse + * + * If necessary creates an opaque req object and starts parsing len bytes of + * buf. This may be undersize (more parts coming) in which case \p req will be + * set on entry next time indicating a continuation. + * + * \p type and \p opaque are ignored if it it's not the first buffer that + * creates the req object. + * + * Return code is >= 0 if completed, representing the amount of unused data in + * the input buffer. -1 indicates more input data needed, <-1 indicates an + * error from the LWSJRPCWKE_ set above + */ + +LWS_VISIBLE LWS_EXTERN int +lws_jrpc_obj_parse(struct lws_jrpc *jrpc, int type, void *opaque, + const char *buf, size_t l, struct lws_jrpc_obj **r); + +/* + * lws_jrpc_obj_destroy() - detach and destroy a JRPC request or response + * + * \param _r: pointer to pointer to JRPC request to detach and free + * + * Detaches the req from its JRPC context and frees it and any internal + * allocations. + */ +LWS_VISIBLE LWS_EXTERN void +lws_jrpc_obj_destroy(struct lws_jrpc_obj **_r); + +/* + * lws_jrpc_obj_get_opaque() - retreive the opaque pointer bound to the req + * + * \param r: pointer to pointer to JRPC request + * + * Returns the opaque pointer for a req given when it was parsed / created. + */ +LWS_VISIBLE LWS_EXTERN void * +lws_jrpc_obj_get_opaque(const struct lws_jrpc_obj *r); + +/* + * lws_jrpc_obj_id() - retreive the object's id string + * + * \param r: pointer to pointer to JRPC object + * + * Returns a pointer to a correctly-typed id for use in a response; if a string, + * then it is already quoted, if an int or null then it's provided without + * quotes. + */ +LWS_VISIBLE LWS_EXTERN const char * +lws_jrpc_obj_id(const struct lws_jrpc_obj *r); + + +/* + * APIs for the opaque JRPC context + */ + +/** + * lws_jrpc_create() - Allocate and initialize a JRPC context + * + * \param methods: the method callbacks and names we can process + * \param opaque: user-defined pointer bound to lws_jrpc ignored by lws + * + * Allocates an opaque lws_jrpc object and binds it to the given array of + * method names and callbacks + */ +LWS_VISIBLE LWS_EXTERN struct lws_jrpc * +lws_jrpc_create(const lws_jrpc_method_t *methods, void *opaque); + +/* + * lws_jrpc_destroy() - destroy an allocated JRPC context + * + * \param jrpc: pointer to pointer to jrpc to destroy + * + * Destroys any ongoing reqs in the JRPC and then destroys the JRPC and sets the + * given pointer to NULL. + */ +LWS_VISIBLE LWS_EXTERN void +lws_jrpc_destroy(struct lws_jrpc **jrpc); diff --git a/include/libwebsockets/lws-lejp.h b/include/libwebsockets/lws-lejp.h index f9f50270d..c44431edf 100644 --- a/include/libwebsockets/lws-lejp.h +++ b/include/libwebsockets/lws-lejp.h @@ -108,6 +108,8 @@ enum lejp_callbacks { LEJPCB_OBJECT_START = 16, LEJPCB_OBJECT_END = 17, + + LEJPCB_USER_START = 32, }; /** diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index b00539978..2156979f9 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -148,6 +148,7 @@ endif() if (LWS_WITH_JOSE) add_subdir_include_dirs(jose) endif() + if (LWS_WITH_COSE) add_subdir_include_dirs(cose) endif() diff --git a/lib/misc/CMakeLists.txt b/lib/misc/CMakeLists.txt index c4d0c3b54..7a1293ae1 100644 --- a/lib/misc/CMakeLists.txt +++ b/lib/misc/CMakeLists.txt @@ -67,6 +67,12 @@ if (LWS_WITH_STRUCT_JSON) misc/lws-struct-lejp.c) endif() +if (LWS_WITH_JSONRPC) + list(APPEND SOURCES + misc/jrpc/jrpc.c) + include_directories(misc/jrpc) +endif() + if (LWS_WITH_STRUCT_SQLITE3) list(APPEND SOURCES misc/lws-struct-sqlite.c) diff --git a/lib/misc/jrpc/jrpc.c b/lib/misc/jrpc/jrpc.c new file mode 100644 index 000000000..3c8cb95d1 --- /dev/null +++ b/lib/misc/jrpc/jrpc.c @@ -0,0 +1,386 @@ +/* + * libwebsockets - small server side websockets and web server implementation + * + * Copyright (C) 2010 - 2020 Andy Green + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + * We use the lejp parse stack to replace the callback context for JSON + * subtrees. + * + * It's optionally done when we see we're in a [] batch of reqs, we pass each + * unitary req to the internal req parser. + * + * Each req does it to hand off the parsing of the parameters section. + */ + +#include +#include "private-lib-misc-jrpc.h" + +static const char * const paths[] = { + "jsonrpc", + "method", + "version", + "params", + "id", + /* only for responses --> */ + "result", + "error", + "code", + "message", + "data", +}; + +enum enum_paths { + LEJPN_JSONRPC, + LEJPN_METHOD, + LEJPN_VERSION, + LEJPN_PARAMS, + LEJPN_ID, + /* only for responses --> */ + LEJPN_RESULT, + LEJPN_ERROR, + LEJPN_E_CODE, + LEJPN_E_MESSAGE, + LEJPN_E_DATA, +}; + +/* + * Get the registered handler for a method name... a registered handler for + * a NULL method name matches any other unmatched name. + */ + +static const lws_jrpc_method_t * +lws_jrpc_method_lookup(lws_jrpc_t *jrpc, const char *method_name) +{ + const lws_jrpc_method_t *m = jrpc->methods, *m_null = NULL; + + while (1) { + + if (!m->method_name) + return m; + + if (!strcmp(method_name, m->method_name)) + return m; + + m++; + } + + return m_null; +} + +static signed char +req_cb(struct lejp_ctx *ctx, char reason) +{ + lws_jrpc_obj_t *r = (lws_jrpc_obj_t *)ctx->user; + lws_jrpc_t *jrpc; + char *p; + + lwsl_warn("%s: %d '%s' %s (sp %d, pst_sp %d)\n", __func__, reason, ctx->path, ctx->buf, ctx->sp, ctx->pst_sp); + + if (reason == LEJPCB_PAIR_NAME && ctx->path_match - 1 == LEJPN_PARAMS) { + + if (r->response) + goto fail_invalid_members; + /* + * Params are a wormhole to another LEJP parser context to deal + * with, chosen based on the method name and the callbacks + * associated with that at init time. + * + * Params may be provided in a toplevel array, called a "batch", + * these are treated as n independent subrequests to be handled + * sequentially, and if the request is parseable, the scope of + * errors is only the current batch entry. + */ + + jrpc = lws_container_of(r->list.owner, lws_jrpc_t, req_owner); + r->pmethod = lws_jrpc_method_lookup(jrpc, r->method); + if (!r->pmethod || !r->pmethod->cb) + /* + * There's nothing we can do with no method binding, or + * one that lacks a callback... + */ + goto fail_method_not_found; + + r->inside_params = 1; + + lwsl_notice("%s: params: entering subparser\n", __func__); + lejp_parser_push(ctx, r, r->pmethod->paths, + (uint8_t)r->pmethod->count_paths, r->pmethod->cb); + } + + if (reason == LEJPCB_COMPLETE && !r->response) { + if (!r->has_jrpc_member) + goto fail_invalid_request; + if (r->method[0] && !r->pmethod) { + jrpc = lws_container_of(r->list.owner, lws_jrpc_t, + req_owner); + r->pmethod = lws_jrpc_method_lookup(jrpc, r->method); + if (!r->pmethod || !r->pmethod->cb) + /* + * There's nothing we can do with no method + * binding, or one that lacks a callback... + */ + goto fail_method_not_found; + } + + /* + * Indicate that the whole of the request has been parsed now + * and the id is known, so the method can complete and finalize + * its response + */ + r->pmethod->cb(ctx, LEJPCB_USER_START); + + return 0; + } + + /* we only match on the prepared path strings */ + if (!(reason & LEJP_FLAG_CB_IS_VALUE) || !ctx->path_match) + return 0; + + if (ctx->path_match - 1 >= LEJPN_RESULT && !r->response) + goto fail_invalid_members; + + switch (ctx->path_match - 1) { + case LEJPN_JSONRPC: + /* + * A String specifying the version of the JSON-RPC protocol. + * MUST be exactly "2.0". + */ + if (ctx->npos != 3 && strcmp(ctx->buf, "2.0")) { + r->parse_result = LWSJRPCWKE__INVALID_REQUEST; + return -1; + } + r->has_jrpc_member = 1; + break; + + case LEJPN_METHOD: + if (r->response) + goto fail_invalid_members; + + /* + * Method is defined to be a string... anything else is invalid + */ + + if (reason != LEJPCB_VAL_STR_END) + goto fail_invalid_request; + + /* + * Restrict the method length to something sane + */ + if (ctx->npos > sizeof(r->method) - 1) + goto fail_method_not_found; + + lws_strnncpy(r->method, ctx->buf, ctx->npos, sizeof(r->method)); + + /* defer trying to use it so we catch parser errors */ + break; + + + + case LEJPN_ID: + /* + * "An identifier established by the Client that MUST contain a + * String, Number, or NULL value if included. If it is not + * included it is assumed to be a notification. The value SHOULD + * normally not be Null and Numbers SHOULD NOT contain + * fractional parts." + * + * We defaulted the id to null, let's continue to store the id + * exactly as it would be reissued, ie, if a string, then we'll + * add the quotes around it now. + * + * Restrict the method length and type to something sane + */ + if (ctx->npos > sizeof(r->id) - 3 || + reason == LEJPCB_VAL_TRUE || + reason == LEJPCB_VAL_FALSE || + /* if float, has "fractional part" */ + reason == LEJPCB_VAL_NUM_FLOAT) + goto fail_invalid_request; + + r->seen_id = 1; + if (reason == LEJPCB_VAL_NULL) + /* it already defaults to null */ + break; + + p = r->id; + if (reason == LEJPCB_VAL_STR_END) + *p++ = '\"'; + + lws_strnncpy(p, ctx->buf, ctx->npos, sizeof(r->id) - 2); + + if (reason == LEJPCB_VAL_STR_END) { + p += strlen(p); + *p++ = '\"'; + *p = '\0'; + } + + break; + + case LEJPN_VERSION: + /* + * Restrict the method length to something sane + */ + if (ctx->npos > sizeof(r->version) - 1) + goto fail_invalid_request; + lws_strnncpy(r->version, ctx->buf, ctx->npos, sizeof(r->version)); + break; + + /* + * Only for responses + */ + + case LEJPN_RESULT: + break; + + case LEJPN_ERROR: + break; + case LEJPN_E_CODE: + break; + case LEJPN_E_MESSAGE: + break; + case LEJPN_E_DATA: + break; + } + + return 0; + +fail_invalid_members: + r->parse_result = LWSJRPCE__INVALID_MEMBERS; + + return -1; + +fail_invalid_request: + r->parse_result = LWSJRPCWKE__INVALID_REQUEST; + + return -1; + +fail_method_not_found: + r->parse_result = LWSJRPCWKE__METHOD_NOT_FOUND; + + return -1; +} + +const char * +lws_jrpc_obj_id(const struct lws_jrpc_obj *r) +{ + return r->id; +} + +/* + * Return code is >= 0 if completed, representing the amount of unused data in + * the input buffer. -1 indicates more input data needed, <-1 indicates an + * error from the LWSJRPCWKE_ set above + */ +int +lws_jrpc_obj_parse(lws_jrpc_t *jrpc, int type, void *opaque, + const char *buf, size_t l, lws_jrpc_obj_t **_r) +{ + lws_jrpc_obj_t *r = *_r; + int n; + + if (!r) { + /* + * We need to init the request object + */ + r = *_r = malloc(sizeof(*r)); + if (!r) + return 1; /* OOM */ + + memset(r, 0, sizeof *r); + + lws_dll2_add_tail(&r->list, &jrpc->req_owner); + r->opaque = opaque; + r->response = type == LWSJRPC_PARSE_RESPONSE; + lws_strncpy(r->id, "null", sizeof(r->id)); + lejp_construct(&r->lejp_ctx, req_cb, r, paths, + LWS_ARRAY_SIZE(paths)); + } + + n = lejp_parse(&r->lejp_ctx, (uint8_t *)buf, (int)l); + lwsl_debug("%s: raw parse result %d\n", __func__, n); + if (n == LEJP_REJECT_CALLBACK) + return r->parse_result; + + if (n < -1) + return LWSJRPCWKE__PARSE_ERROR; + + return n; +} + +void * +lws_jrpc_obj_get_opaque(const struct lws_jrpc_obj * r) +{ + return (void *)r->opaque; +} + +void +lws_jrpc_obj_destroy(lws_jrpc_obj_t **_r) +{ + lws_jrpc_obj_t *r = *_r; + + if (!r) + return; + + lws_dll2_remove(&r->list); + + free(r); + *_r = NULL; +} + +struct lws_jrpc * +lws_jrpc_create(const lws_jrpc_method_t *methods, void *opaque) +{ + lws_jrpc_t *j = malloc(sizeof(*j)); + + if (!j) + return NULL; + + memset(j, 0, sizeof(*j)); + + j->opaque = opaque; + j->methods = methods; + + return j; +} +void * +lws_jrpc_get_opaque(const struct lws_jrpc *jrpc) +{ + return (void *)jrpc->opaque; +} + +void +lws_jrpc_destroy(lws_jrpc_t **_jrpc) +{ + struct lws_jrpc *jrpc = *_jrpc; + + if (!jrpc) + return; + + lws_start_foreach_dll_safe(struct lws_dll2 *, p, p1, + jrpc->req_owner.head) { + lws_jrpc_obj_t *r = lws_container_of(p, lws_jrpc_obj_t, list); + + lws_jrpc_obj_destroy(&r); + } lws_end_foreach_dll_safe(p, p1); + + free(jrpc); + *_jrpc = NULL; +} diff --git a/lib/misc/jrpc/private-lib-misc-jrpc.h b/lib/misc/jrpc/private-lib-misc-jrpc.h new file mode 100644 index 000000000..21107acd8 --- /dev/null +++ b/lib/misc/jrpc/private-lib-misc-jrpc.h @@ -0,0 +1,88 @@ +/* + * libwebsockets - small server side websockets and web server implementation + * + * Copyright (C) 2010 - 2020 Andy Green + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + * This written from scratch, but props to falk-werner for his earlier + * work on top of lws for JRPC. + * + * https://github.com/falk-werner/jrpc + * + * https://www.jsonrpc.org/specification + * + * LWS JRPC takes the approach to stream-parse the incoming JRPC object in + * place to maximize the flexibility and parameter sizes that can be handled. + * Although "id" is often last, actually it has no users except to append the + * same id to the response. + * + * Therefore we parse the outer JSON and treat params as a wormhole to be + * parsed by a method-bound user callback. + * + * Streamed request processing must buffer its output before sending, since + * it does not know until the end if it must replace the intended response + * with an exception. It may not know that it wants to make an exception + * until it really processes all the params either. Results must be held in + * a side buffer until the response is able to complete or has errored. + * + * Types for id, method and params are ill-defined. They're all treated as + * strings internally, so a "method": 1 is handled as the string "1". id + * may be NULL, if so it's explicitly returned in the response with "id":null + * Whether id came in as a non-quoted number is remembered and is reproduced + * when giving the id. + */ + +/* + * Opaque object representing a request both at the sender and receiver + */ + +typedef struct lws_jrpc_obj { + lws_dll2_t list; + + struct lejp_ctx lejp_ctx; + + void *opaque; + const lws_jrpc_method_t *pmethod; /* only look up once if multi part */ + + char id[16]; /* includes quotes if was string */ + char method[48]; + /* + * Eg Sony API "getCurrentExternalTerminalsStatus" (30 chars) + * https://developer.sony.com/develop/audio-control-api/api-references/api-overview-2 + */ + char version[4]; /* Eg for Sony, "2.0" */ + + int parse_result; + + uint8_t count_batch_objects; + + uint8_t seen_id :1; + uint8_t inside_params :1; + uint8_t has_jrpc_member :1; + uint8_t response :1; + +} lws_jrpc_obj_t; + + +typedef struct lws_jrpc { + lws_dll2_owner_t req_owner; + const lws_jrpc_method_t *methods; + void *opaque; +} lws_jrpc_t; diff --git a/lib/misc/lejp.c b/lib/misc/lejp.c index cbce4ee4f..6caa882b7 100644 --- a/lib/misc/lejp.c +++ b/lib/misc/lejp.c @@ -237,7 +237,7 @@ static const char tokens[] = "rue alse ull "; int lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len) { - unsigned char c, n, s; + unsigned char c, n, s, defer = 0; int ret = LEJP_REJECT_UNKNOWN; if (!ctx->sp && !ctx->pst[ctx->pst_sp].ppos) @@ -508,6 +508,8 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len) * smaller than the matching point */ ctx->path_match = 0; + if (ctx->pst_sp && !ctx->sp) + lejp_parser_pop(ctx); if (ctx->outer_array && !ctx->sp) { /* ended on ] */ n = LEJPCB_ARRAY_END; goto completed; @@ -700,6 +702,9 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len) goto completed; } + if (ctx->pst_sp && !ctx->sp) + defer = 1; + /* do LEJP_MP_ARRAY_END processing */ goto redo_character; } @@ -740,6 +745,8 @@ pop_level: if (ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_OBJECT_END)) goto reject_callback; + if (ctx->pst_sp && !ctx->sp) + lejp_parser_pop(ctx); break; case LEJP_MP_ARRAY_END: @@ -763,6 +770,10 @@ array_end: ctx->st[ctx->sp].s = LEJP_MP_COMMA_OR_END; ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_ARRAY_END); + if (defer) { + lejp_parser_pop(ctx); + defer = 0; + } break; } diff --git a/minimal-examples-lowlevel/api-tests/api-test-jrpc/CMakeLists.txt b/minimal-examples-lowlevel/api-tests/api-test-jrpc/CMakeLists.txt new file mode 100644 index 000000000..68cd7cb55 --- /dev/null +++ b/minimal-examples-lowlevel/api-tests/api-test-jrpc/CMakeLists.txt @@ -0,0 +1,80 @@ +project(lws-api-test-jrpc) +cmake_minimum_required(VERSION 2.8.12) +include(CheckCSourceCompiles) + +set(SAMP lws-api-test-jrpc) +set(SRCS main.c) + +# If we are being built as part of lws, confirm current build config supports +# reqconfig, else skip building ourselves. +# +# If we are being built externally, confirm installed lws was configured to +# support reqconfig, else error out with a helpful message about the problem. +# +MACRO(require_lws_config reqconfig _val result) + + if (DEFINED ${reqconfig}) + if (${reqconfig}) + set (rq 1) + else() + set (rq 0) + endif() + else() + set(rq 0) + endif() + + if (${_val} EQUAL ${rq}) + set(SAME 1) + else() + set(SAME 0) + endif() + + if (LWS_WITH_MINIMAL_EXAMPLES AND NOT ${SAME}) + if (${_val}) + message("${SAMP}: skipping as lws being built without ${reqconfig}") + else() + message("${SAMP}: skipping as lws built with ${reqconfig}") + endif() + set(${result} 0) + else() + if (LWS_WITH_MINIMAL_EXAMPLES) + set(MET ${SAME}) + else() + CHECK_C_SOURCE_COMPILES("#include \nint main(void) {\n#if defined(${reqconfig})\n return 0;\n#else\n fail;\n#endif\n return 0;\n}\n" HAS_${reqconfig}) + if (NOT DEFINED HAS_${reqconfig} OR NOT HAS_${reqconfig}) + set(HAS_${reqconfig} 0) + else() + set(HAS_${reqconfig} 1) + endif() + if ((HAS_${reqconfig} AND ${_val}) OR (NOT HAS_${reqconfig} AND NOT ${_val})) + set(MET 1) + else() + set(MET 0) + endif() + endif() + if (NOT MET) + if (${_val}) + message(FATAL_ERROR "This project requires lws must have been configured with ${reqconfig}") + else() + message(FATAL_ERROR "Lws configuration of ${reqconfig} is incompatible with this project") + endif() + endif() + endif() +ENDMACRO() + +set(requirements 1) +require_lws_config(LWS_WITH_LEJP 1 requirements) +require_lws_config(LWS_WITH_JSONRPC 1 requirements) + +if (requirements) + + add_executable(${SAMP} ${SRCS}) + add_test(NAME api-test-jrpc COMMAND lws-api-test-jrpc) + + if (websockets_shared) + target_link_libraries(${SAMP} websockets_shared) + add_dependencies(${SAMP} websockets_shared) + else() + target_link_libraries(${SAMP} websockets) + endif() +endif() diff --git a/minimal-examples-lowlevel/api-tests/api-test-jrpc/README.md b/minimal-examples-lowlevel/api-tests/api-test-jrpc/README.md new file mode 100644 index 000000000..ebe930d24 --- /dev/null +++ b/minimal-examples-lowlevel/api-tests/api-test-jrpc/README.md @@ -0,0 +1,56 @@ +# lws api test lws_struct JSON + +Demonstrates how to use and performs selftests for lws_struct +JSON serialization and deserialization + +## build + +``` + $ cmake . && make +``` + +## usage + +Commandline option|Meaning +---|--- +-d |Debug verbosity in decimal, eg, -d15 + +``` + $ ./lws-api-test-lws_struct-json +[2019/03/30 22:09:09:2529] USER: LWS API selftest: lws_struct JSON +[2019/03/30 22:09:09:2625] NOTICE: main: ++++++++++++++++ test 1 +[2019/03/30 22:09:09:2812] NOTICE: builder.hostname = 'learn', timeout = 1800, targets (2) +[2019/03/30 22:09:09:2822] NOTICE: target.name 'target1' (target 0x543a830) +[2019/03/30 22:09:09:2824] NOTICE: target.name 'target2' (target 0x543a860) +[2019/03/30 22:09:09:2826] NOTICE: main: .... strarting serialization of test 1 +[2019/03/30 22:09:09:2899] NOTICE: ser says 1 +{"schema":"com-warmcat-sai-builder","hostname":"learn","nspawn_timeout":1800,"targets":[{"name":"target1"},{"name":"target2"}]} +[2019/03/30 22:09:09:2929] NOTICE: main: ++++++++++++++++ test 2 +[2019/03/30 22:09:09:2932] NOTICE: builder.hostname = 'learn', timeout = 0, targets (3) +[2019/03/30 22:09:09:2932] NOTICE: target.name 'target1' (target 0x543b060) +[2019/03/30 22:09:09:2933] NOTICE: target.name 'target2' (target 0x543b090) +[2019/03/30 22:09:09:2933] NOTICE: target.name 'target3' (target 0x543b0c0) +[2019/03/30 22:09:09:2934] NOTICE: main: .... strarting serialization of test 2 +[2019/03/30 22:09:09:2935] NOTICE: ser says 1 +{"schema":"com-warmcat-sai-builder","hostname":"learn","nspawn_timeout":0,"targets":[{"name":"target1"},{"name":"target2"},{"name":"target3"}]} +[2019/03/30 22:09:09:2940] NOTICE: main: ++++++++++++++++ test 3 +[2019/03/30 22:09:09:2959] NOTICE: builder.hostname = 'learn', timeout = 1800, targets (2) +[2019/03/30 22:09:09:2960] NOTICE: target.name 'target1' (target 0x543b450) +[2019/03/30 22:09:09:2961] NOTICE: child 0x543b480, target.child.somename 'abc' +[2019/03/30 22:09:09:2961] NOTICE: target.name 'target2' (target 0x543b490) +[2019/03/30 22:09:09:2962] NOTICE: main: .... strarting serialization of test 3 +[2019/03/30 22:09:09:2969] NOTICE: ser says 1 +{"schema":"com-warmcat-sai-builder","hostname":"learn","nspawn_timeout":1800,"targets":[{"name":"target1","child":{"somename":"abc"}},{"name":"target2"}]} +[2019/03/30 22:09:09:2970] NOTICE: main: ++++++++++++++++ test 4 +[2019/03/30 22:09:09:2971] NOTICE: builder.hostname = 'learn', timeout = 1800, targets (0) +[2019/03/30 22:09:09:2971] NOTICE: main: .... strarting serialization of test 4 +[2019/03/30 22:09:09:2973] NOTICE: ser says 1 +{"schema":"com-warmcat-sai-builder","hostname":"learn","nspawn_timeout":1800} +[2019/03/30 22:09:09:2974] NOTICE: main: ++++++++++++++++ test 5 +[2019/03/30 22:09:09:2978] NOTICE: builder.hostname = '', timeout = 0, targets (0) +[2019/03/30 22:09:09:2979] NOTICE: main: .... strarting serialization of test 5 +[2019/03/30 22:09:09:2980] NOTICE: ser says 1 +{"schema":"com-warmcat-sai-builder","hostname":"","nspawn_timeout":0} +[2019/03/30 22:09:09:2982] USER: Completed: PASS +``` + diff --git a/minimal-examples-lowlevel/api-tests/api-test-jrpc/main.c b/minimal-examples-lowlevel/api-tests/api-test-jrpc/main.c new file mode 100644 index 000000000..8f39b1603 --- /dev/null +++ b/minimal-examples-lowlevel/api-tests/api-test-jrpc/main.c @@ -0,0 +1,280 @@ +/* + * lws-api-test-jrpc + * + * Written in 2010-2020 by Andy Green + * + * This file is made available under the Creative Commons CC0 1.0 + * Universal Public Domain Dedication. + * + * sanity tests for jrpc + */ + +#include + +/* + * These came from https://www.jsonrpc.org/specification but amended since we + * do not support batch + */ + +static const char * const jrpc_request_tests[] = { + + "{" /* req 1 */ + "\"jsonrpc\":" "\"2.0\", " + "\"method\":" "\"subtract\", " + "\"params\":" "[42, 23], " + "\"id\":" "1" + "}", + "{" /* req 2 */ + "\"jsonrpc\":" "\"2.0\", " + "\"method\":" "\"subtract\", " + "\"params\":" "[23, 42], " + "\"id\":" "2" + "}", + "{" /* req 3 */ + "\"jsonrpc\":" "\"2.0\", " + "\"method\":" "\"subtract\", " + "\"params\":" "{" + "\"subtrahend\":" "23, " + "\"minuend\":" "42" + "}, \"id\":" "3" + "}", + /* req 4 */ + "{\"jsonrpc\": \"2.0\"," + "\"method\": \"update\", " + "\"params\": [1,2,3,4,5]}", + + /* req 5 */ + "{\"jsonrpc\": \"2.0\", \"method\": \"foobar\"}", + + /* req 6: unknown method: well-known error -32601 Method Not Found */ + "{\"jsonrpc\": \"2.0\", \"method\": \"noexist\", \"id\": \"1\"}", + + /* req 7: Invalid JSON should yield well-known error -32700 Parse Error */ + "{\"jsonrpc\": \"2.0\", \"method\": \"foobar, \"params\": \"bar\", \"baz]", + + /* req 8: Invalid req (method must be string): wke -32600 Invalid Request */ + "{\"jsonrpc\": \"2.0\", \"method\": 1, \"params\": \"bar\"}", + + /* req 9: Incomplete JSON, just -32700 Parse Error */ + "{\"jsonrpc\": \"2.0\", \"method\"}", + + /* req 10: OK */ + "{\"jsonrpc\": \"2.0\", \"method\": \"sum\", \"params\": [1,2,4], \"id\": \"1\"}", + + /* req 11: OK (notify) */ + "{\"jsonrpc\": \"2.0\", \"method\": \"notify_hello\", \"params\": [7]}", + + /* req 12: OK */ + "{\"jsonrpc\": \"2.0\", \"method\": \"subtract\", \"params\": [42,23], \"id\": \"2\"}", + + /* req 13: -32600 */ + "{\"foo\": \"boo\"}", + + /* req 14: -32601 */ + "{\"jsonrpc\": \"2.0\", \"method\": \"noexist\", \"params\": {\"name\": \"myself\"}, \"id\": \"5\"}", + + /* req 15: OK */ + "{\"jsonrpc\": \"2.0\", \"method\": \"get_data\", \"id\": \"9\"}", + + /* req 16: OK (notify) */ + "{\"jsonrpc\": \"2.0\", \"method\": \"notify_sum\", \"params\": [1,2,4]}", + + /* req 17: OK (notify) */ + "{\"jsonrpc\": \"2.0\", \"method\": \"notify_hello\", \"params\": [7]}", +}; + +static const char * const jrpc_response_tests[] = { + + "{" /* req 1 */ + "\"jsonrpc\":" "\"2.0\"," + "\"id\":" "1, " + "\"response\":" "\"string\"" + "}", + "{" /* req 2 */ + "\"jsonrpc\":" "\"2.0\"," + "\"id\":" "2, " + "\"response\":" "123" + "}", + "{" /* req 3 */ + "\"jsonrpc\":" "\"2.0\"," + "\"id\":" "3, " + "\"response\":" "[1,2,3]" + "}", + "{" /* req 4 */ + "\"jsonrpc\":" "\"2.0\"," + "\"id\":" "4, " + "\"response\":" "{\"a\": \"b\"}" + "}", + "{" /* req 5 */ + "\"jsonrpc\":" "\"2.0\"," + "\"error\": {" + "\"code\": -32601," + "\"message\":" "\"Method not found\"" + "}," + "\"id\": \"5\"" + "}", +}; + +static int expected_parse_result[] = { + /* 1 */ 0, + /* 2 */ 0, + /* 3 */ 0, + /* 4 */ 0, + /* 5 */ 0, + /* 6 */ LWSJRPCWKE__METHOD_NOT_FOUND, + /* 7 */ LWSJRPCWKE__PARSE_ERROR, + /* 8 */ LWSJRPCWKE__INVALID_REQUEST, + /* 9 */ LWSJRPCWKE__PARSE_ERROR, + /* 10 */ 0, + /* 11 */ 0, + /* 12 */ 0, + /* 13 */ LWSJRPCWKE__INVALID_REQUEST, + /* 14 */ LWSJRPCWKE__METHOD_NOT_FOUND, + /* 15 */ 0, + /* 16 */ 0, + /* 17 */ 0, +}; + +static int expected_parse_result_response[] = { + /* 1 */ 0, + /* 2 */ 0, + /* 3 */ 0, + /* 4 */ 0, + /* 5 */ 0, +}; + +/* + * The Method-specific parser is an lejp parser callback that only sees the + * subtree in request "params": + */ + +static const char * const paths_s1[] = { + "subtrahend", + "minuend", + "[]" +}; +static const char * const paths_s2[] = { + "subtrahend", + "minuend", + "[]" +}; + +static signed char +parse_s1(struct lejp_ctx *ctx, char reason) +{ + // struct lws_jrpc_obj *r; + + /* + * In the canonical examples, this can take either an array like + * [1,2] + * or an object like + * {"subtrahend":23, "minuend":42 } + */ + + lwsl_notice("%s: reason %d, path %s, buf %.*s sp %d, pst_sp %d\n", + __func__, reason, ctx->path, ctx->npos, ctx->buf, ctx->sp, + ctx->pst_sp); + + return 0; +} + +static signed char +parse_s2(struct lejp_ctx *ctx, char reason) +{ + return 0; +} + +static const lws_jrpc_method_t methods[] = { + /* list methods used by the tests that are expected to exist */ + { "subtract", paths_s1, parse_s1, LWS_ARRAY_SIZE(paths_s1) }, + { "foobar", paths_s2, parse_s2, LWS_ARRAY_SIZE(paths_s2) }, + { "update", paths_s2, parse_s2, LWS_ARRAY_SIZE(paths_s2) }, + { "sum", paths_s2, parse_s2, LWS_ARRAY_SIZE(paths_s2) }, + { "get_data", paths_s2, parse_s2, LWS_ARRAY_SIZE(paths_s2) }, + { "notify_hello", paths_s2, parse_s2, LWS_ARRAY_SIZE(paths_s2) }, + { "notify_sum", paths_s2, parse_s2, LWS_ARRAY_SIZE(paths_s2) }, + { NULL, NULL, NULL, 0 } /* sentinel */ +}; + +int main(int argc, const char **argv) +{ + int n, m, e = 0, logs = LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE; + struct lws_jrpc_obj *req; + struct lws_jrpc *jrpc; + const char *p; + + if ((p = lws_cmdline_option(argc, argv, "-d"))) + logs = atoi(p); + + lws_set_log_level(logs, NULL); + lwsl_user("LWS API selftest: JSON-RPC\n"); + + for (m = 0; m < (int)LWS_ARRAY_SIZE(jrpc_request_tests); m++) { + + lwsl_notice("%s: ++++++++++++++++ request %d\n", __func__, m + 1); + + jrpc = lws_jrpc_create(methods, NULL); + if (!jrpc) { + lwsl_err("%s: unable to create JRPC context\n", __func__); + e++; + continue; + } + + req = NULL; + n = lws_jrpc_obj_parse(jrpc, LWSJRPC_PARSE_REQUEST, NULL, + jrpc_request_tests[m], + strlen(jrpc_request_tests[m]), &req); + + lwsl_info("%s: %d\n", __func__, n); + + if (n != expected_parse_result[m]) { + lwsl_err("%s: got %d, expected %d\n", __func__, + n, expected_parse_result[m]); + e++; + } + + lws_jrpc_destroy(&jrpc); + } + + if (e) + goto bail; + + for (m = 0; m < (int)LWS_ARRAY_SIZE(jrpc_response_tests); m++) { + + lwsl_notice("%s: ++++++++++++++++ response %d\n", __func__, m + 1); + + jrpc = lws_jrpc_create(methods, NULL); + if (!jrpc) { + lwsl_err("%s: unable to create JRPC context\n", __func__); + e++; + continue; + } + + req = NULL; + n = lws_jrpc_obj_parse(jrpc, LWSJRPC_PARSE_RESPONSE, NULL, + jrpc_response_tests[m], + strlen(jrpc_response_tests[m]), &req); + + lwsl_info("%s: %d\n", __func__, n); + + if (n != expected_parse_result_response[m]) { + lwsl_err("%s: got %d, expected %d\n", __func__, n, + expected_parse_result[m]); + e++; + } + + lws_jrpc_destroy(&jrpc); + } + + if (e) + goto bail; + + lwsl_user("Completed: PASS\n"); + + return 0; + +bail: + lwsl_user("Completed: FAIL\n"); + + return 1; +}