Compare commits


65 Commits

Author SHA1 Message Date
Přemysl Eric Janouch 6934550068
CMakeLists.txt: declare compatibility with 3.27
Sadly, the 3.5 deprecation warning doesn't go away after this.
2023-08-01 03:14:48 +02:00
Přemysl Eric Janouch cda7e1b1f3
Try harder to find ncursesw 2023-07-24 09:01:54 +02:00
Přemysl Eric Janouch 14c6d285fc
CMakeLists.txt: fix OpenBSD build
Note that we still don't use link_directories() as often as we should.
2023-07-04 07:45:57 +02:00
Přemysl Eric Janouch ab5941aaef
CMakeLists.txt: fix build on macOS 2023-07-04 02:50:00 +02:00
Přemysl Eric Janouch 84d6c658e8
README.adoc: update package information 2023-07-01 22:01:16 +02:00
Přemysl Eric Janouch a89fadf860
Bump liberty, improve fallback manual page output 2022-10-09 01:05:35 +02:00
Přemysl Eric Janouch 4023155b67
Improve link detection suppression in man page 2022-09-30 14:49:51 +02:00
Přemysl Eric Janouch 4ed58dd89a
Bump liberty, make use of its new asciiman.awk 2022-09-25 21:24:18 +02:00
Přemysl Eric Janouch 022668fb23
libedit (editline) seems to work just fine now,
except for not being fully asynchronous.
2022-09-04 17:07:38 +02:00
Přemysl Eric Janouch ba5a6374b6
json-rpc-test-server: add a "wait" method
Considering the server's nature, the global lock-up it causes
shouldn't constitute a major problem.
2022-09-04 15:25:27 +02:00
Přemysl Eric Janouch 67008963cf
Update NEWS 2022-09-04 15:22:46 +02:00
Přemysl Eric Janouch c1b6918db3
Fix libedit history behaviour 2022-09-04 15:22:46 +02:00
Přemysl Eric Janouch 3cf3c0215e
Build with AsciiDoc as well as Asciidoctor 2022-08-24 01:09:30 +02:00
Přemysl Eric Janouch a2a72c8b92
Update .gitignore 2021-10-30 03:34:23 +02:00
Přemysl Eric Janouch 57f89eba07
Add clang-format configuration 2021-10-30 02:59:33 +02:00
Přemysl Eric Janouch 4795ee851d
FindLibEV.cmake: synchronise 2021-10-30 01:56:48 +02:00
Přemysl Eric Janouch 87a644cc59
Fix newer libedit (2021-08-29) 2021-10-28 08:30:41 +02:00
Přemysl Eric Janouch 990cf5a1d4
Reflect the sibling project's new name
Better keep all schizophreny in my own head, rather than all projects.
2021-08-06 19:26:04 +02:00
Přemysl Eric Janouch 4a5c818ba1
json-rpc-shell: respect the NO_COLOR env. variable 2021-07-07 19:16:00 +02:00
Přemysl Eric Janouch af5929a383
CMakeLists.txt: fix copy-pasted variable name 2020-10-30 16:48:02 +01:00
Přemysl Eric Janouch 9f5845fc51
json-rpc-shell.adoc: minor improvements
Documented envvars and added a note about XDG paths.
2020-10-30 04:21:17 +01:00
Přemysl Eric Janouch 3daf254b41
CMakeLists.txt: make this build in OpenBSD 2020-10-30 04:21:16 +01:00
Přemysl Eric Janouch c533fa2fd7
CMakeLists.txt: omit end{if,foreach} expressions
Their usefulness was almost negative.
2020-10-30 04:21:16 +01:00
Přemysl Eric Janouch 2fe2d6bc03
Bump minimum CMake version to 3.0
A nice, round number.  This allows us to remove some boilerplate.
2020-10-30 04:21:16 +01:00
Přemysl Eric Janouch df93937789
CMakeLists.txt: fix an outdated comment 2020-10-30 04:21:15 +01:00
Přemysl Eric Janouch ae447065f7
Bump liberty 2020-10-30 04:21:15 +01:00
Přemysl Eric Janouch f9e157293c
json-rpc-test-server: only return regular files
They can be symlinked.
2020-10-17 23:30:22 +02:00
Přemysl Eric Janouch 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
Přemysl Eric Janouch 710f8e0b2d
json-rpc-test-server: fix function names
Very obviously copied and pasted from the shell.
2020-10-16 23:55:15 +02:00
Přemysl Eric Janouch 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
Přemysl Eric Janouch 6927d022fb
WebSocket: send a User-Agent header 2020-10-15 04:30:48 +02:00
Přemysl Eric Janouch 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
Přemysl Eric Janouch 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
Přemysl Eric Janouch 4236a4943a
WebSocket: adapt to common "await" infrastructure 2020-10-14 13:37:10 +02:00
Přemysl Eric Janouch 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
Přemysl Eric Janouch dfe814316f
This software is no longer simple 2020-10-14 13:36:35 +02:00
Přemysl Eric Janouch efc663a178
WebSocket: some clean-up 2020-10-14 12:25:22 +02:00
Přemysl Eric Janouch 2b8f52ac72
Split out a http-parser wrapper 2020-10-14 12:25:22 +02:00
Přemysl Eric Janouch bb7ffe1da2
Simplify the FAIL macro 2020-10-14 12:25:21 +02:00
Přemysl Eric Janouch 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
Přemysl Eric Janouch 0107d09abc
json-rpc-shell.adoc: document the M-Enter binding 2020-10-14 02:37:50 +02:00
Přemysl Eric Janouch 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
Přemysl Eric Janouch 5854ed1b32
Support reading OpenRPC documents from a file
Bump liberty, it generated incorrect help messages.
2020-10-13 21:48:24 +02:00
Přemysl Eric Janouch 63c8a79479
Factor out init_backend()
The main() function is still way too long.
2020-10-13 21:07:40 +02:00
Přemysl Eric Janouch d489362a28
json-rpc-test-server: implement 2020-10-13 20:54:27 +02:00
Přemysl Eric Janouch c87869bef7
Prevent the last fuck-up from happening again.
2020-10-13 20:30:33 +02:00
Přemysl Eric Janouch fcf65f8377
Add libedit autocompletion back in
I've mistakenly removed it in the M-Enter change.
2020-10-13 20:19:19 +02:00
Přemysl Eric Janouch d820bc2f23
Bump version, update NEWS 2020-10-13 16:03:19 +02:00
Přemysl Eric Janouch b458fc1f99
libedit: bind M-Enter to newline-insert as well 2020-10-13 15:55:37 +02:00
Přemysl Eric Janouch 0771c142fe
json-rpc-test-server: fix reading the request URI 2020-10-13 04:46:08 +02:00
Přemysl Eric Janouch 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
Přemysl Eric Janouch 2221828763
OpenRPC: avoid eating HTTP/transport errors 2020-10-13 04:35:32 +02:00
Přemysl Eric Janouch 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
Přemysl Eric Janouch 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
Přemysl Eric Janouch 5d2cd01db0
json-rpc-test-server: fix a potential memory leak 2020-10-13 02:08:53 +02:00
Přemysl Eric Janouch ee79249d23
json-rpc-shell.adoc: update WebSocket notes also uses WebSockets,
although they don't seem to support notifications (in general).
2020-10-10 05:20:31 +02:00
Přemysl Eric Janouch 160d23018a
Bump liberty
resolve_relative_runtime_unique_filename() used to have a bug.
2020-10-10 05:09:11 +02:00
Přemysl Eric Janouch 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
Přemysl Eric Janouch 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
Přemysl Eric Janouch 20c8578084
Fix use of possibly uninitialised memory 2020-10-10 02:57:14 +02:00
Přemysl Eric Janouch 57a3b4e990
Split make_json_rpc_call() in half 2020-10-10 02:57:13 +02:00
Přemysl Eric Janouch e4d1529b4d
Slightly refactor make_json_rpc_call() 2020-10-10 02:57:13 +02:00
Přemysl Eric Janouch 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
Přemysl Eric Janouch 84702fa47d
Fix handling terminal resizes while the terminal is suspended
GNU Readline has a misfeature.
2020-10-09 20:21:52 +02:00
Přemysl Eric Janouch 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
13 changed files with 1547 additions and 582 deletions

