├── .gitignore ├── setup.cfg ├── MANIFEST.in ├── pics ├── doc.png ├── exec.png ├── help.png ├── list.png ├── raw.png ├── intro.png ├── htmldocs.png ├── customtypes.png └── generated-client.png ├── requirements.txt ├── examples ├── certs │ ├── WARNING │ ├── scripts │ │ ├── gen_server_cert.py │ │ ├── gen_ca.py │ │ └── gen_client_cert.py │ ├── client.crt │ ├── server.crt │ ├── wrong-client.crt │ ├── rootCA.crt │ ├── wrongCA.crt │ ├── wrongCA.key │ ├── client.key │ ├── rootCA.key │ ├── server.key │ ├── wrong-client.key │ └── server.pem ├── realworld │ ├── jsonstore │ │ ├── jsonstore │ │ │ ├── __init__.py │ │ │ └── parser.py │ │ ├── schema.sql │ │ └── jsonstore.py │ └── linux-sys-info.py ├── servertls.py ├── servertls_clientauth.py ├── servertwisted.py ├── serverhttp.py ├── servertls_clientauth_http.py ├── serverunixsocket.py ├── server.py ├── serverhttp_basic_auth.py ├── flask-server.py ├── flask-basic-auth.py ├── concurrency.py ├── concurrency-http.py └── rpcexample.py ├── vagrant ├── README.md ├── devpy3.5 │ └── Vagrantfile └── devpy2.7 │ └── Vagrantfile ├── TODO ├── .travis.yml ├── docker ├── python27 │ ├── build.sh │ └── Dockerfile ├── python35 │ ├── build.sh │ └── Dockerfile └── README.md ├── runtests.py ├── webui ├── templates │ ├── layout.html │ ├── login.html │ └── app.html ├── static │ └── style.css └── webui.py ├── tests ├── runall.py ├── client-tests.py ├── rpcgencode-tests.py ├── lineserver-tests.py ├── rpcdoc-tests.py ├── cmdline-tests.py └── rpcfunction-tests.py ├── rpcsh ├── docs ├── index.rst ├── make.bat ├── Makefile └── conf.py ├── LICENSE ├── CHANGELOG.rst ├── setup.py ├── benchmark.py ├── reflectrpc ├── server.py ├── simpleserver.py ├── cmdline.py ├── testing.py ├── rpcsh.py └── twistedserver.py ├── rpcgencode ├── rpcdoc ├── conformance-test.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | *.pyc 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include CHANGELOG.rst 3 | -------------------------------------------------------------------------------- /pics/doc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aheck/reflectrpc/HEAD/pics/doc.png -------------------------------------------------------------------------------- /pics/exec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aheck/reflectrpc/HEAD/pics/exec.png -------------------------------------------------------------------------------- /pics/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aheck/reflectrpc/HEAD/pics/help.png -------------------------------------------------------------------------------- /pics/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aheck/reflectrpc/HEAD/pics/list.png -------------------------------------------------------------------------------- /pics/raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aheck/reflectrpc/HEAD/pics/raw.png -------------------------------------------------------------------------------- /pics/intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aheck/reflectrpc/HEAD/pics/intro.png -------------------------------------------------------------------------------- /pics/htmldocs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aheck/reflectrpc/HEAD/pics/htmldocs.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | future 2 | service_identity 3 | twisted 4 | pyOpenSSL 5 | pexpect 6 | -------------------------------------------------------------------------------- /pics/customtypes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aheck/reflectrpc/HEAD/pics/customtypes.png -------------------------------------------------------------------------------- /examples/certs/WARNING: -------------------------------------------------------------------------------- 1 | THESE ARE EXAMPLE CERTIFICATES! 2 | 3 | DO NOT USE IN PRODUCTION!!! 4 | -------------------------------------------------------------------------------- /pics/generated-client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aheck/reflectrpc/HEAD/pics/generated-client.png -------------------------------------------------------------------------------- /examples/realworld/jsonstore/jsonstore/__init__.py: -------------------------------------------------------------------------------- 1 | from jsonstore.parser import filter_exp_to_sql_where 2 | -------------------------------------------------------------------------------- /vagrant/README.md: -------------------------------------------------------------------------------- 1 | # Vagrant Development VMs # 2 | 3 | This directory contains Vagrant VMs that can be useful for developing with 4 | ReflectRPC. 5 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - Create a useful example RPC service using Twisted for concurrency 2 | - Implement ReflectRPC in other programming languages (especially the core 3 | components RpcFunction and RpcProcessor) 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.3" 5 | - "3.4" 6 | - "3.5" 7 | # command to install dependencies 8 | install: "pip install -r requirements.txt" 9 | # command to run tests 10 | script: ./runtests.py 11 | -------------------------------------------------------------------------------- /docker/python27/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cp -r ../../examples . 4 | cp -r ../../tests . 5 | cp ../../runtests.py . 6 | 7 | docker build -t 'reflectrpc-test:python2.7' . 8 | 9 | rm -rf examples 10 | rm -rf tests 11 | rm runtests.py 12 | -------------------------------------------------------------------------------- /docker/python35/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cp -r ../../examples . 4 | cp -r ../../tests . 5 | cp ../../runtests.py . 6 | 7 | docker build -t 'reflectrpc-test:python3.5' . 8 | 9 | rm -rf examples 10 | rm -rf tests 11 | rm runtests.py 12 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | python = sys.executable 7 | 8 | os.chdir('tests') 9 | exit_status = os.system("%s runall.py" % (python)) 10 | 11 | if exit_status != 0: 12 | sys.exit(1) 13 | -------------------------------------------------------------------------------- /webui/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ReflectRPC WebUI 5 | 6 | 7 | 8 | 9 | 10 | 11 |

ReflectRPC WebUI

