Compare commits

...

118 Commits

Author SHA1 Message Date
d820bc2f23 Bump version, update NEWS 2020-10-13 16:03:19 +02:00
b458fc1f99 libedit: bind M-Enter to newline-insert as well 2020-10-13 15:55:37 +02:00
0771c142fe json-rpc-test-server: fix reading the request URI 2020-10-13 04:46:08 +02:00
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
2221828763 OpenRPC: avoid eating HTTP/transport errors 2020-10-13 04:35:32 +02:00
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
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
5d2cd01db0 json-rpc-test-server: fix a potential memory leak 2020-10-13 02:08:53 +02:00
ee79249d23 json-rpc-shell.adoc: update WebSocket notes
https://github.com/open-rpc/client-js also uses WebSockets,
although they don't seem to support notifications (in general).
2020-10-10 05:20:31 +02:00
160d23018a Bump liberty
resolve_relative_runtime_unique_filename() used to have a bug.
2020-10-10 05:09:11 +02:00
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
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
20c8578084 Fix use of possibly uninitialised memory 2020-10-10 02:57:14 +02:00
57a3b4e990 Split make_json_rpc_call() in half 2020-10-10 02:57:13 +02:00
e4d1529b4d Slightly refactor make_json_rpc_call() 2020-10-10 02:57:13 +02:00
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
84702fa47d Fix handling terminal resizes while the terminal is suspended
GNU Readline has a misfeature.
2020-10-09 20:21:52 +02:00
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
710f5f197f Make a release, create NEWS 2020-09-05 20:42:02 +02:00
ba68585d14 Streamline the manual page a bit
I have consulted `man 7 man-pages` but overall it's a huge mess.
2020-09-05 06:34:00 +02:00
984e5b4e7f Use saner defaults
So that most of the time users won't need to use any switches.

--pretty-print has been inverted into jq's --compact-output,
and --auto-id has been replaced with barely, if-at-all useful
--null-as-id.
2020-09-05 06:07:45 +02:00
d57a8bd3c7 Improve AsciiDoc compatibility
I need two renderers to work: hswg/libasciidoc and asciidoctor
in man page mode (and ideally in HTML as well).  That should be
covered now.

The triple-plus thing was the first thing that showed good results,
after trying backslashes, single-plus quoting and [] after ://.

The change of the source code block kind could be considered as
unification.  I'm combining tabs with spaces within one document
though, and I should get rid of the tabs in the rest of it then...
2020-09-05 04:36:43 +02:00
2962a644da Write a nice new man page in AsciiDoc
Taking some preliminary steps for inclusion in Linux distributions.

The help message has been slightly improved and the README extended,
with part of it now residing in the man page.

One less GNU dependency, for what it's worth.
2020-09-05 03:51:36 +02:00
6f5ef30293 Move "connecting..." messages to the debug mode
So that the --verbose option does only one thing.
2020-09-05 03:50:14 +02:00
b7b1198be7 Fix libedit a bit more and discourage from using it 2020-09-02 01:08:09 +02:00
633f7007d1 json-rpc-test-server: add a "date" method 2020-09-01 23:41:20 +02:00
f4d178b3f6 Update copyright years 2020-09-01 21:03:53 +02:00
ee5317f865 json-rpc-test-server: reject non-null params in "ping" 2020-09-01 20:56:03 +02:00
20558ecd2b json-rpc-test-server: make sure to set the id in responses 2020-09-01 20:55:22 +02:00
f6225ac6cc Warn on unexpected "id" fields 2020-09-01 20:54:23 +02:00
16ec8261dc Make the verbose mode also show the raw input
To make it more useful for debugging.  We might also tie this
to the --debug option, though that would be a bit chaotic.
2020-09-01 20:37:08 +02:00
e49ff84b74 Strip trailing newlines from editor output 2020-09-01 19:26:37 +02:00
b7c9bfd9f5 Fix libedit crash
Again, I have no idea why it started to happen, it just occured
to me to try to change the call, and it turns out out it works.
2020-09-01 19:02:41 +02:00
f6165164ee Fix prompt attributes under libedit
I can only guess why this works, and I'm not sure I want to /know/.
2020-09-01 19:02:41 +02:00
3a445c2db2 Name change 2020-09-01 19:02:41 +02:00
45d023147a Bump liberty 2020-09-01 19:02:40 +02:00
90b5364b29 Fix running helper programs
A bug was introduced in 5c38087.
2019-09-23 23:07:04 +02:00
1840ac795e json-rpc-test-server: fix some outstanding issues 2018-10-19 01:15:12 +02:00
df38bcf775 Merge in a JSON-RPC 2.0 test server 2018-10-18 07:49:36 +02:00
1c52f9e37e demo-json-rpc-server -> json-rpc-test-server 2018-10-18 07:27:55 +02:00
6e152ae37c More debugging information for static file serving 2018-10-18 07:17:06 +02:00
8d66435568 Remember to set the server context in SCGI requests 2018-10-18 06:41:12 +02:00
d883f4cc71 Finish the FastCGI backend
Bump liberty, also fixing SCGI.
2018-10-18 06:35:29 +02:00
62945cceb3 Finish the WebSocket backend
Of course, everything so far hasn't been tested much.
2018-10-18 04:44:40 +02:00
580f0a0c59 Synthesize EOF events in SCGI 2018-10-18 04:44:39 +02:00
253e35e1e4 Wrap request::write_cb in a function 2018-10-17 23:34:59 +02:00
cf56921c4e Allow WebSockets to micromanage shutdowns
They have their reasons, mostly event-related.
2018-10-17 23:26:54 +02:00
a3ec0942f8 Implement basic connection teardown
I finally understand the codebase again.  It's rather complicated.
2018-10-17 22:59:40 +02:00
efd500ca3c Accelerated daemon quitting 2018-10-17 06:08:11 +02:00
13892fcd0e Clean up client de/allocation 2018-10-17 05:16:17 +02:00
1d638c9170 Say "finalize" instead of "destroy" where appropriate
- _make() returns a struct directly
 - _init() initializes over a pointer
 - _free() deinitializes over a pointer
 - _new() is like _init() but also allocates
 - _destroy() is like _free() but also deallocates

