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:
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();