12 | 13 | {% block content %} 14 | {% endblock %} 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/realworld/jsonstore/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE public.jsonstore 2 | ( 3 | uuid uuid NOT NULL, 4 | obj_name character varying, 5 | data jsonb, 6 | updated timestamp without time zone, 7 | CONSTRAINT pkey PRIMARY KEY (uuid), 8 | CONSTRAINT obj_name_uniq UNIQUE (obj_name) 9 | ) 10 | WITH ( 11 | OIDS=FALSE 12 | ); 13 | ALTER TABLE public.jsonstore 14 | OWNER TO postgres; 15 | -------------------------------------------------------------------------------- /examples/servertls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | 5 | sys.path.append('..') 6 | 7 | import reflectrpc 8 | import reflectrpc.twistedserver 9 | 10 | import rpcexample 11 | 12 | jsonrpc = rpcexample.build_example_rpcservice() 13 | server = reflectrpc.twistedserver.TwistedJsonRpcServer(jsonrpc, 'localhost', 5500) 14 | server.enable_tls('./certs/server.pem') 15 | server.run() 16 | -------------------------------------------------------------------------------- /tests/runall.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import os.path 5 | import sys 6 | 7 | python = sys.executable 8 | 9 | exit_status = 0 10 | 11 | files = os.listdir('.') 12 | for f in files: 13 | if not f.endswith('.py') or f == 'runall.py': continue 14 | 15 | print(f) 16 | print('') 17 | status = os.system("%s %s" % (python, f)) 18 | if status != 0: 19 | exit_status = 1 20 | 21 | sys.exit(exit_status) 22 | -------------------------------------------------------------------------------- /examples/servertls_clientauth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | 5 | sys.path.append('..') 6 | 7 | import reflectrpc 8 | import reflectrpc.twistedserver 9 | 10 | import rpcexample 11 | 12 | jsonrpc = rpcexample.build_example_rpcservice() 13 | server = reflectrpc.twistedserver.TwistedJsonRpcServer(jsonrpc, 'localhost', 5500) 14 | server.enable_tls('./certs/server.pem') 15 | server.enable_client_auth('./certs/rootCA.crt') 16 | server.run() 17 | -------------------------------------------------------------------------------- /examples/servertwisted.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import unicode_literals 4 | from builtins import bytes, dict, list, int, float, str 5 | 6 | import sys 7 | 8 | sys.path.append('..') 9 | 10 | import reflectrpc 11 | import reflectrpc.twistedserver 12 | 13 | import rpcexample 14 | 15 | jsonrpc = rpcexample.build_example_rpcservice() 16 | server = reflectrpc.twistedserver.TwistedJsonRpcServer(jsonrpc, 'localhost', 5500) 17 | server.run() 18 | -------------------------------------------------------------------------------- /examples/serverhttp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import unicode_literals 4 | from builtins import bytes, dict, list, int, float, str 5 | 6 | import sys 7 | 8 | sys.path.append('..') 9 | 10 | import reflectrpc 11 | import reflectrpc.twistedserver 12 | 13 | import rpcexample 14 | 15 | jsonrpc = rpcexample.build_example_rpcservice() 16 | server = reflectrpc.twistedserver.TwistedJsonRpcServer(jsonrpc, 'localhost', 5500) 17 | server.enable_http() 18 | server.run() 19 | -------------------------------------------------------------------------------- /examples/servertls_clientauth_http.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | 5 | sys.path.append('..') 6 | 7 | import reflectrpc 8 | import reflectrpc.twistedserver 9 | 10 | import rpcexample 11 | 12 | jsonrpc = rpcexample.build_example_rpcservice() 13 | server = reflectrpc.twistedserver.TwistedJsonRpcServer(jsonrpc, 'localhost', 5500) 14 | server.enable_http() 15 | server.enable_tls('./certs/server.pem') 16 | server.enable_client_auth('./certs/rootCA.crt') 17 | server.run() 18 | -------------------------------------------------------------------------------- /examples/serverunixsocket.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import unicode_literals 4 | from builtins import bytes, dict, list, int, float, str 5 | 6 | import sys 7 | 8 | sys.path.append('..') 9 | 10 | import reflectrpc 11 | import reflectrpc.twistedserver 12 | 13 | import rpcexample 14 | 15 | jsonrpc = rpcexample.build_example_rpcservice() 16 | server = reflectrpc.twistedserver.TwistedJsonRpcServer(jsonrpc, 17 | 'unix:///tmp/reflectrpc.sock', 0) 18 | server.run() 19 | -------------------------------------------------------------------------------- /examples/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import unicode_literals 4 | from builtins import bytes, dict, list, int, float, str 5 | 6 | import sys 7 | 8 | sys.path.append('..') 9 | 10 | import reflectrpc 11 | import reflectrpc.simpleserver 12 | 13 | import rpcexample 14 | 15 | try: 16 | jsonrpc = rpcexample.build_example_rpcservice() 17 | server = reflectrpc.simpleserver.SimpleJsonRpcServer(jsonrpc, 'localhost', 5500) 18 | server.run() 19 | except KeyboardInterrupt: 20 | sys.exit(0) 21 | -------------------------------------------------------------------------------- /rpcsh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import print_function 4 | 5 | import sys 6 | import argparse 7 | 8 | import reflectrpc.cmdline 9 | from reflectrpc.rpcsh import ReflectRpcShell 10 | 11 | parser = reflectrpc.cmdline.build_cmdline_parser("Interactive shell to access JSON-RPC services (part of the ReflectRPC project)") 12 | 13 | args = parser.parse_args() 14 | client = reflectrpc.cmdline.connect_client(parser, args) 15 | 16 | try: 17 | shell = ReflectRpcShell(client) 18 | 19 | shell.connect() 20 | shell.cmdloop() 21 | except KeyboardInterrupt: 22 | sys.exit(0) 23 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Release Testing # 2 | 3 | This directory contains docker images for testing releases of ReflectRPC. They 4 | install the newest release form PyPI along a Python 2.7 or 3.5 environment. When 5 | you run these images they run the ReflectRPC test suite against the installed PyPI 6 | release of ReflectRPC. 7 | 8 | The purpose of those images is to test each release to make sure the latest 9 | ReflectRPC version on PyPI works as expected. 10 | 11 | ## Usage ## 12 | 13 | ```bash 14 | ./build.sh 15 | docker run 16 | echo $? 17 | ``` 18 | 19 | The output will be 0 if all tests ran successfully. If not check the output for 20 | what went wrong. 21 | -------------------------------------------------------------------------------- /examples/serverhttp_basic_auth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import unicode_literals 4 | from builtins import bytes, dict, list, int, float, str 5 | 6 | import sys 7 | 8 | sys.path.append('..') 9 | 10 | import reflectrpc 11 | import reflectrpc.twistedserver 12 | 13 | import rpcexample 14 | 15 | def check_password(username, password): 16 | if username == 'testuser' and password == '123456': 17 | return True 18 | 19 | return False 20 | 21 | jsonrpc = rpcexample.build_example_rpcservice() 22 | server = reflectrpc.twistedserver.TwistedJsonRpcServer(jsonrpc, 'localhost', 5500) 23 | server.enable_http() 24 | server.enable_http_basic_auth(check_password) 25 | server.run() 26 | -------------------------------------------------------------------------------- /examples/flask-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # 4 | # Connect to this server with: rpcsh localhost 5000 --http 5 | # 6 | 7 | import sys 8 | import json 9 | from flask import Flask, request, Response 10 | 11 | sys.path.append('..') 12 | 13 | import reflectrpc 14 | import reflectrpc.simpleserver 15 | 16 | import rpcexample 17 | 18 | app = Flask(__name__) 19 | 20 | jsonrpc = rpcexample.build_example_rpcservice() 21 | 22 | @app.route('/rpc', methods=['POST']) 23 | def rpc_handler(): 24 | response = jsonrpc.process_request(request.get_data().decode('utf-8')) 25 | reply = json.dumps(response) 26 | 27 | return Response(reply, 200, mimetype='application/json-rpc') 28 | 29 | if __name__ == '__main__': 30 | app.run() 31 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. ReflectRPC documentation master file, created by 2 | sphinx-quickstart on Thu May 26 18:39:28 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to ReflectRPC's documentation! 7 | ====================================== 8 | 9 | This is the auto-generated API documentation of ReflectRPC. If you look for 10 | an introduction checkout the project README_. 11 | 12 | .. _README: https://github.com/aheck/reflectrpc/blob/master/README.md 13 | 14 | .. automodule:: reflectrpc 15 | :members: 16 | 17 | .. automodule:: reflectrpc.client 18 | :members: 19 | 20 | .. automodule:: reflectrpc.simpleserver 21 | :members: 22 | 23 | .. automodule:: reflectrpc.twistedserver 24 | :members: 25 | 26 | Indices and tables 27 | ================== 28 | 29 | * :ref:`genindex` 30 | * :ref:`modindex` 31 | * :ref:`search` 32 | 33 | -------------------------------------------------------------------------------- /docker/python27/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Build this dockerfile with ./build.sh 3 | # 4 | # It runs the ReflectRPC unit tests against the latest ReflectRPC version 5 | # available on PyPI 6 | # 7 | # After this image ran the shell variable $? should be 0 otherwise the tests 8 | # failed 9 | # 10 | FROM ubuntu:16.04 11 | MAINTAINER Andreas Heck "aheck@gmx.de" 12 | 13 | RUN apt-get update && apt-get install -y \ 14 | python \ 15 | python-pip \ 16 | gcc \ 17 | libssl-dev \ 18 | libssl1.0.0 \ 19 | libffi-dev \ 20 | libffi6 21 | RUN pip install reflectrpc pexpect 22 | RUN mkdir /tmp/reflectrpc-test 23 | RUN cp `which rpcsh` /tmp/reflectrpc-test 24 | RUN cp `which rpcdoc` /tmp/reflectrpc-test 25 | RUN cp `which rpcgencode` /tmp/reflectrpc-test 26 | 27 | ADD ./examples /tmp/reflectrpc-test/examples 28 | ADD ./tests /tmp/reflectrpc-test/tests 29 | ADD ./runtests.py /tmp/reflectrpc-test 30 | 31 | WORKDIR /tmp/reflectrpc-test 32 | CMD python runtests.py 33 | -------------------------------------------------------------------------------- /docker/python35/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Build this dockerfile with ./build.sh 3 | # 4 | # It runs the ReflectRPC unit tests against the latest ReflectRPC version 5 | # available on PyPI 6 | # 7 | # After this image ran the shell variable $? should be 0 otherwise the tests 8 | # failed 9 | # 10 | FROM ubuntu:16.04 11 | MAINTAINER Andreas Heck "aheck@gmx.de" 12 | 13 | RUN apt-get update && apt-get install -y \ 14 | python3 \ 15 | python3-pip \ 16 | gcc \ 17 | libssl-dev \ 18 | libssl1.0.0 \ 19 | libffi-dev \ 20 | libffi6 21 | RUN pip3 install reflectrpc pexpect 22 | RUN mkdir /tmp/reflectrpc-test 23 | RUN cp `which rpcsh` /tmp/reflectrpc-test 24 | RUN cp `which rpcdoc` /tmp/reflectrpc-test 25 | RUN cp `which rpcgencode` /tmp/reflectrpc-test 26 | 27 | ADD ./examples /tmp/reflectrpc-test/examples 28 | ADD ./tests /tmp/reflectrpc-test/tests 29 | ADD ./runtests.py /tmp/reflectrpc-test 30 | 31 | WORKDIR /tmp/reflectrpc-test 32 | CMD python3 runtests.py 33 | -------------------------------------------------------------------------------- /examples/certs/scripts/gen_server_cert.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import OpenSSL 4 | import OpenSSL.crypto 5 | 6 | ca_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, 7 | open("rootCA.crt").read()) 8 | ca_key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, 9 | open("rootCA.key").read()) 10 | 11 | key = OpenSSL.crypto.PKey() 12 | key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) 13 | 14 | cert = OpenSSL.crypto.X509() 15 | cert.set_version(3) 16 | cert.set_serial_number(1) 17 | cert.get_subject().CN = "reflectrpc" 18 | cert.gmtime_adj_notBefore(0) 19 | cert.gmtime_adj_notAfter(10*365*24*60*60) 20 | cert.set_issuer(ca_cert.get_subject()) 21 | cert.set_pubkey(key) 22 | cert.sign(ca_key, "sha256") 23 | 24 | f = open("server.crt", "wb") 25 | f.write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)) 26 | f.close() 27 | 28 | f = open("server.key", "wb") 29 | f.write(OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)) 30 | f.close() 31 | -------------------------------------------------------------------------------- /vagrant/devpy3.5/Vagrantfile: -------------------------------------------------------------------------------- 1 | # VM for developing ReflectRPC services with Python 3.5 2 | Vagrant.configure("2") do |config| 3 | config.vm.box = "ubuntu/xenial64" 4 | 5 | config.vm.network "forwarded_port", guest: 5500, host: 5600 6 | 7 | config.vm.network "private_network", ip: "192.168.33.10" 8 | config.vm.network "public_network" 9 | 10 | config.vm.synced_folder "../../examples/", "/vagrant_data" 11 | 12 | config.vm.provider "virtualbox" do |vb| 13 | vb.name = "ReflectRPC-devpy3.5" 14 | vb.memory = "1024" 15 | end 16 | 17 | config.vm.provision "shell", inline: <<-SHELL 18 | apt update 19 | apt install -y --no-install-recommends \ 20 | python3 \ 21 | python3-dev \ 22 | python3-pip \ 23 | python3-setuptools \ 24 | gcc \ 25 | libffi-dev \ 26 | libssl-dev \ 27 | libssl1.0.0 \ 28 | vim \ 29 | virtualbox-guest-utils 30 | apt remove -y python3-cryptography 31 | pip3 install reflectrpc 32 | SHELL 33 | end 34 | -------------------------------------------------------------------------------- /vagrant/devpy2.7/Vagrantfile: -------------------------------------------------------------------------------- 1 | # VM for developing ReflectRPC services with Python 2.7 2 | Vagrant.configure("2") do |config| 3 | config.vm.box = "ubuntu/xenial64" 4 | 5 | config.vm.network "forwarded_port", guest: 5500, host: 5400 6 | 7 | config.vm.network "private_network", ip: "192.168.33.9" 8 | config.vm.network "public_network" 9 | 10 | config.vm.synced_folder "../../examples/", "/vagrant_data" 11 | 12 | config.vm.provider "virtualbox" do |vb| 13 | vb.name = "ReflectRPC-devpy2.7" 14 | vb.memory = "1024" 15 | end 16 | 17 | config.vm.provision "shell", inline: <<-SHELL 18 | apt update 19 | apt install -y --no-install-recommends \ 20 | python \ 21 | python-dev \ 22 | python-pip \ 23 | python-setuptools \ 24 | gcc \ 25 | libffi-dev \ 26 | libssl-dev \ 27 | libssl1.0.0 \ 28 | vim \ 29 | virtualbox-guest-utils 30 | apt remove python3 python3-minimal python3.5-minimal -y 31 | pip install reflectrpc 32 | SHELL 33 | end 34 | -------------------------------------------------------------------------------- /examples/certs/client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICnjCCAYYCAQEwDQYJKoZIhvcNAQELBQAwDzENMAsGA1UEAwwETXlDQTAeFw0x 3 | NjA0MTQyMDQ3MDRaFw0yNjA0MTIyMDQ3MDRaMBsxGTAXBgNVBAMMEGV4YW1wbGUt 4 | dXNlcm5hbWUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJP7cy3J0y 5 | 7zx+JmfraSwmji6tuxzh0dgTN+/6kWzn4Xpn8luFEpLO8mbg/MmgT/qJicyCJbNJ 6 | 6bFOeOcQNcTjy5lm0uM7igNhq7XvyAZ8T4ODeBjTJj5yl7ex5Tuu3rDtSFTN+m0w 7 | zYTnfAXb0RKQfukareuRy1VbAmanGMZEO45ZiuGp3zs1BlT3Nc1svYEdV2FRDbQu 8 | 7rOSNGYGNpZjZQmMdtTSYiWIATaRJCOWZEJrTVewPaKeGnXTaiK1O+Dsj+M7xwS2 9 | v8mUK4ocm98314pMCuO1YMb5dZvWdZfV3RIDv6H6nNSkEzBrZa0kSEr4NjLME2kH 10 | PNuEHCcFrck1AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAM8HPsOYbAsExp/PpWDg 11 | vIHhyozJuw5OqsP2bOo810ySfRluZis9QAo71c62suTYuIw4L6p8BgQAomRTfZkh 12 | Eova/W1/bFPI+dohVY9jC5DhB7WwMQ7zJ5A1yxXtPQTCyNewHMFc75bHQ3C3oM/a 13 | RxcpVfP2Uo5GPB5bIA7I9KvAqWtaw9eDVe5UbpFxeaea+DhE7xsB+y4F59+l5rLD 14 | pGsw4XjZw2RGnCYt+6j1b227u+39YIoNDf+TL3Ve+JU45IoBvpPSHF0K1FTmBfyJ 15 | jnPRKRkrfV7rK6jcsmQsX6O5uIZvFjVq7VdcanOuyYS151+swb2/zpo5bwOP7335 16 | fHc= 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /examples/certs/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICnTCCAYWgAwIBAwIBATANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARNeUNB 3 | MB4XDTE2MDQxNDIwMTA1M1oXDTI2MDQxMjIwMTA1M1owFTETMBEGA1UEAwwKcmVm 4 | bGVjdHJwYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKZCVakdMTRN 5 | 9fkDDzpIKhe9CxZeaGCQ5apgJ2pNrRe6GYWenNtLFCHhQtjYGfziKTn4iqxTg6qD 6 | 1dPWlghwk+bzAMEaJxw93QsyvHsUYHx5HTFTtoX4AmapRWaf9pvnBvNTnHkdh80i 7 | fexGbqsIbLi1PLemB3pKJRIoj11YJj6RKhGcSamVwfRNdUpzLOXab4QJs/ft688z 8 | dBFgmzzVrTRQcopPqHPtory4BfcQaUoLeEXO14fZUW7LVqS2U51ecNxGaiLJRXst 9 | s8/1xy8vyNm4GLyYmhBGNBFe1FCWse5Intdj/iszk+vsBkcBs4loLLVC3oJnNBiO 10 | gTuFa6f9QikCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEASq/v0tJRq2c82a61Bvsv 11 | iJWOB5lJ/m1pE9zpoSfWhO+qc6VeIh1kO2KBRo9ydAgemKrTS2OZI5KMU5zKSHvY 12 | cEiK4YZG6SutA9lLuCsBEF9f1nFtK386rkbRJxKj6eN+ZrWk0mdJuDTJHjBKneUs 13 | T5LacVY2XsHkInMGXnCuvmZSI64oFGx+tva29z+qoL+GljtwbQfcGR792zxp5HuE 14 | PYr4Y2eJvdpC3Z6LfLMsXjvU7+m+Ea/UITJRMJtHjgbb9Uuh3cqdy5OmXNObBnGX 15 | 5omDujc9FBH9/DeAxBUqYPPAbsJ0fE50EFEIsFIhW3+lLUcPuj6Bsp5w2y/hkLQD 16 | 6g== 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /examples/certs/wrong-client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICnjCCAYYCAQEwDQYJKoZIhvcNAQELBQAwDzENMAsGA1UEAwwETXlDQTAeFw0x 3 | NjA1MTAyMTE1MDNaFw0yNjA1MDgyMTE1MDNaMBsxGTAXBgNVBAMMEGV4YW1wbGUt 4 | dXNlcm5hbWUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8XG+Cn/F6 5 | /vm2OjkxUMy2OASABi5y8NC2+/iQkKa3gl4O+VMB0sipK08nadEjtwH8lOfcdfzx 6 | T1ZoXE8Dxogmzc3g1AaDAuTuDfwfRkskbpccEYt0gkoDQmtYPf4WhyrgkTnKvcCp 7 | 5+QxBdzlXbLQWeN6a1YKtuvpZAlBB8ix+nRWQAlotVPWQSRO4tNJnzm3B/u55Rzx 8 | KLmEgcXf1W8tvsewQCFWzPgGNKMMG0unoGeKxUqHCQ/fifO5+ndfYRhr6MXKivGr 9 | kBR9RqqTmUvdaDjZ9AoUX72JPsjpltmBLTtEsQ4a0hCC1naJFHOEwKV+lXn71tnA 10 | wSDzIDUEa8pXAgMBAAEwDQYJKoZIhvcNAQELBQADggEBABksvVzGHalUh6KOUQOW 11 | qaw0ujVZozyVXyK4/p4+El0AQME3xpJ7s75r/wM7XILyWXxKme0EsbDJRZ1G1eF5 12 | T3V0m/iUjHDyo0E+gc6wW0d1KkTskoQX8T8/Ic6Woxw1yiGYE2LoSBn2afRuaNsN 13 | AeqF4nneUAuRPmmiDC4dn0CaTivTcLSSzA++3ZjyuNXmZvrFsa4OOZNaAWAXJzw9 14 | 2s2OWu7oxuXS7msEfvq/8lppoj9gaJ6aCbFPm8BHro6Y+XZPvfy+tMR64Z5sb7+/ 15 | KHwYNUzCHyYUGR5qHMyidNLdrggvKcbZ1sG0McO8LXZrhpNWQLeyAsM6ACK6hHAG 16 | aU4= 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /examples/certs/rootCA.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC3jCCAcagAwIBAwIBATANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARNeUNB 3 | MB4XDTE2MDQxNDIwMDg1OFoXDTI2MDQxMjIwMDg1OFowDzENMAsGA1UEAwwETXlD 4 | QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPlSxZ7aOap5Wg5+KmqA 5 | w9rPJIQqDtKculzupma0mLMCESExGyv3h/E811a81aXeMfxI5mN5N1Q1/9NLxhGv 6 | U92yl/nOMaNzR3DqnVyI/1wg0KFSvroo5m9IOi9LLMUxobP7DjHgsYclnoGY4nRu 7 | Xzv6vg54UwE9Qf1dDEkfEy1vG6kEgUfnVXne7E6V4VQebhHAGBCyk/jkox8O0sFK 8 | pICXRLXOHkC9TAi3+swJpwrNPfl4BZmDvzovAfu1H2/T6X38k8R/lDMI6X8XrPCe 9 | AZ/Q/uUyf6JtivOlRPwuS/I9BuzWScpmnyVG6IL+hgcPdogfIvD6yIkEEvpHWgwN 10 | d30CAwEAAaNFMEMwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYw 11 | HQYDVR0OBBYEFBUZ+xzXjzYNIQv5aRQS2GvkCDhgMA0GCSqGSIb3DQEBCwUAA4IB 12 | AQAj3K5ZQzJgPckr7GFbuWYi4oITXefTejJkX2xe5kVe89nUNh7UGYZMY8ZjIBiL 13 | 3g8YhLOgLNavZLjZBn2fwbcwCeubCMTTg2HKxynAqGtle//n1k6fY7go/Oc6+7AM 14 | 79na8lva0CqQxFV5kldrKtVmPS/A3MQjrr0ZPFcDTHMCbUoHLrPFZafzdkRPLk0a 15 | te0ARk3TW3uXD1nXMnswYgho/trRc20+wS9mRSRRVKXQE/qv/+AMLHl88DxvOVN0 16 | TNhYBkAKFPj/aGP7bjRMhi1Fi1IClzlB3zrdKG0yZy86ecVSdkrHRJOQ8r3LS12B 17 | BeUMj5M+atyw3aYLyr9Z0W+7 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /examples/certs/wrongCA.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC3jCCAcagAwIBAwIBATANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARNeUNB 3 | MB4XDTE2MDUwOTIyMDk0NVoXDTI2MDUwNzIyMDk0NVowDzENMAsGA1UEAwwETXlD 4 | QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALOArd0t5ef0SpXTt0s2 5 | qUr4OAVGfZkz6NIujP+Cb6TqogLveztytIsUmCrMYGStn0uPDSWit5m257zTWFdE 6 | mJUmMZhRgiMaiy+NdJlUvL1WbSjXlJqPdGFtRmcEXIR0r8LXI+BNnFHUjYVkiSZI 7 | +cldGhCBoId340yI+bkBhPuy2j+5b87pM8ZMTBUFFtSb4SBqoL3GojwL3DjaXtPE 8 | A3vt2h1KeRcQOqlRuiLYbildKRyJYXOn3ob34iFG0NiJyO/GW4MOvIIstiLTZ3ud 9 | Om3E/WDGeHsfvYl6myIBX92SEiA/n53ta7/4lCeigdqalzAW5ApFlB7gVe2mZrL8 10 | U5MCAwEAAaNFMEMwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYw 11 | HQYDVR0OBBYEFHTAQ0IDUNoA+BYq1sZipNS4lnb9MA0GCSqGSIb3DQEBCwUAA4IB 12 | AQA7azRx25hP7yFdkFycgUPfqPQdkcf5/lLI4Oa4M4OBrcRdhj3pJ37n7VJbIAzG 13 | /lmley6Dcj0tKsZB6F2CdZZunLnftDnYSMHNkQdDpzj7yFiDDkdq46ZQ0oR7IH5J 14 | 3k0gTq3EoX5y2Y+qtMwQ+cu8qX2StMKboKbFMT/IR/zi0VFvN8e8p19FBV6IPRih 15 | tkWiPFaXvrcGNLu2Y++PZGOklldufChq7av45iU3gxwG09nMFR1EAJQpGQPR9X3W 16 | 08+a5SqnNI4r/rEYCk9XA9TgANtt3OjduS0SnAl99y6ct/H1l005ddFiAKHH2yEp 17 | 0lIQys125htZ9FWTcn7E+hfS 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Andreas Heck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ********* 2 | Changelog 3 | ********* 4 | 5 | 0.7.6 (2016-09-14) 6 | ================== 7 | 8 | - Support for typed arrays 9 | - Autocompletion of type and function names in rpcsh 10 | - Bugfixes for command-line programs 11 | - Autoreconnect in case of failing connections in command-line programs 12 | 13 | 0.7.5 (2016-08-17) 14 | ================== 15 | 16 | - Fixes rpcsh crash with Python 2.7 17 | 18 | 0.7.4 (2016-08-11) 19 | ================== 20 | 21 | - RPC functions run by the Twisted server can now return Deferreds to implement concurrent RPC services 22 | - Linux sys info example service 23 | 24 | 0.7.3 (2016-07-24) 25 | ================== 26 | 27 | - Support for UNIX domain sockets 28 | - New rpcgencode tool to generate client code for a running RPC service 29 | 30 | 0.7.2 (2016-07-01) 31 | ================== 32 | 33 | - Bugfixes 34 | - Automatic generation of service documentation with the new rpcdoc tool 35 | - HTTP Basic Auth for client and Twisted server 36 | - Mechanism for passing context to RPC functions (rpcinfo) 37 | 38 | 0.7.1 (2016-05-12) 39 | ================== 40 | 41 | - Adds missing package dependencies 42 | 43 | 0.7.0 (2016-05-11) 44 | ================== 45 | 46 | - First release 47 | -------------------------------------------------------------------------------- /examples/certs/scripts/gen_ca.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import OpenSSL 4 | 5 | key = OpenSSL.crypto.PKey() 6 | key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) 7 | 8 | ca = OpenSSL.crypto.X509() 9 | ca.set_version(3) 10 | ca.set_serial_number(1) 11 | ca.get_subject().CN = "MyCA" 12 | ca.gmtime_adj_notBefore(0) 13 | ca.gmtime_adj_notAfter(10*365*24*60*60) 14 | ca.set_issuer(ca.get_subject()) 15 | ca.set_pubkey(key) 16 | ca.add_extensions([ 17 | OpenSSL.crypto.X509Extension("basicConstraints".encode("ASCII"), True, 18 | "CA:TRUE, pathlen:0".encode("ASCII")), 19 | OpenSSL.crypto.X509Extension("keyUsage".encode("ASCII"), True, 20 | "keyCertSign, cRLSign".encode("ASCII")), 21 | OpenSSL.crypto.X509Extension("subjectKeyIdentifier".encode("ASCII"), False, "hash".encode("ASCII"), 22 | subject=ca), 23 | ]) 24 | ca.sign(key, "sha256") 25 | 26 | cert_file = open("rootCA.crt", "wb") 27 | cert_file.write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, ca)) 28 | cert_file.close() 29 | 30 | cert_file = open("rootCA.key", "wb") 31 | cert_file.write(OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)) 32 | cert_file.close() 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup 4 | 5 | def read(fname): 6 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 7 | 8 | setup(name = 'reflectrpc', 9 | packages = ['reflectrpc'], 10 | version = '0.7.6', 11 | description = 'Self-describing JSON-RPC services made easy', 12 | long_description=read('CHANGELOG.rst'), 13 | author = 'Andreas Heck', 14 | author_email = 'aheck@gmx.de', 15 | license = 'MIT', 16 | url = 'https://github.com/aheck/reflectrpc', 17 | download_url = 'https://github.com/aheck/reflectrpc/archive/v0.7.6.tar.gz', 18 | include_package_data=True, 19 | keywords = 'json-rpc jsonrpc rpc webservice', 20 | scripts = ['rpcsh', 'rpcdoc', 'rpcgencode'], 21 | install_requires = ['future', 'service_identity', 'twisted', 'pyOpenSSL'], 22 | classifiers = [ 23 | 'Development Status :: 4 - Beta', 24 | 'Intended Audience :: Developers', 25 | 'License :: OSI Approved :: MIT License', 26 | 'Programming Language :: Python :: 2.7', 27 | 'Programming Language :: Python :: 3.3', 28 | 'Programming Language :: Python :: 3.4', 29 | 'Programming Language :: Python :: 3.5' 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /benchmark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import unicode_literals 4 | from builtins import bytes, dict, list, int, float, str 5 | 6 | import argparse 7 | import json 8 | import sys 9 | import time 10 | import unittest 11 | 12 | from reflectrpc.client import RpcClient 13 | from reflectrpc.testing import ServerRunner 14 | 15 | parser = argparse.ArgumentParser( 16 | description="ReflectRPC benchmark to run against a server program that listens on localhost:5500") 17 | 18 | args = parser.parse_args() 19 | 20 | # reset argv so unittest.main() does not try to interpret our arguments 21 | sys.argv = [sys.argv[0]] 22 | 23 | client = RpcClient('localhost', 5500) 24 | 25 | # ensure we are alread connected when the benchmark starts 26 | client.rpc_call_raw('{"method": "echo", "params": ["Hello Server"], "id": 1}') 27 | 28 | millis_start = int(round(time.time() * 1000)) 29 | num_requests = 5000 30 | 31 | for i in range(num_requests): 32 | result = client.rpc_call_raw('{"method": "echo", "params": ["Hello Server"], "id": 1}') 33 | 34 | millis_stop = int(round(time.time() * 1000)) 35 | millis_spent = millis_stop - millis_start 36 | 37 | print("Requests: %d" % (num_requests)) 38 | print("Time: %d ms" % (millis_spent)) 39 | print("Requests per Second: %d" % (num_requests * (1000 / millis_spent))) 40 | -------------------------------------------------------------------------------- /webui/static/style.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | text-align: center; 3 | } 4 | 5 | a.function:visited { 6 | color: #0000FF; 7 | } 8 | 9 | a.function:hover { 10 | text-decoration: underline; 11 | } 12 | 13 | a.function { 14 | color: #0000ff; 15 | text-decoration: none; 16 | } 17 | 18 | div.function_wrapper { 19 | background-color: #f9f9f9; 20 | padding: 0.5em; 21 | margin-bottom: 1em; 22 | border: 1px solid #f3f3f3; 23 | } 24 | 25 | div.login_center { 26 | text-align: center; 27 | } 28 | 29 | fieldset.login { 30 | text-align: left; 31 | display: inline-block; 32 | padding: 0.75em; 33 | border: 1px solid #f3f3f3; 34 | background-color: #f9f9f9; 35 | } 36 | 37 | fieldset.login legend { 38 | font-weight: bold; 39 | } 40 | 41 | table.login td { 42 | padding: 0.5em; 43 | } 44 | 45 | table.connected_to { 46 | margin: 0; 47 | padding: 0; 48 | border-collapse: collapse; 49 | background-color: #f9f9f9; 50 | border: 1px solid #f3f3f3; 51 | } 52 | 53 | table.connected_to tr { 54 | margin: 0; 55 | padding: 0; 56 | } 57 | 58 | table.connected_to td { 59 | padding: 0.5em; 60 | margin: 0; 61 | border: 1px solid #f3f3f3; 62 | } 63 | 64 | div.result { 65 | white-space: pre; 66 | } 67 | 68 | div.error { 69 | color: red; 70 | text-width: bold; 71 | } 72 | 73 | table.params td { 74 | vertical-align: top; 75 | padding: 0.3em; 76 | } 77 | -------------------------------------------------------------------------------- /examples/flask-basic-auth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # 4 | # Connect to this server with: rpcsh localhost 5000 --http --http-basic-user testuser 5 | # 6 | 7 | import sys 8 | import json 9 | from functools import wraps 10 | from flask import Flask, request, Response 11 | 12 | sys.path.append('..') 13 | 14 | import reflectrpc 15 | import reflectrpc.simpleserver 16 | 17 | import rpcexample 18 | 19 | app = Flask(__name__) 20 | 21 | jsonrpc = rpcexample.build_example_rpcservice() 22 | 23 | def check_auth(username, password): 24 | return username == 'testuser' and password == '123456' 25 | 26 | def authenticate(): 27 | return Response('Login required', 401, 28 | {'WWW-Authenticate': 'Basic realm="ReflectRPC"'}) 29 | 30 | def requires_auth(f): 31 | @wraps(f) 32 | def decorated(): 33 | auth = request.authorization 34 | if not auth or not check_auth(auth.username, auth.password): 35 | return authenticate() 36 | return f(auth.username) 37 | return decorated 38 | 39 | @app.route('/rpc', methods=['POST']) 40 | @requires_auth 41 | def rpc_handler(username): 42 | rpcinfo = {'authenticated': True, 'username': username} 43 | response = jsonrpc.process_request(request.get_data().decode('utf-8'), 44 | rpcinfo) 45 | reply = json.dumps(response) 46 | 47 | return Response(reply, 200, mimetype='application/json-rpc') 48 | 49 | if __name__ == '__main__': 50 | app.run() 51 | -------------------------------------------------------------------------------- /webui/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block content %} 3 | 16 | 17 |
18 |
19 |
20 |
21 | 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /examples/certs/wrongCA.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAs4Ct3S3l5/RKldO3SzapSvg4BUZ9mTPo0i6M/4JvpOqiAu97 3 | O3K0ixSYKsxgZK2fS48NJaK3mbbnvNNYV0SYlSYxmFGCIxqLL410mVS8vVZtKNeU 4 | mo90YW1GZwRchHSvwtcj4E2cUdSNhWSJJkj5yV0aEIGgh3fjTIj5uQGE+7LaP7lv 5 | zukzxkxMFQUW1JvhIGqgvcaiPAvcONpe08QDe+3aHUp5FxA6qVG6IthuKV0pHIlh 6 | c6fehvfiIUbQ2InI78Zbgw68giy2ItNne506bcT9YMZ4ex+9iXqbIgFf3ZISID+f 7 | ne1rv/iUJ6KB2pqXMBbkCkWUHuBV7aZmsvxTkwIDAQABAoIBAQCjBbrhpU2n27Xh 8 | XOaa6InYDJbUM7Dd8scAHEbxxwSeQnnhMJ4633IY3htUw0jIJucFOGY4SA93CyZr 9 | 14Xju+jXjFh+fYgzWWgPR+kdWFgRnOyGq0PLG34W/ady6AMeSNtXmQx8KgBOUTw2 10 | aZglrSEuP9/sHc4tOjS0zbH+0+Jys/UdGEWIZZ8hAnJ6J/Kv59gN0VEZCvAHb4uJ 11 | d9siW4BszNfhxD8969lp4gqahWNZEiIlpqs2wggJceCBL/zJyE0ygZWNXr3v1vNM 12 | z8rUz1fnja+qEO5OLKFTm/ZpA9AmsJMxZ1xgg62u/5BScZ2LJWD8kjMXI4sAOscH 13 | EcjednQBAoGBAO1bLFd3ev+yu3zrtV/P1/7adFHR2aXy0k06biRCGoTayJGsLYT/ 14 | /PUFKfkqm6/NUPj5hOtov+adekaaVsVvUofDE3ym96y2jLNLYy4VSnPtEf8R0WEu 15 | +iNTgLXJ2vP6rrGC7KrVGFMELCG/xJPfiHxKTSupjv3TbsHN4+/3WxKfAoGBAMGa 16 | K8a4sKYCWsXRHJjpZkIcfm7yIuYvfTFut8UxFUAblUwYWwWysS0MgHCbIKzi+X2F 17 | zQ89ouGQ4VmEqiKPdNDjI2u7qKFZkuwdnl4g19AtIY01WtXsybvK6ILG1T8cJBeF 18 | rKJx/B+sMF9VsNq50ccR4tcHtDz0KJnlkjkj9a6NAoGBAK72FxB+hU84u9WQlkkD 19 | F4/IDhhF3O7juDuvR4M4qv2lnFUtGvzACgG/BbqiutJzQS3WGDHDLDndeUXT/QRa 20 | U/a5SIMJPOa2Ra7gckKE9TXQ2gQwaSv/CenCYs0d92UDM4SsIrKmk+CV4cYa6tep 21 | 3Zzo1EvMGBhoo2r+zveTWTG3AoGBAKci89vxIf+PVNImPyv7gx5b/wLE40AZi1kb 22 | nmcMgq3/oho5hIscwzyC6HdOVR0sLfshgfBAY9eb/hAMKd0AP/b9wFyHe4MgE2jo 23 | AQp+DBJag/amMy8v5tDK0YPlJ7/+CWKNMoZjJSgqvO/wyGdruCDF3jGJIx7kIhEK 24 | UfwmgTkZAoGAOC+JxD0WCg1KtEOgU/am0G/J3+VsNuzBgwggPSDnJ5vIdXgwc2ml 25 | NHQPGhAh/JPGJWdrE+v0LqF8uJ+O2i9kD4C/X+tVOPRfP+sntoDndAmkjrzXmu/Q 26 | ojBVc1zTgSuVL1cn+YXeXfc1dywBIRwE+X5o/0+aA7bwMsvjO6Z/JO0= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /reflectrpc/server.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from builtins import bytes, dict, list, int, float, str 3 | 4 | import json 5 | 6 | from abc import ABCMeta, abstractmethod 7 | 8 | class AbstractJsonRpcServer(object): 9 | """ 10 | Abstract base class for line based JSON-RPC servers 11 | """ 12 | __metaclass__=ABCMeta 13 | 14 | def __init__(self, rpcprocessor, conn, rpcinfo=None): 15 | """ 16 | Constructor 17 | 18 | Args: 19 | rpcprocessor (RpcProcessor): RpcProcessor with the RPCs to be served 20 | conn (any): An abstract connection object to be used in the user 21 | implemented send_data method 22 | """ 23 | self.buf = '' 24 | self.rpcprocessor = rpcprocessor 25 | self.conn = conn 26 | self.rpcinfo = rpcinfo 27 | 28 | def data_received(self, data): 29 | self.buf += data.decode('utf-8') 30 | 31 | count = self.buf.count("\n") 32 | if count > 0: 33 | lines = self.buf.splitlines() 34 | 35 | for i in range(count): 36 | line = lines.pop(0) 37 | reply = self.rpcprocessor.process_request(line, self.rpcinfo) 38 | 39 | # in case of a notification request process_request returns None 40 | # and we send no reply back 41 | if reply: 42 | reply_line = json.dumps(reply) + "\r\n" 43 | self.send_data(reply_line.encode("utf-8")) 44 | 45 | self.buf = '' 46 | if lines: 47 | self.buf = lines[0] 48 | 49 | """ 50 | Abstract method you must override to send a reply back to the client 51 | """ 52 | @abstractmethod 53 | def send_data(self, data): 54 | pass 55 | -------------------------------------------------------------------------------- /examples/certs/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJP7cy3J0y7zx+ 3 | JmfraSwmji6tuxzh0dgTN+/6kWzn4Xpn8luFEpLO8mbg/MmgT/qJicyCJbNJ6bFO 4 | eOcQNcTjy5lm0uM7igNhq7XvyAZ8T4ODeBjTJj5yl7ex5Tuu3rDtSFTN+m0wzYTn 5 | fAXb0RKQfukareuRy1VbAmanGMZEO45ZiuGp3zs1BlT3Nc1svYEdV2FRDbQu7rOS 6 | NGYGNpZjZQmMdtTSYiWIATaRJCOWZEJrTVewPaKeGnXTaiK1O+Dsj+M7xwS2v8mU 7 | K4ocm98314pMCuO1YMb5dZvWdZfV3RIDv6H6nNSkEzBrZa0kSEr4NjLME2kHPNuE 8 | HCcFrck1AgMBAAECggEAeYEOipbIEkh+rWtisq79CQovBJVECtM3MeND8HR83EM2 9 | NCwPNXjRSkDv/EajNTcUfJXF843vgWCmvEoit5a/GQmDxKDusLPS9tVFM1ABGmyn 10 | amjIFDOy4FzZe357Wkj5aUmSagoYgq6S32/x2ZWRL8xv0LvQzmWFUz1P3PMIQYjh 11 | davqDbGqJU1iW0z/YBC3M8ONAV6zxTmHbsWL803HlFlSCchkAsE64mv5NL/b3Z6/ 12 | KHjp/pn71XTiapIHHZ+5JAWMVc6z/7M+j7+vuc+3H+iBpNsYfNTl10FrjBn7UQVc 13 | o+VtHR461xe3HhgL/oiX0npDDtDy/iYOzUz9hxkmwQKBgQDnnSNhQvir4yhc6Q8X 14 | wH8HDlM5AXhiR0GxNxTU3fuWCxAIAW8uOLCF/uPyEu7qHE+04ylKSywM1O3v/Xh/ 15 | jyrxBJhXhTmirW+4K+UtxyKqfD9gW96YwLNm90gDQAXk1crWFiT+UZto/AkD+6fP 16 | /1kdqjK6F6SgL5iFl6DhCCwZEwKBgQDecCDncZc23s9SyMxfdhqCT6U5+wlvEle7 17 | ZHWf5yiBNg/Qjxpm3VxW8iQGRiVh3X4O/HFdQxbMCGs0Kd5EvhcAPNw29j95VpgG 18 | NUB2q2WikVyHBJZP+hO8rpeUkQSClhB9SqdDD7m8DjIKImtLc66KZjkpWe6eGGK5 19 | lwC91m/llwKBgGwjwywbP83JbsQKkOBvISAjQRohpJqGWJaseA53YosIuUBzovx6 20 | vXiirL0Ot/wYoeJ8GYA1nuiS1lEyEHvGVa9YjOR4MJPS1JGx9PwtuPLOtiyAWGsL 21 | tffCRx41W0sfwue4SSdw5NXcrzzr18tWlr4yBCM8/MFJ17WwOQ44aZZ/AoGAfPFm 22 | yhoX4g5NTibUBVsFkOTD3kmQBv+6n9vR5julmM6RG4kGP3lfgMcKTSvhm5MaV7ic 23 | xNIGYBzqeqZ53wsXSF7nI0g2ZyWoxvPqfb3QnPwhiQWemNXTeExpgF3ktqUsJfRk 24 | 91pB7cvbd63VrtAP3lWFDiEh01PHQI/9LqTgvYkCgYEAl6nTayYYcVobRupTLYPE 25 | PtSLwVWRRg8ZUGJJGW8fbr6KQaL6GOGfjsiOqkmzcEln9B7eRgl26lV5H9CnbI3w 26 | z7CAR2GwGPhlYYV6zJOQxuwRhbm49MJhc33oEjjnkYxy9EVcBM9VFxrRUHUAnjfV 27 | 48J/nn1LLmzH3mH8NQzwrSE= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /examples/certs/rootCA.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD5UsWe2jmqeVoO 3 | fipqgMPazySEKg7SnLpc7qZmtJizAhEhMRsr94fxPNdWvNWl3jH8SOZjeTdUNf/T 4 | S8YRr1Pdspf5zjGjc0dw6p1ciP9cINChUr66KOZvSDovSyzFMaGz+w4x4LGHJZ6B 5 | mOJ0bl87+r4OeFMBPUH9XQxJHxMtbxupBIFH51V53uxOleFUHm4RwBgQspP45KMf 6 | DtLBSqSAl0S1zh5AvUwIt/rMCacKzT35eAWZg786LwH7tR9v0+l9/JPEf5QzCOl/ 7 | F6zwngGf0P7lMn+ibYrzpUT8LkvyPQbs1knKZp8lRuiC/oYHD3aIHyLw+siJBBL6 8 | R1oMDXd9AgMBAAECggEAGMUXaxvFJOGfh9cTAbe/PYrc1uCSzmvRvA/dqlCA5y/q 9 | YcDOMUULVXU086IZG7yCfM0FTpapX41p2CUsDW+8xkbLAH9ywQlf7KDyd1IJBK8h 10 | 6xUca8RKeH3VggNW9FRk+0uS8nOfT4z2PWvh+61Het8DaM6deH8Mgk5vPQiAUEAK 11 | bV02eHxmW94G36kqWnALP3wcUpA8WZftTh2fMuLTUOdqrJ6Jfx6OYAeaS0rIJgQI 12 | lK2iHeX4R3b4kIr7sWNimj6FqdtsJIQ0e4CcBndwe6K5kcmfYonCVm+VodTCOzYC 13 | IO2cqJhUlNhSaMChoT8pcG3j8Pg+GVTbyZRuiByqAQKBgQD9KjwCur/XOqOFTKAa 14 | iq1HOeGxi/pn6GxyU8SkcptB3XOIQSWz9nssi6udT8AyiVje06/QgB/0LjBkZRGP 15 | t/K4KVIhdpzudSSK/sRrqd6Zx3Ng72tp5eeQ7YUC7VdSzayzIuW23gv8wW21PueD 16 | sShu3CZNhvyGn4mWPKdvWLce/QKBgQD8HYY/+AahHJPg08ySE7K/JAOGOLwli1Pd 17 | XAXBTDUE/gOhPwULRZPOeF7NbZ5JQ4V9q3SB3dHMU8eRsbUglz4aB5j0HhxJqUaV 18 | 84YIf4E02MAt7DdAqlpMlY+43GEYXNGK6k8IPYHJx6Q6k4LD0xlyti6HDwEBzQ2c 19 | X+G7GrJigQKBgGjWC4HJ2dWD9+EkaQx0rurW4kpGwAw1aGszmzPKLdbpoIUlDTrn 20 | 7/vzXr7HHr0OuVmU6bJ6zECuu+VnIQ2VzBJNMjCfUL9CjT5t3+MBtieSO4pBuwZg 21 | aTNP2IvswwOMHl8ULXI7o1UouIicovya6TZ3PflO09XfzPcE6QUoBLT1AoGAHGXe 22 | 91mitYcYiRySUGnzmmAeHYJKfxggjDCPXWSOHE/YbNNCaDCgI4Ofehg82hfG847a 23 | gr5PoWpWcmzH8DHZBumQKv8xRILStpVFpbNnBGLd0s5mstv9a97032fDcBEUcUdP 24 | O6hh9C6OqyJuekxO84Ld3syr2l4UiGascHzjVwECgYEAher57iPzFdebEtGNPm+T 25 | oUUzWMS5Ix3vfl6OWREvnOEXx/+q2iQOJcUElKbwL4JYg2Xn67z+WBXpxbZgQQBA 26 | hCIHQigVYRiloifAcLzaGfaWPOGEYLjW/VsUFVJJzBPtPbNIljFtGH6nYlgyLyTC 27 | dXvQVl2Q0c3SJA/Nx6Au2fg= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /examples/certs/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCmQlWpHTE0TfX5 3 | Aw86SCoXvQsWXmhgkOWqYCdqTa0XuhmFnpzbSxQh4ULY2Bn84ik5+IqsU4Oqg9XT 4 | 1pYIcJPm8wDBGiccPd0LMrx7FGB8eR0xU7aF+AJmqUVmn/ab5wbzU5x5HYfNIn3s 5 | Rm6rCGy4tTy3pgd6SiUSKI9dWCY+kSoRnEmplcH0TXVKcyzl2m+ECbP37evPM3QR 6 | YJs81a00UHKKT6hz7aK8uAX3EGlKC3hFzteH2VFuy1aktlOdXnDcRmoiyUV7LbPP 7 | 9ccvL8jZuBi8mJoQRjQRXtRQlrHuSJ7XY/4rM5Pr7AZHAbOJaCy1Qt6CZzQYjoE7 8 | hWun/UIpAgMBAAECggEAT6m1NcUBEJjSZTBsGXb+hEVWjK9LwAltokdUW4FAkP/g 9 | vr+TVRgSW3F+ADz7psoPCvHmMFAL5KYqzMgjN4QJuj1xfRU07DlQMs9qtGa9HKdD 10 | r6D28hY1wE8XK+c12NnH4MuNTBM0QLxoLdBJsrXkslRU9YIeTyA7xwmcOBPGr29E 11 | zus8V+pIYNWJuwk558Z5AybA0hecGbCAq/84EAHwF3Pn9S2YNMpmwye4usGTqL/S 12 | xJTCKViA0rr9XtMr2LYZ//5YEuejCI/x0KayrmJ8uaQ98dwBvBvuVuAscwPUl3nG 13 | HMkSj9wKLn0VrinfWZXCw/MnvgVoneOMrtUKmBdbAQKBgQDdeLQLnkSfCZkCokLE 14 | pW7o8V4QdagonRec6yLeCZcDpGcurFeRilE3DTCGfPN/SMlZh/MCQAm3DCIfl/kO 15 | gKnZPV+jWy/ofkQ778qrD58GoCgpeDhg1PA5EXkDLkG5zLrPNqVh6Lv8Wi7wWvZT 16 | JJMDoQNI38u9Hof4Ezwi+0iv0QKBgQDALgNlS+k5ZFKAQBvCADWEor+xlth0SWc6 17 | +pxWojbqtFHQy8bWidk9pRLz9C8GdDXpbAeB4ZM5tG45TSNyLeQZLVH2hqS+keV0 18 | SKUKs3jc/wEThyTcieUElIbyBvyVsGFNdoWlqOvDPslL5Y8OpYVH6eOxmr0xp6L1 19 | Vf2V+3Ua2QKBgFliJ7gwrh1JsFlhx3S6F+Mn1wDpm26YyDjqpW3bjPlJVuN9ZvI0 20 | UsbXKeh9cYDDjY/20FruIX2hBfyeR0RVJTeqD3lMii9ZFoziIHednF7+MHdcL9TU 21 | 3AcMSDzCZIBqYlLTCThUx9n3Q855x8SSlEr4puy4de/j7Jhwmuq7ZAChAoGAayM7 22 | 0WUYiF5dgBI9Z1Img+MXazHlSi8B1eeQ8NtOMlqEohp4p3ICIlO81TP0Y2y2AYOw 23 | S8AuC6WDLX7LnAPpff++CenWPken28QD/os/fjTLrM9SxYA6pOsIsDUk626BUGYa 24 | 69fYV+jQ3/cCYe/09bp2rbTOdLg4KP3feZXOG0ECgYEAvfS7c3abGpj4qU3/1xlf 25 | Gox16RngJytBG+VXt3f55J4Oz7eywmdRjPc2S9wRq3+Jtn1s7+ivwVIel0btg0i2 26 | TWtN3/YMYy3GGqwPMNxWejcHYCsSdItdKv/uT2LWrxPHoVkmoeCeG1sXK1OjWQPX 27 | BfgdJHivXJL9lh3Fug0qJmg= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /examples/certs/wrong-client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8XG+Cn/F6/vm2 3 | OjkxUMy2OASABi5y8NC2+/iQkKa3gl4O+VMB0sipK08nadEjtwH8lOfcdfzxT1Zo 4 | XE8Dxogmzc3g1AaDAuTuDfwfRkskbpccEYt0gkoDQmtYPf4WhyrgkTnKvcCp5+Qx 5 | BdzlXbLQWeN6a1YKtuvpZAlBB8ix+nRWQAlotVPWQSRO4tNJnzm3B/u55RzxKLmE 6 | gcXf1W8tvsewQCFWzPgGNKMMG0unoGeKxUqHCQ/fifO5+ndfYRhr6MXKivGrkBR9 7 | RqqTmUvdaDjZ9AoUX72JPsjpltmBLTtEsQ4a0hCC1naJFHOEwKV+lXn71tnAwSDz 8 | IDUEa8pXAgMBAAECggEBAJkEnrIHLS02JbYb5ophkWwWZdF7NBC8AVIlr/ABEu7R 9 | QYf4k65PhiOnw03JcNUKvtpqPVGjqDCAuzlcg/QVPFYJqs6ScBfOKhwZ0E+30yNt 10 | k+SBfEDR9z5ensW41smGVRbJ46EINZPRhlcs5B6Q18rauymgOO3LXCrl0X66zBeI 11 | cQHZtN3L6/MurdhkGCQGVsOlSfs3tVMT8xDI7ibrt8bK/8bYM6YHLjkyzmzPDpyh 12 | 7Ho8+fnWgWagMs5AP5o/LYjHAls2+s76GlOxWxynovK5KOzsfr+us7kOCeZtGSlU 13 | uk0yL0jfFdXuLk3KqhI9vbPBFz9IjCOXuHGM5fhzE4ECgYEA77qylGH08eiKeFpD 14 | RixnzP07W8f/dUnHypFv3L2Uag+eEl0C8xl/Ln9FMhCKM9cEPHSkh2VtNfv+dxXt 15 | YGyrlu3zqPmVdat0r4YdA7XaznNfbPY7Y6LeOKljn3/9WOjogqVfdVCwaWoSKAcQ 16 | CnQrjSnba/ob3bIPugZDj7wpxfkCgYEAySU2y+NRvfwz/ke0DT+JYpltxpolH5Te 17 | p0zy6T/68FVQpTyopXVMdXljzol8HKTev/kxjVwTqUeq6GUuTBkRGPn+IycUsN1F 18 | PZXmTSGqBvZENf3aXkwND+WAmHI4mun7sVe/efZqSQUhEcmP0Q6LZswO5jD4IZTj 19 | C0BjITK/5s8CgYBqM1Iza+XgWP0m7g3Jg5iEdlaahVJFOmc49Q8SQiYCimKjjfLl 20 | kREHnzgfQraG2qU0xxOwK52jAbysMtmTEvE9DrMX02GD3G336DjoUOLa/L90fOy4 21 | agl6HohUu5WXkq6WWf6c8R8FxAjBFMflaat1gOqEviskHmLbLTU28suquQKBgQCN 22 | a0GsdcVZ2sC/bbBUhDBWh2Lb+DJTkvo8C+jZP2dxo7oQgqZbti2shk06wATnqq8F 23 | r/E73tnf8Yhi3gh/7jvMIK+iDW8JrnhpBUQnRJzjSi/I+hKWq8efPo98HwpXOF0C 24 | YZw45pvxfM60T89yf1RZUOzEwPcjvScoGPvZMnIjlwKBgBCF54B72Qc4Ne5LG1et 25 | NLB8KaOyVB+TxvXdEthRealr0iRZor44pu2DvG6CWZuvA5usLb/laNgQ/qKLZF1H 26 | j0cdbBQj8R4zQEswKGL67DVe5KHNn3E07qjGKbv5GBABaj3D+L0lja9jQ0BHZrAb 27 | yhDBZYavcwmO387LzY4evIru 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /tests/client-tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import unicode_literals 4 | from builtins import bytes, dict, list, int, float, str 5 | 6 | import errno 7 | import sys 8 | import json 9 | import os 10 | import unittest 11 | import time 12 | 13 | sys.path.append('..') 14 | 15 | from reflectrpc.client import RpcClient 16 | from reflectrpc.client import RpcError 17 | from reflectrpc.client import NetworkError 18 | from reflectrpc.client import HttpException 19 | from reflectrpc.testing import FakeServer 20 | 21 | class ClientTests(unittest.TestCase): 22 | def test_client_simple(self): 23 | server = FakeServer('localhost', 5500) 24 | server.add_reply('{"error": null, "result": "Hello Server", "id": 1}') 25 | server.run() 26 | 27 | client = RpcClient('localhost', 5500) 28 | 29 | try: 30 | result = client.rpc_call('echo', 'Hello Server') 31 | server.stop() 32 | request = server.requests.pop() 33 | 34 | expected = {'method': 'echo', 'params': ['Hello Server'], 'id': 1} 35 | self.assertEqual(json.loads(request), expected) 36 | self.assertEqual(result, 'Hello Server') 37 | finally: 38 | client.close_connection() 39 | 40 | def test_client_http_invalid_answer(self): 41 | server = FakeServer('localhost', 5500) 42 | server.add_reply('{"error": null, "result": "Hello Server", "id": 1}') 43 | server.run() 44 | 45 | client = RpcClient('localhost', 5500) 46 | client.enable_http() 47 | 48 | try: 49 | with self.assertRaises(HttpException) as cm: 50 | client.rpc_call('echo', 'Hello Server') 51 | 52 | self.assertEqual(cm.exception.message, "Received invalid HTTP response: Couldn't find a HTTP header") 53 | server.stop() 54 | finally: 55 | client.close_connection() 56 | 57 | 58 | if __name__ == '__main__': 59 | unittest.main() 60 | -------------------------------------------------------------------------------- /tests/rpcgencode-tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import unicode_literals 4 | from builtins import bytes, dict, list, int, float, str 5 | 6 | import errno 7 | import sys 8 | import json 9 | import os 10 | import os.path 11 | import shutil 12 | import subprocess 13 | import tempfile 14 | import unittest 15 | 16 | sys.path.append('..') 17 | 18 | from reflectrpc.testing import ServerRunner 19 | 20 | class RpcGenCodeTests(unittest.TestCase): 21 | def test_basic_operation(self): 22 | server = ServerRunner('../examples/server.py', 5500) 23 | server.run() 24 | 25 | python = sys.executable 26 | cwd = os.getcwd() 27 | 28 | try: 29 | dirname = tempfile.mkdtemp() 30 | packagedir = os.path.join(dirname, 'example') 31 | os.mkdir(packagedir) 32 | filename = os.path.join(packagedir, '__init__.py') 33 | status = os.system('%s ../rpcgencode localhost 5500 %s' % (python, filename)) 34 | 35 | if status != 0: 36 | self.fail('rpcgencode returned with a non-zero status code') 37 | 38 | if not os.path.exists(filename): 39 | self.fail("File '%s' was not created by rpcgencode" % (filename)) 40 | 41 | status = os.system('%s -m py_compile %s' % (python, filename)) 42 | 43 | if status != 0: 44 | self.fail("Syntax error in file '%s'" % (filename)) 45 | 46 | os.chdir(dirname) 47 | 48 | cmd = "%s -c 'import sys; sys.path.append(\"%s/..\"); import example; c = example.ServiceClient(); print(c.echo(\"Hello Server\"))'" % (python, cwd) 49 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True) 50 | (out, status) = proc.communicate() 51 | 52 | self.assertEqual('Hello Server\n', out.decode('utf-8')) 53 | finally: 54 | server.stop() 55 | shutil.rmtree(dirname) 56 | os.chdir(cwd) 57 | 58 | 59 | if __name__ == '__main__': 60 | unittest.main() 61 | -------------------------------------------------------------------------------- /examples/certs/scripts/gen_client_cert.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import OpenSSL 4 | import OpenSSL.crypto 5 | 6 | def gen_cert_request(cname): 7 | pkey = OpenSSL.crypto.PKey() 8 | pkey.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) 9 | 10 | req = OpenSSL.crypto.X509Req() 11 | req.get_subject().CN = cname 12 | req.set_pubkey(pkey) 13 | req.sign(pkey, 'sha512') 14 | 15 | req_file = OpenSSL.crypto.dump_certificate_request(OpenSSL.crypto.FILETYPE_PEM, req) 16 | key_file = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, pkey) 17 | 18 | return req_file, key_file 19 | 20 | def sign_cert_request(ca_cert, ca_key, cert_request, serial): 21 | req = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, 22 | cert_request) 23 | 24 | cert = OpenSSL.crypto.X509() 25 | cert.set_subject(req.get_subject()) 26 | cert.set_serial_number(1) 27 | cert.gmtime_adj_notBefore(0) 28 | cert.gmtime_adj_notAfter(10*365*24*60*60) 29 | cert.set_issuer(ca_cert.get_subject()) 30 | cert.set_pubkey(req.get_pubkey()) 31 | cert.sign(ca_key, "sha256") 32 | 33 | cert_file = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, 34 | cert) 35 | 36 | return cert_file 37 | 38 | req, private_key = gen_cert_request("example-username") 39 | 40 | with open('rootCA.crt', 'rb') as f: 41 | ca_cert_content=f.read() 42 | 43 | ca_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, 44 | ca_cert_content) 45 | 46 | with open('rootCA.key', 'rb') as f: 47 | ca_key_content=f.read() 48 | 49 | ca_key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, 50 | ca_key_content) 51 | 52 | cert = sign_cert_request(ca_cert, ca_key, req, 1) 53 | 54 | # write the cert and key to disk 55 | with open('client.crt', 'wb') as f: 56 | f.write(cert) 57 | 58 | with open('client.key', 'wb') as f: 59 | f.write(private_key) 60 | -------------------------------------------------------------------------------- /examples/concurrency.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | 5 | import twisted.internet.defer as defer 6 | from twisted.internet import task 7 | from twisted.internet import reactor 8 | 9 | sys.path.append('..') 10 | 11 | from reflectrpc import RpcFunction 12 | from reflectrpc import RpcProcessor 13 | from reflectrpc import JsonRpcError 14 | import reflectrpc.twistedserver 15 | 16 | def slow_operation(): 17 | def calc_value(value): 18 | return 42 19 | 20 | return task.deferLater(reactor, 1, calc_value, None) 21 | 22 | def fast_operation(): 23 | return 41 24 | 25 | def deferred_error(): 26 | def calc_result(value): 27 | raise JsonRpcError("You wanted an error, here you have it!") 28 | 29 | return task.deferLater(reactor, 0.1, calc_result, None) 30 | 31 | def deferred_internal_error(): 32 | def calc_result(value): 33 | return 56 / 0 34 | 35 | return task.deferLater(reactor, 0.1, calc_result, None) 36 | 37 | jsonrpc = RpcProcessor() 38 | jsonrpc.set_description("Concurrency Example RPC Service", 39 | "This service demonstrates concurrency with the Twisted Server", "1.0") 40 | 41 | slow_func = reflectrpc.RpcFunction(slow_operation, 'slow_operation', 'Calculate ultimate answer', 42 | 'int', 'Ultimate answer') 43 | jsonrpc.add_function(slow_func) 44 | 45 | fast_func = reflectrpc.RpcFunction(fast_operation, 'fast_operation', 46 | 'Calculate fast approximation of the ultimate answer', 47 | 'int', 'Approximation of the ultimate answer') 48 | jsonrpc.add_function(fast_func) 49 | 50 | error_func = reflectrpc.RpcFunction(deferred_error, 'deferred_error', 'Raise a JsonRpcError from a deferred function', 51 | 'int', 'Nothing of interest') 52 | jsonrpc.add_function(error_func) 53 | 54 | internal_error_func = reflectrpc.RpcFunction(deferred_internal_error, 'deferred_internal_error', 55 | 'Raise an internal error from adeferred function', 'int', 'Nothing of interest') 56 | jsonrpc.add_function(internal_error_func) 57 | 58 | server = reflectrpc.twistedserver.TwistedJsonRpcServer(jsonrpc, 'localhost', 5500) 59 | server.run() 60 | -------------------------------------------------------------------------------- /examples/concurrency-http.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | 5 | import twisted.internet.defer as defer 6 | from twisted.internet import task 7 | from twisted.internet import reactor 8 | 9 | sys.path.append('..') 10 | 11 | from reflectrpc import RpcFunction 12 | from reflectrpc import RpcProcessor 13 | from reflectrpc import JsonRpcError 14 | import reflectrpc.twistedserver 15 | 16 | def slow_operation(): 17 | def calc_value(value): 18 | return 42 19 | 20 | return task.deferLater(reactor, 1, calc_value, None) 21 | 22 | def fast_operation(): 23 | return 41 24 | 25 | def deferred_error(): 26 | def calc_result(value): 27 | raise JsonRpcError("You wanted an error, here you have it!") 28 | 29 | return task.deferLater(reactor, 0.1, calc_result, None) 30 | 31 | def deferred_internal_error(): 32 | def calc_result(value): 33 | return 56 / 0 34 | 35 | return task.deferLater(reactor, 0.1, calc_result, None) 36 | 37 | jsonrpc = RpcProcessor() 38 | jsonrpc.set_description("Concurrency Example RPC Service", 39 | "This service demonstrates concurrency with the Twisted Server", "1.0") 40 | 41 | slow_func = reflectrpc.RpcFunction(slow_operation, 'slow_operation', 'Calculate ultimate answer', 42 | 'int', 'Ultimate answer') 43 | jsonrpc.add_function(slow_func) 44 | 45 | fast_func = reflectrpc.RpcFunction(fast_operation, 'fast_operation', 46 | 'Calculate fast approximation of the ultimate answer', 47 | 'int', 'Approximation of the ultimate answer') 48 | jsonrpc.add_function(fast_func) 49 | 50 | error_func = reflectrpc.RpcFunction(deferred_error, 'deferred_error', 'Raise a JsonRpcError from a deferred function', 51 | 'int', 'Nothing of interest') 52 | jsonrpc.add_function(error_func) 53 | 54 | internal_error_func = reflectrpc.RpcFunction(deferred_internal_error, 'deferred_internal_error', 55 | 'Raise an internal error from adeferred function', 'int', 'Nothing of interest') 56 | jsonrpc.add_function(internal_error_func) 57 | 58 | server = reflectrpc.twistedserver.TwistedJsonRpcServer(jsonrpc, 'localhost', 5500) 59 | server.enable_http() 60 | server.run() 61 | -------------------------------------------------------------------------------- /reflectrpc/simpleserver.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from __future__ import unicode_literals 3 | from builtins import bytes, dict, list, int, float, str 4 | 5 | import os 6 | import sys 7 | import json 8 | import socket 9 | 10 | import reflectrpc.server 11 | 12 | if sys.version_info.major == 2: 13 | class ConnectionResetError(Exception): 14 | pass 15 | 16 | class JsonRpcServer(reflectrpc.server.AbstractJsonRpcServer): 17 | """ 18 | Blocking socket implementation of AbstractJsonRpcServer 19 | """ 20 | def send_data(self, data): 21 | self.conn.sendall(data) 22 | 23 | class SimpleJsonRpcServer(object): 24 | """ 25 | Simple JSON-RPC server for line-terminated messages 26 | 27 | Not a production quality server, handles only one connection at a time. 28 | """ 29 | def __init__(self, rpcprocessor, host, port): 30 | """ 31 | Constructor 32 | 33 | Args: 34 | rpcprocessor (RpcProcessor): RPC implementation 35 | host (str): Hostname or IP to listen on 36 | port (int): TCP port to listen on 37 | """ 38 | self.rpcprocessor = rpcprocessor 39 | self.host = host 40 | self.port = port 41 | 42 | def run(self): 43 | """ 44 | Start the server and listen on host:port 45 | """ 46 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 47 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 48 | 49 | try: 50 | self.socket.bind((self.host, self.port)) 51 | except OSError as e: 52 | print("ERROR: " + e.strerror, file=sys.stderr) 53 | sys.exit(1) 54 | 55 | self.socket.listen(10) 56 | print("Listening on %s:%d" % (self.host, self.port)) 57 | 58 | while 1: 59 | conn, addr = self.socket.accept() 60 | self.server = JsonRpcServer(self.rpcprocessor, conn) 61 | 62 | try: 63 | self.__handle_connection(conn) 64 | except ConnectionResetError: 65 | pass 66 | except UnicodeDecodeError as e: 67 | print(e) 68 | conn.close() 69 | 70 | def __handle_connection(self, conn): 71 | """ 72 | Serve a single client connection 73 | """ 74 | data = conn.recv(4096) 75 | 76 | while data: 77 | self.server.data_received(data) 78 | data = conn.recv(4096) 79 | -------------------------------------------------------------------------------- /webui/templates/app.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block content %} 3 | 45 | 46 | Connected to: 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
Host:{{host}}
Port{{port}}
Over HTTP:{{http}}
61 | 62 |
63 | 64 | {% for func in functions %} 65 |
66 | {{func['name_with_params']}}
67 |
68 |