Finalization is a matching concept in garbage-collected languages.
2018-10-17 03:53:07 +02:00
83363e6383 FastCGI: make it work at least in theory 2018-10-17 03:50:39 +02:00
272145ace2 Clarify EOF behaviour 2018-10-17 02:21:19 +02:00
4c54bc42b9 Clean up and better document client_vtable 2018-10-16 04:45:36 +02:00
7d922352ea Rename client_vtable::destroy to finalize
Matches a similar concept from garbage-collected languages.
2018-10-16 04:34:00 +02:00
7f6db9d39f Improve WebSocket shutdown 2018-10-16 04:33:59 +02:00
7cefdd496f Cleanup 2018-10-16 04:05:42 +02:00
e9530f450e Call ws_handler_start() 2018-10-16 01:47:51 +02:00
dda22c2cd5 Eliminate unnecessary user_data pointers
The CONTAINER_OF macro can find the parent structure just as well.
2018-10-16 01:47:50 +02:00
7aff9c3475 Improve documentation 2018-10-16 01:47:50 +02:00
a14edb72e9 Make Doxygen a bit more useful 2018-10-15 05:07:57 +02:00
267a9a561b Eliminate some warnings 2018-10-15 03:28:09 +02:00
441c89f654 Handle FastCGI null request IDs better 2018-10-15 03:04:39 +02:00
14ded260a0 Clarify and degrade FastCGI multiplexing
No need to support more than 255 concurrent requests on one connection.
2018-10-15 03:02:49 +02:00
8b334e9c91 Fix fcgi_muxer_send()
Outgoing records were missing padding and the reserved field.
2018-10-13 04:08:43 +02:00
fd17b4e504 Update code comments 2018-10-13 04:07:45 +02:00
d182bcef3b More transient errors 2018-10-12 20:02:42 +02:00
329fc9b88f Bump liberty
Eliminates some fall-through warnings.
2018-10-11 21:03:34 +02:00
711d73f481 Fix text message UTF-8 validation 2018-06-24 06:12:16 +02:00
df340c13ed Add the missing Date header 2018-06-24 06:12:16 +02:00
8e986a6040 Remove .travis.yml
We don't depend on any proprietary services no longer.  I'll have to
make my own replacements with blackjack and hookers.  Until then,
the file stays in the commit log as an example.
2018-06-24 06:12:16 +02:00
131debe985 Bump liberty 2018-06-24 06:12:16 +02:00
b312c022ae Update README 2018-06-24 00:42:59 +02:00
4078c8845c Relicense to 0BSD, update mail address
I've come to the conclusion that copyright mostly just stands in the way
of software development.  In my jurisdiction I cannot give up my own
copyright and 0BSD seems to be the closest thing to public domain.

The updated mail address, also used in my author/committer lines,
is shorter and looks nicer.  People rarely interact anyway.
2018-06-24 00:21:10 +02:00
333efdc70f CMakeLists.txt: fix variable name 2018-06-24 00:15:53 +02:00
5e88608286 Bump liberty and http-parser 2017-06-20 14:04:40 +02:00
ca90e9df83 Fix teardown 2017-06-20 14:01:23 +02:00
419147beec Update README 2017-06-20 14:01:10 +02:00
b85d1d74a4 Don't respond to notifications 2017-02-06 20:48:14 +01:00
d35e733c6e Bump liberty
Fixing a bug in the WebSocket frame parser.
2017-02-06 19:47:24 +01:00
e6f9e53229 Fix segfault in WebSocket parsing 2017-02-06 18:51:52 +01:00
2986f6cda0 Fix segfault on client destruction etc. 2017-02-06 18:30:02 +01:00
c4ebf2ccd5 Fix segfault on missing Sec-WebSocket-Key 2017-02-06 18:28:53 +01:00
a785dc9670 WebSocket: fix header parsing 2017-02-06 18:28:40 +01:00
2b7d455471 Fix quitting 2017-02-06 17:18:24 +01:00
0ec0685714 Bump liberty 2017-02-05 22:44:01 +01:00
733451cf2a Fix and update LICENSE 2017-02-03 22:43:09 +01:00
369f94f5ab Travis CI: brevify notifications 2017-02-03 22:38:56 +01:00
7b94a03e8c Update IRC server address 2016-03-14 21:18:27 +01:00
8b66a3f074 Bump liberty 2016-01-17 04:48:17 +01:00
f273151447 Overall revision after a year
Use something closer to inheritance for clients
2016-01-17 04:48:00 +01:00
a95867dbee Fix daemonization 2016-01-16 22:33:57 +01:00
5298d802bb Fix compiler warning 2016-01-16 22:16:01 +01:00
af3cb3aaba Bump liberty 2016-01-16 06:41:31 +01:00
0f62ef26f5 Fix FindLibMagic.cmake 2016-01-16 06:37:56 +01:00
3c7b57bba9 Convert README to AsciiDoc 2015-09-27 02:11:38 +02:00
98bbea72d2 Fix README 2015-09-27 02:10:19 +02:00
df5b7ad71a Update README 2015-05-07 20:28:24 +02:00
6785d3a9ed Implement shutdown 2015-04-10 02:44:13 +02:00
1944f9f17d Travis CI: Change IRC notification address 2015-04-10 01:46:05 +02:00
4dbdc849d9 Steady progress
On the WebSocket service.

It's not too far from being finished now.  I just have to make some
sense of the code again and make sure it's correct.

Now that json-rpc-shell should be able to run against this, I can
also finally test if both of them work as they should.
2015-04-10 01:42:41 +02:00
db6dff4216 Move a lot of stuff to liberty 2015-03-29 03:14:20 +02:00
8aa232d32e Add and fix some preliminary tests 2015-03-23 20:12:53 +01:00
6e9109df4c Don't allow whitespace in base64 2015-03-23 20:12:02 +01:00
987eae5661 Steady progress
Renamed some constants, added basic UTF-8 validation.
2015-03-23 16:47:21 +01:00
9b7dd630e3 WebSockets improvements
- validate more HTTP stuff, use the newer RFC
 - validate the base64 key
2015-03-22 22:35:58 +01:00
c87d684154 Steady progress
Started parsing Content-Type properly after studying the HTTP RFC
for a significant period of time.

Some further WebSockets stuff.
2015-03-15 04:32:04 +01:00
23eb4cca38 Steady progress
Still in a state of total chaos, it appears.
2015-03-14 19:37:00 +01:00
4337038819 Try to lock a PID file 2015-03-11 23:57:25 +01:00
012a57b357 Steady progress
Some further refactoring, added a few comments, etc.

It's not about adding huge chunks of code anymore, and I'm slowly
moving towards getting the details right.

