kore

An easy to use, scalable and secure web application framework for writing web APIs in C.
Commits | Files | Refs | README | LICENSE | git clone https://git.kore.io/kore.git

commit 6ba8dd439bf9b424a23a29f86b9f5db467fb95a6
parent 2cf83aea3c3224a1e2ec6008812a9609d9399715
Author: Joris Vink <joris@coders.se>
Date:   Fri, 15 Jul 2016 22:25:52 +0200

Merge pull request #135 from raphaelmonrouzeau/master

Add conditional JSON-RPC support
Diffstat:
Makefile | 9+++++++++
examples/jsonrpc/.gitignore | 7+++++++
examples/jsonrpc/README.md | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
examples/jsonrpc/conf/build.conf | 19+++++++++++++++++++
examples/jsonrpc/conf/jsonrpc.conf | 14++++++++++++++
examples/jsonrpc/src/home.c | 14++++++++++++++
examples/jsonrpc/src/v1.c | 116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
examples/jsonrpc/test/integ/jsonrpc.bats | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
includes/jsonrpc.h | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/jsonrpc.c | 478+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/kore.c | 3+++
11 files changed, 935 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile @@ -30,6 +30,9 @@ endif ifneq ("$(NOHTTP)", "") CFLAGS+=-DKORE_NO_HTTP + ifneq ("$(JSONRPC)", "") + $(error "JSONRPC support needs HTTP") + endif else S_SRC+= src/auth.c src/accesslog.c src/http.c \ src/validator.c src/websocket.c @@ -57,6 +60,12 @@ ifneq ("$(TASKS)", "") CFLAGS+=-DKORE_USE_TASKS endif +ifneq ("$(JSONRPC)", "") + S_SRC+=src/jsonrpc.c + LDFLAGS+=-lyajl + CFLAGS+=-DKORE_USE_JSONRPC +endif + OSNAME=$(shell uname -s | sed -e 's/[-_].*//g' | tr A-Z a-z) ifeq ("$(OSNAME)", "darwin") CFLAGS+=-I/opt/local/include/ -I/usr/local/opt/openssl/include diff --git a/examples/jsonrpc/.gitignore b/examples/jsonrpc/.gitignore @@ -0,0 +1,7 @@ +*.o +.objs +jsonrpc.so +assets.h +cert +.*.swp +.*.swo diff --git a/examples/jsonrpc/README.md b/examples/jsonrpc/README.md @@ -0,0 +1,67 @@ +This example demonstrates how you can use the JSON-RPC module in your +application. + +Note that the module depends upon the third-party library `yajl` (Yet Another +JSON library) to parse and produce messages. + +As for the `yajl_json` example, conf/build.conf shows how to link to the +library. + +This example needs kore having been compiled with `JSONRPC` (and so `HTTP`) +activated. + +Run: +``` + $ kore run +``` + +Test: +``` + $ curl -i -k \ + -d '{"id":1,"jsonrpc":"2.0","method":"echo","params":["Hello world"]}' \ + https://127.0.0.1:8888/v1 +``` +The result should echo back the string at `params`: Hello world. + +Alternatively, if you have bats installed: +``` + $ bats test/integ/jsonrpc.bats +``` +Will run a small test suite. + + +The yajl repo is available @ https://github.com/lloyd/yajl + + +JSONRPC Request Lifetime +------------------------ + +Currently, one HTTP request will (in most cases) provoke one and only one +response. Batch mode is not supported yet, neither is websocket. + +As such `jsonrpc\_error` and `jsonrpc\_result` do clean the request after call. + +If however you want to abort the processed request, like by returning +`KORE\_RESULT\_ERROR`, after it having been read, you need to clean it by +calling `jsonrpc\_destroy\_request`. Other than that you shouldn't think about +this function. + + +Message Handling Log +-------------------- + +The `jsonrpc\_request` keeps a log of messages with levels similar to those of +syslog. Messages are added with jsonrpc_log(). + +By default messages of the log are added to the data member of the error +responses if at levels EMERG, ERROR, WARNING and NOTICE. + +If you dont want log messages to be outputted zero the log_levels flag of the +jsonrpc_request. + + +Formatting responses +-------------------- + +By default responses are not prettyfied. To do that set the appropriate flag in +the jsonrpc_request structure. diff --git a/examples/jsonrpc/conf/build.conf b/examples/jsonrpc/conf/build.conf @@ -0,0 +1,19 @@ +# jsonrpc build config +# You can switch flavors using: kore flavor [newflavor] + +# The cflags below are shared between flavors +cflags=-Wall -Wmissing-declarations -Wshadow +cflags=-Wstrict-prototypes -Wmissing-prototypes +cflags=-Wpointer-arith -Wcast-qual -Wsign-compare + +dev { + # These cflags are added to the shared ones when + # you build the "dev" flavor. + cflags=-g + ldflags=-lyajl +} + +#prod { +# You can specify additional CFLAGS here which are only +# included if you build with the "prod" flavor. +#} diff --git a/examples/jsonrpc/conf/jsonrpc.conf b/examples/jsonrpc/conf/jsonrpc.conf @@ -0,0 +1,14 @@ +# Placeholder configuration + +bind 127.0.0.1 8888 +load ./jsonrpc.so + +tls_dhparam dh2048.pem + +domain 127.0.0.1 { + certfile cert/server.crt + certkey cert/server.key + + static / homepage + static /v1 v1 +} diff --git a/examples/jsonrpc/src/home.c b/examples/jsonrpc/src/home.c @@ -0,0 +1,14 @@ +#include <kore/kore.h> +#include <kore/http.h> + +int homepage(struct http_request *); + +int +homepage(struct http_request *req) +{ + static const char response_body[] = "JSON-RPC API\n"; + + http_response_header(req, "content-type", "text/plain"); + http_response(req, 200, response_body, sizeof(response_body) - 1); + return (KORE_RESULT_OK); +} diff --git a/examples/jsonrpc/src/v1.c b/examples/jsonrpc/src/v1.c @@ -0,0 +1,116 @@ +#include <time.h> +#include <xlocale.h> +#include <yajl/yajl_gen.h> +#include <yajl/yajl_tree.h> +#include <kore/kore.h> +#include <kore/http.h> +#include <kore/jsonrpc.h> + +int v1(struct http_request *); + +static int +write_string(struct jsonrpc_request *req, void *ctx) +{ + const unsigned char *str = (unsigned char *)ctx; + + return yajl_gen_string(req->gen, str, strlen((const char *)str)); +} + +static int +write_string_array_params(struct jsonrpc_request *req, void *ctx) +{ + int status = 0; + + if (!YAJL_GEN_KO(status = yajl_gen_array_open(req->gen))) { + for (size_t i = 0; i < req->params->u.array.len; i++) { + yajl_val yajl_str = req->params->u.array.values[i]; + char *str = YAJL_GET_STRING(yajl_str); + + if (YAJL_GEN_KO(status = yajl_gen_string(req->gen, + (unsigned char *)str, strlen(str)))) + break; + } + if (status == 0) + status = yajl_gen_array_close(req->gen); + } + + return status; +} + +int +v1(struct http_request *http_req) +{ + struct jsonrpc_request req; + int ret; + + /* We only allow POST/PUT methods. */ + if (http_req->method != HTTP_METHOD_POST && + http_req->method != HTTP_METHOD_PUT) { + http_response_header(http_req, "allow", "POST, PUT"); + http_response(http_req, HTTP_STATUS_METHOD_NOT_ALLOWED, NULL, 0); + return (KORE_RESULT_OK); + } + + /* Read JSON-RPC request. */ + if ((ret = jsonrpc_read_request(http_req, &req)) != 0) + return jsonrpc_error(&req, ret, NULL); + + /* Echo command takes and gives back params. */ + if (strcmp(req.method, "echo") == 0) { + if (!YAJL_IS_ARRAY(req.params)) { + jsonrpc_log(&req, LOG_ERR, + "Echo only accepts positional params"); + return jsonrpc_error(&req, JSONRPC_INVALID_PARAMS, NULL); + } + for (size_t i = 0; i < req.params->u.array.len; i++) { + yajl_val v = req.params->u.array.values[i]; + if (!YAJL_IS_STRING(v)) { + jsonrpc_log(&req, -3, + "Echo only accepts strings"); + return jsonrpc_error(&req, + JSONRPC_INVALID_PARAMS, NULL); + } + } + return jsonrpc_result(&req, write_string_array_params, NULL); + } + + /* Date command displays date and time according to parameters. */ + if (strcmp(req.method, "date") == 0) { + time_t time_value; + struct tm time_info; + char timestamp[33]; + struct tm *(*gettm)(const time_t *, struct tm *) = + localtime_r; + + if (YAJL_IS_OBJECT(req.params)) { + const char *path[] = {"local", NULL}; + yajl_val bf; + + bf = yajl_tree_get(req.params, path, yajl_t_false); + if (bf != NULL) + gettm = gmtime_r; + } else if (req.params != NULL) { + jsonrpc_log(&req, LOG_ERR, + "Date only accepts named params"); + return jsonrpc_error(&req, JSONRPC_INVALID_PARAMS, NULL); + } + + if ((time_value = time(NULL)) == -1) + return jsonrpc_error(&req, -2, + "Failed to get date time"); + + if (gettm(&time_value, &time_info) == NULL) + return jsonrpc_error(&req, -3, + "Failed to get date time info"); + + memset(timestamp, 0, sizeof(timestamp)); + if (strftime_l(timestamp, sizeof(timestamp) - 1, "%c", + &time_info, LC_GLOBAL_LOCALE) == 0) + return jsonrpc_error(&req, -4, + "Failed to get printable date time"); + + return jsonrpc_result(&req, write_string, timestamp); + } + + return jsonrpc_error(&req, JSONRPC_METHOD_NOT_FOUND, NULL); +} diff --git a/examples/jsonrpc/test/integ/jsonrpc.bats b/examples/jsonrpc/test/integ/jsonrpc.bats @@ -0,0 +1,121 @@ +#!/usr/bin/env bats + +# Simple and non exhaustive test suite using bats: +# https://github.com/sstephenson/bats + +PIDFILE=run/jsonrpc.pid +CONFFILE=conf/jsonrpc.conf + +# Start and stop have to be tweaked before being used +stop_app() { + if [ -f "$PIDFILE" ]; then + kill -QUIT `cat "$PIDFILE"` + sleep 3 + fi + if [ -f "$PIDFILE" ]; then + kill -KILL `cat "$PIDFILE"` + sleep 2 + fi +} + +start_app() { + stop_app + kore -nrc "$CONFFILE" +} + +query_with_content_type() { + curl -q \ + -H "Content-Type: $1" \ + -X POST \ + --raw \ + -d "$2" \ + -s -S \ + --insecure \ + "https://127.0.0.1:8888/v1" +} + +query() { + query_with_content_type "application/json" "$1" +} + +grepstr() { + declare result=$1 + shift + printf "%s" "$result" | grep "$@" >/dev/null +} + +printrep() { + declare query=$1 + declare result=$2 + printf "Sent:\n" + printf "%s\n" "$query" + printf "Received:\n" + printf "%s\n" "$result" +} + +@test "requests with no protocol returns nothing" { + query='{"method":"foo","id":"foo"}' + result=`query "$query"` + printrep "$query" "$result" + [ "$result" = "" ] +} +@test "requests with invalid protocol (1) returns nothing" { + query='{"jsonrpc":"1.0","method":"foo","id":"foo"}' + result=`query "$query"` + printrep "$query" "$result" + [ "$result" = "" ] +} +@test "requests with invalid protocol (2) returns nothing" { + query='{"jsonrpc":2.0,"method":"foo","id":"foo"}' + result=`query "$query"` + printrep "$query" "$result" + [ "$result" = "" ] +} + +@test "requests with no method raise errors" { + query='{"jsonrpc":"2.0","id":"foo"}' + result=`query "$query"` + printrep "$query" "$result" + grepstr "$result" '"error"[ \t\n]*:[ \t\n]*{[ \t\n]*"code"' +} +@test "requests with invalid method raise errors" { + query='{"jsonrpc":"2.0","method":1,"id":"foo"}' + result=`query "$query"` + printrep "$query" "$result" + grepstr "$result" '"error"[ \t\n]*:[ \t\n]*{[ \t\n]*"code"' +} +@test "requests with unknown method raise errors" { + query='{"jsonrpc":"2.0","method":"foobar","id":"foo"}' + result=`query "$query"` + printrep "$query" "$result" + grepstr "$result" '"error"[ \t\n]*:[ \t\n]*{[ \t\n]*"code"' +} + +@test "error responses give back the string request id" { + query='{"jsonrpc":"2.0","id":"foo"}' + result=`query "$query"` + printrep "$query" "$result" + grepstr "$result" '"error"[ \t\n]*:[ \t\n]*{[ \t\n]*"code"' + grepstr "$result" '"id"[ \t\n]*:[ \t\n]*"foo"' +} +@test "error responses give back the integer request id" { + query='{"jsonrpc":"2.0","id":1}' + result=`query "$query"` + printrep "$query" "$result" + grepstr "$result" '"error"[ \t\n]*:[ \t\n]*{[ \t\n]*"code"' + grepstr "$result" '"id"[ \t\n]*:[ \t\n]*1' +} +@test "result responses give back the string request id" { + query='{"jsonrpc":"2.0","method":"echo","params":["foobar"],"id":"tau"}' + result=`query "$query"` + printrep "$query" "$result" + grepstr "$result" '"result"[ \t\n]*:[ \t\n]*[[ \t\n]*"foobar"[ \t\n]*]' + grepstr "$result" '"id"[ \t\n]*:[ \t\n]*"tau"' +} +@test "result responses give back the integer request id" { + query='{"jsonrpc":"2.0","method":"echo","params":["foobar"],"id":6}' + result=`query "$query"` + printrep "$query" "$result" + grepstr "$result" '"result"[ \t\n]*:[ \t\n]*[[ \t\n]*"foobar"[ \t\n]*]' + grepstr "$result" '"id"[ \t\n]*:[ \t\n]*6' +} diff --git a/includes/jsonrpc.h b/includes/jsonrpc.h @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2016 Raphaël Monrouzeau <raphael.monrouzeau@gmail.com> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#if !defined(KORE_NO_HTTP) + +#ifndef __H_JSONRPC_H +#define __H_JSONRPC_H + +#if defined(__cplusplus) +extern "C" { +#endif + +/* JSON RPC request handling log entry. */ +struct jsonrpc_log +{ + char *msg; + struct jsonrpc_log *next, *prev; + int lvl; +}; + +/* JSON RPC request. */ +struct jsonrpc_request +{ + struct jsonrpc_log log; + struct kore_buf buf; + struct http_request *http; + yajl_gen gen; + yajl_val json; + yajl_val id; + char *method; + yajl_val params; + unsigned int flags; + int log_levels; +}; + +#define YAJL_GEN_CONST_STRING(CTX, STR) \ + yajl_gen_string((CTX), (unsigned char *)(STR), sizeof (STR) - 1) + +#define YAJL_GEN_CONST_NUMBER(CTX, STR) \ + yajl_gen_number((CTX), (unsigned char *)(STR), sizeof (STR) - 1) + +#define YAJL_GEN_KO(OPERATION) \ + ((OPERATION) != yajl_gen_status_ok) + +enum jsonrpc_error_code +{ +#define JSONRPC_PARSE_ERROR_MSG "Parse error" + JSONRPC_PARSE_ERROR = -32700, +#define JSONRPC_INVALID_REQUEST_MSG "Invalid Request" + JSONRPC_INVALID_REQUEST = -32600, +#define JSONRPC_METHOD_NOT_FOUND_MSG "Method not found" + JSONRPC_METHOD_NOT_FOUND = -32601, +#define JSONRPC_INVALID_PARAMS_MSG "Invalid params" + JSONRPC_INVALID_PARAMS = -32602, +#define JSONRPC_INTERNAL_ERROR_MSG "Internal error" + JSONRPC_INTERNAL_ERROR = -32603, +#define JSONRPC_SERVER_ERROR_MSG "Server error" + JSONRPC_SERVER_ERROR = -32000, +#define JSONRPC_LIMIT_REACHED_MSG "Limit reached" + JSONRPC_LIMIT_REACHED = -31997 +}; + +void jsonrpc_log(struct jsonrpc_request *, int, const char *, ...); +int jsonrpc_read_request(struct http_request *, struct jsonrpc_request *); +void jsonrpc_destroy_request(struct jsonrpc_request *); +int jsonrpc_error(struct jsonrpc_request *, int, const char *); +int jsonrpc_result(struct jsonrpc_request *, + int (*)(struct jsonrpc_request *, void *), void *); +#if defined(__cplusplus) +} +#endif +#endif /* !__H_JSONRPC_H */ + +#endif /* ! KORE_NO_HTTP */ diff --git a/src/jsonrpc.c b/src/jsonrpc.c @@ -0,0 +1,478 @@ +/* + * Copyright (c) 2016 Raphaël Monrouzeau <raphael.monrouzeau@gmail.com> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <limits.h> +#include <stdbool.h> + +#include <yajl/yajl_tree.h> +#include <yajl/yajl_gen.h> + +#include "kore.h" +#include "http.h" +#include "jsonrpc.h" + +static void +init_log(struct jsonrpc_log *log) +{ + log->msg = NULL; + log->next = log; + log->prev = log; +} + +static void +append_log(struct jsonrpc_log *prev, int lvl, char *msg) +{ + struct jsonrpc_log *new = kore_malloc(sizeof(struct jsonrpc_log)); + + new->lvl = lvl; + new->msg = msg; + + new->prev = prev; + new->next = prev->next; + prev->next->prev = new; + prev->next = new; +} + +static void +free_log(struct jsonrpc_log *root) +{ + for (struct jsonrpc_log *it = root->next; it != root; it = it->next) { + kore_free(it); + } +} + +static void +init_request(struct jsonrpc_request *req) +{ + init_log(&req->log); + kore_buf_init(&req->buf, 256); + req->gen = NULL; + req->http = NULL; + req->json = NULL; + req->id = NULL; + req->method = NULL; + req->params = NULL; + req->log_levels = (1 << LOG_EMERG) | (1 << LOG_ERR) | (1 << LOG_WARNING) + | (1 << LOG_NOTICE); + req->flags = 0; +} + +void +jsonrpc_destroy_request(struct jsonrpc_request *req) +{ + if (req->gen != NULL) { + yajl_gen_free(req->gen); + req->gen = NULL; + } + if (req->json != NULL) { + yajl_tree_free(req->json); + req->json = NULL; + } + kore_buf_cleanup(&req->buf); + free_log(&req->log); +} + +void +jsonrpc_log(struct jsonrpc_request *req, int lvl, const char *fmt, ...) +{ + va_list ap; + char *msg; + size_t start = req->buf.offset; + + va_start(ap, fmt); + kore_buf_appendv(&req->buf, fmt, ap); + va_end(ap); + + msg = kore_buf_stringify(&req->buf, NULL) + start; + + append_log(&req->log, lvl, msg); +} + +static int +read_json_body(struct http_request *http_req, struct jsonrpc_request *req) +{ + char *body_string; + ssize_t body_len = 0, chunk_len; + u_int8_t chunk_buffer[BUFSIZ]; + char error_buffer[1024]; + + for (;;) { + chunk_len = http_body_read(http_req, chunk_buffer, + sizeof(chunk_buffer)); + if (chunk_len == -1) { + jsonrpc_log(req, LOG_CRIT, + "Failed to read request body"); + return (JSONRPC_SERVER_ERROR); + } + + if (chunk_len == 0) + break; + + if (body_len > SSIZE_MAX - chunk_len) { + jsonrpc_log(req, LOG_CRIT, + "Request body bigger than the platform accepts"); + return (JSONRPC_SERVER_ERROR); + } + body_len += chunk_len; + + kore_buf_append(&req->buf, chunk_buffer, chunk_len); + } + + /* Grab our body data as a NUL-terminated string. */ + body_string = kore_buf_stringify(&req->buf, NULL); + + /* Parse the body via yajl now. */ + *error_buffer = 0; + req->json = yajl_tree_parse(body_string, error_buffer, + sizeof(error_buffer)); + if (req->json == NULL) { + if (strlen(error_buffer)) { + jsonrpc_log(req, LOG_ERR, "Invalid json: %s", + error_buffer); + } else { + jsonrpc_log(req, LOG_ERR, "Invalid json"); + } + return (JSONRPC_PARSE_ERROR); + } + + return (0); +} + +static int +parse_json_body(struct jsonrpc_request *req) +{ + static const char *proto_path[] = { "jsonrpc", NULL }; + static const char *id_path[] = { "id", NULL }; + static const char *method_path[] = { "method", NULL }; + static const char *params_path[] = { "params", NULL }; + + /* Check protocol first. */ + yajl_val proto = yajl_tree_get(req->json, proto_path, yajl_t_string); + if (proto == NULL) { + jsonrpc_log(req, LOG_ERR, + "JSON-RPC protocol MUST be indicated and \"2.0\""); + return (JSONRPC_PARSE_ERROR); + } + + char *proto_string = YAJL_GET_STRING(proto); + if (proto_string == NULL) { + jsonrpc_log(req, LOG_ERR, + "JSON-RPC protocol MUST be indicated and \"2.0\""); + return (JSONRPC_PARSE_ERROR); + } + + if (strcmp("2.0", proto_string) != 0) { + jsonrpc_log(req, LOG_ERR, + "JSON-RPC protocol MUST be indicated and \"2.0\""); + return (JSONRPC_PARSE_ERROR); + } + + /* Check id. */ + if ((req->id = yajl_tree_get(req->json, id_path, yajl_t_any)) != NULL) { + if (YAJL_IS_NUMBER(req->id)) { + if (!YAJL_IS_INTEGER(req->id)) { + jsonrpc_log(req, LOG_ERR, + "JSON-RPC id SHOULD NOT contain fractional" + " parts"); + return (JSONRPC_PARSE_ERROR); + } + } else if (!YAJL_IS_STRING(req->id)) { + jsonrpc_log(req, LOG_ERR, + "JSON-RPC id MUST contain a String or Number"); + return (JSONRPC_PARSE_ERROR); + } + } + + /* Check method. */ + if ((req->method = YAJL_GET_STRING(yajl_tree_get(req->json, method_path, + yajl_t_string))) == NULL) { + jsonrpc_log(req, LOG_ERR, + "JSON-RPC method MUST exist and be a String"); + return (JSONRPC_PARSE_ERROR); + } + + /* Check params. */ + req->params = yajl_tree_get(req->json, params_path, yajl_t_any); + if (!(req->params == NULL || YAJL_IS_ARRAY(req->params) + || YAJL_IS_OBJECT(req->params))) { + jsonrpc_log(req, LOG_ERR, + "JSON-RPC params MUST be Object or Array"); + return (JSONRPC_PARSE_ERROR); + } + + return (0); +} + +int +jsonrpc_read_request(struct http_request *http_req, struct jsonrpc_request *req) +{ + int ret; + + init_request(req); + req->http = http_req; + + if ((ret = read_json_body(http_req, req)) != 0) + return (ret); + + return parse_json_body(req); +} + +static int +write_id(yajl_gen gen, yajl_val id) +{ + int status; + + if (YAJL_GEN_KO(status = YAJL_GEN_CONST_STRING(gen, "id"))) + return (status); + + if (YAJL_IS_NULL(id)) + return yajl_gen_null(gen); + + if (YAJL_IS_NUMBER(id)) { + if (YAJL_IS_INTEGER(id)) + return yajl_gen_integer(gen, YAJL_GET_INTEGER(id)); + return yajl_gen_null(gen); + } + + if (YAJL_IS_STRING(id)) { + char *id_str = YAJL_GET_STRING(id); + + return yajl_gen_string(gen, (unsigned char *)id_str, + strlen(id_str)); + } + + return yajl_gen_null(gen); +} + +static int +open_response(yajl_gen genctx, yajl_val id) +{ + int status; + + if (YAJL_GEN_KO(status = yajl_gen_map_open(genctx))) + goto failed; + if (YAJL_GEN_KO(status = YAJL_GEN_CONST_STRING(genctx, "jsonrpc"))) + goto failed; + if (YAJL_GEN_KO(status = YAJL_GEN_CONST_STRING(genctx, "2.0"))) + goto failed; + status = write_id(genctx, id); +failed: + return (status); +} + +static int +close_response(yajl_gen genctx) +{ + int status; + + if (YAJL_GEN_KO(status = yajl_gen_map_close(genctx))) + goto failed; + status = yajl_gen_map_close(genctx); +failed: + return (status); +} + +static int +write_log(struct jsonrpc_request *req) +{ + bool wrote_smth = false; + int status = 0; + + for (struct jsonrpc_log *log = req->log.next; log != &req->log; + log = log->next) { + + if (((1 << log->lvl) & req->log_levels) == 0) + continue; + + if (!wrote_smth) { + if (YAJL_GEN_KO(status = YAJL_GEN_CONST_STRING(req->gen, + "data"))) + goto failed; + if (YAJL_GEN_KO(status = yajl_gen_array_open(req->gen))) + goto failed; + yajl_gen_config(req->gen, yajl_gen_validate_utf8, 1); + wrote_smth = true; + } + + if (YAJL_GEN_KO(status = yajl_gen_array_open(req->gen))) + goto failed; + if (YAJL_GEN_KO(status = yajl_gen_integer(req->gen, log->lvl))) + goto failed; + if (YAJL_GEN_KO(status = yajl_gen_string(req->gen, + (unsigned char *)log->msg, strlen(log->msg)))) + goto failed; + if (YAJL_GEN_KO(status = yajl_gen_array_close(req->gen))) + goto failed; + } + + if (wrote_smth) { + yajl_gen_config(req->gen, yajl_gen_validate_utf8, 0); + status = yajl_gen_array_close(req->gen); + } +failed: + return (status); +} + +static int +write_error(struct jsonrpc_request *req, int code, const char *message) +{ + int status; + + yajl_gen_config(req->gen, yajl_gen_validate_utf8, 0); + + if (YAJL_GEN_KO(status = open_response(req->gen, req->id))) + goto failed; + if (YAJL_GEN_KO(status = YAJL_GEN_CONST_STRING(req->gen, "error"))) + goto failed; + if (YAJL_GEN_KO(status = yajl_gen_map_open(req->gen))) + goto failed; + if (YAJL_GEN_KO(status = YAJL_GEN_CONST_STRING(req->gen, "code"))) + goto failed; + if (YAJL_GEN_KO(status = yajl_gen_integer(req->gen, code))) + goto failed; + if (YAJL_GEN_KO(status = YAJL_GEN_CONST_STRING(req->gen, "message"))) + goto failed; + + yajl_gen_config(req->gen, yajl_gen_validate_utf8, 1); + + if (YAJL_GEN_KO(status = yajl_gen_string(req->gen, + (const unsigned char *)message, strlen(message)))) + goto failed; + + yajl_gen_config(req->gen, yajl_gen_validate_utf8, 0); + + if (YAJL_GEN_KO(status = write_log(req))) + goto failed; + + status = close_response(req->gen); +failed: + return (status); +} + +static const char * +known_msg(int code) +{ + switch (code) { + case JSONRPC_PARSE_ERROR: + return (JSONRPC_PARSE_ERROR_MSG); + case JSONRPC_INVALID_REQUEST: + return (JSONRPC_INVALID_REQUEST_MSG); + case JSONRPC_METHOD_NOT_FOUND: + return (JSONRPC_METHOD_NOT_FOUND_MSG); + case JSONRPC_INVALID_PARAMS: + return (JSONRPC_INVALID_PARAMS_MSG); + case JSONRPC_INTERNAL_ERROR: + return (JSONRPC_INTERNAL_ERROR_MSG); + case JSONRPC_SERVER_ERROR: + return (JSONRPC_SERVER_ERROR_MSG); + case JSONRPC_LIMIT_REACHED: + return (JSONRPC_LIMIT_REACHED_MSG); + default: + return (NULL); + } +} + +int +jsonrpc_error(struct jsonrpc_request *req, int code, const char *msg) +{ + char *msg_fallback; + const unsigned char *body = NULL; + size_t body_len = 0; + int status; + + if (req->id == NULL) + goto succeeded; + + if ((req->gen = yajl_gen_alloc(NULL)) == NULL) { + kore_log(LOG_ERR, "jsonrpc_error: Failed to allocate yajl gen"); + goto failed; + } + + yajl_gen_config(req->gen, yajl_gen_beautify, + req->flags & yajl_gen_beautify); + + if (msg == NULL) + msg = known_msg(code); + + if (msg == NULL) { + size_t start = req->buf.offset; + kore_buf_appendf(&req->buf, "%d", code); + msg_fallback = kore_buf_stringify(&req->buf, NULL) + start; + } + + if (YAJL_GEN_KO(status = write_error(req, code, + msg ? msg : msg_fallback))) { + kore_log(LOG_ERR, "jsonrpc_error: Failed to yajl gen text [%d]", + status); + goto failed; + } + + http_response_header(req->http, "content-type", "application/json"); + yajl_gen_get_buf(req->gen, &body, &body_len); +succeeded: + http_response(req->http, 200, body, body_len); + if (req->gen != NULL) + yajl_gen_clear(req->gen); + jsonrpc_destroy_request(req); + return (KORE_RESULT_OK); +failed: + http_response(req->http, 500, NULL, 0); + jsonrpc_destroy_request(req); + return (KORE_RESULT_OK); +} + +int +jsonrpc_result(struct jsonrpc_request *req, + int (*write_result)(struct jsonrpc_request *, void *), void *ctx) +{ + const unsigned char *body = NULL; + size_t body_len = 0; + + if (req->id == NULL) + goto succeeded; + + if ((req->gen = yajl_gen_alloc(NULL)) == NULL) { + kore_log(LOG_ERR, "jsonrpc_result: Failed to allocate yajl gen"); + goto failed; + } + + yajl_gen_config(req->gen, yajl_gen_beautify, + req->flags & yajl_gen_beautify); + + yajl_gen_config(req->gen, yajl_gen_validate_utf8, 0); + + if (YAJL_GEN_KO(open_response(req->gen, req->id))) + goto failed; + if (YAJL_GEN_KO(YAJL_GEN_CONST_STRING(req->gen, "result"))) + goto failed; + if (YAJL_GEN_KO(write_result(req, ctx))) + goto failed; + if (YAJL_GEN_KO(yajl_gen_map_close(req->gen))) + goto failed; + + http_response_header(req->http, "content-type", "application/json"); + yajl_gen_get_buf(req->gen, &body, &body_len); +succeeded: + http_response(req->http, 200, body, body_len); + if (req->gen != NULL) + yajl_gen_clear(req->gen); + jsonrpc_destroy_request(req); + return (KORE_RESULT_OK); +failed: + http_response(req->http, 500, NULL, 0); + jsonrpc_destroy_request(req); + return (KORE_RESULT_OK); +} diff --git a/src/kore.c b/src/kore.c @@ -415,6 +415,9 @@ kore_server_start(void) #if defined(KORE_USE_TASKS) kore_log(LOG_NOTICE, "tasks built-in enabled"); #endif +#if defined(KORE_USE_JSONRPC) + kore_log(LOG_NOTICE, "jsonrpc built-in enabled"); +#endif kore_platform_proctitle("kore [parent]"); kore_msg_init();