.clang-format Normal file
View File

@ -0,0 +1,33 @@
# clang-format is fairly limited, and these rules are approximate:
# - array initializers can get terribly mangled with clang-format 12.0,
# - sometimes it still aligns with space characters,
# - EV_DEFAULT_ and EV_A_ are always taken as identifiers,
# - struct name NL { NL ... NL } NL name; is unachievable.
BasedOnStyle: GNU
ColumnLimit: 80
IndentWidth: 4
TabWidth: 4
UseTab: ForContinuationAndIndentation
BreakBeforeBraces: Allman
SpaceAfterCStyleCast: true
AlignAfterOpenBracket: DontAlign
AlignOperands: DontAlign
AlignConsecutiveMacros: Consecutive
AllowAllArgumentsOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
IndentGotoLabels: false
# IncludeCategories has some potential, but it may also break the build.
# Note that the documentation says the value should be "Never".
SortIncludes: false
# This is a compromise, it generally works out aesthetically better.
BinPackArguments: false
# Unfortunately, this can't be told to align to column 40 or so.
SpacesBeforeTrailingComments: 2
# liberty-specific macro body wrappers.
MacroBlockBegin: "BLOCK_START"
MacroBlockEnd: "BLOCK_END"
ForEachMacros: ["LIST_FOR_EACH"]