There's still a ton of TODO items, though.
2015-03-11 00:25:46 +01:00
3c0e48a429 Refactoring 2015-03-10 20:48:25 +01:00
5885d1aa69 Some intial WebSockets code 2015-03-09 23:32:01 +01:00
931fc441f6 Steady progress
Added static content serving with sane content type detection.

Started working on WebSockets (meanwhile neither SCGI or FastCGI is
finished and almost nothing has been tested).
2015-03-08 09:41:10 +01:00
9e0c9dd6d8 Steady progress
Still trying to figure out FastCGI.

At least I've finally implemented the JSON-RPC handler.
2015-03-08 05:51:51 +01:00
2733ead30f Figuring out how to close the connection 2015-03-06 19:49:33 +01:00
0b0d64124b Steady progress 2015-03-05 08:47:20 +01:00
a54230bddb Steady progress
I'm trying to figure out everything at once, i.e. the entire structure
of the application from top to bottom, trying to converge on a workable
design while refactoring still doesn't hurt as much as it would once
it's established.
2015-03-02 23:11:29 +01:00
8a3241d5c4 Initial commit
Not even the demo is able to compile yet.

I'm just tracking my progress.
2015-03-02 19:14:37 +01:00
10 changed files with 3645 additions and 199 deletions

View File

@@ -13,7 +13,7 @@ if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
endif ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
# Version
set (project_VERSION_MAJOR "0")
set (project_VERSION_MAJOR "1")
set (project_VERSION_MINOR "1")
set (project_VERSION_PATCH "0")
@@ -70,6 +70,16 @@ include_directories (${PROJECT_BINARY_DIR})
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c http-parser/http_parser.c)
target_link_libraries (${PROJECT_NAME} ${project_libraries})
# Development tools
find_package (LibMagic)
if (LIBMAGIC_FOUND)
include_directories (${LIBMAGIC_INCLUDE_DIRS})
add_executable (json-rpc-test-server
json-rpc-test-server.c http-parser/http_parser.c)
target_link_libraries (json-rpc-test-server
${project_libraries} ${LIBMAGIC_LIBRARIES})
endif ()
# The files to be installed
include (GNUInstallDirs)
install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
@@ -77,18 +87,20 @@ install (PROGRAMS json-format.pl DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
# Generate documentation from program help
find_program (HELP2MAN_EXECUTABLE help2man)
if (NOT HELP2MAN_EXECUTABLE)
message (FATAL_ERROR "help2man not found")
endif (NOT HELP2MAN_EXECUTABLE)
find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor)
if (NOT ASCIIDOCTOR_EXECUTABLE)
message (FATAL_ERROR "asciidoctor not found")
endif (NOT ASCIIDOCTOR_EXECUTABLE)
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 ${HELP2MAN_EXECUTABLE} -N
"${PROJECT_BINARY_DIR}/${page}" -o ${page_output}
DEPENDS ${page}
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)
@@ -101,9 +113,10 @@ foreach (page ${project_MAN_PAGES})
endforeach (page)
# CPack
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Shell for running JSON-RPC 2.0 queries")
set (CPACK_PACKAGE_VENDOR "Premysl Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Janouch <p@janouch.name>")
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY
"A shell for running JSON-RPC 2.0 queries")
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})

View File

@@ -1,4 +1,4 @@
Copyright (c) 2014 - 2016, Přemysl Janouch <p@janouch.name>
Copyright (c) 2014 - 2020, 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.

15
NEWS Normal file
View File

@@ -0,0 +1,15 @@
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

@@ -17,36 +17,30 @@ you get the following niceties:
- 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
Supported transports
--------------------
- HTTP
- HTTPS
- WebSocket
- WebSocket over TLS
WebSockets
~~~~~~~~~~
The JSON-RPC 2.0 spec 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 a notification, the
client waits for a message from the server in response.
There's no support so far for any protocol extensions, nor for specifying
the higher-level protocol (the "Sec-Ws-Protocol" HTTP field).
Documentation
-------------
See the link:json-rpc-shell.adoc[man page] for information about usage.
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.
Building and Usage
------------------
Build dependencies: CMake, pkg-config, help2man,
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.
$ git clone --recursive https://git.janouch.name/p/json-rpc-shell.git
$ mkdir json-rpc-shell/build
$ cd json-rpc-shell/build
@@ -65,7 +59,12 @@ Or you can try telling CMake to make a package for you. For Debian it is:
Note that for versions of CMake before 2.8.9, you need to prefix `cpack` with
`fakeroot` or file ownership will end up wrong.
Run the program with `--help` to obtain usage information.
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.
Contributing and Support
------------------------

10
cmake/FindLibMagic.cmake Normal file
View File

@@ -0,0 +1,10 @@
# Public Domain
find_library (LIBMAGIC_LIBRARIES magic)
find_path (LIBMAGIC_INCLUDE_DIRS magic.h)
include (FindPackageHandleStandardArgs)
find_package_handle_standard_args (LibMagic DEFAULT_MSG
LIBMAGIC_LIBRARIES LIBMAGIC_INCLUDE_DIRS)
mark_as_advanced (LIBMAGIC_LIBRARIES LIBMAGIC_INCLUDE_DIRS)

183
json-rpc-shell.adoc Normal file
View File