{{func['description']}}

69 |

Params:

70 | 71 | {% for param in func['params'] %} 72 | 73 | 74 | 75 | {% if param['control'] == 'textarea' %} 76 | 77 | {% else %} 78 | 79 | {% endif %} 80 | 81 | 82 | {% endfor %} 83 |
{{param['name']}}{{param['type']}}{{param['description']}}
84 |

Returns: {{func['result_type']}} - {{func['result_desc']}}

85 | 86 |

87 |
88 |

89 |

90 |
91 |
92 | {% endfor %} 93 | 94 | {% endblock %} 95 | -------------------------------------------------------------------------------- /examples/certs/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICnTCCAYWgAwIBAwIBATANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARNeUNB 3 | MB4XDTE2MDQxNDIwMTA1M1oXDTI2MDQxMjIwMTA1M1owFTETMBEGA1UEAwwKcmVm 4 | bGVjdHJwYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKZCVakdMTRN 5 | 9fkDDzpIKhe9CxZeaGCQ5apgJ2pNrRe6GYWenNtLFCHhQtjYGfziKTn4iqxTg6qD 6 | 1dPWlghwk+bzAMEaJxw93QsyvHsUYHx5HTFTtoX4AmapRWaf9pvnBvNTnHkdh80i 7 | fexGbqsIbLi1PLemB3pKJRIoj11YJj6RKhGcSamVwfRNdUpzLOXab4QJs/ft688z 8 | dBFgmzzVrTRQcopPqHPtory4BfcQaUoLeEXO14fZUW7LVqS2U51ecNxGaiLJRXst 9 | s8/1xy8vyNm4GLyYmhBGNBFe1FCWse5Intdj/iszk+vsBkcBs4loLLVC3oJnNBiO 10 | gTuFa6f9QikCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEASq/v0tJRq2c82a61Bvsv 11 | iJWOB5lJ/m1pE9zpoSfWhO+qc6VeIh1kO2KBRo9ydAgemKrTS2OZI5KMU5zKSHvY 12 | cEiK4YZG6SutA9lLuCsBEF9f1nFtK386rkbRJxKj6eN+ZrWk0mdJuDTJHjBKneUs 13 | T5LacVY2XsHkInMGXnCuvmZSI64oFGx+tva29z+qoL+GljtwbQfcGR792zxp5HuE 14 | PYr4Y2eJvdpC3Z6LfLMsXjvU7+m+Ea/UITJRMJtHjgbb9Uuh3cqdy5OmXNObBnGX 15 | 5omDujc9FBH9/DeAxBUqYPPAbsJ0fE50EFEIsFIhW3+lLUcPuj6Bsp5w2y/hkLQD 16 | 6g== 17 | -----END CERTIFICATE----- 18 | -----BEGIN PRIVATE KEY----- 19 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCmQlWpHTE0TfX5 20 | Aw86SCoXvQsWXmhgkOWqYCdqTa0XuhmFnpzbSxQh4ULY2Bn84ik5+IqsU4Oqg9XT 21 | 1pYIcJPm8wDBGiccPd0LMrx7FGB8eR0xU7aF+AJmqUVmn/ab5wbzU5x5HYfNIn3s 22 | Rm6rCGy4tTy3pgd6SiUSKI9dWCY+kSoRnEmplcH0TXVKcyzl2m+ECbP37evPM3QR 23 | YJs81a00UHKKT6hz7aK8uAX3EGlKC3hFzteH2VFuy1aktlOdXnDcRmoiyUV7LbPP 24 | 9ccvL8jZuBi8mJoQRjQRXtRQlrHuSJ7XY/4rM5Pr7AZHAbOJaCy1Qt6CZzQYjoE7 25 | hWun/UIpAgMBAAECggEAT6m1NcUBEJjSZTBsGXb+hEVWjK9LwAltokdUW4FAkP/g 26 | vr+TVRgSW3F+ADz7psoPCvHmMFAL5KYqzMgjN4QJuj1xfRU07DlQMs9qtGa9HKdD 27 | r6D28hY1wE8XK+c12NnH4MuNTBM0QLxoLdBJsrXkslRU9YIeTyA7xwmcOBPGr29E 28 | zus8V+pIYNWJuwk558Z5AybA0hecGbCAq/84EAHwF3Pn9S2YNMpmwye4usGTqL/S 29 | xJTCKViA0rr9XtMr2LYZ//5YEuejCI/x0KayrmJ8uaQ98dwBvBvuVuAscwPUl3nG 30 | HMkSj9wKLn0VrinfWZXCw/MnvgVoneOMrtUKmBdbAQKBgQDdeLQLnkSfCZkCokLE 31 | pW7o8V4QdagonRec6yLeCZcDpGcurFeRilE3DTCGfPN/SMlZh/MCQAm3DCIfl/kO 32 | gKnZPV+jWy/ofkQ778qrD58GoCgpeDhg1PA5EXkDLkG5zLrPNqVh6Lv8Wi7wWvZT 33 | JJMDoQNI38u9Hof4Ezwi+0iv0QKBgQDALgNlS+k5ZFKAQBvCADWEor+xlth0SWc6 34 | +pxWojbqtFHQy8bWidk9pRLz9C8GdDXpbAeB4ZM5tG45TSNyLeQZLVH2hqS+keV0 35 | SKUKs3jc/wEThyTcieUElIbyBvyVsGFNdoWlqOvDPslL5Y8OpYVH6eOxmr0xp6L1 36 | Vf2V+3Ua2QKBgFliJ7gwrh1JsFlhx3S6F+Mn1wDpm26YyDjqpW3bjPlJVuN9ZvI0 37 | UsbXKeh9cYDDjY/20FruIX2hBfyeR0RVJTeqD3lMii9ZFoziIHednF7+MHdcL9TU 38 | 3AcMSDzCZIBqYlLTCThUx9n3Q855x8SSlEr4puy4de/j7Jhwmuq7ZAChAoGAayM7 39 | 0WUYiF5dgBI9Z1Img+MXazHlSi8B1eeQ8NtOMlqEohp4p3ICIlO81TP0Y2y2AYOw 40 | S8AuC6WDLX7LnAPpff++CenWPken28QD/os/fjTLrM9SxYA6pOsIsDUk626BUGYa 41 | 69fYV+jQ3/cCYe/09bp2rbTOdLg4KP3feZXOG0ECgYEAvfS7c3abGpj4qU3/1xlf 42 | Gox16RngJytBG+VXt3f55J4Oz7eywmdRjPc2S9wRq3+Jtn1s7+ivwVIel0btg0i2 43 | TWtN3/YMYy3GGqwPMNxWejcHYCsSdItdKv/uT2LWrxPHoVkmoeCeG1sXK1OjWQPX 44 | BfgdJHivXJL9lh3Fug0qJmg= 45 | -----END PRIVATE KEY----- 46 | -------------------------------------------------------------------------------- /examples/realworld/jsonstore/jsonstore/parser.py: -------------------------------------------------------------------------------- 1 | import pyparsing as pp 2 | 3 | # 4 | # Parser for jsonstore filter expressions 5 | # 6 | # A filter expression looks something like this: 7 | # 8 | # field1 = 'test' AND (score > 3 OR score = 0) 9 | # 10 | 11 | 12 | # Helper functions to annotate the parse tree with extra information 13 | def setTypeFieldname(toks): 14 | toks['token_type'] = 'fieldname' 15 | return toks 16 | 17 | def setTypeValue(toks): 18 | toks['token_type'] = 'value' 19 | toks[0] = "'" + toks[0] + "'" 20 | return toks 21 | 22 | # The pyparsing grammar of the filter expression syntax 23 | lpar = pp.Literal('(').suppress() 24 | rpar = pp.Literal(')').suppress() 25 | fieldname = pp.Regex('[a-zA-Z_][a-zA-Z_0-9]*').setParseAction(setTypeFieldname) 26 | operator = pp.Or([pp.Word('=<>', max=1), pp.Literal('<='), pp.Literal('>=')]) 27 | number = pp.Word(pp.nums) 28 | string = pp.QuotedString("'", escChar='\\') 29 | real = pp.Word(pp.nums + '.' + pp.nums) 30 | value = pp.Or([number, string, real]).setParseAction(setTypeValue) 31 | conjunction = pp.Or(['AND', 'OR']) 32 | expression = pp.Forward() 33 | comparison = pp.Group(fieldname) + operator + pp.Group(value) | pp.Group(lpar + expression + rpar) 34 | expression << comparison + pp.ZeroOrMore(conjunction + expression) 35 | grammar = pp.Or([pp.LineStart() + pp.LineEnd(), pp.LineStart() + expression + pp.LineEnd()]) 36 | 37 | def filter_exp_to_sql_where(filter_exp): 38 | """ 39 | Parses a filter expression and returns a Postgres WHERE clause to fetch 40 | the data from our JSONB column 41 | """ 42 | filter_exp = filter_exp.strip() 43 | parse_tree = grammar.parseString(filter_exp) 44 | if len(parse_tree) == 0: 45 | return '' 46 | 47 | return parse_tree_to_sql_where(parse_tree) 48 | 49 | def parse_tree_to_sql_where(parse_tree): 50 | """ 51 | Walks a parse tree of a filter expression and generates a Postgres WHERE 52 | clause from it 53 | """ 54 | def next_element(): 55 | if len(parse_tree) > 0: 56 | return parse_tree.pop(0) 57 | 58 | where_clause = '(' 59 | cur = next_element() 60 | 61 | while cur: 62 | if isinstance(cur, str): 63 | where_clause += str(cur) 64 | if len(parse_tree) > 0: 65 | where_clause += ' ' 66 | else: 67 | if 'token_type' in cur and cur['token_type'] in ('fieldname', 'value'): 68 | if cur['token_type'] == 'fieldname': 69 | where_clause += 'data->>\'' + str(cur[0]) + '\'' 70 | else: 71 | where_clause += str(cur[0]) 72 | 73 | if len(parse_tree) > 0: 74 | where_clause += ' ' 75 | else: 76 | where_clause += parse_tree_to_sql_where(cur) 77 | if len(parse_tree) > 0: 78 | where_clause += ' ' 79 | 80 | cur = next_element() 81 | 82 | where_clause += ')' 83 | 84 | return where_clause 85 | -------------------------------------------------------------------------------- /tests/lineserver-tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import unicode_literals 4 | from builtins import bytes, dict, list, int, float, str 5 | 6 | import json 7 | import sys 8 | import unittest 9 | 10 | sys.path.append('..') 11 | 12 | from reflectrpc import RpcProcessor 13 | from reflectrpc import RpcFunction 14 | from reflectrpc.server import AbstractJsonRpcServer 15 | 16 | def echo(msg): 17 | return msg 18 | 19 | class DummyServer(AbstractJsonRpcServer): 20 | def send_data(self, data): 21 | if not hasattr(self, 'responses'): 22 | self.responses = [] 23 | 24 | self.responses.append(data) 25 | 26 | class LineServerTests(unittest.TestCase): 27 | def test_invalid_json(self): 28 | rpc = RpcProcessor() 29 | 30 | echo_func = RpcFunction(echo, 'echo', 'Returns what it was given', 31 | 'string', 'Same value as the first parameter') 32 | echo_func.add_param('string', 'message', 'Message to send back') 33 | 34 | rpc.add_function(echo_func) 35 | server = DummyServer(rpc, None) 36 | 37 | # data with linebreak gets processed immediatelly 38 | server.data_received(b"data\r\n") 39 | self.assertEqual(1, len(server.responses)) 40 | 41 | # without linebreak it doesn't get processed 42 | server.data_received(b"data") 43 | server.data_received(b"data") 44 | server.data_received(b"data") 45 | self.assertEqual(1, len(server.responses)) 46 | 47 | # once the linebreak arrives data gets processed again 48 | server.data_received(b"\r\n") 49 | self.assertEqual(2, len(server.responses)) 50 | 51 | def test_json_messages(self): 52 | rpc = RpcProcessor() 53 | 54 | echo_func = RpcFunction(echo, 'echo', 'Returns what it was given', 55 | 'string', 'Same value as the first parameter') 56 | echo_func.add_param('string', 'message', 'Message to send back') 57 | 58 | rpc.add_function(echo_func) 59 | server = DummyServer(rpc, None) 60 | 61 | # JSON-RPC call with linebreak gets processed immediatelly 62 | server.data_received(b'{"method": "echo", "params": ["Hello Server"], "id": 1}\r\n') 63 | self.assertEqual(1, len(server.responses)) 64 | msgstr = server.responses[0].decode("utf-8") 65 | msg = json.loads(msgstr) 66 | self.assertEqual({"result": "Hello Server", "error": None, "id": 1}, msg) 67 | 68 | # if JSON is received in chunks the request must be processed after the linebreak 69 | server.data_received(b'{"method":') 70 | server.data_received(b' "echo", "params": ["Hello') 71 | server.data_received(b' Server"], "id": 2}\r\n{"method": "echo",') 72 | self.assertEqual(2, len(server.responses)) 73 | msgstr = server.responses[1].decode("utf-8") 74 | msg = json.loads(msgstr) 75 | self.assertEqual({"result": "Hello Server", "error": None, "id": 2}, msg) 76 | 77 | # completing the next message should work too 78 | server.data_received(b' "params": ["Hello Echo"], "id": 3}\r\n') 79 | self.assertEqual(3, len(server.responses)) 80 | msgstr = server.responses[2].decode("utf-8") 81 | msg = json.loads(msgstr) 82 | self.assertEqual({"result": "Hello Echo", "error": None, "id": 3}, msg) 83 | 84 | if __name__ == '__main__': 85 | unittest.main() 86 | -------------------------------------------------------------------------------- /rpcgencode: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import print_function 4 | 5 | from datetime import datetime 6 | import sys 7 | 8 | import reflectrpc 9 | import reflectrpc.cmdline 10 | 11 | parser = reflectrpc.cmdline.build_cmdline_parser("Generate client code from a running ReflectRPC service") 12 | 13 | # Add our program-specific arguments 14 | parser.add_argument("outfile", metavar='OUTFILE', type=str, help="Output file") 15 | 16 | args = parser.parse_args() 17 | client = reflectrpc.cmdline.connect_client(parser, args) 18 | client.enable_auto_reconnect() 19 | 20 | (service_description, functions, custom_types) = reflectrpc.cmdline.fetch_service_metainfo(client) 21 | 22 | f = open(args.outfile, "w") 23 | 24 | f.write('"""\n') 25 | f.write('Auto generated client code for service %s %s\n' % 26 | (service_description['name'], service_description['version'])) 27 | f.write('Description: %s\n' % (service_description['description'])) 28 | f.write('Generated at %s (UTC)\n' % (str(datetime.utcnow().replace(microsecond=0)))) 29 | f.write('Generated by ReflectRPC (rpcgencode) %s\n' % (reflectrpc.version)) 30 | 31 | if args.host.startswith('unix://'): 32 | f.write('Generated from %s\n' % (args.host)) 33 | else: 34 | f.write('Generated from %s:%d\n' % (args.host, args.port)) 35 | 36 | f.write('"""\n\n') 37 | 38 | f.write('from reflectrpc.client import RpcClient\n') 39 | f.write('from reflectrpc.client import RpcError\n\n') 40 | 41 | f.write('class ServiceClient(object):\n') 42 | f.write(' def __init__(self):\n') 43 | if args.host.startswith('unix://'): 44 | f.write(" self.client = RpcClient('%s')\n" % (args.host)) 45 | else: 46 | f.write(" self.client = RpcClient('%s', %d)\n" % (args.host, args.port)) 47 | 48 | # derive the options for RpcClient from the args with which the user called us 49 | if args.http: 50 | f.write(" self.client.enable_http(%s)\n" % (args.http_path)) 51 | 52 | if args.tls: 53 | f.write(" self.client.enable_tls(%s, %s)\n" % (args.ca, args.check_hostname)) 54 | 55 | if args.cert and args.key: 56 | f.write(" self.client.enable_client_auth(%s, %s)\n" % (args.cert, args.key)) 57 | 58 | f.write('\n') 59 | 60 | for i, func_desc in enumerate(functions): 61 | paramlist = [param['name'] for param in func_desc['params']] 62 | methodparams = ['self'] + paramlist 63 | methodparamlist = ', '.join(methodparams) 64 | paramlist = ', '.join(paramlist) 65 | 66 | f.write(' def %s(%s):\n' % (func_desc['name'], methodparamlist)) 67 | f.write(' """\n') 68 | f.write(' %s' % (func_desc['description'])) 69 | f.write('\n') 70 | if len(paramlist): 71 | f.write('\n') 72 | f.write(' Args:\n') 73 | for param in func_desc['params']: 74 | f.write(' %s (%s): %s\n' % (param['name'], 75 | reflectrpc.json2py(param['type']), param['description'])) 76 | f.write('\n') 77 | f.write(' Returns:\n') 78 | f.write(' %s: %s\n\n' % (reflectrpc.json2py(func_desc['result_type']), func_desc['result_desc'])) 79 | f.write(' Raises:\n') 80 | f.write(' RpcError: Error returned by the server\n') 81 | f.write(' """\n') 82 | if len(paramlist): 83 | f.write(" return self.client.rpc_call('%s', %s)" % (func_desc['name'], paramlist)) 84 | else: 85 | f.write(" return self.client.rpc_call('%s')" % (func_desc['name'])) 86 | 87 | if i < len(functions) - 1: 88 | f.write('\n\n') 89 | 90 | f.close() 91 | -------------------------------------------------------------------------------- /examples/rpcexample.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from builtins import bytes, dict, list, int, float, str 3 | 4 | import reflectrpc 5 | 6 | def echo(message): 7 | return message 8 | 9 | def add(a, b): 10 | return int(a) + int(b) 11 | 12 | def sub(a, b): 13 | return int(a) - int(b) 14 | 15 | def mul(a, b): 16 | return int(a) * int(b) 17 | 18 | def div(a, b): 19 | return float(a) / float(b) 20 | 21 | def notify(value): 22 | print("Notify: %s" % (value)) 23 | 24 | def enum_echo(phone_type): 25 | return phone_type_enum.resolve(phone_type) 26 | 27 | def hash_echo(address_hash): 28 | return address_hash 29 | 30 | def is_authenticated(rpcinfo): 31 | return rpcinfo['authenticated'] 32 | 33 | def get_username(rpcinfo): 34 | return rpcinfo['username'] 35 | 36 | def echo_ints(ints): 37 | return ints 38 | 39 | def build_example_rpcservice(): 40 | phone_type_enum = reflectrpc.JsonEnumType('PhoneType', 'Type of a phone number') 41 | phone_type_enum.add_value('HOME', 'Home phone') 42 | phone_type_enum.add_value('WORK', 'Work phone') 43 | phone_type_enum.add_value('MOBILE', 'Mobile phone') 44 | phone_type_enum.add_value('FAX', 'FAX number') 45 | 46 | address_hash = reflectrpc.JsonHashType('Address', 'Street address') 47 | address_hash.add_field('firstname', 'string', 'First name') 48 | address_hash.add_field('lastname', 'string', 'Last name') 49 | address_hash.add_field('street1', 'string', 'First address line') 50 | address_hash.add_field('street2', 'string', 'Second address line') 51 | address_hash.add_field('zipcode', 'string', 'Zip code') 52 | address_hash.add_field('city', 'string', 'City') 53 | 54 | jsonrpc = reflectrpc.RpcProcessor() 55 | jsonrpc.set_description("Example RPC Service", 56 | "This is an example service for ReflectRPC", "1.0") 57 | 58 | # register types 59 | jsonrpc.add_custom_type(phone_type_enum) 60 | jsonrpc.add_custom_type(address_hash) 61 | 62 | echo_func = reflectrpc.RpcFunction(echo, 'echo', 'Returns the message it was sent', 63 | 'string', 'The message previously received') 64 | echo_func.add_param('string', 'message', 'The message we will send back') 65 | jsonrpc.add_function(echo_func) 66 | 67 | add_func = reflectrpc.RpcFunction(add, 'add', 'Adds two numbers', 'int', 68 | 'Sum of the two numbers') 69 | add_func.add_param('int', 'a', 'First number to add') 70 | add_func.add_param('int', 'b', 'Second number to add') 71 | jsonrpc.add_function(add_func) 72 | 73 | sub_func = reflectrpc.RpcFunction(sub, 'sub', 'Subtracts one number from another', 'int', 74 | 'Difference of the two numbers') 75 | sub_func.add_param('int', 'a', 'Number to subtract from') 76 | sub_func.add_param('int', 'b', 'Number to subtract') 77 | jsonrpc.add_function(sub_func) 78 | 79 | mul_func = reflectrpc.RpcFunction(mul, 'mul', 'Multiplies two numbers', 'int', 80 | 'Product of the two numbers') 81 | mul_func.add_param('int', 'a', 'First factor') 82 | mul_func.add_param('int', 'b', 'Second factor') 83 | jsonrpc.add_function(mul_func) 84 | 85 | div_func = reflectrpc.RpcFunction(div, 'div', 'Divide a number by another number', 86 | 'float', 'Ratio of the two numbers') 87 | div_func.add_param('float', 'a', 'Dividend') 88 | div_func.add_param('float', 'b', 'Divisor') 89 | jsonrpc.add_function(div_func) 90 | 91 | enum_echo_func = reflectrpc.RpcFunction(enum_echo, 'enum_echo', 'Test the phone type enum', 92 | 'int', 'Phone type') 93 | enum_echo_func.add_param('PhoneType', 'phone_type', 'Type of phone number') 94 | jsonrpc.add_function(enum_echo_func) 95 | 96 | hash_echo_func = reflectrpc.RpcFunction(hash_echo, 'hash_echo', 'Test the address hash type', 97 | 'hash', 'Address hash') 98 | hash_echo_func.add_param('Address', 'address', 'Address hash') 99 | jsonrpc.add_function(hash_echo_func) 100 | 101 | notify_func = reflectrpc.RpcFunction(notify, 'notify', 'Test function for notify requests', 102 | 'bool', '') 103 | notify_func.add_param('string', 'value', 'A value to print on the server side') 104 | jsonrpc.add_function(notify_func) 105 | 106 | authenticated_func = reflectrpc.RpcFunction(is_authenticated, 'is_authenticated', 107 | 'Checks if we have an authenticated connection', 108 | 'bool', 'The authentication status') 109 | authenticated_func.require_rpcinfo() 110 | jsonrpc.add_function(authenticated_func) 111 | 112 | username_func = reflectrpc.RpcFunction(get_username, 'get_username', 113 | 'Gets the username of the logged in user', 114 | 'string', 'The username of the logged in user') 115 | username_func.require_rpcinfo() 116 | jsonrpc.add_function(username_func) 117 | 118 | ints_func = reflectrpc.RpcFunction(echo_ints, 'echo_ints', 'Expects an array of ints and returns it', 119 | 'array', 'An array of integers') 120 | ints_func.add_param('array', 'ints', 'An array of ints') 121 | jsonrpc.add_function(ints_func) 122 | 123 | return jsonrpc 124 | -------------------------------------------------------------------------------- /webui/webui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | from flask import Flask, Response, render_template, request, session 6 | 7 | sys.path.append('..') 8 | 9 | import reflectrpc 10 | from reflectrpc.client import RpcClient 11 | from reflectrpc.client import RpcError 12 | from reflectrpc.client import NetworkError 13 | from reflectrpc.client import HttpException 14 | 15 | app = Flask(__name__) 16 | 17 | # not secure but since this application is supposed to run on your local PC 18 | # and not in a production environment we don't care for the moment 19 | app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT' 20 | 21 | def connect_client(host, port, http, username, password): 22 | client = RpcClient(host, port) 23 | if http: 24 | client.enable_http() 25 | if username and password: 26 | client.enable_http_basic_auth(username, password) 27 | 28 | return client 29 | 30 | @app.route('/', methods=['GET', 'POST']) 31 | def index_page(): 32 | if request.method == 'POST': 33 | host = '' 34 | port = 0 35 | http = False 36 | username = '' 37 | password = '' 38 | 39 | if 'host' in request.form: 40 | host = request.form['host'] 41 | if 'port' in request.form: 42 | port = int(request.form['port']) 43 | if 'http' in request.form: 44 | http = True 45 | if 'username' in request.form: 46 | username = request.form['username'] 47 | if 'password' in request.form: 48 | password = request.form['password'] 49 | 50 | session['host'] = host 51 | session['port'] = port 52 | session['http'] = http 53 | session['username'] = username 54 | session['password'] = password 55 | 56 | http_label = 'No' 57 | if http: 58 | http_label = 'Yes' 59 | 60 | service_description = '' 61 | functions = [] 62 | custom_types = [] 63 | 64 | try: 65 | client = connect_client(host, port, http, username, password) 66 | 67 | try: 68 | service_description = client.rpc_call('__describe_service') 69 | except RpcError: 70 | print("Call to '__describe_service' failed", file=sys.stderr) 71 | 72 | try: 73 | functions = client.rpc_call('__describe_functions') 74 | except RpcError: 75 | print("Call to '__describe_functions' failed", file=sys.stderr) 76 | 77 | try: 78 | custom_types = client.rpc_call('__describe_custom_types') 79 | except RpcError: 80 | print("Call to '__describe_custom_types' failed", file=sys.stderr) 81 | except NetworkError as e: 82 | return render_template('login.html', error=str(e)) 83 | except HttpException as e: 84 | if e.status == '401': 85 | return render_template('login.html', error='Authentication failed') 86 | else: 87 | return render_template('login.html', error=str(e)) 88 | finally: 89 | client.close_connection() 90 | 91 | for func in functions: 92 | func['name_with_params'] = func['name'] + '(' 93 | first = True 94 | for param in func['params']: 95 | if not first: 96 | func['name_with_params'] += ', ' 97 | func['name_with_params'] += param['name'] 98 | 99 | if param['type'].startswith('array') or param['type'] in ['base64', 'hash'] or param['type'][0].isupper(): 100 | param['control'] = 'textarea' 101 | else: 102 | param['control'] = 'lineedit' 103 | 104 | first = False 105 | 106 | func['name_with_params'] += ')' 107 | 108 | return render_template('app.html', functions=functions, 109 | service_description=service_description, 110 | custom_types=custom_types, host=host, port=port, http=http_label) 111 | else: 112 | return render_template('login.html') 113 | 114 | @app.route('/call_jsonrpc', methods=['POST']) 115 | def call_json_rpc(): 116 | funcname = request.form.get('funcname', '', type=str) 117 | params = request.form.get('params', '', type=str) 118 | 119 | req_id = 0 120 | if 'req_id' in session: 121 | req_id = int(session['req_id']) 122 | 123 | req_id += 1 124 | session['req_id'] = req_id 125 | 126 | client = None 127 | result = None 128 | 129 | try: 130 | client = connect_client(session['host'], session['port'], session['http'], 131 | session['username'], session['password']) 132 | result = client.rpc_call_raw('{"method": "%s", "params": [%s], "id": %d}' 133 | % (funcname, params, req_id)) 134 | except NetworkError as e: 135 | return Response("Failed to connect to JSON-RPC server", 136 | status=500, mimetype='text/plain') 137 | finally: 138 | client.close_connection() 139 | 140 | return Response(result, mimetype='application/json') 141 | 142 | if __name__ == '__main__': 143 | app.run(debug=True) 144 | -------------------------------------------------------------------------------- /reflectrpc/cmdline.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals, print_function 2 | from builtins import bytes, dict, list, int, float, str 3 | 4 | import argparse 5 | import getpass 6 | import sys 7 | 8 | import reflectrpc 9 | import reflectrpc.client 10 | from reflectrpc.client import RpcClient 11 | from reflectrpc.client import RpcError 12 | 13 | def build_cmdline_parser(description): 14 | """ 15 | Generic command-line parsing for ReflectRPC tools 16 | 17 | Args: 18 | description (str): Description of the program for which we parse 19 | command-line args 20 | 21 | Returns: 22 | argparse.ArgumentParser: Preinitialized parser 23 | """ 24 | parser = argparse.ArgumentParser(description=description) 25 | 26 | parser.add_argument("host", metavar='HOST', type=str, help="Host to connect to") 27 | parser.add_argument("port", metavar='PORT', type=int, help="Port to connect to (omit if HOST is a UNIX domain socket)") 28 | 29 | parser.add_argument('--http', default=False, action='store_true', 30 | help='Use HTTP as transport protocol') 31 | parser.add_argument('--http-path', default='', 32 | help='HTTP path to send RPC calls to (default is /rpc)') 33 | parser.add_argument('--http-basic-user', default='', 34 | help='Username for HTTP basic auth (password will be requested on the terminal)') 35 | 36 | parser.add_argument('-t', '--tls', default=False, action='store_true', 37 | help='Use TLS authentication and encryption on the RPC connection') 38 | parser.add_argument('--check-hostname', default=False, action='store_true', 39 | help='Check server hostname against its TLS certificate') 40 | parser.add_argument('-C', '--ca', default='', 41 | help='Certificate Authority to check the server certificate against') 42 | parser.add_argument('-k', '--key', default='', 43 | help='Key for client authentication') 44 | parser.add_argument('-c', '--cert', default='', 45 | help='Certificate for client authentication') 46 | 47 | # make the PORT argument optional if HOST is a UNIX domain socket 48 | pos = -1 49 | for i, elem in enumerate(sys.argv): 50 | if elem.startswith('unix://'): 51 | pos = i 52 | break 53 | 54 | if pos > -1: 55 | sys.argv.insert(pos + 1, '0') 56 | 57 | return parser 58 | 59 | def connect_client(parser, args): 60 | """ 61 | Create and connect an RpcClient object based on parsed command-line args 62 | 63 | Args: 64 | parser (argparse.Parser): Parser (only used for printing help in case of error) 65 | args (argparse.Namespace): Parsed command-line args 66 | 67 | Returns: 68 | reflectrpc.RpcClient: Connected RpcClient client 69 | """ 70 | client = RpcClient(args.host, args.port) 71 | 72 | if args.http: 73 | if args.http_path: 74 | client.enable_http(args.http_path) 75 | else: 76 | client.enable_http() 77 | 78 | if args.tls: 79 | client.enable_tls(args.ca, args.check_hostname) 80 | 81 | if args.cert or args.key: 82 | if not args.key: 83 | parser.print_help() 84 | print("--cert also requires --key\n") 85 | sys.exit(1) 86 | 87 | if not args.cert: 88 | parser.print_help() 89 | print("--key also requires --cert\n") 90 | sys.exit(1) 91 | 92 | if not args.ca: 93 | parser.print_help() 94 | print("Client auth requires --ca\n") 95 | sys.exit(1) 96 | 97 | client.enable_client_auth(args.cert, args.key) 98 | 99 | if args.http_basic_user: 100 | password = getpass.getpass() 101 | client.enable_http_basic_auth(args.http_basic_user, password) 102 | 103 | return client 104 | 105 | def fetch_service_metainfo(client): 106 | """ 107 | Fetch all metainformation from a service 108 | """ 109 | service_description = '' 110 | functions = [] 111 | custom_types = [] 112 | 113 | try: 114 | try: 115 | service_description = client.rpc_call('__describe_service') 116 | except RpcError: 117 | print("Call to '__describe_service' failed", file=sys.stderr) 118 | 119 | try: 120 | functions = client.rpc_call('__describe_functions') 121 | except RpcError: 122 | print("Call to '__describe_functions' failed", file=sys.stderr) 123 | 124 | try: 125 | custom_types = client.rpc_call('__describe_custom_types') 126 | except RpcError: 127 | print("Call to '__describe_custom_types' failed", file=sys.stderr) 128 | except reflectrpc.client.NetworkError as e: 129 | print(e, file=sys.stderr) 130 | print('', file=sys.stderr) 131 | connection_failed_error(client.host, 132 | client.port, True) 133 | sys.exit(1) 134 | except reflectrpc.client.HttpException as e: 135 | if e.status == '401': 136 | print('Authentication failed\n', file=sys.stderr) 137 | connection_failed_error(client.host, 138 | client.port, True) 139 | 140 | return service_description, functions, custom_types 141 | 142 | def connection_failed_error(host, port, exit=False): 143 | if host.startswith('unix://'): 144 | print("Failed to connect to %s" % (host)) 145 | else: 146 | print("Failed to connect to %s on TCP port %d" % (host, port)) 147 | 148 | if exit: 149 | sys.exit(1) 150 | -------------------------------------------------------------------------------- /rpcdoc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from datetime import datetime 4 | import sys 5 | import argparse 6 | 7 | import reflectrpc 8 | import reflectrpc.cmdline 9 | 10 | def format_type(typename): 11 | if not len(typename): 12 | return typename 13 | 14 | if typename[0].isupper(): 15 | return '%s' % (typename, typename) 16 | elif typename.startswith('array<'): 17 | subtypename = typename[len('array<'):-1] 18 | return 'array<%s>' % (format_type(subtypename)) 19 | 20 | return typename 21 | 22 | parser = reflectrpc.cmdline.build_cmdline_parser("Generate documentation from a running ReflectRPC service") 23 | 24 | # Add our program-specific arguments 25 | parser.add_argument("outfile", metavar='OUTFILE', type=str, help="Output file") 26 | 27 | args = parser.parse_args() 28 | client = reflectrpc.cmdline.connect_client(parser, args) 29 | client.enable_auto_reconnect() 30 | 31 | (service_description, functions, custom_types) = reflectrpc.cmdline.fetch_service_metainfo(client) 32 | 33 | f = open(args.outfile, "w") 34 | 35 | style = """ 36 | 101 | """ 102 | 103 | title = service_description['name'] + ' - ReflectRPC Service Documentation' 104 | 105 | f.write('\n') 106 | f.write('\n\n\n') 107 | f.write('%s\n%s\n\n' % (title, style)) 108 | 109 | f.write('

