Compare commits

...

47 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
Update README
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 rpc.discover 2020-10-13 20:54:27 +02:00
Přemysl Eric Janouch c87869bef7
Cleanup
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
12 changed files with 1212 additions and 458 deletions

33
.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"]

2
.gitignore vendored
View File

@ -7,3 +7,5 @@
/json-rpc-shell.files
/json-rpc-shell.creator*
/json-rpc-shell.includes
/json-rpc-shell.cflags
/json-rpc-shell.cxxflags

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)
@ -10,54 +10,65 @@ if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
# -Wunused-function is pretty annoying here, as everything is static
set (CMAKE_C_FLAGS
"${CMAKE_C_FLAGS} -std=c99 -Wall -Wextra -Wno-unused-function")
endif ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
# Version
set (project_VERSION_MAJOR "1")
set (project_VERSION_MINOR "1")
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
set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
set (CMAKE_MODULE_PATH
"${PROJECT_SOURCE_DIR}/cmake;${PROJECT_SOURCE_DIR}/liberty/cmake")
# 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}
${libssl_LIBRARIES} ${LIBEV_LIBRARIES})
include_directories (${dependencies_INCLUDE_DIRS}
${libssl_INCLUDE_DIRS} ${LIBEV_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 ()
if ("${CMAKE_SYSTEM_NAME}" MATCHES "BSD")
# 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})
elseif (CURSES_FOUND)
list (APPEND project_libraries ${CURSES_LIBRARY})
include_directories (${CURSES_INCLUDE_DIR})
else (CURSES_FOUND)
else ()
message (SEND_ERROR "Curses not found")
endif (ncursesw_FOUND)
endif ()
if ((WANT_READLINE AND WANT_LIBEDIT) OR (NOT WANT_READLINE AND NOT WANT_LIBEDIT))
message (SEND_ERROR "You have to choose either GNU Readline or libedit")
elseif (WANT_READLINE)
list (APPEND project_libraries readline)
# OpenBSD's default readline is too old
if ("${CMAKE_SYSTEM_NAME}" MATCHES "OpenBSD")
include_directories (${OPENBSD_LOCALBASE}/include/ereadline)
list (APPEND project_libraries ereadline)
else ()
list (APPEND project_libraries readline)
endif ()
elseif (WANT_LIBEDIT)
pkg_check_modules (libedit REQUIRED libedit)
list (APPEND project_libraries ${libedit_LIBRARIES})
include_directories (${libedit_INCLUDE_DIRS})
endif ((WANT_READLINE AND WANT_LIBEDIT) OR (NOT WANT_READLINE AND NOT WANT_LIBEDIT))
endif ()
# Generate a configuration file
set (HAVE_READLINE "${WANT_READLINE}")
@ -86,23 +97,43 @@ install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
install (PROGRAMS json-format.pl DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
# Generate documentation from program help
# Generate documentation from text markup
find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor)
if (NOT ASCIIDOCTOR_EXECUTABLE)
message (FATAL_ERROR "asciidoctor not found")
endif (NOT ASCIIDOCTOR_EXECUTABLE)
find_program (A2X_EXECUTABLE a2x)
if (NOT ASCIIDOCTOR_EXECUTABLE AND NOT A2X_EXECUTABLE)
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}
COMMAND ${ASCIIDOCTOR_EXECUTABLE} -b manpage
-a release-version=${project_VERSION}
"${PROJECT_SOURCE_DIR}/${page}.adoc"
-o "${page_output}"
DEPENDS ${page}.adoc
COMMENT "Generating man page for ${page}" VERBATIM)
endforeach (page)
if (ASCIIDOCTOR_EXECUTABLE)
add_custom_command (OUTPUT ${page_output}
COMMAND ${ASCIIDOCTOR_EXECUTABLE} -b manpage
-a release-version=${PROJECT_VERSION}
-o "${page_output}"
"${PROJECT_SOURCE_DIR}/${page}.adoc"
DEPENDS ${page}.adoc
COMMENT "Generating man page for ${page}" VERBATIM)
elseif (A2X_EXECUTABLE)
add_custom_command (OUTPUT ${page_output}
COMMAND ${A2X_EXECUTABLE} --doctype manpage --format manpage
-a release-version=${PROJECT_VERSION}
-D "${PROJECT_BINARY_DIR}"
"${PROJECT_SOURCE_DIR}/${page}.adoc"
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}"
DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}")
endforeach (page)
endforeach ()
# CPack
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY
"A shell for running JSON-RPC 2.0 queries")
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "A shell for JSON-RPC 2.0")
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
set (CPACK_PACKAGE_VERSION_MAJOR ${project_VERSION_MAJOR})
set (CPACK_PACKAGE_VERSION_MINOR ${project_VERSION_MINOR})
set (CPACK_PACKAGE_VERSION_PATCH ${project_VERSION_PATCH})
set (CPACK_GENERATOR "TGZ;ZIP")
set (CPACK_PACKAGE_FILE_NAME
"${PROJECT_NAME}-${project_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${project_VERSION}")
"${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${PROJECT_VERSION}")
set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${project_VERSION}")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}")
include (CPack)