@@ -0,0 +1,183 @@
json-rpc-shell(1)
=================
:doctype: manpage
:manmanual: json-rpc-shell Manual
:mansource: json-rpc-shell {release-version}
Name
----
json-rpc-shell - a simple JSON-RPC 2.0 shell
Synopsis
--------
*json-rpc-shell* [_OPTION_]... _ENDPOINT_
Description
-----------
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).
*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
of the protocol's noisy envelope. At your option, it can then also be
pretty-printed, rendered with adjustable syntax highlighting, or even piped
through another program such as the *less*(1) pager or the *jq*(1) JSON
processor.
Usage
~~~~~
Three things may appear on the internal command line, in a sequence. The first
one is always the name of the JSON-RPC method to call, as a bare word, separated
from the rest by white space. Following that, you may enter three kinds of JSON
values. If it is an object or an array, it constitutes the method parameters.
If it is a string or a number, it is taken as the "id" to use for the request,
which would be chosen for you automatically if left unspecified. Finally,
a null value indicates that the request should be sent as a notification,
lacking the ID completely. Booleans cannot be used for anything.
The response to the method call may be piped through external commands, the same
way you would do it in a Unix shell.
Exit the program by pressing C-c or C-d. No special keywords are reserved for
this action as they might conflict with method names.
Options
-------
Controlling output
~~~~~~~~~~~~~~~~~~
*-c*, *--compact-output*::
Do not pretty-print responses. Normally, spaces and newlines are added
where appropriate to improve readability.
*--color*=_WHEN_::
By default, when the output of the program is a terminal, JSON responses
are syntax-highlighted. This corresponds to the _auto_ setting. You may
also set this to _always_ or _never_. In either case, color is never
applied when piping to another program.
*-v*, *--verbose*::
Print raw requests and responses, including the JSON-RPC 2.0 envelope.
*-d*, *--debug*::
Print even more information to help debug various issues.
Protocol
~~~~~~~~
*-n*, *--null-as-id*::
Normally, entering a null JSON value on the command line causes
a notification to be sent. With this option, it is sent as the "id"
field of a normal request, which is discouraged by the specification.
*-t*, *--trust-all*::
Trust all SSL/TLS certificates. Useful in case that the certificate is
self-signed, or when the CA isn't in your CA store. Beware that this option
is about as good as using plain unencrypted HTTP.
*-o* _ORIGIN_, *--origin*=_ORIGIN_::
Set the HTTP Origin header to _ORIGIN_. Some servers may need this.
*-O*, *--openrpc*::
Call "rpc.discover" upon start-up in order to pull in OpenRPC data for
tab completion of method names.
Program information
~~~~~~~~~~~~~~~~~~~
*-h*, *--help*::
Display a help message and exit.
*-V*, *--version*::
Output version information and exit.
*--write-default-cfg*[**=**__PATH__]::
Write a default configuration file, show its path and exit.
Files
-----
_~/.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
a new one for editing.
_~/.local/share/json-rpc-shell/history_::
All your past method invocations are stored here upon exit and loaded back
on start-up.
Notes
-----
Editing
~~~~~~~
While single-line editing on the command line may be satisfactory for simple
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
newlines.
WebSockets
~~~~~~~~~~
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
a notification, the client waits for a message from the server in response.
Should any message arrive unexpectedly, you will receive a warning.
There is no support so far for any protocol extensions, nor for specifying
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.
Examples
--------
Running some queries against json-rpc-test-server, included in the source
distribution of this program (public services are hard to find):
Methods without parameters
~~~~~~~~~~~~~~~~~~~~~~~~~~
$ json-rpc-shell ws://localhost:1234
json-rpc> ping
"pong"
json-rpc> date
{
"year": 2020,
"month": 9,
"day": 5,
"hours": 2,
"minutes": 23,
"seconds": 51
}
Notification with a parameter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Notifications never produce a response, not even when the method is not known
to the server:
$ json-rpc-shell ws://localhost:1234
json-rpc> notify {"events": ["conquest", "war", "famine", "death"]} null
[Notification]
Piping in and out
~~~~~~~~~~~~~~~~~
GNU Readline always repeats the prompt, which makes this a bit less useful
for invoking from other programs:
$ echo 'ping | jq ascii_upcase' | json-rpc-shell ws://localhost:1234
json-rpc> ping | jq ascii_upcase
"PONG"
Reporting bugs
--------------
Use https://git.janouch.name/p/json-rpc-shell to report bugs, request features,
or submit pull requests.
See also
--------
*jq*(1), *readline*(3) or *editline*(7)
Specifications
~~~~~~~~~~~~~~
https://www.jsonrpc.org/specification +
https://www.json.org

View File