.gitignore vendored
View File

@ -7,3 +7,5 @@

View File

@ -1,5 +1,5 @@
project (json-rpc-shell C)
cmake_minimum_required (VERSION 2.8.5)
cmake_minimum_required (VERSION 3.0...3.27)
project (json-rpc-shell VERSION 1.1.0 LANGUAGES C)
# Options
option (WANT_READLINE "Use GNU Readline for the UI (better)" ON)
# -Wunused-function is pretty annoying here, as everything is static
"${CMAKE_C_FLAGS} -std=c99 -Wall -Wextra -Wno-unused-function")
# Version
set (project_VERSION_MAJOR "1")
set (project_VERSION_MINOR "0")
set (project_VERSION_PATCH "0")
set (project_VERSION "${project_VERSION_MAJOR}")
set (project_VERSION "${project_VERSION}.${project_VERSION_MINOR}")
set (project_VERSION "${project_VERSION}.${project_VERSION_PATCH}")
endif ()
# For custom modules
# Dependencies
find_package (Curses)
find_package (Ncursesw)
find_package (PkgConfig REQUIRED)
pkg_check_modules (dependencies REQUIRED libcurl jansson)
# Note that cURL can link to a different version of libssl than we do,
# in which case the results are undefined
pkg_check_modules (libssl REQUIRED libssl libcrypto)
pkg_check_modules (dependencies REQUIRED libcurl jansson libssl libcrypto)
find_package (LibEV REQUIRED)
pkg_check_modules (ncursesw ncursesw)
set (project_libraries ${dependencies_LIBRARIES}
include_directories (${dependencies_INCLUDE_DIRS}
set (project_libraries ${dependencies_LIBRARIES} ${LibEV_LIBRARIES})
include_directories (${dependencies_INCLUDE_DIRS} ${LibEV_INCLUDE_DIRS})
link_directories (${dependencies_LIBRARY_DIRS})
if (ncursesw_FOUND)
list (APPEND project_libraries ${ncursesw_LIBRARIES})
include_directories (${ncursesw_INCLUDE_DIRS})
# -liconv may or may not be a part of libc
find_library (iconv_LIBRARIES iconv)
if (iconv_LIBRARIES)
list (APPEND project_libraries ${iconv_LIBRARIES})
endif ()
# Need this for SIGWINCH in FreeBSD and OpenBSD respectively;
# our POSIX version macros make it undefined
add_definitions (-D__BSD_VISIBLE=1 -D_BSD_SOURCE=1)
elseif (APPLE)
add_definitions (-D_DARWIN_C_SOURCE)
endif ()
if (Ncursesw_FOUND)
list (APPEND project_libraries ${Ncursesw_LIBRARIES})
include_directories (${Ncursesw_INCLUDE_DIRS})
link_directories (${Ncursesw_LIBRARY_DIRS})
list (APPEND project_libraries ${CURSES_LIBRARY})
include_directories (${CURSES_INCLUDE_DIR})
else ()
message (SEND_ERROR "Curses not found")
endif (ncursesw_FOUND)
endif ()
message (SEND_ERROR "You have to choose either GNU Readline or libedit")
list (APPEND project_libraries readline)
# OpenBSD's default readline is too old
include_directories (${OPENBSD_LOCALBASE}/include/ereadline)
list (APPEND project_libraries ereadline)
else ()
list (APPEND project_libraries readline)
endif ()
pkg_check_modules (libedit REQUIRED libedit)
list (APPEND project_libraries ${libedit_LIBRARIES})
include_directories (${libedit_INCLUDE_DIRS})
endif ()
# Generate a configuration file
# Generate documentation from program help
# Generate documentation from text markup
find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor)
message (FATAL_ERROR "asciidoctor not found")
find_program (A2X_EXECUTABLE a2x)
message (WARNING "Neither asciidoctor nor a2x were found, "
"falling back to a substandard manual page generator")
endif ()
foreach (page ${PROJECT_NAME})
set (page_output "${PROJECT_BINARY_DIR}/${page}.1")
list (APPEND project_MAN_PAGES "${page_output}")
add_custom_command (OUTPUT ${page_output}
-a release-version=${project_VERSION}
-o "${page_output}"
DEPENDS ${page}.adoc
COMMENT "Generating man page for ${page}" VERBATIM)
endforeach (page)
add_custom_command (OUTPUT ${page_output}
-a release-version=${PROJECT_VERSION}
-o "${page_output}"
DEPENDS ${page}.adoc
COMMENT "Generating man page for ${page}" VERBATIM)
add_custom_command (OUTPUT ${page_output}
COMMAND ${A2X_EXECUTABLE} --doctype manpage --format manpage
-a release-version=${PROJECT_VERSION}
DEPENDS ${page}.adoc
COMMENT "Generating man page for ${page}" VERBATIM)
else ()
set (ASCIIMAN ${PROJECT_SOURCE_DIR}/liberty/tools/asciiman.awk)
add_custom_command (OUTPUT ${page_output}
COMMAND env LC_ALL=C asciidoc-release-version=${PROJECT_VERSION}
awk -f ${ASCIIMAN} "${PROJECT_SOURCE_DIR}/${page}.adoc"
> ${page_output}
DEPENDS ${page}.adoc ${ASCIIMAN}
COMMENT "Generating man page for ${page}" VERBATIM)
endif ()
endforeach ()
add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES})
@ -110,23 +141,19 @@ foreach (page ${project_MAN_PAGES})
string (REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}")
install (FILES "${page}"
endforeach (page)
endforeach ()
# CPack
"A shell for running JSON-RPC 2.0 queries")
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <>")
set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user")
include (CPack)