ReflectRPC Service Documentation

\n') 110 | f.write('') 111 | f.write('\n' % (service_description['name'])) 112 | f.write('\n' % (service_description['version'])) 113 | f.write('\n' % (service_description['description'])) 114 | f.write('\n') 115 | 116 | if args.host.startswith('unix://'): 117 | f.write('\n' % (args.host)) 118 | else: 119 | f.write('\n' % (args.host, args.port)) 120 | 121 | f.write('\n' % (str(datetime.utcnow().replace(microsecond=0)))) 122 | f.write('\n' % (reflectrpc.version)) 123 | f.write('
Service Name:%s
Version:%s
Description:%s
  
Generated from:%s
Generated from:%s:%d
Generated at:%s (UTC)
Generated by:ReflectRPC (rpcdoc) %s
') 124 | 125 | f.write('

Types

\n') 126 | for t in custom_types: 127 | if t['type'] == 'enum': 128 | f.write('\n' % (t['name'])) 129 | f.write('

%s (Enum)

\n' % (t['name'])) 130 | f.write('
%s
\n' % (t['description'])) 131 | f.write('

Enum values:

') 132 | f.write('') 133 | f.write('') 134 | for value in t['values']: 135 | f.write('\n' % (value['intvalue'], value['name'], value['description'])) 136 | f.write('
Int valueString valueDescription
%d%s%s
') 137 | elif t['type'] == 'hash': 138 | f.write('\n' % (t['name'])) 139 | f.write('

