├── .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 |
22 |
51 |
52 | {{error}}
53 |
54 |
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 | | Host: |
50 | {{host}} |
51 |
52 |
53 | | Port |
54 | {{port}} |
55 |
56 |
57 | | Over HTTP: |
58 | {{http}} |
59 |
60 |
61 |
62 |
63 |
64 | {% for func in functions %}
65 |
66 |
{{func['name_with_params']}}
67 |
68 |
{{func['description']}}
69 |
Params:
70 |
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('| Service Name: | %s |
\n' % (service_description['name']))
112 | f.write('| Version: | %s |
\n' % (service_description['version']))
113 | f.write('| Description: | %s |
\n' % (service_description['description']))
114 | f.write('| | |
\n')
115 |
116 | if args.host.startswith('unix://'):
117 | f.write('| Generated from: | %s |
\n' % (args.host))
118 | else:
119 | f.write('| Generated from: | %s:%d |
\n' % (args.host, args.port))
120 |
121 | f.write('| Generated at: | %s (UTC) |
\n' % (str(datetime.utcnow().replace(microsecond=0))))
122 | f.write('| Generated by: | ReflectRPC (rpcdoc) %s |
\n' % (reflectrpc.version))
123 | f.write('
')
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('| Int value | String value | Description |
')
134 | for value in t['values']:
135 | f.write('| %d | %s | %s |
\n' % (value['intvalue'], value['name'], value['description']))
136 | f.write('
')
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('| Name | Type | Description |
')
144 | for field in t['fields']:
145 | f.write('| %s | %s | %s |
\n' % (field['name'], format_type(field['type']), field['description']))
146 | f.write('
')
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('| %s: | %s | - | %s |
\n' % (param['name'], format_type(param['type']), param['description']))
162 | f.write('
')
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 | [](https://travis-ci.org/aheck/reflectrpc) [](http://reflectrpc.readthedocs.io/en/latest/?badge=latest) [](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 | 
74 |
75 | Now you can get a list of RPC functions available on the server:
76 |
77 | 
78 |
79 | You can take a look at the documentation of a function and its parameters:
80 |
81 | 
82 |
83 | You can call it from *rpcsh*:
84 |
85 | 
86 |
87 | Or send a literal JSON-RPC request to the server:
88 |
89 | 
90 |
91 | To get an overview of what *rpcsh* can do just type *help*:
92 |
93 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------