An encrypted RPC (Remote Procedure Call) package that sends lisp expressions like
(+ 1 1)
from a client to a server for evaluation, either immediate or deferred.
- usocket - for networking
- ironclad - for encryption
- cl-marshal - for serialization of lisp objects
;; the key is the arguments given to (ironclad:make-cipher) and NIL
;; means not to use any cipher, but send in cleartext
(defparameter *my-cipher*
`(:blowfish :key ,(string-to-utf-8-bytes "Don't You Dare Use This Key") :mode :cbc))
(asdf:load-system "cl-srpc")
;; define a server object on an IP and a port
(defparameter *server*
(make-instance 'cl-srpc:server
:cipher-args *my-cipher*
:address "127.0.0.1"
:port 50000))
;; optionally, test our cipher the first time we use it
(cl-srpc:test-cipher *server*)
;; and start the server
(cl-sprc:start-server *server*)
(asdf:load-system "cl-srpc")
(defparameter *client*
(make-instance 'cl-srpc:client
:cipher-args *my-cipher*
:remote-address "127.0.0.1" ;; same as server
:remote-port 50000)) ;; same as server
;; then evaluate 1+2, and return immediately
(cl-srpc:execute-remote-call *client* :expression '(+ 1 2))
==> (3) ;; values returned inside list
;; next evaluate multiple values
(Cl-srpc:execute-remote-call *client* :expression '(values 1 2 3))
==> (1 2 3) ;; multiple values returned in a list
;; now evaluate an expression that throws an error. Note that
;; the special symbol cl-srpc:cl-srpc-remote-error indicates
;; an error occurred.
(cl-srpc:execute-remote-call *client* :expression '(/ 1 "two"))
==> (cl-srpc:cl-srpc-remote-error ;; special symbol indicating error
type-error ;; the type of the error
"The value \"two\" is not of type number")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; One can also use :RETURN-PROMISE to return immediately, but
;; create a REMOTE-PROMISE object that can be used to retrieve
;; the result of the computation later
;;
(defparameter *promise*
(cl-srpc:execute-remote-call
*client*
:expression '(progn (sleep 10) (+ 1 2))
:promise-lifespan 1000 ;; optional longevity on server
:return-promise t)) ;; return immediatly
*promise*
==> #<cl-srpc:remote-promise (un-evaluated)>
(cl-srpc:evaluate-promise *promise*)
==> NIL ;; result is not ready yet
(cl-srpc:evaluate-promise *promise*)
==> (3) ;; now we got a result, and the *promise* is used up
*promise*
==> #<cl-srpc:remote-promise (evaluated)>
;; now try forcing re-evaluation of an evaluated promise
(setf (cl-srpc:remote-promise-evaluated *promise*) nil)
(cl-srpc:evaluate-promise *promise*)
==> (cl-srpc:cl-srpc-remote-error ;; error: promise not found on server
cl-srpc::no-cached-result-found
"no cached result for RESULT-ID: 078e2716335269....")
The PROMISE-LIFESPAN is how many seconds the cached result is kept on the server before being deleted by a cleanup thread. By default it is 1 day.
Internals are documented, but are not of concern to most users.
-
CLIENT connects and waits
-
SERVER sends protocol-version
CL-SRPC 1.0
-
CLIENT reads this, and exits on incompatible version.
-
SERVER sends encrypted block (see below for definition) with nothing but header
REQUEST-ID: xxxxxxxxx
This request id will be returned by the client in its request, and will be used as an identifier for deferred evaluations.
-
CLIENT reads this REQUEST-ID encrypted block.
-
CLIENT sends request in an encrypted block. The encrypted block contains the (encrypted) REQUEST-ID header, to prevent replay attacks. It also contains a COMMAND header, and a serialized representation of expression to be evaluated.
-
SERVER reads an encrypted block and decrypts the encrypted block into a string, which is eval'ed.
-
SERVER writes an encrypted block containing the evaluated expression.
-
CLIENT readst the encrypted block, and closes connection.
An encrypted block, upon decryption, consists of internal headers of the form
HEADER: [value\n
followed by a blank line, followed by [content-text]
.
Each encrypted block is preceded by plaintext external headers:
CONTENT-LENGTH: nn\n
\n
The encrypted block sent by the client has the header
COMMAND: [command]
where [command] is one of
- EVAL - evaluate the cl-marshal encoded lisp in <content-text), and return it as cl-marshal text
- EVAL-AND-CACHE - evaluate, but do so in another thread, but return a REQUEST-ID header that can be used to retrieve this cached value later
- RETRIEVE-CACHED-VALUE - retrieve the value passed in the REQUEST-ID header and return it as cl-marshal encoded text
The encrypted block returned by the server has the header REQUEST-ID followed by the cl-marshal encoded content being returned.
The structure of encrypted blocks is described in the documentation inside cl-srpc-utils.lisp.
The functions encrypt-string-to-byte8-vec and decrypt-byte8-vec-to-string handle block encryption/decryption transparently. The function test-cipher verifies that encryption/decryption works for a given cipher works inside a CL-SRPC-CONNECTION object
For a given ironclad cipher with a block size, the first BLOCK-SIZE elements are the IV (initialiation vector), and the remaining elements are ciphertext.
The ciphertext, when decrypted by ironclad, has the form
Nhhhhhhh...TTTT...ppp...
- [N] - one NPAD byte giving the length of the padding at the end; padding is both to make the input a multiple of cipher block size, and to randomize length of messages.
- [hhh...] - the +HASH-ALGO+ hash of everything that follows.
- [TTT...] - the plaintext string, after it was converted to utf-8 byte array.
- [pppp...] - some padding at end, so that the total length is a multiple of the block size, and so the length of the transmission is randomized. This padding is stripped out during decryption.