@@ -1,7 +1,7 @@
/*
* json-rpc-shell.c: simple JSON-RPC 2.0 shell
*
* Copyright (c) 2014 - 2016, Přemysl Janouch <p@janouch.name>
* Copyright (c) 2014 - 2020, 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.
@@ -141,6 +141,8 @@ struct input
void (*on_input) (char *line, void *user_data);
/// User requested external line editing
void (*on_run_editor) (const char *line, void *user_data);
/// Tab completion generator, returns locale encoding strings or NULL
char *(*complete_start_word) (const char *text, int state);
};
struct input_vtable
@@ -246,16 +248,42 @@ input_rl_on_run_editor (int count, int key)
return 0;
}
static int
input_rl_newline_insert (int count, int key)
{
(void) count;
(void) key;
rl_insert_text ("\n");
return 0;
}
static int
input_rl_on_startup (void)
{
rl_add_defun ("run-editor", input_rl_on_run_editor, -1);
rl_bind_keyseq ("\\ee", rl_named_function ("run-editor"));
rl_add_defun ("newline-insert", input_rl_newline_insert, -1);
rl_bind_keyseq ("\\e\\r", rl_named_function ("newline-insert"));
return 0;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static char **
app_readline_completion (const char *text, int start, int end)
{
(void) end;
// Only customize matches for the first token, which is the method name
if (start)
return NULL;
// Don't iterate over filenames and stuff in this case
rl_attempted_completion_over = true;
return rl_completion_matches (text, g_input_rl->super.complete_start_word);
}
static void
input_rl_start (struct input *input, const char *program_name)
{
@@ -268,6 +296,9 @@ input_rl_start (struct input *input, const char *program_name)
rl_readline_name = slash ? ++slash : program_name;
rl_startup_hook = input_rl_on_startup;
rl_catch_sigwinch = false;
rl_change_environment = false;
rl_attempted_completion_function = app_readline_completion;
hard_assert (self->prompt != NULL);
rl_callback_handler_install (self->prompt, input_rl_on_input);
@@ -347,8 +378,7 @@ input_rl_show (struct input *input)
rl_replace_line (self->saved_line, false);
rl_point = self->saved_point;
rl_mark = self->saved_mark;
free (self->saved_line);
self->saved_line = NULL;
cstr_set (&self->saved_line, NULL);
rl_redisplay ();
}
@@ -357,20 +387,17 @@ static void
input_rl_set_prompt (struct input *input, char *prompt)
{
struct input_rl *self = (struct input_rl *) input;
free (self->prompt);
self->prompt = prompt;
cstr_set (&self->prompt, prompt);
if (!self->active)
if (!self->active || self->prompt_shown <= 0)
return;
// First reset the prompt to work around a bug in readline
rl_set_prompt ("");
if (self->prompt_shown > 0)
rl_redisplay ();
rl_redisplay ();
rl_set_prompt (self->prompt);
if (self->prompt_shown > 0)
rl_redisplay ();
rl_redisplay ();
}
static bool
@@ -531,7 +558,8 @@ input_el_redisplay (struct input_el *self)
el_push (self->editline, x);
// We have to do this or it gets stuck and nothing is done
(void) el_gets (self->editline, NULL);
int count = 0;
(void) el_wgets (self->editline, &count);
}
static char *
@@ -615,16 +643,26 @@ input_el_on_run_editor (EditLine *editline, int key)
return CC_NORM;
}
static unsigned char
input_el_on_newline_insert (EditLine *editline, int key)
{
(void) key;
el_insertstr (editline, "\n");
return CC_REFRESH;
}
static void
input_el_install_prompt (struct input_el *self)
{
// XXX: the ignore doesn't quite work, see https://gnats.netbsd.org/47539
el_set (self->editline, EL_PROMPT_ESC,
input_el_make_prompt, INPUT_START_IGNORE);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static unsigned char input_el_on_complete (EditLine *editline, int key);
static void
input_el_start (struct input *input, const char *program_name)
{
@@ -633,7 +671,7 @@ input_el_start (struct input *input, const char *program_name)
el_set (self->editline, EL_CLIENTDATA, self);
input_el_install_prompt (self);
el_set (self->editline, EL_SIGNAL, false);
el_set (self->editline, EL_UNBUFFERED, true);
el_set (self->editline, EL_UNBUFFERED, isatty (fileno (stdin)));
el_set (self->editline, EL_EDITOR, "emacs");
el_wset (self->editline, EL_HIST, history_w, self->history);
@@ -642,16 +680,18 @@ input_el_start (struct input *input, const char *program_name)
// Just what are you doing?
el_set (self->editline, EL_BIND, "^u", "vi-kill-line-prev", NULL);
// It's probably better to handle this ourselves
// It's probably better to handle these ourselves
el_set (self->editline, EL_ADDFN,
"send-line", "Send line", input_el_on_return);
el_set (self->editline, EL_BIND, "\n", "send-line", NULL);
// It's probably better to handle this ourselves
el_set (self->editline, EL_ADDFN,
"run-editor", "Run editor to edit line", input_el_on_run_editor);
el_set (self->editline, EL_BIND, "M-e", "run-editor", NULL);
el_set (self->editline, EL_ADDFN,
"newline-insert", "Insert a newline", input_el_on_newline_insert);
el_set (self->editline, EL_BIND, "M-\n", "newline-insert", NULL);
// Source the user's defaults file
el_source (self->editline, NULL);
@@ -745,8 +785,7 @@ static void
input_el_set_prompt (struct input *input, char *prompt)
{
struct input_el *self = (struct input_el *) input;
free (self->prompt);
self->prompt = prompt;
cstr_set (&self->prompt, prompt);
if (self->prompt_shown > 0)
input_el_redisplay (self);
@@ -781,6 +820,121 @@ input_el_ding (struct input *input)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static int
input_el_collate (const void *a, const void *b)
{
return strcoll (*(const char **) a, *(const char **) b);
}
static struct strv
input_el_collect_candidates (struct input_el *self, const char *word)
{
struct strv v = strv_make ();
int i = 0; char *candidate = NULL;
while ((candidate = self->super.complete_start_word (word, i++)))
strv_append_owned (&v, candidate);
qsort (v.vector, v.len, sizeof *v.vector, input_el_collate);
return v;
}
static void
input_el_print_candidates (struct input_el *self, const struct strv *v)
{
EditLine *editline = self->editline;
// This insanity seems to be required to make it reprint the prompt
const LineInfoW *info = el_wline (editline);
int from_cursor_until_end = info->lastchar - info->cursor;
el_cursor (editline, from_cursor_until_end);
el_insertstr (editline, "\n");
input_el_redisplay (self);
el_wdeletestr (editline, 1);
el_set (editline, EL_REFRESH);
input_el_hide (&self->super);
for (size_t i = 0; i < v->len; i++)
printf ("%s\n", v->vector[i]);
input_el_show (&self->super);
el_cursor (editline, -from_cursor_until_end);
}
static void
input_el_insert_common_prefix (EditLine *editline, const struct strv *v)
{
char *p[v->len]; memcpy (p, v->vector, sizeof p);
mbstate_t state[v->len]; memset (state, 0, sizeof state);
wchar_t want[2] = {}; size_t len;
while ((len = mbrtowc (&want[0], p[0], strlen (p[0]), &state[0])) > 0)
{
p[0] += len;
for (size_t i = 1; i < v->len; i++)
{
wchar_t found = 0;
if ((len = mbrtowc (&found, p[i], strlen (p[i]), &state[i])) <= 0
|| found != want[0])
return;
p[i] += len;
}
el_winsertstr (editline, want);
}
}
static unsigned char
input_el_on_complete (EditLine *editline, int key)
{
(void) key;
struct input_el *self;
el_get (editline, EL_CLIENTDATA, &self);
// First prepare what Readline would have normally done for us...
const LineInfo *info_mb = el_line (editline);
int len = info_mb->lastchar - info_mb->buffer;
int point = info_mb->cursor - info_mb->buffer;
char *word = xstrndup (info_mb->buffer, len);
int start = point;
while (start && !isspace_ascii (word[start - 1]))
start--;
// Only complete the first word, when we're at the end of it
if (start != 0
|| (word[point] && !isspace_ascii (word[point]))
|| (point && isspace_ascii (word[point - 1])))
{
free (word);
return CC_REFRESH_BEEP;
}
word[point] = '\0';
int word_len = mbstowcs (NULL, word, 0);
struct strv v = input_el_collect_candidates (self, word);
free (word);
if (!v.len)
{
strv_free (&v);
return CC_REFRESH_BEEP;
}
// Remove the original word and replace it with the best (sub)match
el_wdeletestr (editline, word_len);
if (v.len == 1)
{
el_insertstr (editline, v.vector[0]);
el_insertstr (editline, " ");
strv_free (&v);
return CC_REFRESH;
}
input_el_insert_common_prefix (editline, &v);
input_el_print_candidates (self, &v);
strv_free (&v);
return CC_REFRESH_BEEP;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
input_el_load_history (struct input *input, const char *filename,
struct error **e)
@@ -833,6 +987,17 @@ input_el_on_tty_readable (struct input *input)
int count = 0;
const wchar_t *buf = el_wgets (self->editline, &count);
// Editline works in a funny NO_TTY mode when the input is not a tty,
// we cannot use EL_UNBUFFERED and expect sane results then
int unbuffered = 0;
if (!el_get (self->editline, EL_UNBUFFERED, &unbuffered) && !unbuffered)
{
char *entered_line = buf ? input_el_wcstombs (buf) : NULL;
self->super.on_input (entered_line, self->super.user_data);
free (entered_line);
return;
}
// Process data from our newline handler (async-friendly handling)
if (self->entered_line)
{
@@ -841,8 +1006,7 @@ input_el_on_tty_readable (struct input *input)
self->prompt_shown = 0;
self->super.on_input (self->entered_line, self->super.user_data);
free (self->entered_line);
self->entered_line = NULL;
cstr_set (&self->entered_line, NULL);
// Forbid editline from trying to erase the old prompt (or worse)
// and let it redisplay the prompt in its clean state
@@ -918,14 +1082,16 @@ static struct app_context
struct backend *backend; ///< Our current backend
char *editor_filename; ///< File for input line editor
struct str_map methods; ///< Methods detected via OpenRPC
struct config config; ///< Program configuration
enum color_mode color_mode; ///< Colour output mode
bool pretty_print; ///< Whether to pretty print
bool compact; ///< Whether to not pretty print
bool verbose; ///< Print requests
bool trust_all; ///< Don't verify peer certificates
bool openrpc; ///< OpenRPC method name completion
bool auto_id; ///< Use automatically generated ID's
bool null_as_id; ///< JSON null is used as an ID
int64_t next_id; ///< Next autogenerated ID
iconv_t term_to_utf8; ///< Terminal encoding to UTF-8
@@ -1226,10 +1392,9 @@ on_config_attribute_change (struct config_item *item)
ssize_t id = attr_by_name (item->schema->name);
if (id != -1)
{
free (ctx->attrs[id]);
ctx->attrs[id] = xstrdup (item->type == CONFIG_ITEM_NULL
cstr_set (&ctx->attrs[id], xstrdup (item->type == CONFIG_ITEM_NULL
? ctx->attrs_defaults[id]
: item->value.string.str);
: item->value.string.str));
}
}
@@ -1672,7 +1837,7 @@ backend_ws_establish_connection (struct ws_context *self,
else
real_host = buf;
if (self->ctx->verbose)
if (g_debug_mode)
{
char *address = format_host_port_pair (real_host, port);
print_status ("connecting to %s...", address);
@@ -2705,6 +2870,7 @@ static void
resume_terminal (struct app_context *ctx)
{
ctx->input->vtable->prepare (ctx->input, true);
ctx->input->vtable->on_terminal_resized (ctx->input);
ev_io_start (EV_DEFAULT_ &ctx->tty_watcher);
ctx->input->vtable->show (ctx->input);
}
@@ -2738,8 +2904,19 @@ display_via_pipeline (struct app_context *ctx,
}
static bool
parse_response (struct app_context *ctx, struct str *buf, const char *pipeline)
process_response (struct app_context *ctx, const json_t *id, struct str *buf,
const char *pipeline)
{
if (!id)
{
printf ("[Notification]\n");
if (!buf->len)
return true;
print_warning ("we have been sent data back for a notification");
return false;
}
json_error_t e;
json_t *response;
if (!(response = json_loadb (buf->str, buf->len, JSON_DECODE_ANY, &e)))
@@ -2758,9 +2935,15 @@ parse_response (struct app_context *ctx, struct str *buf, const char *pipeline)
else if (!json_is_string (v) || strcmp (json_string_value (v), "2.0"))
print_warning ("invalid `%s' field in response", "jsonrpc");
json_t *result = json_object_get (response, "result");
json_t *error = json_object_get (response, "error");
json_t *data = NULL;
json_t *returned_id = json_object_get (response, "id");
json_t *result = json_object_get (response, "result");
json_t *error = json_object_get (response, "error");
json_t *data = NULL;
if (!returned_id)
print_warning ("`%s' field not present in response", "id");
if (!json_equal (id, returned_id))
print_warning ("mismatching `%s' field in response", "id");
if (!result && !error)
PARSE_FAIL ("neither `result' nor `error' present in response");
@@ -2817,7 +3000,7 @@ parse_response (struct app_context *ctx, struct str *buf, const char *pipeline)
if (result)
{
int flags = JSON_ENCODE_ANY;
if (ctx->pretty_print)
if (!ctx->compact)
flags |= JSON_INDENT (2);
char *utf8 = json_dumps (result, flags);
@@ -2842,6 +3025,82 @@ fail:
return success;
}
static void
maybe_print_verbose (struct app_context *ctx, intptr_t attribute,
char *utf8, size_t len)
{
if (!ctx->verbose)
return;
char *term = iconv_xstrdup (ctx->term_from_utf8, utf8, len, NULL);
if (!term)
{
print_error ("%s: %s", "verbose", "character conversion failed");
return;
}
ctx->input->vtable->hide (ctx->input);
print_attributed (ctx, stdout, attribute, "%s", term);
fputs ("\n", stdout);
free (term);
ctx->input->vtable->show (ctx->input);
}
static struct error *
json_rpc_call_raw (struct app_context *ctx,
const char *method, json_t *id, json_t *params, struct str *buf)
{
json_t *request = json_object ();
json_object_set_new (request, "jsonrpc", json_string ("2.0"));
json_object_set_new (request, "method", json_string (method));
if (id) json_object_set (request, "id", id);
if (params) json_object_set (request, "params", params);
char *req_utf8 = json_dumps (request, 0);
json_decref (request);
maybe_print_verbose (ctx, ATTR_OUTGOING, req_utf8, -1);
struct error *error = NULL;
ctx->backend->vtable->make_call (ctx->backend, req_utf8,
id != NULL /* expect_content */, buf, &error);
free (req_utf8);
if (error)
return error;
maybe_print_verbose (ctx, ATTR_INCOMING, buf->str, buf->len + 1);
return NULL;
}
static void
make_json_rpc_call (struct app_context *ctx,
const char *method, json_t *id, json_t *params, const char *pipeline)
{
struct str buf = str_make ();
struct error *e = json_rpc_call_raw (ctx, method, id, params, &buf);
if (e)
{
print_error ("%s", e->message);
error_free (e);
}
else if (!process_response (ctx, id, &buf, pipeline))
{
char *s = iconv_xstrdup (ctx->term_from_utf8,
buf.str, buf.len + 1 /* null byte */, NULL);
if (!s)
print_error ("character conversion failed for `%s'",
"raw response data");
else if (!ctx->verbose /* already printed */)
printf ("%s: %s\n", "raw response data", s);
free (s);
}
str_free (&buf);
}
static bool
is_valid_json_rpc_id (json_t *v)
{
@@ -2855,71 +3114,6 @@ is_valid_json_rpc_params (json_t *v)
return json_is_array (v) || json_is_object (v);
}
static void
make_json_rpc_call (struct app_context *ctx,
const char *method, json_t *id, json_t *params, const char *pipeline)
{
json_t *request = json_object ();
json_object_set_new (request, "jsonrpc", json_string ("2.0"));
json_object_set_new (request, "method", json_string (method));
if (id) json_object_set (request, "id", id);
if (params) json_object_set (request, "params", params);
char *req_utf8 = json_dumps (request, 0);
if (ctx->verbose)
{
char *req_term = iconv_xstrdup
(ctx->term_from_utf8, req_utf8, -1, NULL);
if (!req_term)
print_error ("%s: %s", "verbose", "character conversion failed");
else
{
print_attributed (ctx, stdout, ATTR_OUTGOING, "%s", req_term);
fputs ("\n", stdout);
}
free (req_term);
}
struct str buf = str_make ();
struct error *e = NULL;
if (!ctx->backend->vtable->make_call
(ctx->backend, req_utf8, id != NULL, &buf, &e))
{
print_error ("%s", e->message);
error_free (e);
goto fail;
}
bool success = false;
if (id)
success = parse_response (ctx, &buf, pipeline);
else
{
printf ("[Notification]\n");
if (buf.len)
print_warning ("we have been sent data back for a notification");
else
success = true;
}
if (!success)
{
char *s = iconv_xstrdup (ctx->term_from_utf8,
buf.str, buf.len + 1 /* null byte */, NULL);
if (!s)
print_error ("character conversion failed for `%s'",
"raw response data");
else
printf ("%s: %s\n", "raw response data", s);
free (s);
}
fail:
str_free (&buf);
free (req_utf8);
json_decref (request);
}
static void
process_input (char *user_input, void *user_data)
{
@@ -3011,9 +3205,16 @@ process_input (char *user_input, void *user_data)
*target = json_incref (args[i]);
}
if (!id && ctx->auto_id)
if (!id)
id = json_integer (ctx->next_id++);
// Use nulls to send notifications, unless a special switch is used
if (!ctx->null_as_id && json_is_null (id))
{
json_decref (id);
id = NULL;
}
make_json_rpc_call (ctx, method, id, params, pipeline);
fail_parse:
@@ -3028,56 +3229,81 @@ fail:
free (input);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// --- OpenRPC information extraction ------------------------------------------
static void
parse_rpc_discover (struct app_context *ctx, struct str *buf, struct error **e)
{
// Just optimistically punch through, I don't have time for this shit
json_error_t error;
json_t *response = NULL, *result = NULL, *value = NULL;
if (!(response = json_loadb (buf->str, buf->len, 0, &error)))
error_set (e, "parse failure: %s", error.text);
else if (!(result = json_object_get (response, "result"))
|| !(result = json_object_get (result, "methods")))
error_set (e, "unsupported");
else
{
const char *name = NULL;
for (size_t i = 0; (value = json_array_get (result, i)); i++)
if ((value = json_object_get (value, "name"))
&& (name = json_string_value (value)))
str_map_set (&ctx->methods, name, (void *) 1);
}
json_decref (response);
}
static void
init_openrpc (struct app_context *ctx)
{
if (!ctx->openrpc)
return;
json_t *id = json_integer (ctx->next_id++);
struct str buf = str_make ();
struct error *error;
if (!(error = json_rpc_call_raw (ctx, "rpc.discover", id, NULL, &buf)))
parse_rpc_discover (ctx, &buf, &error);
json_decref (id);
if (error)
{
print_error ("OpenRPC: %s", error->message);
error_free (error);
}
str_free (&buf);
}
static char *
complete_method_name (const char *text, int state)
{
static struct str_map_iter iter;
if (!state)
iter = str_map_iter_make (&g_ctx.methods);
char *input;
size_t len;
if (!(input = iconv_xstrdup (g_ctx.term_to_utf8, (char *) text, -1, &len)))
{
print_error ("character conversion failed for `%s'", "user input");
return NULL;
}
char *match = NULL;
while (str_map_iter_next (&iter)
&& (strncasecmp_ascii (input, iter.link->key, len - 1 /* XXX */)
|| !(match = iconv_xstrdup (g_ctx.term_from_utf8,
iter.link->key, iter.link->key_length + 1, NULL))))
;
free (input);
return match;
}
// --- Main program ------------------------------------------------------------
// The ability to use an external editor on the input line has been shamelessly
// copypasted from degesch with minor changes only.
/// This differs from the non-unique version in that we expect the filename
/// to be something like a pattern for mkstemp(), so the resulting path can
/// reside in a system-wide directory with no risk of a conflict.
static char *
resolve_relative_runtime_unique_filename (const char *filename)
{
struct str path = str_make ();
const char *runtime_dir = getenv ("XDG_RUNTIME_DIR");
const char *tmpdir = getenv ("TMPDIR");
if (runtime_dir && *runtime_dir == '/')
str_append (&path, runtime_dir);
else if (tmpdir && *tmpdir == '/')
str_append (&path, tmpdir);
else
str_append (&path, "/tmp");
str_append_printf (&path, "/%s/%s", PROGRAM_NAME, filename);
// Try to create the file's ancestors;
// typically the user will want to immediately create a file in there
const char *last_slash = strrchr (path.str, '/');
if (last_slash && last_slash != path.str)
{
char *copy = xstrndup (path.str, last_slash - path.str);
(void) mkdir_with_parents (copy, NULL);
free (copy);
}
return str_steal (&path);
}
static bool
xwrite (int fd, const char *data, size_t len, struct error **e)
{
size_t written = 0;
while (written < len)
{
ssize_t res = write (fd, data + written, len - written);
if (res >= 0)
written += res;
else if (errno != EINTR)
FAIL ("%s", strerror (errno));
}
return true;
}
static bool
dump_line_to_file (const char *line, char *template, struct error **e)
{
@@ -3097,7 +3323,7 @@ static char *
try_dump_line_to_file (const char *line)
{
char *template = resolve_filename
("input.XXXXXX", resolve_relative_runtime_unique_filename);
("input.XXXXXX", resolve_relative_runtime_template);
struct error *e = NULL;
if (dump_line_to_file (line, template, &e))
@@ -3176,16 +3402,21 @@ process_edited_input (struct app_context *ctx)
print_error ("%s: %s", "input editing failed", e->message);
error_free (e);
}
else if (!ctx->input->vtable->replace_line (ctx->input, input.str))
print_error ("%s: %s", "input editing failed",
"could not re-insert modified text");
else
{
// Strip trailing newlines, added automatically by editors
while (input.len && strchr ("\r\n", input.str[input.len - 1]))
input.str[--input.len] = 0;
if (!ctx->input->vtable->replace_line (ctx->input, input.str))
print_error ("%s: %s", "input editing failed",
"could not re-insert modified text");
}
if (unlink (ctx->editor_filename))
print_error ("could not unlink `%s': %s",
ctx->editor_filename, strerror (errno));
free (ctx->editor_filename);
ctx->editor_filename = NULL;
str_free (&input);
}
@@ -3203,7 +3434,7 @@ on_child (EV_P_ ev_child *handle, int revents)
kill (-handle->rpid, SIGKILL);
return;
}
// I don't recognize this child (we should also check PID)
// I don't recognize this child (we should also check its PID)
if (!ctx->editor_filename)
return;
@@ -3216,6 +3447,8 @@ on_child (EV_P_ ev_child *handle, int revents)
print_error ("editor returned status %d", WEXITSTATUS (status));
else
process_edited_input (ctx);
cstr_set (&ctx->editor_filename, NULL);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -3250,8 +3483,14 @@ on_tty_readable (EV_P_ ev_io *handle, int revents)
{
// rl_callback_read_char() is not reentrant, may happen on EOF
ev_io_stop (EV_DEFAULT_ &ctx->tty_watcher);
ctx->input->vtable->on_tty_readable (ctx->input);
ev_io_start (EV_DEFAULT_ &ctx->tty_watcher);
// Don't make ourselves receive a SIGTTIN. Ideally we'd prevent
// reentrancy without inciting conflicts with
// {suspend,resume}_terminal() but I can't figure anything out.
if (!ctx->editor_filename)
ev_io_start (EV_DEFAULT_ &ctx->tty_watcher);
}
}
@@ -3294,24 +3533,26 @@ parse_program_arguments (struct app_context *ctx, int argc, char **argv,
{
static const struct opt opts[] =
{
{ '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" },
{ 'a', "auto-id", NULL, 0, "automatic `id' fields" },
{ 'o', "origin", "O", 0, "set the HTTP Origin header" },
{ 'p', "pretty", NULL, 0, "pretty-print the responses" },
{ 't', "trust-all", NULL, 0, "don't care about SSL/TLS certificates" },
{ 'v', "verbose", NULL, 0, "print the request before sending" },
{ 'c', "color", "WHEN", OPT_LONG_ONLY,
{ 'c', "compact-output", NULL, 0, "do not pretty-print responses" },
{ 'C', "color", "WHEN", OPT_LONG_ONLY,
"colorize output: never, always, or auto" },
{ 'n', "null-as-id", NULL, 0, "JSON null is used as an `id'" },
{ 'o', "origin", "O", 0, "set the HTTP Origin header" },
// So far you have to explicitly enable this rather than disable
{ 'O', "openrpc", NULL, 0, "method name completion using OpenRPC" },
{ 't', "trust-all", NULL, 0, "don't care about SSL/TLS certificates" },
{ 'v', "verbose", NULL, 0, "print raw requests and responses" },
{ 'w', "write-default-cfg", "FILENAME",
OPT_OPTIONAL_ARG | OPT_LONG_ONLY,
"write a default configuration file and exit" },
{ 'd', "debug", NULL, 0, "run in debug mode" },
{ 'h', "help", NULL, 0, "display this help message and exit" },
{ 'V', "version", NULL, 0, "output version information and exit" },
{ 0, NULL, NULL, 0, NULL }
};
struct opt_handler oh = opt_handler_make (argc, argv, opts,
"ENDPOINT", "Simple JSON-RPC shell.");
"ENDPOINT", "A simple JSON-RPC 2.0 shell.");
int c;
while ((c = opt_handler_get (&oh)) != -1)
@@ -3328,12 +3569,13 @@ parse_program_arguments (struct app_context *ctx, int argc, char **argv,
exit (EXIT_SUCCESS);
case 'o': *origin = optarg; break;
case 'a': ctx->auto_id = true; break;
case 'p': ctx->pretty_print = true; break;
case 'O': ctx->openrpc = true; break;
case 'n': ctx->null_as_id = true; break;
case 'c': ctx->compact = true; break;
case 't': ctx->trust_all = true; break;
case 'v': ctx->verbose = true; break;
case 'c':
case 'C':
if (!strcasecmp (optarg, "never"))
ctx->color_mode = COLOR_NEVER;
else if (!strcasecmp (optarg, "always"))
@@ -3384,7 +3626,9 @@ main (int argc, char *argv[])
g_ctx.input->user_data = &g_ctx;
g_ctx.input->on_input = process_input;
g_ctx.input->on_run_editor = run_editor;
g_ctx.input->complete_start_word = complete_method_name;
g_ctx.methods = str_map_make (NULL);
init_colors (&g_ctx);
load_configuration (&g_ctx);
@@ -3453,7 +3697,7 @@ main (int argc, char *argv[])
{
// XXX: to be completely correct, we should use tputs, but we cannot
g_ctx.input->vtable->set_prompt (g_ctx.input,
xstrdup_printf ("%c%s%cjson-rpc> %c%s%c",
xstrdup_printf ("%c%s%cjson-rpc>%c%s%c ",
INPUT_START_IGNORE, g_ctx.attrs[ATTR_PROMPT],
INPUT_END_IGNORE,
INPUT_START_IGNORE, g_ctx.attrs[ATTR_RESET],
@@ -3464,6 +3708,7 @@ main (int argc, char *argv[])
g_ctx.input->vtable->start (g_ctx.input, PROGRAM_NAME);
ev_set_userdata (EV_DEFAULT_ &g_ctx);
init_openrpc (&g_ctx);
ev_run (EV_DEFAULT_ 0);
// User has terminated the program, let's save the history and clean up
@@ -3486,6 +3731,7 @@ main (int argc, char *argv[])
iconv_close (g_ctx.term_from_utf8);
iconv_close (g_ctx.term_to_utf8);
str_map_free (&g_ctx.methods);
config_free (&g_ctx.config);
free_terminal ();
ev_loop_destroy (EV_DEFAULT);

2980
json-rpc-test-server.c Normal file

File diff suppressed because it is too large Load Diff

Submodule liberty updated: bb30c7d86e...e029aae1d3