View File

@ -1,4 +1,4 @@
Copyright (c) 2014 - 2020, Přemysl Eric Janouch <>
Copyright (c) 2014 - 2022, Přemysl Eric Janouch <>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.

View File

@ -1,3 +1,31 @@
* Add a backend for co-processes, such as language servers
* Support reading OpenRPC documents from a file
* Respect the NO_COLOR environment variable
* Miscellaneous libedit (editline) fixes
* json-rpc-test-server: implement OpenRPC discovery
* json-rpc-test-server: only serve regular files
* json-rpc-test-server: miscellaneous WebSocket fixes
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
'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:
This software was originally created as a replacement for[a different shell] made by
Vladimir Dzhuvinov, in order to avoid Java, but has evolved since.
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
@ -26,19 +29,17 @@ The rest of this README will concern itself with externalities.
Regular releases are sporadic. git master should be stable enough. You can get
a package with the latest development version from Archlinux's AUR.
Regular releases are sporadic. git master should be stable enough.
You can get a package with the latest development version using Arch Linux's[AUR],
or as a[Nix derivation].
Build dependencies: CMake, pkg-config, asciidoctor,
liberty (included), http-parser (included) +
Runtime dependencies: libev, Jansson, cURL, openssl,
readline or libedit >= 2013-07-12,
Avoid libedit if you can, in general it works but at the moment history is
acting up and I have no clue about fixing it. Multiline editing is also
misbehaving there.
Build dependencies: CMake, pkg-config, liberty (included),
http-parser (included), asciidoctor or asciidoc (recommended but optional) +
Runtime dependencies:
libev, Jansson, cURL, openssl, readline or libedit >= 2013-07-12
$ git clone --recursive
$ mkdir json-rpc-shell/build
@ -55,15 +56,12 @@ Or you can try telling CMake to make a package for you. For Debian it is:
$ cpack -G DEB
# dpkg -i json-rpc-shell-*.deb
Note that for versions of CMake before 2.8.9, you need to prefix `cpack` with
`fakeroot` or file ownership will end up wrong.
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

@ -5,14 +5,16 @@
# Some distributions do add it, though
find_package (PkgConfig REQUIRED)
pkg_check_modules (LIBEV QUIET libev)
pkg_check_modules (LibEV QUIET libev)
find_path (LIBEV_INCLUDE_DIRS ev.h)
find_library (LIBEV_LIBRARIES NAMES ev)
set (required_vars LibEV_LIBRARIES)
find_path (LibEV_INCLUDE_DIRS ev.h)
find_library (LibEV_LIBRARIES NAMES ev)
list (APPEND required_vars LibEV_INCLUDE_DIRS)
endif ()
include (FindPackageHandleStandardArgs)
mark_as_advanced (LibEV_LIBRARIES LibEV_INCLUDE_DIRS)

View File

@ -2,7 +2,7 @@
#define CONFIG_H
#define PROGRAM_VERSION "${project_VERSION}"
#cmakedefine HAVE_READLINE
#cmakedefine HAVE_EDITLINE