View File

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

17
NEWS
View File

@ -1,3 +1,20 @@
Unreleased
* 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

View File

@ -2,23 +2,25 @@ json-rpc-shell
==============
:compact-option:
'json-rpc-shell' is a simple shell for running JSON-RPC 2.0 queries.
'json-rpc-shell' is a shell for running JSON-RPC 2.0 queries.
This software has been created as a replacement for the following shell, which
is written in Java: http://software.dzhuvinov.com/json-rpc-2.0-shell.html
This software was originally created as a replacement for
http://software.dzhuvinov.com/json-rpc-2.0-shell.html[a different shell] made by
Vladimir Dzhuvinov, in order to avoid Java, but has evolved since.
Features
--------
In addition to most of the features provided by Vladimir Dzhuvinov's shell
you get the following niceties:
In addition to most of the features provided by its predecessor, you will get
the following niceties:
- configurable JSON syntax highlight, which with prettyprinting turned on
helps you make sense of the results significantly
- ability to pipe output through a shell command, so that you can view the
results in your favourite editor or redirect them to a file
- ability to edit the input line in your favourite editor as well with Alt+E
- WebSockets (RFC 6455) can also be used as a transport rather than HTTP
- support for method name tab completion using OpenRPC discovery
- WebSocket (RFC 6455) can also be used as a transport rather than HTTP
- even Language Server Protocol servers may be launched as a slave command
- support for method name tab completion using OpenRPC discovery or file input
Documentation
-------------
@ -27,19 +29,17 @@ The rest of this README will concern itself with externalities.
Packages
--------
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
https://aur.archlinux.org/packages/json-rpc-shell-git[AUR],
or as a https://git.janouch.name/p/nixexprs[Nix derivation].
Building
--------
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 https://git.janouch.name/p/json-rpc-shell.git
$ mkdir json-rpc-shell/build
@ -56,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)
if (NOT LIBEV_FOUND)
find_path (LIBEV_INCLUDE_DIRS ev.h)
find_library (LIBEV_LIBRARIES NAMES ev)
set (required_vars LibEV_LIBRARIES)
if (NOT LibEV_FOUND)
find_path (LibEV_INCLUDE_DIRS ev.h)
find_library (LibEV_LIBRARIES NAMES ev)
list (APPEND required_vars LibEV_INCLUDE_DIRS)
endif ()
if (LIBEV_INCLUDE_DIRS AND LIBEV_LIBRARIES)
set (LIBEV_FOUND TRUE)
endif (LIBEV_INCLUDE_DIRS AND LIBEV_LIBRARIES)
endif (NOT LIBEV_FOUND)
include (FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS (LibEV DEFAULT_MSG ${required_vars})
mark_as_advanced (LibEV_LIBRARIES LibEV_INCLUDE_DIRS)

View File

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

View File

@ -6,16 +6,17 @@ json-rpc-shell(1)
Name
----
json-rpc-shell - a simple JSON-RPC 2.0 shell
json-rpc-shell - a shell for JSON-RPC 2.0
Synopsis
--------
*json-rpc-shell* [_OPTION_]... _ENDPOINT_
*json-rpc-shell* [_OPTION_]... { _ENDPOINT_ | _COMMAND_ [_ARG_]... }
Description
-----------
: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,9 +77,14 @@ Protocol
*-o* _ORIGIN_, *--origin*=_ORIGIN_::
Set the HTTP Origin header to _ORIGIN_. Some servers may need this.
*-O*, *--openrpc*::
*-O*[__PATH__], *--openrpc*[**=**__PATH__]::
Call "rpc.discover" upon start-up in order to pull in OpenRPC data for
tab completion of method names.
tab completion of method names. If a path is given, it is read from a file.
*-e*, *--execute*::
Rather than an _ENDPOINT_, accept a command line to execute and communicate
with using the JSON-RPC 2.0 protocol variation used in the Language Server
Protocol.
Program information
~~~~~~~~~~~~~~~~~~~
@ -91,8 +97,16 @@ Program information
*--write-default-cfg*[**=**__PATH__]::
Write a default configuration file, show its path and exit.
Environment
-----------
*VISUAL*, *EDITOR*::
The editor program to be launched by the M-e key binding.
If neither variable is set, it defaults to *vi*(1).
Files
-----
*json-rpc-shell* follows the XDG Base Directory Specification.
_~/.config/json-rpc-shell/json-rpc-shell.conf_::
The configuration file, in which you can configure color output and
CA certificate paths. Use the *--write-default-cfg* option to create
@ -111,11 +125,11 @@ requests, it is often convenient or even necessary to run a full text editor
in order to construct complex objects or arrays, and may even be used to import
data from elsewhere. You can launch an editor for the current request using
the M-e key combination. Both *readline*(3) and *editline*(7) also support
multiline editing natively, though you need to press C-v C-j in order to insert
multiline editing natively, press either M-Enter or C-v C-j in order to insert
newlines.
WebSockets
~~~~~~~~~~
WebSocket
~~~~~~~~~
The JSON-RPC 2.0 specification doesn't say almost anything about underlying
transports. 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
@ -127,8 +141,7 @@ the higher-level protocol (the "Sec-Ws-Protocol" HTTP field).
Bugs
----
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.
Examples
--------

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 <p@janouch.name>
* Copyright (c) 2015 - 2022, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
@ -525,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.
@ -537,7 +537,7 @@ fcgi_muxer_push (struct fcgi_muxer *self, const void *data, size_t len)
enum ws_handler_state
{
WS_HANDLER_CONNECTING, ///< Parsing HTTP
WS_HANDLER_OPEN, ///< Parsing WebSockets frames
WS_HANDLER_OPEN, ///< Parsing WebSocket frames
WS_HANDLER_CLOSING, ///< Partial closure by us
WS_HANDLER_FLUSHING, ///< Just waiting for client EOF
WS_HANDLER_CLOSED ///< Dead, both sides closed
@ -851,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)
{
@ -858,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 */);
@ -1003,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;
}
@ -1018,13 +1024,6 @@ ws_handler_on_url (http_parser *parser, const char *at, size_t 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)
@ -1066,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);
@ -1110,7 +1110,7 @@ ws_handler_finish_handshake (struct ws_handler *self)
if (!connection || strcasecmp_ascii (connection, "Upgrade"))
FAIL_HANDSHAKE (HTTP_400_BAD_REQUEST);
// Check if we can actually upgrade the protocol to WebSockets
// Check if we can actually upgrade the protocol to WebSocket
const char *upgrade = str_map_find (&self->headers, "Upgrade");
struct http_protocol *offered_upgrades = NULL;
bool can_upgrade = false;
@ -1268,11 +1268,13 @@ ws_handler_push (struct ws_handler *self, const void *data, size_t len)
ev_timer_stop (EV_DEFAULT_ &self->handshake_timeout_watcher);
if (err == HPE_CB_headers_complete)
{
print_debug ("WS handshake failed: %s", "missing `Upgrade' field");
else
print_debug ("WS handshake failed: %s",
http_errno_description (err));
FAIL_HANDSHAKE (HTTP_426_UPGRADE_REQUIRED,
"Upgrade: websocket", SEC_WS_VERSION ": 13");
}
print_debug ("WS handshake failed: %s", http_errno_description (err));
FAIL_HANDSHAKE (HTTP_400_BAD_REQUEST);
}
return true;
@ -1286,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" },
@ -1446,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 ("rpc.discover", json_pack ("{ss}", "$ref",
"https://github.com/open-rpc/meta-schema/raw/master/schema.json")),
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)
{
@ -1458,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)
{
@ -1487,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 },
{ "rpc.discover", json_rpc_discover },
{ "wait", json_rpc_wait },
};
if (!json_is_object (request))
@ -1545,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)))
@ -1620,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
@ -1750,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");
@ -1867,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 ();
@ -1913,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).
@ -2048,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);
}
@ -2359,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);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -2419,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
@ -2515,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;
do
{
client_co_parse (self, data, len, &n_parsed);
data += n_parsed;
}
while ((len -= n_parsed));
}
static void
client_co_run (struct server_context *ctx)
{
struct co_context self = {};
self.ctx = ctx;
self.message = str_make ();
http_parser_init (&self.parser, HTTP_REQUEST);
self.parser.data = &self;
self.pending_fake_starter = true;
hard_assert (set_blocking (STDIN_FILENO, false));
struct str buf = str_make ();
struct pollfd pfd = { .fd = STDIN_FILENO, .events = POLLIN };
while (true)
{
if (poll (&pfd, 1, -1) <= 0)
exit_fatal ("poll: %s", strerror (errno));
str_remove_slice (&buf, 0, buf.len);
enum socket_io_result result = socket_io_try_read (pfd.fd, &buf);
int errno_saved = errno;
if (buf.len)
client_co_on_data (&self, buf.str, buf.len);
if (result == SOCKET_IO_ERROR)
exit_fatal ("read: %s", strerror (errno_saved));
if (result == SOCKET_IO_EOF)
break;
}
str_free (&buf);
str_free (&self.message);
}
// --- Basic server stuff ------------------------------------------------------
typedef struct client *(*client_create_fn) (EV_P_ int sock_fd);
@ -2878,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" },
@ -2902,6 +3140,9 @@ parse_program_arguments (int argc, char **argv)
case 't':
test_main (argc, argv);
exit (EXIT_SUCCESS);
case 's':
*running_as_slave = true;
break;
case 'd':
g_debug_mode = true;
break;
@ -2934,7 +3175,8 @@ parse_program_arguments (int argc, char **argv)
int
main (int argc, char *argv[])
{
parse_program_arguments (argc, argv);
bool running_as_a_slave = false;
parse_program_arguments (argc, argv, &running_as_a_slave);
print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting");
@ -2949,6 +3191,15 @@ main (int argc, char *argv[])
exit (EXIT_FAILURE);
}
// There's a lot of unnecessary left-over scaffolding in this program,
// for testing purposes assume that everything is synchronous
if (running_as_a_slave)
{
client_co_run (&ctx);
server_context_free (&ctx);
return EXIT_SUCCESS;
}
struct ev_loop *loop;
if (!(loop = EV_DEFAULT))
exit_fatal ("libev initialization failed");

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