%s (Named Hash)

\n' % (t['name'])) 140 | f.write('
%s
\n' % (t['description'])) 141 | f.write('

Fields of this type:

') 142 | f.write('') 143 | f.write('') 144 | for field in t['fields']: 145 | f.write('\n' % (field['name'], format_type(field['type']), field['description'])) 146 | f.write('
NameTypeDescription
%s%s%s
') 147 | else: 148 | f.write('

Unknown class of custom type: %s

\n' % (t['type'])) 149 | 150 | f.write('

Functions

\n') 151 | for func_desc in functions: 152 | paramlist = [param['name'] for param in func_desc['params']] 153 | paramlist = ', '.join(paramlist) 154 | 155 | f.write('\n' % (func_desc['name'])) 156 | f.write("

%s

\n" % (func_desc['name'])) 157 | f.write('
%s(%s) - %s
' % (func_desc['name'], paramlist, func_desc['description'])) 158 | f.write('

Parameters:

') 159 | f.write('') 160 | for param in func_desc['params']: 161 | f.write('\n' % (param['name'], format_type(param['type']), param['description'])) 162 | f.write('
%s:
%s
-%s
') 163 | 164 | f.write('

Returns: %s - %s

\n' % (format_type(func_desc['result_type']), func_desc['result_desc'])) 165 | 166 | f.write('\n') 167 | f.close() 168 | -------------------------------------------------------------------------------- /tests/rpcdoc-tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import unicode_literals 4 | from builtins import bytes, dict, list, int, float, str 5 | 6 | import errno 7 | import sys 8 | import json 9 | import os 10 | import os.path 11 | import pexpect 12 | import shutil 13 | import subprocess 14 | import tempfile 15 | import unittest 16 | 17 | sys.path.append('..') 18 | 19 | from reflectrpc.testing import ServerRunner 20 | 21 | class RpcDocTests(unittest.TestCase): 22 | def test_rpcdoc_basic_operation(self): 23 | try: 24 | server = ServerRunner('../examples/server.py', 5500) 25 | server.run() 26 | 27 | python = sys.executable 28 | 29 | dirname = tempfile.mkdtemp() 30 | filename = os.path.join(dirname, 'doc.html') 31 | status = os.system('%s ../rpcdoc localhost 5500 %s' % (python, filename)) 32 | 33 | if status != 0: 34 | self.fail('rpcdoc returned with a non-zero status code') 35 | 36 | if not os.path.exists(filename): 37 | self.fail("File '%s' was not created by rpcdoc" % (filename)) 38 | 39 | statinfo = os.stat(filename) 40 | self.assertGreater(statinfo.st_size, 0) 41 | finally: 42 | server.stop() 43 | shutil.rmtree(dirname) 44 | 45 | def test_rpcdoc_unix_socket(self): 46 | try: 47 | server = ServerRunner('../examples/serverunixsocket.py', '/tmp/reflectrpc.sock') 48 | server.run() 49 | 50 | python = sys.executable 51 | 52 | dirname = tempfile.mkdtemp() 53 | filename = os.path.join(dirname, 'doc.html') 54 | status = os.system('%s ../rpcdoc unix:///tmp/reflectrpc.sock %s' % (python, filename)) 55 | 56 | if status != 0: 57 | self.fail('rpcdoc returned with a non-zero status code') 58 | 59 | if not os.path.exists(filename): 60 | self.fail("File '%s' was not created by rpcdoc" % (filename)) 61 | 62 | statinfo = os.stat(filename) 63 | self.assertGreater(statinfo.st_size, 0) 64 | finally: 65 | server.stop() 66 | shutil.rmtree(dirname) 67 | 68 | def test_rpcdoc_http(self): 69 | try: 70 | server = ServerRunner('../examples/serverhttp.py', 5500) 71 | server.run() 72 | 73 | python = sys.executable 74 | 75 | dirname = tempfile.mkdtemp() 76 | filename = os.path.join(dirname, 'doc.html') 77 | status = os.system('%s ../rpcdoc localhost 5500 %s --http' % (python, filename)) 78 | 79 | if status != 0: 80 | self.fail('rpcdoc returned with a non-zero status code') 81 | 82 | if not os.path.exists(filename): 83 | self.fail("File '%s' was not created by rpcdoc" % (filename)) 84 | 85 | statinfo = os.stat(filename) 86 | self.assertGreater(statinfo.st_size, 0) 87 | finally: 88 | server.stop() 89 | shutil.rmtree(dirname) 90 | 91 | def test_rpcdoc_http_basic_auth(self): 92 | try: 93 | server = ServerRunner('../examples/serverhttp.py', 5500) 94 | server.run() 95 | 96 | python = sys.executable 97 | 98 | dirname = tempfile.mkdtemp() 99 | filename = os.path.join(dirname, 'doc.html') 100 | child = pexpect.spawn('%s ../rpcdoc localhost 5500 %s --http --http-basic-user testuser' % (python, filename)) 101 | child.expect('Password: ') 102 | child.sendline('123456') 103 | child.read() 104 | child.wait() 105 | 106 | if child.status != 0: 107 | self.fail('rpcdoc returned with a non-zero status code') 108 | 109 | if not os.path.exists(filename): 110 | self.fail("File '%s' was not created by rpcdoc" % (filename)) 111 | 112 | statinfo = os.stat(filename) 113 | self.assertGreater(statinfo.st_size, 0) 114 | finally: 115 | server.stop() 116 | shutil.rmtree(dirname) 117 | 118 | def test_rpcdoc_tls(self): 119 | try: 120 | server = ServerRunner('../examples/servertls.py', 5500) 121 | server.run() 122 | 123 | python = sys.executable 124 | 125 | dirname = tempfile.mkdtemp() 126 | filename = os.path.join(dirname, 'doc.html') 127 | status = os.system('%s ../rpcdoc localhost 5500 %s --tls' % (python, filename)) 128 | 129 | if status != 0: 130 | self.fail('rpcdoc returned with a non-zero status code') 131 | 132 | if not os.path.exists(filename): 133 | self.fail("File '%s' was not created by rpcdoc" % (filename)) 134 | 135 | statinfo = os.stat(filename) 136 | self.assertGreater(statinfo.st_size, 0) 137 | finally: 138 | server.stop() 139 | shutil.rmtree(dirname) 140 | 141 | def test_rpcdoc_tls_client_auth(self): 142 | try: 143 | server = ServerRunner('../examples/servertls_clientauth.py', 5500) 144 | server.run() 145 | 146 | python = sys.executable 147 | 148 | dirname = tempfile.mkdtemp() 149 | filename = os.path.join(dirname, 'doc.html') 150 | status = os.system('%s ../rpcdoc localhost 5500 %s --tls --ca ../examples/certs/rootCA.crt --key ../examples/certs/client.key --cert ../examples/certs/client.crt' % (python, filename)) 151 | 152 | if status != 0: 153 | self.fail('rpcdoc returned with a non-zero status code') 154 | 155 | if not os.path.exists(filename): 156 | self.fail("File '%s' was not created by rpcdoc" % (filename)) 157 | 158 | statinfo = os.stat(filename) 159 | self.assertGreater(statinfo.st_size, 0) 160 | finally: 161 | server.stop() 162 | shutil.rmtree(dirname) 163 | 164 | 165 | if __name__ == '__main__': 166 | unittest.main() 167 | -------------------------------------------------------------------------------- /tests/cmdline-tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import unicode_literals 4 | from builtins import bytes, dict, list, int, float, str 5 | 6 | import os 7 | import sys 8 | import unittest 9 | 10 | import pexpect 11 | 12 | sys.path.append('..') 13 | 14 | from reflectrpc.testing import ServerRunner 15 | 16 | # Generic tests for ReflectRPC command-line utils 17 | class CmdlineTests(unittest.TestCase): 18 | def __init__(self, *args, **kwargs): 19 | super(CmdlineTests, self).__init__(*args, **kwargs) 20 | self.cmdline_programs = ['rpcsh', 'rpcdoc', 'rpcgencode'] 21 | 22 | def test_cmdline_expect_connection_fails(self): 23 | for cmd in self.cmdline_programs: 24 | # connect although no server is running 25 | try: 26 | python = sys.executable 27 | outfile = '' 28 | if cmd != 'rpcsh': 29 | outfile = 'outfile.html' 30 | 31 | child = pexpect.spawn('%s ../%s localhost 5500 %s' % (python, cmd, outfile)) 32 | 33 | child.expect('NetworkError: \[Errno \d+\] Connection refused\r\n') 34 | child.expect('\r\n') 35 | child.expect('Failed to connect to localhost on TCP port 5500\r\n') 36 | finally: 37 | child.close(True) 38 | 39 | # connect to a TLS server without enabling TLS 40 | try: 41 | server = ServerRunner('../examples/servertls.py', 5500) 42 | server.run() 43 | 44 | python = sys.executable 45 | outfile = '' 46 | if cmd != 'rpcsh': 47 | outfile = 'outfile.html' 48 | 49 | child = pexpect.spawn('%s ../%s localhost 5500 %s' % (python, cmd, outfile)) 50 | 51 | child.expect('NetworkError: Non-JSON content received\r\n') 52 | child.expect('\r\n') 53 | child.expect('Failed to connect to localhost on TCP port 5500\r\n') 54 | finally: 55 | child.close(True) 56 | server.stop() 57 | 58 | # connect to a Non-TLS server with enabled TLS 59 | try: 60 | server = ServerRunner('../examples/server.py', 5500) 61 | server.run() 62 | 63 | python = sys.executable 64 | outfile = '' 65 | if cmd != 'rpcsh': 66 | outfile = 'outfile.html' 67 | 68 | child = pexpect.spawn('%s ../%s localhost 5500 %s --tls' % (python, cmd, outfile)) 69 | 70 | child.expect('NetworkError: EOF occurred in violation of protocol \(_ssl.c:\d+\)\r\n') 71 | child.expect('\r\n') 72 | child.expect('Failed to connect to localhost on TCP port 5500\r\n') 73 | finally: 74 | child.close(True) 75 | server.stop() 76 | 77 | # connect to a TLS server but fail the hostname check 78 | try: 79 | server = ServerRunner('../examples/servertls.py', 5500) 80 | server.run() 81 | 82 | python = sys.executable 83 | outfile = '' 84 | if cmd != 'rpcsh': 85 | outfile = 'outfile.html' 86 | 87 | child = pexpect.spawn('%s ../%s localhost 5500 %s --tls --ca ../examples/certs/rootCA.crt --check-hostname' % (python, cmd, outfile)) 88 | 89 | child.expect("NetworkError: TLSHostnameError: Host name 'localhost' doesn't match certificate host 'reflectrpc'\r\n") 90 | child.expect('\r\n') 91 | child.expect('Failed to connect to localhost on TCP port 5500\r\n') 92 | finally: 93 | child.close(True) 94 | server.stop() 95 | 96 | def test_cmdline_expect_wrong_arguments(self): 97 | for cmd in self.cmdline_programs: 98 | # check what happens when rpcsh is called without parameters 99 | try: 100 | python = sys.executable 101 | child = pexpect.spawn('%s ../%s' % (python, cmd)) 102 | 103 | python3_errstr = '%s: error: the following arguments are required: HOST, PORT, OUTFILE' % (cmd) 104 | if cmd == 'rpcsh': 105 | python3_errstr = '%s: error: the following arguments are required: HOST, PORT' % (cmd) 106 | 107 | child.expect('\r\n(%s: error: too few arguments|%s)\r\n' % (cmd, python3_errstr)) 108 | finally: 109 | child.close(True) 110 | 111 | # check that --cert doesn't work without --key 112 | try: 113 | python = sys.executable 114 | outfile = '' 115 | if cmd != 'rpcsh': 116 | outfile = 'outfile.html' 117 | 118 | child = pexpect.spawn('%s ../%s localhost 5500 %s --tls --ca ../examples/certs/rootCA.crt --cert ../examples/certs/rootCA.crt' % (python, cmd, outfile)) 119 | 120 | child.expect('\r\n--cert also requires --key\r\n') 121 | finally: 122 | child.close(True) 123 | 124 | # check that --key doesn't work without --cert 125 | try: 126 | python = sys.executable 127 | outfile = '' 128 | if cmd != 'rpcsh': 129 | outfile = 'outfile.html' 130 | 131 | child = pexpect.spawn('%s ../%s localhost 5500 %s --tls --ca ../examples/certs/rootCA.crt --key ../examples/certs/client.key' % (python, cmd, outfile)) 132 | 133 | child.expect('\r\n--key also requires --cert\r\n') 134 | finally: 135 | child.close(True) 136 | 137 | # check that --cert and --key don't work without --ca 138 | try: 139 | python = sys.executable 140 | outfile = '' 141 | if cmd != 'rpcsh': 142 | outfile = 'outfile.html' 143 | 144 | child = pexpect.spawn('%s ../%s localhost 5500 %s --tls --cert ../examples/certs/rootCA.crt --key ../examples/certs/client.key' % (python, cmd, outfile)) 145 | 146 | child.expect('\r\nClient auth requires --ca\r\n') 147 | finally: 148 | child.close(True) 149 | 150 | if __name__ == '__main__': 151 | unittest.main() 152 | -------------------------------------------------------------------------------- /reflectrpc/testing.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from builtins import bytes, dict, list, int, float, str 3 | 4 | import errno 5 | import os 6 | import os.path 7 | import sys 8 | import signal 9 | import socket 10 | import threading 11 | import time 12 | 13 | if sys.version_info.major == 2: 14 | class ConnectionRefusedError(Exception): 15 | pass 16 | 17 | class PortFreeTimeout(Exception): 18 | def __init__(self, port): 19 | self.port = port 20 | 21 | def __str__(self): 22 | return "PortFreeTimeout: Port %d is not free" % (self.port) 23 | 24 | class PortReadyTimeout(Exception): 25 | def __init__(self, port): 26 | self.port = port 27 | 28 | def __str__(self): 29 | return "PortReadyTimeout: Port %d is not ready for TCP connections" % (self.port) 30 | 31 | def wait_for_unix_socket_gone(socket_path, timeout): 32 | start_time = time.time() 33 | sock = None 34 | while (time.time() - start_time < timeout): 35 | if not os.path.exists(socket_path): 36 | return 37 | 38 | time.sleep(0.5) 39 | 40 | raise PortFreeTimeout(socket_path) 41 | 42 | def wait_for_unix_socket_in_use(socket_path, timeout): 43 | start_time = time.time() 44 | sock = None 45 | while (time.time() - start_time < timeout): 46 | if os.path.exists(socket_path): 47 | return 48 | 49 | time.sleep(0.5) 50 | 51 | raise PortReadyTimeout(socket_path) 52 | 53 | def wait_for_free_port(host, port, timeout): 54 | """ 55 | Waits for a TCP port to become free 56 | 57 | Args: 58 | host (str): TCP host to wait for 59 | port (int): TCP port to wait for 60 | timeout (int): Timeout in seconds until we give up waiting 61 | 62 | Raises: 63 | PortFreeTimeout: If port doesn't become free after timeout seconds 64 | """ 65 | start_time = time.time() 66 | sock = None 67 | while (time.time() - start_time < timeout): 68 | try: 69 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 70 | sock.connect((host, port)) 71 | sock.close() 72 | except (ConnectionRefusedError, socket.error) as e: 73 | if type(e) == socket.error and e.errno != errno.ECONNREFUSED: 74 | raise e 75 | 76 | # success 77 | sock.close() 78 | return 79 | 80 | time.sleep(0.5) 81 | 82 | raise PortFreeTimeout(port) 83 | 84 | def wait_for_tcp_port_in_use(host, port, timeout): 85 | """ 86 | Waits for a TCP port to become ready to accept connections 87 | 88 | Args: 89 | host (str): TCP host to wait for 90 | port (int): TCP port to wait for 91 | timeout (int): Timeout in seconds until we give up waiting 92 | 93 | Raises: 94 | PortReadyTimeout: If the port is not ready after timeout seconds 95 | """ 96 | start_time = time.time() 97 | sock = None 98 | while (time.time() - start_time < timeout): 99 | try: 100 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 101 | sock.connect((host, port)) 102 | sock.close() 103 | return 104 | except (ConnectionRefusedError, socket.error) as e: 105 | if type(e) == socket.error and e.errno != errno.ECONNREFUSED: 106 | raise e 107 | 108 | # still waiting 109 | sock.close() 110 | 111 | raise PortReadyTimeout(port) 112 | 113 | class FakeServer(object): 114 | """ 115 | Runs a TCP server in a thread and replies from a list of pre-defined replies 116 | """ 117 | def __init__(self, host, port): 118 | self.host = host 119 | self.port = port 120 | 121 | self.replies = [] 122 | self.requests = [] 123 | 124 | def add_reply(self, reply): 125 | self.replies.append(reply) 126 | 127 | def run(self): 128 | self.thread = threading.Thread(target = self._run, args = ()) 129 | self.thread.start() 130 | 131 | wait_for_tcp_port_in_use(self.host, self.port, 5) 132 | 133 | def _run(self): 134 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 135 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 136 | 137 | sock.bind((self.host, self.port)) 138 | sock.listen(10) 139 | 140 | while self.replies: 141 | try: 142 | conn, addr = sock.accept() 143 | 144 | request = conn.recv(4096) 145 | if not len(request): 146 | conn.close() 147 | continue 148 | 149 | self.requests.append(request.decode('utf-8')) 150 | 151 | reply = self.replies.pop(0) 152 | conn.sendall(reply.encode('utf-8')) 153 | conn.sendall(b'\r\n') 154 | conn.close() 155 | except ConnectionResetError: 156 | pass 157 | 158 | sock.close() 159 | 160 | def stop(self): 161 | self.thread.join() 162 | if self.thread.is_alive(): 163 | raise RuntimeError("Failed to join on FakeServer thread") 164 | 165 | wait_for_free_port(self.host, self.port, 5) 166 | 167 | class ServerRunner(object): 168 | """ 169 | Runs a server program in a subprocess and allows to stop it again 170 | """ 171 | def __init__(self, path, port): 172 | self.directory = os.path.dirname(path) 173 | self.server_program = os.path.basename(path) 174 | self.host = 'localhost' 175 | self.port = port 176 | self.pid = None 177 | self.timeout = 5 178 | 179 | def run(self): 180 | # we don't fork before we know that the TCP port/UNIX socket is free 181 | if isinstance(self.port, int): 182 | wait_for_free_port(self.host, self.port, self.timeout) 183 | else: 184 | wait_for_unix_socket_gone(self.port, self.timeout) 185 | 186 | pid = os.fork() 187 | 188 | if not pid: 189 | # child 190 | os.chdir(self.directory) 191 | 192 | if self.server_program.endswith('.py'): 193 | python = sys.executable 194 | os.execl(python, python, self.server_program) 195 | else: 196 | os.execl(self.server_program, self.server_program) 197 | else: 198 | # parent 199 | self.pid = pid 200 | 201 | if isinstance(self.port, int): 202 | wait_for_tcp_port_in_use(self.host, self.port, self.timeout) 203 | else: 204 | wait_for_unix_socket_in_use(self.port, self.timeout) 205 | 206 | def stop(self): 207 | os.kill(self.pid, signal.SIGINT) 208 | os.waitpid(self.pid, 0) 209 | 210 | if isinstance(self.port, int): 211 | wait_for_free_port(self.host, self.port, self.timeout) 212 | else: 213 | wait_for_unix_socket_gone(self.port, self.timeout) 214 | -------------------------------------------------------------------------------- /tests/rpcfunction-tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import unicode_literals 4 | from builtins import bytes, dict, list, int, float, str 5 | 6 | import sys 7 | import unittest 8 | 9 | sys.path.append('..') 10 | 11 | from reflectrpc import RpcFunction 12 | 13 | def dummy_function(): 14 | pass 15 | 16 | class RpcFunctionTests(unittest.TestCase): 17 | def test_valid_types_in_constructor(self): 18 | try: 19 | RpcFunction(dummy_function, 'dummy', 20 | 'Dummy function', 'int', 'Return value') 21 | except: 22 | self.fail("Constructor of RpcFunction raised unexpected exception!") 23 | 24 | try: 25 | RpcFunction(dummy_function, 'dummy', 26 | 'Dummy function', 'bool', 'Return value') 27 | except: 28 | self.fail("Constructor of RpcFunction raised unexpected exception!") 29 | 30 | try: 31 | RpcFunction(dummy_function, 'dummy', 32 | 'Dummy function', 'float', 'Return value') 33 | except: 34 | self.fail("Constructor of RpcFunction raised unexpected exception!") 35 | 36 | try: 37 | RpcFunction(dummy_function, 'dummy', 38 | 'Dummy function', 'string', 'Return value') 39 | except: 40 | self.fail("Constructor of RpcFunction raised unexpected exception!") 41 | 42 | try: 43 | RpcFunction(dummy_function, 'dummy', 44 | 'Dummy function', 'array', 'Return value') 45 | except: 46 | self.fail("Constructor of RpcFunction raised unexpected exception!") 47 | 48 | try: 49 | RpcFunction(dummy_function, 'dummy', 50 | 'Dummy function', 'hash', 'Return value') 51 | except: 52 | self.fail("Constructor of RpcFunction raised unexpected exception!") 53 | 54 | try: 55 | RpcFunction(dummy_function, 'dummy', 56 | 'Dummy function', 'base64', 'Return value') 57 | except: 58 | self.fail("Constructor of RpcFunction raised unexpected exception!") 59 | 60 | def test_invalid_type_in_constructor(self): 61 | self.assertRaises(ValueError, RpcFunction, dummy_function, 'dummy', 62 | 'Dummy function', 'noint', 'Return value of invalid type') 63 | 64 | def test_valid_types_in_add_param(self): 65 | dummy_func = RpcFunction(dummy_function, 'dummy', 66 | 'Dummy function', 'int', 'Return value') 67 | 68 | try: 69 | dummy_func.add_param('int', 'a', 'First parameter') 70 | except: 71 | self.fail("add_param raised unexpected exception!") 72 | 73 | try: 74 | dummy_func.add_param('bool', 'a', 'First parameter') 75 | except: 76 | self.fail("add_param raised unexpected exception!") 77 | 78 | try: 79 | dummy_func.add_param('float', 'a', 'First parameter') 80 | except: 81 | self.fail("add_param raised unexpected exception!") 82 | 83 | try: 84 | dummy_func.add_param('string', 'a', 'First parameter') 85 | except: 86 | self.fail("add_param raised unexpected exception!") 87 | 88 | try: 89 | dummy_func.add_param('array', 'a', 'First parameter') 90 | except: 91 | self.fail("add_param raised unexpected exception!") 92 | 93 | try: 94 | dummy_func.add_param('hash', 'a', 'First parameter') 95 | except: 96 | self.fail("add_param raised unexpected exception!") 97 | 98 | try: 99 | dummy_func.add_param('base64', 'a', 'First parameter') 100 | except: 101 | self.fail("add_param raised unexpected exception!") 102 | 103 | def test_invalid_type_in_add_param(self): 104 | dummy_func = RpcFunction(dummy_function, 'dummy', 105 | 'Dummy function', 'int', 'Return value') 106 | self.assertRaises(ValueError, dummy_func.add_param, 'noint', 'a', 'First parameter') 107 | 108 | # Custom type tests 109 | # 110 | # custom types start with a captial letter and their check is postponed to 111 | # the time the function gets registered so they should always be accepted 112 | # when building a RpcFunction object, whether they exist or not 113 | 114 | def test_custom_type_as_result_type(self): 115 | try: 116 | RpcFunction(dummy_function, 'dummy', 117 | 'Dummy function', 'ImaginaryCustomType', 'Return value') 118 | except: 119 | self.fail("RpcFunction constructor raised an unexpected exception!") 120 | 121 | def test_custom_type_in_add_param(self): 122 | dummy_func = RpcFunction(dummy_function, 'dummy', 123 | 'Dummy function', 'int', 'Return value') 124 | try: 125 | dummy_func.add_param('ImaginaryCustomType', 'a', 'First parameter') 126 | except: 127 | self.fail("add_param raised an unexpected exception!") 128 | 129 | # Typed array tests 130 | def test_typed_array_as_result_type(self): 131 | try: 132 | RpcFunction(dummy_function, 'dummy', 'Dummy function', 133 | 'array', 'Typed array of integers') 134 | except: 135 | self.fail('RpcFunction constructor raised an unexpected exception') 136 | 137 | def test_typed_array_with_custom_type_as_result_type(self): 138 | try: 139 | RpcFunction(dummy_function, 'dummy', 'Dummy function', 140 | 'array', 'Returns the array passed by the caller') 141 | except: 142 | self.fail('RpcFunction constructor raised an unexpected exception') 143 | 144 | def test_invalid_typed_array_as_result_type(self): 145 | self.assertRaises(ValueError, RpcFunction, dummy_function, 'dummy', 146 | 'Dummy function', 'array', 'Return value') 147 | 148 | def test_typed_array_in_add_param(self): 149 | dummy_func = RpcFunction(dummy_function, 'dummy', 150 | 'Dummy function', 'int', 'Return value') 151 | try: 152 | dummy_func.add_param('array', 'a', 'Typed array of integers') 153 | except: 154 | self.fail("add_param raised an unexpected exception!") 155 | 156 | def test_invalid_typed_array_in_add_param(self): 157 | dummy_func = RpcFunction(dummy_function, 'dummy', 158 | 'Dummy function', 'int', 'Return value') 159 | self.assertRaises(ValueError, dummy_func.add_param, 'array', 'a', 'First parameter') 160 | 161 | def test_typed_array_with_custom_type_in_add_param(self): 162 | dummy_func = RpcFunction(dummy_function, 'dummy', 163 | 'Dummy function', 'int', 'Return value') 164 | try: 165 | dummy_func.add_param('array', 'a', 'First parameter') 166 | except: 167 | self.fail("add_param raised an unexpected exception!") 168 | 169 | if __name__ == '__main__': 170 | unittest.main() 171 | -------------------------------------------------------------------------------- /examples/realworld/linux-sys-info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import re 4 | 5 | from twisted.internet import threads, reactor, defer 6 | 7 | import reflectrpc 8 | from reflectrpc.twistedserver import TwistedJsonRpcServer 9 | 10 | # 11 | # This example RPC service shows how to use ReflectRPC to create a production 12 | # service using Twisted for concurrency 13 | # 14 | # The service runs only on Linux and provides access to system level 15 | # information in the /proc filesystem. 16 | # 17 | 18 | def get_cpuinfo(): 19 | # callback function for blocking file IO 20 | def read_cpuinfo_file(): 21 | data = '' 22 | with open('/proc/cpuinfo', 'r') as f: 23 | data=f.read() 24 | 25 | return data 26 | 27 | # callback function for parsing the file data once it has been read and for 28 | # wrapping it into a CPUInfo structure that can be sent to the client 29 | def parse_data(data): 30 | cpuinfo = {} 31 | cpuinfo['numCPUs'] = 0 32 | 33 | data = data.rstrip() 34 | cpus = data.split('\n\n') 35 | 36 | cpuinfo['numCPUs'] = len(cpus) 37 | cpuinfo['cpus'] = [] 38 | 39 | for entry in cpus: 40 | cpu = {} 41 | 42 | m = re.search(r'^processor\s*:\s+(\d+)$', data, re.MULTILINE) 43 | if m: 44 | cpu['processor'] = int(m.group(1)) 45 | 46 | m = re.search(r'^vendor_id\s*:\s+(\w.+)$', data, re.MULTILINE) 47 | if m: 48 | cpu['vendor_id'] = m.group(1) 49 | 50 | m = re.search(r'^cpu family\s*:\s+(\d+)$', data, re.MULTILINE) 51 | if m: 52 | cpu['cpu_family'] = int(m.group(1)) 53 | 54 | m = re.search(r'^model\s*:\s+(\d+)$', data, re.MULTILINE) 55 | if m: 56 | cpu['model'] = int(m.group(1)) 57 | 58 | m = re.search(r'^model name\s*:\s+(\w.+)$', data, re.MULTILINE) 59 | if m: 60 | cpu['model_name'] = m.group(1) 61 | 62 | m = re.search(r'^stepping\s*:\s+(\d+)$', data, re.MULTILINE) 63 | if m: 64 | cpu['stepping'] = int(m.group(1)) 65 | 66 | m = re.search(r'^cpu MHz\s*:\s+(\d+(\.\d+)?)$', data, re.MULTILINE) 67 | if m: 68 | cpu['cpu_mhz'] = float(m.group(1)) 69 | 70 | m = re.search(r'^cache size\s*:\s+(\d+)\s+KB$', data, re.MULTILINE) 71 | if m: 72 | cpu['cache_size'] = int(m.group(1)) 73 | 74 | m = re.search(r'^physical id\s*:\s+(\d+)$', data, re.MULTILINE) 75 | if m: 76 | cpu['physical_id'] = int(m.group(1)) 77 | 78 | m = re.search(r'^core id\s*:\s+(\d+)$', data, re.MULTILINE) 79 | if m: 80 | cpu['core_id'] = int(m.group(1)) 81 | 82 | m = re.search(r'^cpu cores\s*:\s+(\d+)$', data, re.MULTILINE) 83 | if m: 84 | cpu['cpu_cores'] = int(m.group(1)) 85 | 86 | m = re.search(r'^fpu\s*:\s+(\w+)$', data, re.MULTILINE) 87 | if m: 88 | cpu['fpu'] = False 89 | if m.group(1) == 'yes': 90 | cpu['fpu'] = True 91 | 92 | m = re.search(r'^flags\s*:\s+(\w.+)$', data, re.MULTILINE) 93 | if m: 94 | cpu['flags'] = m.group(1).split(' ') 95 | 96 | m = re.search(r'^bogomips\s*:\s+(\d+(\.\d+)?)$', data, re.MULTILINE) 97 | if m: 98 | cpu['bogomips'] = float(m.group(1)) 99 | 100 | cpuinfo['cpus'].append(cpu) 101 | 102 | return cpuinfo 103 | 104 | # read the /proc file in a thread and return a deferred 105 | d = threads.deferToThread(read_cpuinfo_file) 106 | # setup the callback to parse the file data once it is ready 107 | d.addCallback(parse_data) 108 | 109 | return d 110 | 111 | def get_meminfo(): 112 | # callback function for blocking file IO 113 | def read_meminfo_file(): 114 | data = '' 115 | with open('/proc/meminfo', 'r') as f: 116 | data=f.read() 117 | 118 | return data 119 | 120 | # callback function for parsing the file data once it has been read and for 121 | # wrapping it into a MemInfo structure that can be sent to the client 122 | def parse_data(data): 123 | result = {} 124 | result['memTotal'] = 0 125 | result['memFree'] = 0 126 | result['memAvailable'] = 0 127 | result['cached'] = 0 128 | result['swapTotal'] = 0 129 | result['swapFree'] = 0 130 | 131 | m = re.search(r'^MemTotal:\s+(\d+)\s+kB$', data, re.MULTILINE) 132 | if m: 133 | result['memTotal'] = int(m.group(1)) 134 | 135 | m = re.search(r'^MemFree:\s+(\d+)\s+kB$', data, re.MULTILINE) 136 | if m: 137 | result['memFree'] = int(m.group(1)) 138 | 139 | m = re.search(r'^MemAvailable:\s+(\d+)\s+kB$', data, re.MULTILINE) 140 | if m: 141 | result['memAvailable'] = int(m.group(1)) 142 | 143 | m = re.search(r'^Cached:\s+(\d+)\s+kB$', data, re.MULTILINE) 144 | if m: 145 | result['cached'] = int(m.group(1)) 146 | 147 | m = re.search(r'^SwapTotal:\s+(\d+)\s+kB$', data, re.MULTILINE) 148 | if m: 149 | result['swapTotal'] = int(m.group(1)) 150 | 151 | m = re.search(r'^SwapFree:\s+(\d+)\s+kB$', data, re.MULTILINE) 152 | if m: 153 | result['swapFree'] = int(m.group(1)) 154 | 155 | return result 156 | 157 | # read the /proc file in a thread and return a deferred 158 | d = threads.deferToThread(read_meminfo_file) 159 | # setup the callback to parse the file data once it is ready 160 | d.addCallback(parse_data) 161 | 162 | return d 163 | 164 | # create custom types 165 | cpuInfo = reflectrpc.JsonHashType('CPUInfo', 'Information about CPUs') 166 | cpuInfo.add_field('numCPUs', 'int', 'Number of CPUs') 167 | cpuInfo.add_field('cpus', 'array', 'Array of the CPUs available on the system') 168 | 169 | cpu = reflectrpc.JsonHashType('CPU', 'Information about a single CPU') 170 | cpu.add_field('processor', 'int', 'Number of this CPU (e.g. 0 for the first CPU in this system)') 171 | cpu.add_field('vendor_id', 'string', 'Vendor name') 172 | cpu.add_field('cpu_family', 'int', 'CPU family ID') 173 | cpu.add_field('model', 'int', 'Model identifier') 174 | cpu.add_field('model_name', 'string', 'Model name of this CPU') 175 | cpu.add_field('stepping', 'int', 'Stepping of the CPU (which basically is the CPUs revision)') 176 | cpu.add_field('cpu_mhz', 'float', 'Clock rate of this CPU in MHz') 177 | cpu.add_field('cache_size', 'int', 'Size of the L2 cache of this CPU in KB') 178 | cpu.add_field('physical_id', 'int', 'ID of the physical processor this CPU belongs to') 179 | cpu.add_field('core_id', 'int', 'ID of CPU core this CPU entry represents') 180 | cpu.add_field('cpu_cores', 'int', 'Number of CPU cores') 181 | cpu.add_field('fpu', 'bool', 'Does this CPU have a floating point unit?') 182 | cpu.add_field('flags', 'array', 'CPU flags describing features supported by this CPU') 183 | cpu.add_field('bogomips', 'float', 'Bogus number to indicate the speed of this CPU') 184 | 185 | memInfo = reflectrpc.JsonHashType('MemInfo', 'Information about system memory') 186 | memInfo.add_field('memTotal', 'int', 'Total system memory in kB') 187 | memInfo.add_field('memFree', 'int', 'Free memory in kB') 188 | memInfo.add_field('memAvailable', 'int', 'Available memory in kB') 189 | memInfo.add_field('cached', 'int', 'Cached pages in kB') 190 | memInfo.add_field('swapTotal', 'int', 'Total swap memory in kB') 191 | memInfo.add_field('swapFree', 'int', 'Free swap memory in kB') 192 | 193 | # create service 194 | jsonrpc = reflectrpc.RpcProcessor() 195 | jsonrpc.set_description("Linux System Information Service", 196 | "This JSON-RPC service provides access to live system information of a Linux server", 197 | reflectrpc.version) 198 | 199 | # register types 200 | jsonrpc.add_custom_type(memInfo) 201 | jsonrpc.add_custom_type(cpuInfo) 202 | jsonrpc.add_custom_type(cpu) 203 | 204 | # register RPC functions 205 | cpuinfo_func = reflectrpc.RpcFunction(get_cpuinfo, 'get_cpuinfo', 'Gets information about the system CPUs', 206 | 'CPUInfo', 'System CPU information') 207 | jsonrpc.add_function(cpuinfo_func) 208 | 209 | meminfo_func = reflectrpc.RpcFunction(get_meminfo, 'get_meminfo', 'Gets information about the system memory', 210 | 'MemInfo', 'System memory information') 211 | jsonrpc.add_function(meminfo_func) 212 | 213 | server = reflectrpc.twistedserver.TwistedJsonRpcServer(jsonrpc, '0.0.0.0', 5500) 214 | server.run() 215 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\ReflectRPC.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\ReflectRPC.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " epub3 to make an epub3" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | @echo " dummy to check syntax errors of document sources" 51 | 52 | .PHONY: clean 53 | clean: 54 | rm -rf $(BUILDDIR)/* 55 | 56 | .PHONY: html 57 | html: 58 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 61 | 62 | .PHONY: dirhtml 63 | dirhtml: 64 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 65 | @echo 66 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 67 | 68 | .PHONY: singlehtml 69 | singlehtml: 70 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 71 | @echo 72 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 73 | 74 | .PHONY: pickle 75 | pickle: 76 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 77 | @echo 78 | @echo "Build finished; now you can process the pickle files." 79 | 80 | .PHONY: json 81 | json: 82 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 83 | @echo 84 | @echo "Build finished; now you can process the JSON files." 85 | 86 | .PHONY: htmlhelp 87 | htmlhelp: 88 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 89 | @echo 90 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 91 | ".hhp project file in $(BUILDDIR)/htmlhelp." 92 | 93 | .PHONY: qthelp 94 | qthelp: 95 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 96 | @echo 97 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 98 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 99 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ReflectRPC.qhcp" 100 | @echo "To view the help file:" 101 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ReflectRPC.qhc" 102 | 103 | .PHONY: applehelp 104 | applehelp: 105 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 106 | @echo 107 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 108 | @echo "N.B. You won't be able to view it unless you put it in" \ 109 | "~/Library/Documentation/Help or install it in your application" \ 110 | "bundle." 111 | 112 | .PHONY: devhelp 113 | devhelp: 114 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 115 | @echo 116 | @echo "Build finished." 117 | @echo "To view the help file:" 118 | @echo "# mkdir -p $$HOME/.local/share/devhelp/ReflectRPC" 119 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ReflectRPC" 120 | @echo "# devhelp" 121 | 122 | .PHONY: epub 123 | epub: 124 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 125 | @echo 126 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 127 | 128 | .PHONY: epub3 129 | epub3: 130 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 131 | @echo 132 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 133 | 134 | .PHONY: latex 135 | latex: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo 138 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 139 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 140 | "(use \`make latexpdf' here to do that automatically)." 141 | 142 | .PHONY: latexpdf 143 | latexpdf: 144 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 145 | @echo "Running LaTeX files through pdflatex..." 146 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 147 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 148 | 149 | .PHONY: latexpdfja 150 | latexpdfja: 151 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 152 | @echo "Running LaTeX files through platex and dvipdfmx..." 153 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 154 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 155 | 156 | .PHONY: text 157 | text: 158 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 159 | @echo 160 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 161 | 162 | .PHONY: man 163 | man: 164 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 165 | @echo 166 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 167 | 168 | .PHONY: texinfo 169 | texinfo: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo 172 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 173 | @echo "Run \`make' in that directory to run these through makeinfo" \ 174 | "(use \`make info' here to do that automatically)." 175 | 176 | .PHONY: info 177 | info: 178 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 179 | @echo "Running Texinfo files through makeinfo..." 180 | make -C $(BUILDDIR)/texinfo info 181 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 182 | 183 | .PHONY: gettext 184 | gettext: 185 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 186 | @echo 187 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 188 | 189 | .PHONY: changes 190 | changes: 191 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 192 | @echo 193 | @echo "The overview file is in $(BUILDDIR)/changes." 194 | 195 | .PHONY: linkcheck 196 | linkcheck: 197 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 198 | @echo 199 | @echo "Link check complete; look for any errors in the above output " \ 200 | "or in $(BUILDDIR)/linkcheck/output.txt." 201 | 202 | .PHONY: doctest 203 | doctest: 204 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 205 | @echo "Testing of doctests in the sources finished, look at the " \ 206 | "results in $(BUILDDIR)/doctest/output.txt." 207 | 208 | .PHONY: coverage 209 | coverage: 210 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 211 | @echo "Testing of coverage in the sources finished, look at the " \ 212 | "results in $(BUILDDIR)/coverage/python.txt." 213 | 214 | .PHONY: xml 215 | xml: 216 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 217 | @echo 218 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 219 | 220 | .PHONY: pseudoxml 221 | pseudoxml: 222 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 223 | @echo 224 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 225 | 226 | .PHONY: dummy 227 | dummy: 228 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 229 | @echo 230 | @echo "Build finished. Dummy builder generates no files." 231 | -------------------------------------------------------------------------------- /examples/realworld/jsonstore/jsonstore.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from txpostgres import txpostgres 4 | 5 | from twisted.internet import reactor 6 | from twisted.python import log, util 7 | 8 | import json 9 | import pyparsing as pp 10 | import sys 11 | import uuid 12 | 13 | import reflectrpc 14 | from reflectrpc import RpcProcessor 15 | from reflectrpc import RpcFunction 16 | from reflectrpc import JsonRpcError 17 | from reflectrpc.twistedserver import TwistedJsonRpcServer 18 | 19 | from jsonstore import filter_exp_to_sql_where 20 | 21 | db_connections = {} 22 | 23 | # 24 | # This JSON-RPC service implements a JSON object store that uses PostgreSQL and 25 | # its jsonb datatype as a storage backend. 26 | # 27 | 28 | class FilterExpressionParseError(JsonRpcError): 29 | def __init__(self, msg): 30 | """ 31 | Constructor 32 | 33 | Args: 34 | msg (str): Error message 35 | """ 36 | self.msg = msg 37 | self.name = 'FilterExpressionParseError' 38 | 39 | # Helper functions 40 | 41 | def get_db_connection(): 42 | conn = txpostgres.Connection() 43 | d = conn.connect('host=localhost port=5432 user=postgres dbname=jsonstore') 44 | 45 | return conn, d 46 | 47 | # 48 | # Implementation of JSON-RPC functions 49 | # 50 | 51 | def get_object(rpcinfo, uuid): 52 | conn, d = get_db_connection() 53 | 54 | d.addCallback(lambda _: conn.runQuery("SELECT uuid, obj_name, data, updated FROM jsonstore WHERE uuid=%s", (uuid, ))) 55 | def return_result(data): 56 | conn.close() 57 | 58 | data = data[0] 59 | obj = data[2] 60 | obj['_id'] = data[0] 61 | obj['_name'] = data[1] 62 | 63 | return obj 64 | 65 | d.addCallback(return_result) 66 | 67 | return d 68 | 69 | def get_object_by_name(rpcinfo, obj_name): 70 | conn, d = get_db_connection() 71 | 72 | d.addCallback(lambda _: conn.runQuery("SELECT uuid, obj_name, data, updated FROM jsonstore WHERE obj_name=%s", (obj_name, ))) 73 | def return_result(data): 74 | conn.close() 75 | 76 | data = data[0] 77 | obj = data[2] 78 | obj['_id'] = data[0] 79 | obj['_name'] = data[1] 80 | 81 | return obj 82 | 83 | d.addCallback(return_result) 84 | 85 | return d 86 | 87 | def find_objects(rpcinfo, filter_exp): 88 | conn, d = get_db_connection() 89 | 90 | query = "SELECT uuid, obj_name, data, updated FROM jsonstore" 91 | where_clause = '' 92 | try: 93 | where_clause = filter_exp_to_sql_where(filter_exp) 94 | except pp.ParseException as e: 95 | raise FilterExpressionParseError(str(e)) 96 | 97 | print(where_clause) 98 | if where_clause: 99 | query += ' WHERE ' + where_clause 100 | 101 | d.addCallback(lambda _: conn.runQuery(query)) 102 | 103 | def return_result(data): 104 | conn.close() 105 | 106 | result = [] 107 | 108 | for row in data: 109 | obj = row[2] 110 | obj['_id'] = row[0] 111 | obj['_name'] = row[1] 112 | result.append(obj) 113 | 114 | return result 115 | 116 | d.addCallback(return_result) 117 | 118 | return d 119 | 120 | def insert_object(rpcinfo, obj): 121 | conn, d = get_db_connection() 122 | obj_json = json.dumps(obj) 123 | 124 | new_uuid = str(uuid.uuid4()) 125 | d.addCallback(lambda _: conn.runOperation("INSERT INTO jsonstore (uuid, data) VALUES (%s, %s)", 126 | (new_uuid, obj_json))) 127 | def return_result(data): 128 | conn.close() 129 | 130 | return data 131 | 132 | d.addCallback(return_result) 133 | 134 | return d 135 | 136 | def insert_object_with_name(rpcinfo, obj_name, obj): 137 | conn, d = get_db_connection() 138 | obj_json = json.dumps(obj) 139 | 140 | new_uuid = str(uuid.uuid4()) 141 | d.addCallback(lambda _: conn.runOperation("INSERT INTO jsonstore (uuid, obj_name, data) VALUES (%s, %s, %s)", 142 | (new_uuid, obj_name, obj_json))) 143 | def return_result(data): 144 | conn.close() 145 | 146 | return data 147 | 148 | d.addCallback(return_result) 149 | 150 | return d 151 | 152 | def update_object(rpcinfo, uuid, obj): 153 | conn, d = get_db_connection() 154 | obj_json = json.dumps(obj) 155 | 156 | d.addCallback(lambda _: conn.runOperation("UPDATE jsonstore SET data=%s WHERE uuid=%s", 157 | (obj_json, uuid))) 158 | def return_result(data): 159 | conn.close() 160 | 161 | return data 162 | 163 | d.addCallback(return_result) 164 | 165 | return d 166 | 167 | def update_object_by_name(rpcinfo, name, obj): 168 | conn, d = get_db_connection() 169 | obj_json = json.dumps(obj) 170 | 171 | d.addCallback(lambda _: conn.runOperation("UPDATE jsonstore SET data=%s WHERE obj_name=%s", 172 | (obj_json, name))) 173 | def return_result(data): 174 | conn.close() 175 | 176 | return data 177 | 178 | d.addCallback(return_result) 179 | 180 | return d 181 | 182 | def delete_object(rpcinfo, uuid): 183 | conn, d = get_db_connection() 184 | 185 | d.addCallback(lambda _: conn.runOperation("DELETE FROM jsonstore WHERE uuid=%s", (uuid, ))) 186 | 187 | def return_result(data): 188 | conn.close() 189 | return True 190 | 191 | return d 192 | 193 | def delete_object_by_name(rpcinfo, name): 194 | conn, d = get_db_connection() 195 | 196 | d.addCallback(lambda _: conn.runOperation("DELETE FROM jsonstore WHERE obj_name=%s", (name, ))) 197 | 198 | def return_result(data): 199 | conn.close() 200 | return True 201 | 202 | return d 203 | 204 | 205 | # Create service object 206 | jsonrpc = RpcProcessor() 207 | jsonrpc.set_description("JSON Store Service", 208 | "JSON-RPC service for storing JSON objects in a PostgreSQL database", 209 | reflectrpc.version) 210 | 211 | # Register functions 212 | get_object_func = RpcFunction(get_object, 'get_object', 'Gets a JSON object by its UUID', 213 | 'hash', 'JSON object') 214 | get_object_func.add_param('string', 'uuid', 'UUID of the JSON object to retrieve') 215 | get_object_func.require_rpcinfo() 216 | jsonrpc.add_function(get_object_func) 217 | 218 | get_object_by_name_func = RpcFunction(get_object_by_name, 'get_object_by_name', 'Gets a JSON object by its name', 'hash', 'JSON object') 219 | get_object_by_name_func.add_param('string', 'name', 'Name of the JSON object to retrieve') 220 | get_object_by_name_func.require_rpcinfo() 221 | jsonrpc.add_function(get_object_by_name_func) 222 | 223 | find_objects_func = RpcFunction(find_objects, 'find_objects', 'Finds JSON objects which match a filter', 'array', 'List of matching JSON object') 224 | find_objects_func.add_param('string', 'filter', 'Filter for the JSON objects to retrieve (e.g. "field1 = \'test\' AND score > 3")') 225 | find_objects_func.require_rpcinfo() 226 | jsonrpc.add_function(find_objects_func) 227 | 228 | insert_object_func = RpcFunction(insert_object, 'insert_object', 'Inserts a new JSON object', 229 | 'bool', 'true on success, false on failure') 230 | insert_object_func.add_param('hash', 'obj', 'The JSON object to insert') 231 | insert_object_func.require_rpcinfo() 232 | jsonrpc.add_function(insert_object_func) 233 | 234 | insert_object_with_name_func = RpcFunction(insert_object_with_name, 'insert_object_with_name', 'Inserts a new JSON object with a user supplied name', 'bool', 'true on success, false on failure') 235 | insert_object_with_name_func.add_param('string', 'obj_name', 'The name of the new JSON object') 236 | insert_object_with_name_func.add_param('hash', 'obj', 'The JSON object to insert') 237 | insert_object_with_name_func.require_rpcinfo() 238 | jsonrpc.add_function(insert_object_with_name_func) 239 | 240 | update_object_func = RpcFunction(update_object, 'update_object', 'Updates an existing JSON object', 241 | 'bool', 'true on success, false on failure') 242 | update_object_func.add_param('string', 'uuid', 'The UUID of the JSON object to update') 243 | update_object_func.add_param('hash', 'obj', 'The new version of the JSON object') 244 | update_object_func.require_rpcinfo() 245 | jsonrpc.add_function(update_object_func) 246 | 247 | update_object_by_name_func = RpcFunction(update_object_by_name, 'update_object_by_name', 'Updates an existing JSON object', 'bool', 'true on success, false on failure') 248 | update_object_by_name_func.add_param('string', 'obj_name', 'The name of the JSON object to update') 249 | update_object_by_name_func.add_param('hash', 'obj', 'The new version of the JSON object') 250 | update_object_by_name_func.require_rpcinfo() 251 | jsonrpc.add_function(update_object_by_name_func) 252 | 253 | delete_object_func = RpcFunction(delete_object, 'delete_object', 'Deletes a JSON object identified by its UUID', 'bool', 'true on success, false on failure') 254 | delete_object_func.add_param('string', 'uuid', 'UUID of the object to delete') 255 | delete_object_func.require_rpcinfo() 256 | jsonrpc.add_function(delete_object_func) 257 | 258 | delete_object_by_name_func = RpcFunction(delete_object_by_name, 'delete_object_by_name', 'Deletes a JSON object identified by its name', 'bool', 'true on success, false on failure') 259 | delete_object_by_name_func.add_param('string', 'name', 'Name of the object to delete') 260 | delete_object_by_name_func.require_rpcinfo() 261 | jsonrpc.add_function(delete_object_by_name_func) 262 | 263 | # Run the server 264 | server = reflectrpc.twistedserver.TwistedJsonRpcServer(jsonrpc, '0.0.0.0', 5500) 265 | server.run() 266 | -------------------------------------------------------------------------------- /conformance-test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import unicode_literals 4 | from builtins import bytes, dict, list, int, float, str 5 | 6 | import argparse 7 | import json 8 | import sys 9 | import unittest 10 | 11 | from reflectrpc.client import RpcClient 12 | from reflectrpc.testing import ServerRunner 13 | 14 | server_program = None 15 | 16 | class ConformanceTest(unittest.TestCase): 17 | # Table driven conformance test that can also be run against 18 | # implementations in other programming languages 19 | def test_conformance(self): 20 | global server_program 21 | 22 | funcs_description = [{'description': 'Returns the message it was sent', 23 | 'name': 'echo', 24 | 'params': [{'description': 'The message we will send back', 25 | 'name': 'message', 26 | 'type': 'string'}], 27 | 'result_desc': 'The message previously received', 28 | 'result_type': 'string'}, 29 | {'description': 'Adds two numbers', 30 | 'name': 'add', 31 | 'params': [{'description': 'First number to add', 32 | 'name': 'a', 33 | 'type': 'int'}, 34 | {'description': 'Second number to add', 35 | 'name': 'b', 36 | 'type': 'int'}], 37 | 'result_desc': 'Sum of the two numbers', 38 | 'result_type': 'int'}, 39 | {'description': 'Subtracts one number from another', 40 | 'name': 'sub', 41 | 'params': [{'description': 'Number to subtract from', 42 | 'name': 'a', 43 | 'type': 'int'}, 44 | {'description': 'Number to subtract', 45 | 'name': 'b', 46 | 'type': 'int'}], 47 | 'result_desc': 'Difference of the two numbers', 48 | 'result_type': 'int'}, 49 | {'description': 'Multiplies two numbers', 50 | 'name': 'mul', 51 | 'params': [{'description': 'First factor', 52 | 'name': 'a', 53 | 'type': 'int'}, 54 | {'description': 'Second factor', 55 | 'name': 'b', 56 | 'type': 'int'}], 57 | 'result_desc': 'Product of the two numbers', 58 | 'result_type': 'int'}, 59 | {'description': 'Divide a number by another number', 60 | 'name': 'div', 61 | 'params': [{'description': 'Dividend', 62 | 'name': 'a', 63 | 'type': 'float'}, 64 | {'description': 'Divisor', 65 | 'name': 'b', 66 | 'type': 'float'}], 67 | 'result_desc': 'Ratio of the two numbers', 68 | 'result_type': 'float'}, 69 | {'description': 'Test the phone type enum', 70 | 'name': 'enum_echo', 71 | 'params': [{'description': 'Type of phone number', 72 | 'name': 'phone_type', 73 | 'type': 'PhoneType'}], 74 | 'result_desc': 'Phone type', 75 | 'result_type': 'int'}, 76 | {'description': 'Test the address hash type', 77 | 'name': 'hash_echo', 78 | 'params': [{'description': 'Address hash', 79 | 'name': 'address', 80 | 'type': 'Address'}], 81 | 'result_desc': 'Address hash', 82 | 'result_type': 'hash'}, 83 | {'description': 'Test function for notify requests', 84 | 'name': 'notify', 85 | 'params': [{'description': 'A value to print on the server side', 86 | 'name': 'value', 87 | 'type': 'string'}], 88 | 'result_desc': '', 89 | 'result_type': 'bool'}, 90 | {'description': 'Checks if we have an authenticated connection', 91 | 'name': 'is_authenticated', 92 | 'params': [], 93 | 'result_desc': 'The authentication status', 94 | 'result_type': 'bool'}, 95 | {'description': 'Gets the username of the logged in user', 96 | 'name': 'get_username', 97 | 'params': [], 98 | 'result_desc': 'The username of the logged in user', 99 | 'result_type': 'string'}] 100 | 101 | types_description = [{'description': 'Type of a phone number', 102 | 'name': 'PhoneType', 103 | 'type': 'enum', 104 | 'values': [{'description': 'Home phone', 105 | 'intvalue': 0, 106 | 'name': 'HOME'}, 107 | {'description': 'Work phone', 108 | 'intvalue': 1, 109 | 'name': 'WORK'}, 110 | {'description': 'Mobile phone', 111 | 'intvalue': 2, 112 | 'name': 'MOBILE'}, 113 | {'description': 'FAX number', 114 | 'intvalue': 3, 115 | 'name': 'FAX'}]}, 116 | {'description': 'Street address', 117 | 'fields': [{'description': 'First name', 118 | 'name': 'firstname', 119 | 'type': 'string'}, 120 | {'description': 'Last name', 121 | 'name': 'lastname', 122 | 'type': 'string'}, 123 | {'description': 'First address line', 124 | 'name': 'street1', 125 | 'type': 'string'}, 126 | {'description': 'Second address line', 127 | 'name': 'street2', 128 | 'type': 'string'}, 129 | {'description': 'Zip code', 130 | 'name': 'zipcode', 131 | 'type': 'string'}, 132 | {'description': 'City', 133 | 'name': 'city', 134 | 'type': 'string'}], 135 | 'name': 'Address', 136 | 'type': 'hash'}] 137 | 138 | tests = [ 139 | ['{"method": "echo", "params": ["Hello Server"], "id": 1}', 140 | '{"result": "Hello Server", "error": null, "id": 1}'], 141 | ['{"method": "add", "params": [5, 6], "id": 2}', 142 | '{"result": 11, "error": null, "id": 2}'], 143 | 144 | # test non-int IDs 145 | ['{"method": "echo", "params": ["Hello"], "id": "abcd1234"}', 146 | '{"result": "Hello", "error": null, "id": "abcd1234"}'], 147 | ['{"method": "add", "params": [34, 67], "id": 3.14}', 148 | '{"result": 101, "error": null, "id": 3.14}'], 149 | 150 | # test descriptions 151 | ['{"method": "__describe_service", "params": [], "id": 3}', 152 | '{"result": {"version": "1.0", "name": "Example RPC Service", "description": "This is an example service for ReflectRPC", "custom_fields": {}}, "error": null, "id": 3}'], 153 | ['{"method": "__describe_functions", "params": [], "id": 4}', 154 | '{"result": %s, "error": null, "id": 4}' % (json.dumps(funcs_description))], 155 | ['{"method": "__describe_custom_types", "params": [], "id": 5}', 156 | '{"result": %s, "error": null, "id": 5}' % (json.dumps(types_description))] 157 | ] 158 | 159 | server = ServerRunner(server_program, 5500) 160 | server.run() 161 | 162 | client = RpcClient('localhost', 5500) 163 | 164 | self.maxDiff = None 165 | 166 | request = None 167 | expected_result = None 168 | result_str = None 169 | i = 0 170 | 171 | try: 172 | for test in tests: 173 | i += 1 174 | 175 | request = test[0] 176 | expected_result = json.loads(test[1]) 177 | 178 | result_str = client.rpc_call_raw(request) 179 | result_dict = json.loads(result_str) 180 | self.assertEqual(result_dict, expected_result) 181 | except AssertionError as e: 182 | print("Test number %d failed: " % (i)) 183 | print(request) 184 | 185 | raise e 186 | finally: 187 | server.stop() 188 | 189 | 190 | parser = argparse.ArgumentParser( 191 | description="ReflectRPC conformance test to run against a server program that listens on localhost:5500") 192 | 193 | parser.add_argument("server_program", metavar='SERVER', type=str, 194 | help="Server program to run the test against") 195 | 196 | args = parser.parse_args() 197 | server_program = args.server_program 198 | 199 | # reset argv so unittest.main() does not try to interpret our arguments 200 | sys.argv = [sys.argv[0]] 201 | 202 | if __name__ == '__main__': 203 | unittest.main() 204 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # ReflectRPC documentation build configuration file, created by 5 | # sphinx-quickstart on Thu May 26 18:39:28 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | sys.path.insert(0, os.path.abspath('..')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # source_suffix = ['.rst', '.md'] 42 | source_suffix = '.rst' 43 | 44 | # The encoding of source files. 45 | #source_encoding = 'utf-8-sig' 46 | 47 | # The master toctree document. 48 | master_doc = 'index' 49 | 50 | # General information about the project. 51 | project = 'ReflectRPC' 52 | copyright = '2016, Andreas Heck' 53 | author = 'Andreas Heck' 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = '0.7' 61 | # The full version, including alpha/beta/rc tags. 62 | release = '0.7' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # There are two options for replacing |today|: either, you set today to some 72 | # non-false value, then it is used: 73 | #today = '' 74 | # Else, today_fmt is used as the format for a strftime call. 75 | #today_fmt = '%B %d, %Y' 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | # This patterns also effect to html_static_path and html_extra_path 80 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 81 | 82 | # The reST default role (used for this markup: `text`) to use for all 83 | # documents. 84 | #default_role = None 85 | 86 | # If true, '()' will be appended to :func: etc. cross-reference text. 87 | #add_function_parentheses = True 88 | 89 | # If true, the current module name will be prepended to all description 90 | # unit titles (such as .. function::). 91 | #add_module_names = True 92 | 93 | # If true, sectionauthor and moduleauthor directives will be shown in the 94 | # output. They are ignored by default. 95 | #show_authors = False 96 | 97 | # The name of the Pygments (syntax highlighting) style to use. 98 | pygments_style = 'sphinx' 99 | 100 | # A list of ignored prefixes for module index sorting. 101 | #modindex_common_prefix = [] 102 | 103 | # If true, keep warnings as "system message" paragraphs in the built documents. 104 | #keep_warnings = False 105 | 106 | # If true, `todo` and `todoList` produce output, else they produce nothing. 107 | todo_include_todos = False 108 | 109 | 110 | # -- Options for HTML output ---------------------------------------------- 111 | 112 | # The theme to use for HTML and HTML Help pages. See the documentation for 113 | # a list of builtin themes. 114 | html_theme = 'alabaster' 115 | 116 | # Theme options are theme-specific and customize the look and feel of a theme 117 | # further. For a list of options available for each theme, see the 118 | # documentation. 119 | #html_theme_options = {} 120 | 121 | # Add any paths that contain custom themes here, relative to this directory. 122 | #html_theme_path = [] 123 | 124 | # The name for this set of Sphinx documents. 125 | # " v documentation" by default. 126 | #html_title = 'ReflectRPC v0.7' 127 | 128 | # A shorter title for the navigation bar. Default is the same as html_title. 129 | #html_short_title = None 130 | 131 | # The name of an image file (relative to this directory) to place at the top 132 | # of the sidebar. 133 | #html_logo = None 134 | 135 | # The name of an image file (relative to this directory) to use as a favicon of 136 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 137 | # pixels large. 138 | #html_favicon = None 139 | 140 | # Add any paths that contain custom static files (such as style sheets) here, 141 | # relative to this directory. They are copied after the builtin static files, 142 | # so a file named "default.css" will overwrite the builtin "default.css". 143 | html_static_path = ['_static'] 144 | 145 | # Add any extra paths that contain custom files (such as robots.txt or 146 | # .htaccess) here, relative to this directory. These files are copied 147 | # directly to the root of the documentation. 148 | #html_extra_path = [] 149 | 150 | # If not None, a 'Last updated on:' timestamp is inserted at every page 151 | # bottom, using the given strftime format. 152 | # The empty string is equivalent to '%b %d, %Y'. 153 | #html_last_updated_fmt = None 154 | 155 | # If true, SmartyPants will be used to convert quotes and dashes to 156 | # typographically correct entities. 157 | #html_use_smartypants = True 158 | 159 | # Custom sidebar templates, maps document names to template names. 160 | #html_sidebars = {} 161 | 162 | # Additional templates that should be rendered to pages, maps page names to 163 | # template names. 164 | #html_additional_pages = {} 165 | 166 | # If false, no module index is generated. 167 | #html_domain_indices = True 168 | 169 | # If false, no index is generated. 170 | #html_use_index = True 171 | 172 | # If true, the index is split into individual pages for each letter. 173 | #html_split_index = False 174 | 175 | # If true, links to the reST sources are added to the pages. 176 | #html_show_sourcelink = True 177 | 178 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 179 | #html_show_sphinx = True 180 | 181 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 182 | #html_show_copyright = True 183 | 184 | # If true, an OpenSearch description file will be output, and all pages will 185 | # contain a tag referring to it. The value of this option must be the 186 | # base URL from which the finished HTML is served. 187 | #html_use_opensearch = '' 188 | 189 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 190 | #html_file_suffix = None 191 | 192 | # Language to be used for generating the HTML full-text search index. 193 | # Sphinx supports the following languages: 194 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 195 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 196 | #html_search_language = 'en' 197 | 198 | # A dictionary with options for the search language support, empty by default. 199 | # 'ja' uses this config value. 200 | # 'zh' user can custom change `jieba` dictionary path. 201 | #html_search_options = {'type': 'default'} 202 | 203 | # The name of a javascript file (relative to the configuration directory) that 204 | # implements a search results scorer. If empty, the default will be used. 205 | #html_search_scorer = 'scorer.js' 206 | 207 | # Output file base name for HTML help builder. 208 | htmlhelp_basename = 'ReflectRPCdoc' 209 | 210 | # -- Options for LaTeX output --------------------------------------------- 211 | 212 | latex_elements = { 213 | # The paper size ('letterpaper' or 'a4paper'). 214 | #'papersize': 'letterpaper', 215 | 216 | # The font size ('10pt', '11pt' or '12pt'). 217 | #'pointsize': '10pt', 218 | 219 | # Additional stuff for the LaTeX preamble. 220 | #'preamble': '', 221 | 222 | # Latex figure (float) alignment 223 | #'figure_align': 'htbp', 224 | } 225 | 226 | # Grouping the document tree into LaTeX files. List of tuples 227 | # (source start file, target name, title, 228 | # author, documentclass [howto, manual, or own class]). 229 | latex_documents = [ 230 | (master_doc, 'ReflectRPC.tex', 'ReflectRPC Documentation', 231 | 'Andreas Heck', 'manual'), 232 | ] 233 | 234 | # The name of an image file (relative to this directory) to place at the top of 235 | # the title page. 236 | #latex_logo = None 237 | 238 | # For "manual" documents, if this is true, then toplevel headings are parts, 239 | # not chapters. 240 | #latex_use_parts = False 241 | 242 | # If true, show page references after internal links. 243 | #latex_show_pagerefs = False 244 | 245 | # If true, show URL addresses after external links. 246 | #latex_show_urls = False 247 | 248 | # Documents to append as an appendix to all manuals. 249 | #latex_appendices = [] 250 | 251 | # If false, no module index is generated. 252 | #latex_domain_indices = True 253 | 254 | 255 | # -- Options for manual page output --------------------------------------- 256 | 257 | # One entry per manual page. List of tuples 258 | # (source start file, name, description, authors, manual section). 259 | man_pages = [ 260 | (master_doc, 'reflectrpc', 'ReflectRPC Documentation', 261 | [author], 1) 262 | ] 263 | 264 | # If true, show URL addresses after external links. 265 | #man_show_urls = False 266 | 267 | 268 | # -- Options for Texinfo output ------------------------------------------- 269 | 270 | # Grouping the document tree into Texinfo files. List of tuples 271 | # (source start file, target name, title, author, 272 | # dir menu entry, description, category) 273 | texinfo_documents = [ 274 | (master_doc, 'ReflectRPC', 'ReflectRPC Documentation', 275 | author, 'ReflectRPC', 'One line description of project.', 276 | 'Miscellaneous'), 277 | ] 278 | 279 | # Documents to append as an appendix to all manuals. 280 | #texinfo_appendices = [] 281 | 282 | # If false, no module index is generated. 283 | #texinfo_domain_indices = True 284 | 285 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 286 | #texinfo_show_urls = 'footnote' 287 | 288 | # If true, do not generate a @detailmenu in the "Top" node's menu. 289 | #texinfo_no_detailmenu = False 290 | -------------------------------------------------------------------------------- /reflectrpc/rpcsh.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals, print_function 2 | from builtins import bytes, dict, list, int, float, str 3 | 4 | import json 5 | import sys 6 | 7 | from cmd import Cmd 8 | 9 | from reflectrpc.client import RpcClient 10 | from reflectrpc.client import RpcError 11 | import reflectrpc 12 | import reflectrpc.cmdline 13 | 14 | def print_types(types): 15 | for t in types: 16 | if t['type'] == 'enum': 17 | print('enum: %s' % (t['name'])) 18 | print('Description: %s' % (t['description'])) 19 | for value in t['values']: 20 | print(' [%d] %s - %s' % (value['intvalue'], value['name'], value['description'])) 21 | elif t['type'] == 'hash': 22 | print('hash: %s' % (t['name'])) 23 | print('Description: %s' % (t['description'])) 24 | for field in t['fields']: 25 | print(' [%s] %s - %s' % (field['type'], field['name'], field['description'])) 26 | else: 27 | print('Unknown class of custom type: %s' % (t['type'])) 28 | 29 | def print_functions(functions): 30 | for func_desc in functions: 31 | paramlist = [param['name'] for param in func_desc['params']] 32 | paramlist = ', '.join(paramlist) 33 | 34 | print("%s(%s) - %s" % (func_desc['name'], paramlist, func_desc['description'])) 35 | for param in func_desc['params']: 36 | print(" [%s] %s - %s" % (param['type'], param['name'], param['description'])) 37 | print(" Result: %s - %s" % (func_desc['result_type'], func_desc['result_desc'])) 38 | 39 | def split_exec_line(line): 40 | tokens = [] 41 | curtoken = '' 42 | intoken = False 43 | instring = False 44 | lastc = '' 45 | arraylevel = 0 46 | hashlevel = 0 47 | 48 | for c in line: 49 | if c.isspace(): 50 | if not intoken: 51 | lastc = c 52 | continue 53 | 54 | # end of token? 55 | if not arraylevel and not hashlevel and not instring: 56 | tokens.append(curtoken.strip()) 57 | curtoken = '' 58 | intoken = False 59 | else: 60 | intoken = True 61 | 62 | if intoken: 63 | curtoken += c 64 | 65 | if c == '"': 66 | if lastc != '\\': 67 | instring = not instring 68 | elif c == '[': 69 | if not instring: 70 | arraylevel += 1 71 | elif c == ']': 72 | if not instring: 73 | arraylevel -= 1 74 | elif c == '{': 75 | if not instring: 76 | hashlevel += 1 77 | elif c == '}': 78 | if not instring: 79 | hashlevel -= 1 80 | 81 | lastc = c 82 | 83 | if len(curtoken.strip()): 84 | tokens.append(curtoken.strip()) 85 | 86 | # type casting 87 | itertokens = iter(tokens) 88 | next(itertokens) # skip first token which is the method name 89 | for i, t in enumerate(itertokens): 90 | i += 1 91 | try: 92 | tokens[i] = json.loads(t) 93 | except ValueError as e: 94 | print("Invalid JSON in parameter %i:" % (i)) 95 | print("'%s'" % (t)) 96 | return None 97 | 98 | return tokens 99 | 100 | class ReflectRpcShell(Cmd): 101 | def __init__(self, client): 102 | if issubclass(Cmd, object): 103 | super().__init__() 104 | else: 105 | Cmd.__init__(self) 106 | 107 | self.client = client 108 | 109 | def connect(self): 110 | self.client.enable_auto_reconnect() 111 | 112 | try: 113 | self.retrieve_service_description() 114 | self.retrieve_functions() 115 | self.retrieve_custom_types() 116 | except reflectrpc.client.NetworkError as e: 117 | print(e, file=sys.stderr) 118 | print('', file=sys.stderr) 119 | reflectrpc.cmdline.connection_failed_error(self.client.host, 120 | self.client.port, True) 121 | except reflectrpc.client.HttpException as e: 122 | if e.status == '401': 123 | print('Authentication failed\n', file=sys.stderr) 124 | reflectrpc.cmdline.connection_failed_error(self.client.host, 125 | self.client.port, True) 126 | 127 | raise e 128 | 129 | self.prompt = '(rpc) ' 130 | if self.client.host.startswith('unix://'): 131 | self.intro = "ReflectRPC Shell\n================\n\nType 'help' for available commands\n\nRPC server: %s" % (self.client.host) 132 | else: 133 | self.intro = "ReflectRPC Shell\n================\n\nType 'help' for available commands\n\nRPC server: %s:%i" % (self.client.host, self.client.port) 134 | 135 | if self.service_description: 136 | self.intro += "\n\nSelf-description of the Service:\n================================\n" 137 | if self.service_description['name']: 138 | self.intro += self.service_description['name'] 139 | if self.service_description['version']: 140 | self.intro += " (%s)\n" % (self.service_description['version']) 141 | if self.service_description['description']: 142 | self.intro += self.service_description['description'] 143 | 144 | def retrieve_service_description(self): 145 | self.service_description = '' 146 | try: 147 | self.service_description = self.client.rpc_call('__describe_service') 148 | except RpcError: 149 | pass 150 | 151 | def retrieve_functions(self): 152 | self.functions = [] 153 | try: 154 | self.functions = self.client.rpc_call('__describe_functions') 155 | except RpcError: 156 | pass 157 | 158 | def retrieve_custom_types(self): 159 | self.custom_types = [] 160 | try: 161 | self.custom_types = self.client.rpc_call('__describe_custom_types') 162 | except RpcError: 163 | pass 164 | 165 | def complete_doc(self, text, line, start_index, end_index): 166 | return self.function_completion(text, line) 167 | 168 | def complete_exec(self, text, line, start_index, end_index): 169 | return self.function_completion(text, line) 170 | 171 | def complete_notify(self, text, line, start_index, end_index): 172 | return self.function_completion(text, line) 173 | 174 | def function_completion(self, text, line): 175 | if len(line.split()) > 2: 176 | return [] 177 | 178 | if len(line.split()) == 2 and text == '': 179 | return [] 180 | 181 | result = [f['name'] for f in self.functions if f['name'].startswith(text)] 182 | 183 | if len(result) == 1 and result[0] == text: 184 | return [] 185 | 186 | return result 187 | 188 | def complete_type(self, text, line, start_index, end_index): 189 | if len(line.split()) > 2: 190 | return [] 191 | 192 | if len(line.split()) == 2 and text == '': 193 | return [] 194 | 195 | result = [t['name'] for t in self.custom_types if t['name'].startswith(text)] 196 | 197 | if len(result) == 1 and result[0] == text: 198 | return [] 199 | 200 | return result 201 | 202 | def do_help(self, line): 203 | if not line: 204 | print("list - List all RPC functions advertised by the server") 205 | print("doc - Show the documentation of a RPC function") 206 | print("type - Show the documentation of a custom RPC type") 207 | print("types - List all custom RPC types advertised by the server") 208 | print("exec - Execute an RPC call") 209 | print("notify - Execute an RPC call but tell the server to send no response") 210 | print("raw - Directly send a raw JSON-RPC message to the server") 211 | print("quit - Quit this program") 212 | print("help - Print this message. 'help [command]' prints a") 213 | print(" detailed help message for a command") 214 | return 215 | 216 | if line == 'list': 217 | print("List all RPC functions advertised by the server") 218 | elif line == 'doc': 219 | print("Show the documentation of an RPC function") 220 | print("Example:") 221 | print(" doc echo") 222 | elif line == 'type': 223 | print("Shos the documentation of a custom RPC type") 224 | print("Example:") 225 | print(" type PhoneType") 226 | elif line == 'types': 227 | print("List all custom RPC types advertised by the server") 228 | elif line == 'exec': 229 | print("Execute an RPC call") 230 | print("Examples:") 231 | print(" exec echo \"Hello RPC server\"") 232 | print(" exec add 4 8") 233 | elif line == 'notify': 234 | print("Execute an RPC call but tell the server to send no response") 235 | print("Example:") 236 | print(" notify rpc_function") 237 | elif line == 'raw': 238 | print("Directly send a raw JSON-RPC message to the server") 239 | print("Example:") 240 | print(' raw {"method": "echo", "params": ["Hello Server"], "id": 1}') 241 | elif line == 'quit': 242 | print("Quit this program") 243 | elif line == 'help': 244 | pass 245 | else: 246 | print("No help available for unknown command:", line) 247 | 248 | def do_type(self, line): 249 | if not line: 250 | print("You have to pass the name of a custom RPC type: 'type [typename]'") 251 | return 252 | 253 | t = [t for t in self.custom_types if t['name'] == line] 254 | 255 | if not t: 256 | print("Unknown custom RPC type:", line) 257 | 258 | print_types(t) 259 | 260 | def do_types(self, line): 261 | for t in self.custom_types: 262 | print(t['name']) 263 | 264 | def do_exec(self, line): 265 | tokens = split_exec_line(line) 266 | 267 | if not tokens: 268 | return 269 | 270 | method = tokens.pop(0) 271 | try: 272 | result = self.client.rpc_call(method, *tokens) 273 | print("Server replied:", json.dumps(result, indent=4, sort_keys=True)) 274 | except RpcError as e: 275 | print(e) 276 | 277 | def do_notify(self, line): 278 | tokens = split_exec_line(line) 279 | 280 | if not tokens: 281 | return 282 | 283 | method = tokens.pop(0) 284 | self.client.rpc_notify(method, *tokens) 285 | 286 | def do_raw(self, line): 287 | print(self.client.rpc_call_raw(line)) 288 | 289 | def do_doc(self, line): 290 | if not line: 291 | print("You have to pass the name of an RPC function: 'doc [function]'") 292 | return 293 | 294 | function = [func for func in self.functions if func['name'] == line] 295 | 296 | if not function: 297 | print("Unknown RPC function:", line) 298 | 299 | print_functions(function) 300 | 301 | def do_list(self, line): 302 | for func in self.functions: 303 | paramlist = [param['name'] for param in func['params']] 304 | print("%s(%s)" % (func['name'], ', '.join(paramlist))) 305 | 306 | def do_quit(self, line): 307 | sys.exit(0) 308 | 309 | def do_EOF(self, line): 310 | sys.exit(0) 311 | -------------------------------------------------------------------------------- /reflectrpc/twistedserver.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from builtins import bytes, dict, list, int, float, str 3 | 4 | import os 5 | import sys 6 | import json 7 | 8 | from zope.interface import implementer 9 | from twisted.internet import defer 10 | from twisted.web.guard import HTTPAuthSessionWrapper 11 | from twisted.web.guard import BasicCredentialFactory 12 | from twisted.cred import portal, checkers, credentials, error as credError 13 | from twisted.web.resource import IResource 14 | from twisted.web.resource import NoResource 15 | from twisted.web import server, resource 16 | from twisted.internet.protocol import Protocol, Factory 17 | from twisted.internet import reactor, ssl 18 | from twisted.python import log 19 | from twisted.internet.defer import Deferred 20 | from twisted.protocols.basic import LineReceiver 21 | from twisted.web.server import NOT_DONE_YET 22 | 23 | import reflectrpc.server 24 | 25 | class PasswordChecker(object): 26 | credentialInterfaces = (credentials.IUsernamePassword,) 27 | @implementer(checkers.ICredentialsChecker) 28 | 29 | def __init__(self, check_function): 30 | """ 31 | Constructor 32 | 33 | Args: 34 | check_function (callable): A callable that checks a username and a 35 | password 36 | """ 37 | self.check_function = check_function 38 | 39 | def requestAvatarId(self, credentials): 40 | username = credentials.username 41 | password = credentials.password 42 | 43 | if type(username) == bytes: 44 | username = username.decode('utf-8') 45 | 46 | if type(password) == bytes: 47 | password = password.decode('utf-8') 48 | 49 | if self.check_function(username, password): 50 | return defer.succeed(username) 51 | else: 52 | return defer.fail(credError.UnauthorizedLogin("Login failed")) 53 | 54 | class HttpPasswordRealm(object): 55 | @implementer(portal.IRealm) 56 | 57 | def __init__(self, resource): 58 | self.resource = resource 59 | 60 | def requestAvatar(self, credentials, mind, *interfaces): 61 | if IResource in interfaces: 62 | return (IResource, self.resource, lambda: None) 63 | raise NotImplementedError() 64 | 65 | class JsonRpcProtocol(LineReceiver): 66 | """ 67 | Twisted protocol adapter 68 | """ 69 | def __init__(self): 70 | self.rpcinfo = None 71 | self.initialized = False 72 | 73 | #self.server = JsonRpcServer(self.factory.rpcprocessor, 74 | # self.transport, rpcinfo) 75 | 76 | def lineReceived(self, line): 77 | if not self.initialized: 78 | self.initialized = True 79 | if self.factory.tls_client_auth_enabled: 80 | self.username = self.transport.getPeerCertificate().get_subject().commonName 81 | self.rpcinfo = {} 82 | self.rpcinfo['authenticated'] = True 83 | self.rpcinfo['username'] = self.username 84 | 85 | rpcprocessor = self.factory.rpcprocessor 86 | reply = rpcprocessor.process_request(line.decode('utf-8'), self.rpcinfo) 87 | 88 | if isinstance(reply['result'], Deferred): 89 | def handler(value): 90 | reply['result'] = value 91 | self.sendLine(json.dumps(reply).encode('utf-8')) 92 | 93 | def error_handler(error): 94 | r = rpcprocessor.handle_error(error.value, reply) 95 | self.sendLine(json.dumps(r).encode('utf-8')) 96 | 97 | d = reply['result'] 98 | d.addCallback(handler) 99 | d.addErrback(error_handler) 100 | else: 101 | self.sendLine(json.dumps(reply).encode('utf-8')) 102 | 103 | class JsonRpcProtocolFactory(Factory): 104 | """ 105 | Factory to create JsonRpcProtocol objects 106 | """ 107 | protocol = JsonRpcProtocol 108 | 109 | def __init__(self, rpcprocessor, tls_client_auth_enabled): 110 | self.rpcprocessor = rpcprocessor 111 | self.tls_client_auth_enabled = tls_client_auth_enabled 112 | 113 | class RootResource(resource.Resource): 114 | def __init__(self, rpc): 115 | resource.Resource.__init__(self) 116 | self.rpc = rpc 117 | 118 | def getChild(self, name, request): 119 | if name == b'rpc': 120 | return self.rpc 121 | else: 122 | return NoResource() 123 | 124 | class JsonRpcHttpResource(resource.Resource): 125 | isLeaf = True 126 | 127 | def __init__(self): 128 | resource.Resource.__init__(self) 129 | 130 | def render_POST(self, request): 131 | rpcinfo = None 132 | 133 | if self.tls_client_auth_enabled: 134 | self.username = request.transport.getPeerCertificate().get_subject().commonName 135 | rpcinfo = {} 136 | rpcinfo['authenticated'] = True 137 | rpcinfo['username'] = self.username 138 | elif request.getUser(): 139 | rpcinfo = {} 140 | rpcinfo['authenticated'] = True 141 | rpcinfo['username'] = request.getUser().decode('utf-8') 142 | 143 | data = request.content.getvalue().decode('utf-8') 144 | reply = self.rpcprocessor.process_request(data, rpcinfo) 145 | request.setHeader(b"Content-Type", b"application/json-rpc") 146 | 147 | if isinstance(reply['result'], Deferred): 148 | def delayed_render(value): 149 | reply['result'] = value 150 | 151 | data = json.dumps(reply).encode('utf-8') 152 | header_value = str(len(data)).encode('utf-8') 153 | request.setHeader(b"Content-Length", header_value) 154 | request.write(data) 155 | request.finish() 156 | 157 | def error_handler(error): 158 | r = self.rpcprocessor.handle_error(error.value, reply) 159 | 160 | data = json.dumps(r).encode('utf-8') 161 | header_value = str(len(data)).encode('utf-8') 162 | request.setHeader(b"Content-Length", header_value) 163 | request.write(data) 164 | request.finish() 165 | 166 | d = reply['result'] 167 | d.addCallback(delayed_render) 168 | d.addErrback(error_handler) 169 | 170 | return NOT_DONE_YET 171 | 172 | data = json.dumps(reply).encode('utf-8') 173 | header_value = str(len(data)).encode('utf-8') 174 | request.setHeader(b"Content-Length", header_value) 175 | return data 176 | 177 | class TwistedJsonRpcServer(object): 178 | """ 179 | JSON-RPC server for line-terminated messages based on Twisted 180 | """ 181 | def __init__(self, rpcprocessor, host, port): 182 | """ 183 | Constructor 184 | 185 | Args: 186 | rpcprocessor (RpcProcessor): RPC implementation 187 | host (str): Hostname, IP or UNIX domain socket to listen on. A UNIX 188 | Domain Socket might look like this: unix:///tmp/my.sock 189 | port (int): TCP port to listen on (if host is a UNIX Domain Socket 190 | this value is ignored) 191 | """ 192 | self.host = host 193 | self.port = port 194 | self.rpcprocessor = rpcprocessor 195 | 196 | self.tls_enabled = False 197 | self.tls_client_auth_enabled = False 198 | self.cert = None 199 | self.client_auth_ca = None 200 | self.http_enabled = False 201 | self.http_basic_auth_enabled = False 202 | self.passwdCheckFunction = None 203 | 204 | self.unix_socket_backlog = 50 205 | self.unix_socket_mode = 438 206 | self.unix_socket_want_pid = False 207 | 208 | def enable_tls(self, pem_file): 209 | """ 210 | Enable TLS authentication and encryption for this server 211 | 212 | Args: 213 | pem_file (str): Path of a PEM file containing server cert and key 214 | """ 215 | self.tls_enabled = True 216 | 217 | with open(pem_file) as f: pem_data = f.read() 218 | self.cert = ssl.PrivateCertificate.loadPEM(pem_data) 219 | 220 | def enable_client_auth(self, ca_file): 221 | """ 222 | Enable TLS client authentication 223 | 224 | The client needs to present a certificate that validates against our CA 225 | to be authenticated 226 | 227 | Args: 228 | ca_file (str): Path of a PEM file containing a CA cert to validate the client certs against 229 | """ 230 | self.tls_client_auth_enabled = True 231 | 232 | with open(ca_file) as f: ca_data = f.read() 233 | self.client_auth_ca = ssl.Certificate.loadPEM(ca_data) 234 | 235 | def enable_http(self): 236 | """ 237 | Enables HTTP as transport protocol 238 | 239 | JSON-RPC requests are to be sent to '/rpc' as HTTP POST requests with 240 | content type 'application/json-rpc'. The server sends the reply in 241 | the response body. 242 | """ 243 | self.http_enabled = True 244 | 245 | def enable_http_basic_auth(self, passwdCheckFunction): 246 | """ 247 | Enables HTTP Basic Auth 248 | 249 | Args: 250 | passwdCheckFunction (callable): Takes a username and a password as 251 | argument and checks if they are 252 | valid 253 | """ 254 | self.http_basic_auth_enabled = True 255 | self.passwdCheckFunction = passwdCheckFunction 256 | 257 | def set_unix_socket_backlog(self, backlog): 258 | """ 259 | Sets the number of client connections accepted in case we listen on a 260 | UNIX Domain Socket 261 | 262 | Args: 263 | backlog (int): Number of client connections allowed 264 | """ 265 | self.unix_socket_backlog = backlog 266 | 267 | def set_unix_socket_mode(self, mode): 268 | """ 269 | Sets the file permission mode used in case we listen on a UNIX Domain 270 | Socket 271 | 272 | Args: 273 | mode (int): UNIX file permission mode to protect the Domain Socket 274 | """ 275 | self.unix_socket_mode = mode 276 | 277 | def enable_unix_socket_want_pid(self): 278 | """ 279 | Enable the creation of a PID file in case you listen on a UNIX Domain 280 | Socket 281 | """ 282 | self.unix_socket_want_pid = True 283 | 284 | def run(self): 285 | """ 286 | Start the server and listen on host:port 287 | """ 288 | f = None 289 | unix_prefix = 'unix://' 290 | 291 | if self.http_enabled: 292 | rpc = JsonRpcHttpResource() 293 | rpc.rpcprocessor = self.rpcprocessor 294 | rpc.tls_client_auth_enabled = self.tls_client_auth_enabled 295 | 296 | if self.http_basic_auth_enabled: 297 | checker = PasswordChecker(self.passwdCheckFunction) 298 | realm = HttpPasswordRealm(rpc) 299 | p = portal.Portal(realm, [checker]) 300 | 301 | realm_name = 'Reflect RPC' 302 | 303 | if sys.version_info.major == 2: 304 | realm_name = realm_name.encode('utf-8') 305 | 306 | credentialFactory = BasicCredentialFactory(realm_name) 307 | rpc = HTTPAuthSessionWrapper(p, [credentialFactory]) 308 | 309 | root = RootResource(rpc) 310 | 311 | f = server.Site(root) 312 | else: 313 | f = JsonRpcProtocolFactory(self.rpcprocessor, 314 | self.tls_client_auth_enabled) 315 | 316 | if self.tls_enabled: 317 | if not self.tls_client_auth_enabled: 318 | reactor.listenSSL(self.port, f, self.cert.options(), 319 | interface=self.host) 320 | else: 321 | reactor.listenSSL(self.port, f, 322 | self.cert.options(self.client_auth_ca), 323 | interface=self.host) 324 | else: 325 | if self.host.startswith(unix_prefix): 326 | path = self.host[len(unix_prefix):] 327 | reactor.listenUNIX(path, f, backlog=self.unix_socket_backlog, 328 | mode=self.unix_socket_mode, wantPID=self.unix_socket_want_pid) 329 | else: 330 | reactor.listenTCP(self.port, f, interface=self.host) 331 | 332 | if self.host.startswith(unix_prefix): 333 | print("Listening on %s" % (self.host)) 334 | else: 335 | print("Listening on %s:%d" % (self.host, self.port)) 336 | 337 | reactor.run() 338 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReflectRPC # 2 | 3 | [![Build Status](https://travis-ci.org/aheck/reflectrpc.svg?branch=master)](https://travis-ci.org/aheck/reflectrpc) [![Documentation Status](https://readthedocs.org/projects/reflectrpc/badge/?version=latest)](http://reflectrpc.readthedocs.io/en/latest/?badge=latest) [![PyPI](https://img.shields.io/pypi/v/reflectrpc.svg)](https://pypi.python.org/pypi/reflectrpc) 4 | 5 | Self-describing JSON-RPC services made easy 6 | 7 | ## Contents 8 | 9 | - [What is ReflectRPC?](#what-is-reflectrpc) 10 | - [Installation](#installation) 11 | - [Features](#features) 12 | - [Datatypes](#datatypes) 13 | - [Custom Datatypes](#custom-datatypes) 14 | - [Returning Errors](#returning-errors) 15 | - [Serving RPCs](#serving-rpcs) 16 | - [Generating Documentation](#generating-documentation) 17 | - [Generating Client Code](#generating-client-code) 18 | - [Supported Python Versions](#supported-python-versions) 19 | - [License](#license) 20 | - [How to Contribute](#how-to-contribute) 21 | - [Contact](#contact) 22 | 23 | ## What is ReflectRPC? ## 24 | 25 | ReflectRPC is a Python library implementing an RPC client and server using 26 | the JSON-RPC 1.0 protocol. What sets it apart from most other such 27 | implementations is that it allows the client to get a comprehensive 28 | description of the functions exposed by the server. This includes type 29 | information of parameters and return values as well as human readable 30 | JavaDoc-like descriptions of all fields. To retrieve this information the 31 | client only has to call the special RPC function *\_\_describe\_functions* and 32 | it will get a data structure containing the whole description of all RPC 33 | functions provided by the server. 34 | 35 | This ability to use reflection is utilized by the included JSON-RPC shell 36 | *rpcsh*. It can connect to every JSON-RPC server serving line terminated 37 | JSON-RPC 1.0 over a plain socket and can be used to call RPC functions on the 38 | server and display the results. If the server implements the 39 | *\_\_describe\_functions* interface it can also list all RPC functions provided 40 | by the server and show a description of the functions and their parameters. 41 | 42 | ReflectRPC does not change the JSON-RPC 1.0 protocol in any way and strives to 43 | be as compatible as possible. It only adds some special builtin RPC calls to 44 | your service to make it self-describing. That way any JSON-RPC 1.0 compliant 45 | client can talk to it while a client aware of ReflectRPC can access the extra 46 | features it provides. 47 | 48 | ### Example ### 49 | 50 | Write a function and register it (including its documentation): 51 | ```python 52 | import reflectrpc 53 | import reflectrpc.simpleserver 54 | 55 | def add(a, b): 56 | return int(a) + int(b) 57 | 58 | rpc = reflectrpc.RpcProcessor() 59 | 60 | add_func = reflectrpc.RpcFunction(add, 'add', 'Adds two numbers', 'int', 61 | 'Sum of the two numbers') 62 | add_func.add_param('int', 'a', 'First int to add') 63 | add_func.add_param('int', 'b', 'Second int to add') 64 | rpc.add_function(add_func) 65 | 66 | server = reflectrpc.simpleserver.SimpleJsonRpcServer(rpc, 'localhost', 5500) 67 | server.run() 68 | ``` 69 | 70 | Connect to the server: 71 | > rpcsh localhost 5500 72 | 73 | ![rpcsh](/pics/intro.png) 74 | 75 | Now you can get a list of RPC functions available on the server: 76 | 77 | ![List remote functions](/pics/list.png) 78 | 79 | You can take a look at the documentation of a function and its parameters: 80 | 81 | ![Show documentation of remote function](/pics/doc.png) 82 | 83 | You can call it from *rpcsh*: 84 | 85 | ![Execute remote function](/pics/exec.png) 86 | 87 | Or send a literal JSON-RPC request to the server: 88 | 89 | ![Send raw JSON-RPC request to server](/pics/raw.png) 90 | 91 | To get an overview of what *rpcsh* can do just type *help*: 92 | 93 | ![Help](/pics/help.png) 94 | 95 | ## Installation ## 96 | 97 | ReflectRPC is available in the Python Package Index. Therefore you can easily 98 | install it with a single command: 99 | 100 | > pip install reflectrpc 101 | 102 | ## Features ## 103 | 104 | - JSON-RPC 1.0 (it doesn't get any more simple than that) 105 | - Registration and documentation of RPC calls is done in one place 106 | - Type checking 107 | - Special RPC calls allow to get descriptions of the service, available 108 | functions, and custom types 109 | - Interactive shell (*rpcsh*) to explore an RPC service and call its functions 110 | - Baseclass for exceptions that are to be serialized and replied to the 111 | caller while all other exceptions are suppressed as internal errors 112 | - Custom types enum and named hashes (like structs in C) 113 | - Protocol implementation is easily reusable in custom servers 114 | - Twisted-based server that supports TCP and UNIX Domain Sockets, line-based 115 | plain sockets, HTTP, HTTP Basic Auth, TLS, and TLS client auth 116 | - Client that supports TCP and UNIX Domain Sockets, line-based plain sockets, 117 | HTTP, HTTP Basic Auth, TLS, and TLS client auth 118 | - Create HTML documentation from a running RPC service by using the program *rpcdoc* 119 | - Create documented client code from a running RPC service with the program *rpcgencode* 120 | 121 | ## Datatypes ## 122 | 123 | ReflectRPC supports the following basic datatypes: 124 | 125 | |Type |Description | 126 | |------------------|:----------------------------------| 127 | |bool | true or false | 128 | |int | integer number | 129 | |float | floating point number | 130 | |string | string | 131 | |array | JSON array with arbitrary content | 132 | |hash | JSON hash with arbitrary content | 133 | |base64 | Base64 encoded binary data | 134 | |array<*type*> | Typed array. Only elements of the given type are allowed. E.g. array<int>, array<string> etc. Custom types are also supported as elements. | 135 | 136 | ## Custom Datatypes ## 137 | 138 | There are two types of custom datatypes you can define: Enums and named hashes. 139 | For that you have to create an instance of the class *JsonEnumType* or 140 | *JsonHashType*, respectively. This object is filled similarly to *RpcProcessor* 141 | and then registered to your *RpcProcessor* by calling the *add_custom_type* 142 | method. 143 | 144 | But lets look at an example: 145 | 146 | ```python 147 | phone_type_enum = reflectrpc.JsonEnumType('PhoneType', 'Type of a phone number') 148 | phone_type_enum.add_value('HOME', 'Home phone') 149 | phone_type_enum.add_value('WORK', 'Work phone') 150 | phone_type_enum.add_value('MOBILE', 'Mobile phone') 151 | phone_type_enum.add_value('FAX', 'FAX number') 152 | 153 | address_hash = reflectrpc.JsonHashType('Address', 'Street address') 154 | address_hash.add_field('firstname', 'string', 'First name') 155 | address_hash.add_field('lastname', 'string', 'Last name') 156 | address_hash.add_field('street1', 'string', 'First address line') 157 | address_hash.add_field('street2', 'string', 'Second address line') 158 | address_hash.add_field('zipcode', 'string', 'Zip code') 159 | address_hash.add_field('city', 'string', 'City') 160 | 161 | rpc = reflectrpc.RpcProcessor() 162 | rpc.add_custom_type(phone_type_enum) 163 | rpc.add_custom_type(address_hash) 164 | ``` 165 | 166 | This creates an enum named *PhoneType* and a named hash type to hold street 167 | addresses which is named *Address* and registers them to an *RpcProcessor*. 168 | These new types can now be used with all RPC functions that are to be added 169 | to this *RpcProcessor* simply by using their instead of one of the basic 170 | datatype names. All custom type names have to start with an upper-case letter. 171 | 172 | Custom types can be inspected in *rpcsh* with the *type* command: 173 | 174 | ![Inspecting custom datatypes in rpcsh](/pics/customtypes.png) 175 | 176 | ## Returning Errors ## 177 | 178 | A common problem when writing RPC services is returning errors to the user. On 179 | the one hand you want to report as much information about a problem to the 180 | user to make life as easy as possible for him. On the other hand you have to 181 | hide internal errors for security reasons and only make errors produced by the 182 | client visible outside because otherwise you make life easy for people who want 183 | to break into your server. 184 | 185 | Therefore when an RPC function is called ReflectRPC catches all exceptions and 186 | returns only a generic "internal error" in the JSON-RPC reply. To return more 187 | information about an error to the user you can derive custom exception classes 188 | from *JsonRpcError*. All exceptions that are of this class or a subclass 189 | are serialized and returned to the client. 190 | 191 | This allows to serialize exceptions and return them to the user but at the 192 | same time gives you fine-grained control over what error information actually 193 | leaves the server. 194 | 195 | ### Example ### 196 | 197 | We can define two RPC functions named *internal_error()* and *json_error()* to 198 | demonstrate this behaviour. The first function raises a *ValueError*. Internal 199 | exceptions like this must not be visible to the client. The function 200 | *json_error()* on the other hand raises an exception of type *JsonRpcError*. 201 | Since this exception is specially defined for the sole purpose of being 202 | returned to the client it will be serialized and returned as a JSON-RPC error 203 | object. 204 | 205 | ```python 206 | def internal_error(): 207 | raise ValueError("This should not be visible to the client") 208 | 209 | def json_error(): 210 | raise reflectrpc.JsonRpcError("User-visible error") 211 | 212 | rpc = reflectrpc.RpcProcessor() 213 | 214 | error_func1 = reflectrpc.RpcFunction(internal_error, 'internal_error', 'Produces internal error', 215 | 'bool', '') 216 | error_func2 = reflectrpc.RpcFunction(json_error, 'json_error', 'Raises JsonRpcError', 217 | 'bool', '') 218 | 219 | rpc.add_function(error_func1) 220 | rpc.add_function(error_func2) 221 | ``` 222 | 223 | Now a call to *internal_error()* will yield the following response from the 224 | server: 225 | 226 | ```javascript 227 | {"result": null, "error": {"name": "InternalError", "message": "Internal error"}, "id": 1} 228 | ``` 229 | 230 | While the result of *json_error()* will look like this: 231 | 232 | ```javascript 233 | {"result": null, "error": {"name": "JsonRpcError", "message": "User error"}, "id": 2} 234 | ``` 235 | 236 | Both results are as expected. You can send back your own errors over JSON-RPC in 237 | a controlled manner but internal errors are hidden from the client. 238 | 239 | ## Serving RPCs ## 240 | 241 | When you build an RPC service you want to serve it over a network of course. 242 | To make this as easy as possible ReflectRPC already comes with two different 243 | server implementations. The first one is named *SimpleJsonRpcServer* and if 244 | you've read the first example section of this document you've already seen 245 | some example code. *SimpleJsonRpcServer* is a very simple server that serves 246 | JSON-RPC requests over a plain TCP socket, with each JSON message being delimited 247 | by a linebreak. 248 | 249 | That's how it is used: 250 | 251 | ```python 252 | import reflectrpc 253 | import reflectrpc.simpleserver 254 | 255 | # create an RpcProcessor object and register your functions 256 | ... 257 | 258 | server = reflectrpc.simpleserver.SimpleJsonRpcServer(rpc, 'localhost', 5500) 259 | server.run() 260 | ``` 261 | 262 | Since this server only handles one client at a time you only want to use it for 263 | testing purposes. For production use there is a concurrent server 264 | implementation that is also much more feature rich. It is based on the Twisted 265 | framework. 266 | 267 | The following example creates a *TwistedJsonRpcServer* that behaves exactly as 268 | the *SimpleJsonRpcServer* and serves line-delimited JSON-RPC messages over a 269 | plain TCP socket: 270 | 271 | ```python 272 | import reflectrpc 273 | import reflectrpc.twistedserver 274 | 275 | # create an RpcProcessor object and register your functions 276 | ... 277 | 278 | server = reflectrpc.twistedserver.TwistedJsonRpcServer(rpc, 'localhost', 5500) 279 | server.run() 280 | ``` 281 | 282 | Of course it is powered by Twisted and can handle more than one connection at 283 | a time. This server also support TLS encryption, TLS client authentication and 284 | HTTP as an alternative to line-delimited messages. 285 | 286 | The following example code creates a *TwistedJsonRpcServer* that serves JSON-RPC 287 | over HTTP (JSON-RPC message are to be sent as POST requests to '/rpc'). The 288 | connection is encrypted with TLS and the client has to present a valid 289 | certificate that is signed by the CA certificate in the file *clientCA.crt*: 290 | 291 | ```python 292 | import reflectrpc 293 | import reflectrpc.twistedserver 294 | 295 | # create an RpcProcessor object and register your functions 296 | ... 297 | 298 | jsonrpc = rpcexample.build_example_rpcservice() 299 | server = reflectrpc.twistedserver.TwistedJsonRpcServer(jsonrpc, 'localhost', 5500) 300 | server.enable_tls('server.pem') 301 | server.enable_client_auth('clientCA.crt') 302 | server.enable_http() 303 | server.run() 304 | ``` 305 | 306 | ### Custom Servers ### 307 | 308 | If you have custom requirements and want to write your own server that is no 309 | problem at all. All you have to do is pass the request string you receive from 310 | your client to the *process_request* method of an *RpcProcessor* object. It 311 | will the reply as a dictionary or *None* in case of a JSON-RPC notification. 312 | If you get a dictionary you encode it as JSON and send it back to the client. 313 | 314 | ```python 315 | # create an RpcProcessor object and register your functions 316 | ... 317 | 318 | reply = rpc.process_request(line) 319 | 320 | # in case of a notification request process_request returns None 321 | # and we send no reply back 322 | if reply: 323 | reply_line = json.dumps(reply) 324 | send_data(reply_line.encode("utf-8")) 325 | ``` 326 | 327 | ### Authentication ### 328 | 329 | Some protocols like e.g. TLS with client authentication allow to authenticate 330 | the client. Normally, your RPC functions have no idea about in what context 331 | they are called so they also know nothing about authentication. You can change 332 | this by calling the method *require_rpcinfo* on your *RpcFunction* object. Your 333 | function will then be called with a Python dict called *rpcinfo* as its first 334 | parameter which provides your RPC function with some context information: 335 | 336 | ```python 337 | def whoami(rpcinfo): 338 | if rpcinfo['authenticated']: 339 | return 'Username: ' + rpcinfo['username'] 340 | 341 | return 'Not logged in' 342 | 343 | func = RpcFunction(whoami, 'whoami', 'Returns login information', 344 | 'string', 'Login information') 345 | func.require_rpcinfo() 346 | ``` 347 | 348 | Of course your function has to declare an additional parameter for the 349 | *rpcinfo* dict. 350 | 351 | You can also use *rpcinfo* in a custom server to pass your own context 352 | information. Just call *process_request* with your custom *rpcinfo* dict as a 353 | second parameter: 354 | 355 | ```python 356 | rpcinfo = { 357 | 'authenticated': False, 358 | 'username': None, 359 | 'mydata': 'SOMEUSERDATA' 360 | } 361 | 362 | reply = rpc.process_request(line, rpcinfo) 363 | ``` 364 | 365 | This dict will then be passed to every RPC function that declared that it wants 366 | to get the *rpcinfo* dict while all other RPC functions will know nothing about 367 | it. 368 | 369 | ## Generating Documentation ## 370 | 371 | To generate HTML documentation for a running service just call *rpcdoc* from the 372 | commandline and tell it which server to connect to and where to write its 373 | output: 374 | 375 | > rpcdoc localhost 5500 doc.html 376 | 377 | It will output some formatted HTML documentation for your service: 378 | 379 | ![HTML Documentation](/pics/htmldocs.png) 380 | 381 | ## Generating Client Code ## 382 | 383 | It is nice to have a generic JSON-RPC client like the one in 384 | *reflectrpc.client.RpcClient*. But it is even nicer to have a client library 385 | that is specifically made for your particular service. Such a client library 386 | should expose all the RPC calls of your service and have docstrings with the 387 | description of your functions and their parameters, as well as the typing 388 | information. 389 | 390 | Such a client can be generated with the following command: 391 | 392 | > rpcgencode localhost 5500 client.py 393 | 394 | And it will look something like this: 395 | 396 | ![Generated Client](/pics/generated-client.png) 397 | 398 | ## Supported Python Versions ## 399 | 400 | ReflectRPC supports the following Python versions: 401 | 402 | - CPython 2.7 403 | - CPython 3.3 404 | - CPython 3.4 405 | - CPython 3.5 406 | 407 | Current versions of PyPy should also work. 408 | 409 | ## License ## 410 | 411 | ReflectRPC is licensed under the MIT license 412 | 413 | ## How to Contribute ## 414 | 415 | Pull requests are always welcome. 416 | 417 | If you create a pull request for this project you agree that your code will 418 | be released under the terms of the MIT license. 419 | 420 | Ideas for improvements can be found in the TODO file. 421 | 422 | ## Contact ## 423 | 424 | Andreas Heck <> 425 | --------------------------------------------------------------------------------