@ -1 +1 @@
Subproject commit 5d414fcb4b2ccc1ce9d6063292f9c63c9ec67b04
Subproject commit ec8b5ee63f0e51191ea43bb0c6eac7bfbff3141d

View File

@ -6,16 +6,17 @@ json-rpc-shell(1)
json-rpc-shell - a simple JSON-RPC 2.0 shell
json-rpc-shell - a shell for JSON-RPC 2.0
*json-rpc-shell* [_OPTION_]... _ENDPOINT_
*json-rpc-shell* [_OPTION_]... { _ENDPOINT_ | _COMMAND_ [_ARG_]... }
:colon: :
The _ENDPOINT_ must be either an HTTP or a WebSocket URL, with or without TLS
(i.e. one of the _+++http+++://_, _+++https+++://_, _ws://_, _wss://_ schemas).
(i.e. one of the _http{colon}//_, _https{colon}//_, _ws://_, _wss://_ schemas).
*json-rpc-shell* will use it to send any JSON-RPC 2.0 requests you enter on its
command line. The server's response will be parsed and validated, stripping it
@ -76,6 +77,15 @@ Protocol
*-o* _ORIGIN_, *--origin*=_ORIGIN_::
Set the HTTP Origin header to _ORIGIN_. Some servers may need this.
*-O*[__PATH__], *--openrpc*[**=**__PATH__]::
Call "" 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
Program information
*-h*, *--help*::
@ -87,8 +97,16 @@ Program information
Write a default configuration file, show its path and exit.
The editor program to be launched by the M-e key binding.
If neither variable is set, it defaults to *vi*(1).
*json-rpc-shell* follows the XDG Base Directory Specification.
The configuration file, in which you can configure color output and
CA certificate paths. Use the *--write-default-cfg* option to create
@ -107,14 +125,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
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.
@ -124,8 +141,7 @@ the higher-level protocol (the "Sec-Ws-Protocol" HTTP field).
The editline (libedit) frontend is more of a proof of concept that mostly seems
to work but exhibits bugs that are not our fault.
The editline (libedit) frontend may exhibit some unexpected behaviour.

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
* json-rpc-test-server.c: JSON-RPC 2.0 demo server
* Copyright (c) 2015 - 2020, Přemysl Eric Janouch <>
* Copyright (c) 2015 - 2022, Přemysl Eric Janouch <>
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
@ -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_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"))
// 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");
print_debug ("WS handshake failed: %s",
http_errno_description (err));
"Upgrade: websocket", SEC_WS_VERSION ": 13");
print_debug ("WS handshake failed: %s", http_errno_description (err));
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,39 @@ 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 ("[oooo]",
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 ("", json_pack ("{ss}", "$ref",
open_rpc_describe ("wait", json_pack ("{ss}", "type", "null")));
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)
@ -1457,6 +1493,16 @@ json_rpc_ping (struct server_context *ctx, json_t *params)
return json_rpc_response (NULL, json_string ("pong"), NULL);
static json_t *
json_rpc_wait (struct server_context *ctx, json_t *params)
(void) ctx;
(void) params;
sleep (1);
return json_rpc_response (NULL, json_null (), NULL);
static json_t *
json_rpc_date (struct server_context *ctx, json_t *params)
@ -1486,8 +1532,10 @@ 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 },
{ "", json_rpc_discover },
{ "wait", json_rpc_wait },
if (!json_is_object (request))
@ -1544,7 +1592,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 +1666,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 +1818,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 +1937,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 +1988,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 +2123,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 +2436,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 +2497,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 +2593,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;
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;
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)
str_free (&buf);
str_free (&self.message);
// --- Basic server stuff ------------------------------------------------------
typedef struct client *(*client_create_fn) (EV_P_ int sock_fd);
@ -2877,11 +3115,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 +3140,9 @@ parse_program_arguments (int argc, char **argv)
case 't':
test_main (argc, argv);
case 's':
*running_as_slave = true;
case 'd':
g_debug_mode = true;
@ -2933,7 +3175,8 @@ parse_program_arguments (int argc, char **argv)
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 +3191,15 @@ main (int argc, char *argv[])
// 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);
struct ev_loop *loop;
if (!(loop = EV_DEFAULT))
exit_fatal ("libev initialization failed");

@ -1 +1 @@
Subproject commit 1a76b2032e6d18d9f95d9d0bb98edc26023c8618
Subproject commit 7a0cb13a1a653f61b0e715f79156046898d0dd1b