Mercurial > hg > AuthRPC
changeset 15:3c19ae16fc7a
Renamed to AuthRPC
author | Ben Croston <ben@croston.org> |
---|---|
date | Mon, 05 Sep 2011 21:18:18 +0100 |
parents | 45c1d78559e2 |
children | 686b998428de |
files | AuthRPC/__init__.py AuthRPC/client/__init__.py AuthRPC/server/__init__.py AuthRPC/tests.py README.txt setup.py wibble/__init__.py wibble/client/__init__.py wibble/server/__init__.py wibble/tests.py |
diffstat | 8 files changed, 572 insertions(+), 539 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/AuthRPC/client/__init__.py Mon Sep 05 21:18:18 2011 +0100 @@ -0,0 +1,196 @@ +#!/usr/bin/env python + +# Copyright (c) 2011 Ben Croston +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is furnished to do +# so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from uuid import uuid4 +from urlparse import urlparse +import json +import httplib +import copy +import socket +import hashlib +import platform + +if platform.python_version().startswith('3'): + IS_PY3 = True +else: + IS_PY3 = False + +class _Method(object): + def __init__(self, call, name, username=None, password=None): + self.call = call + self.name = name + self._username = username + self._password = password + + def __call__(self, *args, **kwargs): + request = {} + request['id'] = str(uuid4()) + request['method'] = self.name + + if len(kwargs) is not 0: + params = copy.copy(kwargs) + index = 0 + for arg in args: + params[str(index)] = arg + index = index + 1 + elif len(args) is not 0: + params = copy.copy(args) + else: + params = None + request['params'] = params + + if self._username is not None: + request['username'] = self._username + if self._password is not None: + if IS_PY3: + request['password'] = hashlib.md5(self._password.encode()).hexdigest() + else: + request['password'] = hashlib.md5(self._password).hexdigest() + + resp = self.call(json.dumps(request)) + if resp is not None and resp['error'] is None and resp['id'] == request['id']: + return resp['result'] + else: + raise Exception('This is not supposed to happen -- btc') ######## + + def __getattr__(self, name): + return _Method(self.call, "%s.%s" % (self.name, name), self._username, self._password) + +class _JSONRPCTransport(object): + headers = {'Content-Type':'application/json', + 'Accept':'application/json'} + + def __init__(self, uri, proxy_uri=None, user_agent=None): + self.headers['User-Agent'] = user_agent if user_agent is not None else 'AuthRPC' + if proxy_uri is not None: + self.connection_url = urlparse(proxy_uri) + self.request_path = uri + else: + self.connection_url = urlparse(uri) + self.request_path = self.connection_url.path + + def request(self, request_body): + if self.connection_url.scheme == 'http': + if self.connection_url.port is None: + port = 80 + else: + port = self.connection_url.port + connection = httplib.HTTPConnection(self.connection_url.hostname+':'+str(port)) + elif self.connection_url.scheme == 'https': + if self.connection_url.port is None: + port = 443 + else: + port = self.connection_url.port + connection = httplib.HTTPSConnection(self.connection_url.hostname+':'+str(port)) + else: + raise Exception('unsupported transport') + connection.request('POST', self.request_path, body=request_body, headers=self.headers) + return connection.getresponse() + +class BadRequestException(Exception): + """HTTP 400 - Bad Request""" + def __init__(self): + Exception.__init__(self,'HTTP 400 - Bad Request') + +class UnauthorisedException(Exception): + """HTTP 401 - Unauthorised""" + def __init__(self): + Exception.__init__(self,'HTTP 401 - Unauthorised') + +class ForbiddenException(Exception): + """HTTP 403 - Forbidden""" + def __init__(self): + Exception.__init__(self,'HTTP 403 - Forbidden') + +class NotFoundException(Exception): + """HTTP 404 - Not Found""" + def __init__(self): + Exception.__init__(self,'HTTP 404 - Not Found') + +class NetworkSocketException(Exception): + def __init__(self): + Exception.__init__(self,'Network socket exception') + +class BadGatewayException(Exception): + """HTTP 502 - Bad Gateway""" + def __init__(self): + Exception.__init__(self,'HTTP 502 - Bad Gateway') + +class ServerProxy(object): + """ + A client class to communicate with a AuthRPC server + """ + def __init__(self, uri, proxy_uri=None, user_agent=None, username=None, password=None): + """ + uri - the URI of a corresponding AuthRPC server + proxy_uri - the http proxy to use, if any + user_agent - user agent to be used (can be used as part of authentication) + username - username to use in requests + password - password to use in requests + """ + assert uri is not None + self.__transport = _JSONRPCTransport(uri, proxy_uri=proxy_uri, user_agent=user_agent) + self._username = username + self._password = password + + def __request(self, request): + # call a method on the remote server + try: + response = self.__transport.request(request) + except socket.error: + raise NetworkSocketException + if response.status == 200: + if IS_PY3: + return json.loads(response.read().decode()) + else: + return json.loads(response.read()) + elif response.status == 400: + raise BadRequestException + elif response.status == 401: + raise UnauthorisedException + elif response.status == 403: + raise ForbiddenException + elif response.status == 404: + raise NotFoundException + elif response.status == 500: + if IS_PY3: + msg = json.loads(response.read().decode()) + else: + msg = json.loads(response.read()) + raise Exception('JSONRPCError\n%s'%msg['error']['error']) + elif response.status == 502: + raise BadGatewayException + else: + raise Exception('HTTP Status %s'%response.status) + + def __repr__(self): + return ( + "<ServerProxy for %s%s>" % + (self.__host, self.__handler) + ) + + __str__ = __repr__ + + def __getattr__(self, name): + # magic method dispatcher + return _Method(self.__request, name, self._username, self._password) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/AuthRPC/server/__init__.py Mon Sep 05 21:18:18 2011 +0100 @@ -0,0 +1,149 @@ +#!/usr/bin/env python + +# Copyright (c) 2011 Ben Croston +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is furnished to do +# so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from json import loads, dumps +import traceback +import sys +from webob import Request, Response, exc + +class AuthRPCApp(object): + """ + Serve the given object via json-rpc (http://json-rpc.org/) + """ + + def __init__(self, obj, auth=None): + """ + obj - a class containing functions available using jsonrpc + auth - an authentication function (optional) + """ + self.obj = obj + self.auth = auth + + def __call__(self, environ, start_response): + req = Request(environ) + try: + resp = self._process(req) + except ValueError, e: + resp = exc.HTTPBadRequest(str(e)) + except exc.HTTPException, e: + resp = e + return resp(environ, start_response) + + def _process(self, req): + """ + Process the JSONRPC request. + req - a webob Request object + """ + if not req.method == 'POST': + raise exc.HTTPMethodNotAllowed("Only POST allowed").exception + + try: + json = loads(req.body) + except ValueError, e: + raise ValueError('Bad JSON: %s' % e) + + try: + method = json['method'] + params = json['params'] + id = json['id'] + username = json['username'] if 'username' in json else None + password = json['password'] if 'password' in json else None + except KeyError, e: + raise ValueError("JSON body missing parameter: %s" % e) + + if params is None: + params = [] + if not isinstance(params, list): + raise ValueError("Bad params %r: must be a list" % params) + text = traceback.format_exc() + exc_value = sys.exc_info()[1] + error_value = dict( + name='JSONRPCError', + code=100, + message=str(exc_value), + error=text) + return Response( + status=500, + content_type='application/json', + body=dumps(dict(result=None, + error=error_value, + id=id))) + + obj = self.obj + if isinstance(self.obj,tuple) or isinstance(self.obj,list): + for x in self.obj: + if method.startswith('%s.'%x.__class__.__name__): + obj = x + method = method.replace('%s.'%obj.__class__.__name__,'',1) + break + elif method.startswith('%s.'%self.obj.__class__.__name__): + method = method.replace('%s.'%self.obj.__class__.__name__,'',1) + if method.startswith('_'): + raise exc.HTTPForbidden("Bad method name %s: must not start with _" % method).exception + try: + method = getattr(obj, method) + except AttributeError: + raise ValueError("No such method %s" % method) + + if self.auth is not None: + try: + auth_result = self.auth(username, password, req.user_agent) + except: + text = traceback.format_exc() + exc_value = sys.exc_info()[1] + error_value = dict( + name='JSONRPCError', + code=100, + message=str(exc_value), + error=text) + return Response( + status=500, + content_type='application/json', + body=dumps(dict(result=None, + error=error_value, + id=id))) + if not auth_result: + raise exc.HTTPUnauthorized().exception + + try: + result = method(*params) + except: + text = traceback.format_exc() + exc_value = sys.exc_info()[1] + error_value = dict( + name='JSONRPCError', + code=100, + message=str(exc_value), + error=text) + return Response( + status=500, + content_type='application/json', + body=dumps(dict(result=None, + error=error_value, + id=id))) + + return Response( + content_type='application/json', + body=dumps(dict(result=result, + error=None, + id=id))) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/AuthRPC/tests.py Mon Sep 05 21:18:18 2011 +0100 @@ -0,0 +1,186 @@ +#!/usr/bin/env python + +# Copyright (c) 2011 Ben Croston +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is furnished to do +# so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import unittest +import hashlib +from threading import Thread +import time +from wsgiref import simple_server +import platform +import urllib + +try: + urllib.urlopen('http://www.wyre-it.co.uk/') + NO_INTERNET = False +except IOError: + NO_INTERNET = True + +##### server vvv ##### +class api(object): + def mymethod(self): + return 'wibbler woz ere' + + def echo(self, mystring): + return 'ECHO: ' + mystring + + def raiseexception(self): + dividebyzeroerror = 1/0 + + def returnnothing(self): + pass + +def myauth(username, password, useragent=None): + return username == 'testuser' and \ + hashlib.md5('s3cr3t').hexdigest() == password and \ + useragent == 'AuthRPC_unittest' + +def make_server(): + from server import AuthRPCApp + class myhandler(simple_server.WSGIRequestHandler): + def log_request(self, *a, **b): + pass # do not output log messages + application = AuthRPCApp(api(), auth=myauth) + return simple_server.make_server('localhost', 1337, application, handler_class=myhandler) +##### server ^^^ ##### + +##### client vvv ##### +class AuthTest(unittest.TestCase): + def runTest(self): + from client import ServerProxy, UnauthorisedException + self.client = ServerProxy('http://localhost:1337/', + username='testuser', + password='s3cr3t', + user_agent='InternetExploiter') + with self.assertRaises(UnauthorisedException): + self.client.api.mymethod() + + self.client = ServerProxy('http://localhost:1337/', + username='testuser', + password='wrongpassword', + user_agent='AuthRPC_unittest') + with self.assertRaises(UnauthorisedException): + self.client.api.mymethod() + + self.client = ServerProxy('http://localhost:1337/', + username='wronguser', + password='s3cr3t', + user_agent='AuthRPC_unittest') + with self.assertRaises(UnauthorisedException): + self.client.api.mymethod() + + +@unittest.skipIf(NO_INTERNET, 'http://www.wyre-it.co.uk/ not contactable') +class NotFoundTest(unittest.TestCase): + def runTest(self): + from client import ServerProxy, NotFoundException + self.client = ServerProxy('http://www.wyre-it.co.uk/this_should_generate_404.txt') + with self.assertRaises(NotFoundException): + self.client.api.mymethod() + +class NetworkSocketTest(unittest.TestCase): + def runTest(self): + from client import ServerProxy, NetworkSocketException + self.client = ServerProxy('http://localhost:666/') + with self.assertRaises(NetworkSocketException): + self.client.api.mymethod() + +class AuthRPCTests(unittest.TestCase): + def setUp(self): + from client import ServerProxy + self.client = ServerProxy('http://localhost:1337/', + username='testuser', + password='s3cr3t', + user_agent='AuthRPC_unittest') + +class IgnoreClassNameTest(AuthRPCTests): + def runTest(self): + self.assertEqual(self.client.api.mymethod(),self.client.mymethod()) + +class ExceptionTest(AuthRPCTests): + def runTest(self): + with self.assertRaises(Exception): + self.client.raiseexception() + +class BadRequestTest(AuthRPCTests): + def runTest(self): + from client import BadRequestException + with self.assertRaises(BadRequestException): + self.client.FunctionDoesNotExist() + +class EchoTest(AuthRPCTests): + def runTest(self): + if platform.python_version().startswith('3'): + POUND = '\u00A3' + else: + POUND = unicode('\u00A3') + self.assertEqual(self.client.echo(POUND), 'ECHO: ' + POUND) + self.assertEqual(self.client.echo('hello mum!'), 'ECHO: hello mum!') + +class ReturnNothing(AuthRPCTests): + def runTest(self): + self.assertEqual(self.client.returnnothing(), None) +##### client ^^^ ##### + +finished = False +def suite(): + if platform.python_version().startswith('2'): + # create server + def test_wrapper(): + server = make_server() + while not finished: + server.handle_request() + thread = Thread(target=test_wrapper) + thread.start() + time.sleep(0.1) # wait for server thread to start + + # tests are as client + suite = unittest.TestSuite() + suite.addTest(AuthTest()) + suite.addTest(NotFoundTest()) + suite.addTest(NetworkSocketTest()) + suite.addTest(IgnoreClassNameTest()) + suite.addTest(ExceptionTest()) + suite.addTest(BadRequestTest()) + suite.addTest(EchoTest()) + suite.addTest(ReturnNothing()) + return suite + +if __name__ == '__main__': + import sys + if platform.python_version().startswith('2') and 'serve' in sys.argv: + print 'Listening on port 1337 (Ctrl-C qo quit)...' + server = make_server() + try: + server.serve_forever() + except KeyboardInterrupt: + sys.exit() + + unittest.TextTestRunner(verbosity=2).run(suite()) + finished = True + + # make a dummy request to get server thread out of loop + try: + import urllib + urllib.urlopen('http://localhost:1337/') + except: + pass +
--- a/README.txt Mon Sep 05 13:08:07 2011 +0100 +++ b/README.txt Mon Sep 05 21:18:18 2011 +0100 @@ -1,4 +1,38 @@ -With python 3.x, only the client package is available at the moment, until WebOb has been ported to python 3. +This package provides a service based on JSONRPC with some small additions to the standard in order to enable authenticated requests. The WSGI specification is used for data communication. The package is broken down into two halves - a client and a server. For security, the server is best run over HTTPS, although this is not enforced. The server depends on WebOb 1.0.0 and above. This is automatically installed if you have an internet connection, otherwise download and install from http://pypi.python.org/pypi/WebOb +If you install under Python 3, only the client package is available at the moment, until WebOb has been ported to python 3. + +Example Usage (Server): + +:: + + import hashlib + from wsgiref import simple_server + from authrpc.server import AuthRPCApp + + def myauth(username, password, useragent): + return username == 'myuser' and \ + password == hashlib.md5('secret').hexdigest() and \ + useragent == 'myprogram' + + class api(object): + def do_something(self, myvar): + """Your code placed here""" + return 'Something', myvar + + application = AuthRPCApp(api(), auth=myauth) + return simple_server.make_server('localhost', 1234, application) + +Example Usage (Client): + +:: + + from AuthRPC.client import ServerProxy + client = ServerProxy('http://localhost:1234/', + username='myuser', + password='secret', + user_agent='myprogram') + retval = client.do_something('test') +
--- a/setup.py Mon Sep 05 13:08:07 2011 +0100 +++ b/setup.py Mon Sep 05 21:18:18 2011 +0100 @@ -8,7 +8,6 @@ 'Operating System :: OS Independent', 'License :: OSI Approved :: MIT License', 'Intended Audience :: Developers', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Topic :: Software Development', @@ -24,22 +23,22 @@ if platform.python_version().startswith('3'): # we can't build server with python 3 - exclude.append('wibble.server') + exclude.append('authrpc.server') extra['use_2to3'] = True -setup(name = 'Wibble', +setup(name = 'AuthRPC', version = '0.0.1a', packages = find_packages(exclude=exclude), install_requires = install_requires, author = 'Ben Croston', author_email = 'ben@croston.org', - description = 'Stick two pencils up your nose, underpants on your head then run this module.', + description = 'A JSONRPC-like client and server with additions to enable authentication', long_description = open('README.txt').read(), license = 'MIT', - keywords = 'jsonrpc', - url = 'http://www.wyre-it.co.uk/wibble/', + keywords = 'json, rpc, wsgi, auth', + url = 'http://www.wyre-it.co.uk/authrpc/', classifiers = classifiers, platforms = ['Any'], - test_suite = 'wibble.tests.suite', + test_suite = 'AuthRPC.tests.suite', **extra)
--- a/wibble/client/__init__.py Mon Sep 05 13:08:07 2011 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,196 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 2011 Ben Croston -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -# of the Software, and to permit persons to whom the Software is furnished to do -# so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from uuid import uuid4 -from urlparse import urlparse -import json -import httplib -import copy -import socket -import hashlib -import platform - -if platform.python_version().startswith('3'): - IS_PY3 = True -else: - IS_PY3 = False - -class _Method(object): - def __init__(self, call, name, username=None, password=None): - self.call = call - self.name = name - self._username = username - self._password = password - - def __call__(self, *args, **kwargs): - request = {} - request['id'] = str(uuid4()) - request['method'] = self.name - - if len(kwargs) is not 0: - params = copy.copy(kwargs) - index = 0 - for arg in args: - params[str(index)] = arg - index = index + 1 - elif len(args) is not 0: - params = copy.copy(args) - else: - params = None - request['params'] = params - - if self._username is not None: - request['username'] = self._username - if self._password is not None: - if IS_PY3: - request['password'] = hashlib.md5(self._password.encode()).hexdigest() - else: - request['password'] = hashlib.md5(self._password).hexdigest() - - resp = self.call(json.dumps(request)) - if resp is not None and resp['error'] is None and resp['id'] == request['id']: - return resp['result'] - else: - raise Exception('This is not supposed to happen -- btc') ######## - - def __getattr__(self, name): - return _Method(self.call, "%s.%s" % (self.name, name), self._username, self._password) - -class _JSONRPCTransport(object): - headers = {'Content-Type':'application/json', - 'Accept':'application/json'} - - def __init__(self, uri, proxy_uri=None, user_agent=None): - self.headers['User-Agent'] = user_agent if user_agent is not None else 'wibble' - if proxy_uri is not None: - self.connection_url = urlparse(proxy_uri) - self.request_path = uri - else: - self.connection_url = urlparse(uri) - self.request_path = self.connection_url.path - - def request(self, request_body): - if self.connection_url.scheme == 'http': - if self.connection_url.port is None: - port = 80 - else: - port = self.connection_url.port - connection = httplib.HTTPConnection(self.connection_url.hostname+':'+str(port)) - elif self.connection_url.scheme == 'https': - if self.connection_url.port is None: - port = 443 - else: - port = self.connection_url.port - connection = httplib.HTTPSConnection(self.connection_url.hostname+':'+str(port)) - else: - raise Exception('unsupported transport') - connection.request('POST', self.request_path, body=request_body, headers=self.headers) - return connection.getresponse() - -class BadRequestException(Exception): - """HTTP 400 - Bad Request""" - def __init__(self): - Exception.__init__(self,'HTTP 400 - Bad Request') - -class UnauthorisedException(Exception): - """HTTP 401 - Unauthorised""" - def __init__(self): - Exception.__init__(self,'HTTP 401 - Unauthorised') - -class ForbiddenException(Exception): - """HTTP 403 - Forbidden""" - def __init__(self): - Exception.__init__(self,'HTTP 403 - Forbidden') - -class NotFoundException(Exception): - """HTTP 404 - Not Found""" - def __init__(self): - Exception.__init__(self,'HTTP 404 - Not Found') - -class NetworkSocketException(Exception): - def __init__(self): - Exception.__init__(self,'Network socket exception') - -class BadGatewayException(Exception): - """HTTP 502 - Bad Gateway""" - def __init__(self): - Exception.__init__(self,'HTTP 502 - Bad Gateway') - -class ServerProxy(object): - """ - A client class to communicate with a wibble server - """ - def __init__(self, uri, proxy_uri=None, user_agent=None, username=None, password=None): - """ - uri - the URI of a corresponding wibble server - proxy_uri - the http proxy to use, if any - user_agent - user agent to be used (can be used as part of authentication) - username - username to use in requests - password - password to use in requests - """ - assert uri is not None - self.__transport = _JSONRPCTransport(uri, proxy_uri=proxy_uri, user_agent=user_agent) - self._username = username - self._password = password - - def __request(self, request): - # call a method on the remote server - try: - response = self.__transport.request(request) - except socket.error: - raise NetworkSocketException - if response.status == 200: - if IS_PY3: - return json.loads(response.read().decode()) - else: - return json.loads(response.read()) - elif response.status == 400: - raise BadRequestException - elif response.status == 401: - raise UnauthorisedException - elif response.status == 403: - raise ForbiddenException - elif response.status == 404: - raise NotFoundException - elif response.status == 500: - if IS_PY3: - msg = json.loads(response.read().decode()) - else: - msg = json.loads(response.read()) - raise Exception('JSONRPCError\n%s'%msg['error']['error']) - elif response.status == 502: - raise BadGatewayException - else: - raise Exception('HTTP Status %s'%response.status) - - def __repr__(self): - return ( - "<ServerProxy for %s%s>" % - (self.__host, self.__handler) - ) - - __str__ = __repr__ - - def __getattr__(self, name): - # magic method dispatcher - return _Method(self.__request, name, self._username, self._password) -
--- a/wibble/server/__init__.py Mon Sep 05 13:08:07 2011 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,149 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 2011 Ben Croston -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -# of the Software, and to permit persons to whom the Software is furnished to do -# so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from json import loads, dumps -import traceback -import sys -from webob import Request, Response, exc - -class JsonRpcApp(object): - """ - Serve the given object via json-rpc (http://json-rpc.org/) - """ - - def __init__(self, obj, auth=None): - """ - obj - a class containing functions available using jsonrpc - auth - an authentication function (optional) - """ - self.obj = obj - self.auth = auth - - def __call__(self, environ, start_response): - req = Request(environ) - try: - resp = self._process(req) - except ValueError, e: - resp = exc.HTTPBadRequest(str(e)) - except exc.HTTPException, e: - resp = e - return resp(environ, start_response) - - def _process(self, req): - """ - Process the JSONRPC request. - req - a webob Request object - """ - if not req.method == 'POST': - raise exc.HTTPMethodNotAllowed("Only POST allowed").exception - - try: - json = loads(req.body) - except ValueError, e: - raise ValueError('Bad JSON: %s' % e) - - try: - method = json['method'] - params = json['params'] - id = json['id'] - username = json['username'] if 'username' in json else None - password = json['password'] if 'password' in json else None - except KeyError, e: - raise ValueError("JSON body missing parameter: %s" % e) - - if params is None: - params = [] - if not isinstance(params, list): - raise ValueError("Bad params %r: must be a list" % params) - text = traceback.format_exc() - exc_value = sys.exc_info()[1] - error_value = dict( - name='JSONRPCError', - code=100, - message=str(exc_value), - error=text) - return Response( - status=500, - content_type='application/json', - body=dumps(dict(result=None, - error=error_value, - id=id))) - - obj = self.obj - if isinstance(self.obj,tuple) or isinstance(self.obj,list): - for x in self.obj: - if method.startswith('%s.'%x.__class__.__name__): - obj = x - method = method.replace('%s.'%obj.__class__.__name__,'',1) - break - elif method.startswith('%s.'%self.obj.__class__.__name__): - method = method.replace('%s.'%self.obj.__class__.__name__,'',1) - if method.startswith('_'): - raise exc.HTTPForbidden("Bad method name %s: must not start with _" % method).exception - try: - method = getattr(obj, method) - except AttributeError: - raise ValueError("No such method %s" % method) - - if self.auth is not None: - try: - auth_result = self.auth(username, password, req.user_agent) - except: - text = traceback.format_exc() - exc_value = sys.exc_info()[1] - error_value = dict( - name='JSONRPCError', - code=100, - message=str(exc_value), - error=text) - return Response( - status=500, - content_type='application/json', - body=dumps(dict(result=None, - error=error_value, - id=id))) - if not auth_result: - raise exc.HTTPUnauthorized().exception - - try: - result = method(*params) - except: - text = traceback.format_exc() - exc_value = sys.exc_info()[1] - error_value = dict( - name='JSONRPCError', - code=100, - message=str(exc_value), - error=text) - return Response( - status=500, - content_type='application/json', - body=dumps(dict(result=None, - error=error_value, - id=id))) - - return Response( - content_type='application/json', - body=dumps(dict(result=result, - error=None, - id=id))) -
--- a/wibble/tests.py Mon Sep 05 13:08:07 2011 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,186 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 2011 Ben Croston -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -# of the Software, and to permit persons to whom the Software is furnished to do -# so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import unittest -import hashlib -from threading import Thread -import time -from wsgiref import simple_server -import platform -import urllib - -try: - urllib.urlopen('http://www.wyre-it.co.uk/') - NO_INTERNET = False -except IOError: - NO_INTERNET = True - -##### server vvv ##### -class api(object): - def mymethod(self): - return 'wibbler woz ere' - - def echo(self, mystring): - return 'ECHO: ' + mystring - - def raiseexception(self): - dividebyzeroerror = 1/0 - - def returnnothing(self): - pass - -def myauth(username, password, useragent=None): - return username == 'testuser' and \ - hashlib.md5('s3cr3t').hexdigest() == password and \ - useragent == 'wibble_unittest' - -def make_server(): - from server import JsonRpcApp - class myhandler(simple_server.WSGIRequestHandler): - def log_request(self, *a, **b): - pass # do not output log messages - application = JsonRpcApp(api(), auth=myauth) - return simple_server.make_server('localhost', 1337, application, handler_class=myhandler) -##### server ^^^ ##### - -##### client vvv ##### -class AuthTest(unittest.TestCase): - def runTest(self): - from client import ServerProxy, UnauthorisedException - self.client = ServerProxy('http://localhost:1337/', - username='testuser', - password='s3cr3t', - user_agent='InternetExploiter') - with self.assertRaises(UnauthorisedException): - self.assertEqual(self.client.api.mymethod(),self.client.mymethod()) - - self.client = ServerProxy('http://localhost:1337/', - username='testuser', - password='wrongpassword', - user_agent='wibble_unittest') - with self.assertRaises(UnauthorisedException): - self.assertEqual(self.client.api.mymethod(),self.client.mymethod()) - - self.client = ServerProxy('http://localhost:1337/', - username='wronguser', - password='s3cr3t', - user_agent='wibble_unittest') - with self.assertRaises(UnauthorisedException): - self.assertEqual(self.client.api.mymethod(),self.client.mymethod()) - - -@unittest.skipIf(NO_INTERNET,'www.wyre-it.co.uk:80 not contactable') -class NotFoundTest(unittest.TestCase): - def runTest(self): - from client import ServerProxy, NotFoundException - self.client = ServerProxy('http://www.wyre-it.co.uk/notfound.txt') - with self.assertRaises(NotFoundException): - self.assertEqual(self.client.api.mymethod(),self.client.mymethod()) - -class NetworkSocketTest(unittest.TestCase): - def runTest(self): - from client import ServerProxy, NetworkSocketException - self.client = ServerProxy('http://localhost:666/') - with self.assertRaises(NetworkSocketException): - self.assertEqual(self.client.api.mymethod(),self.client.mymethod()) - -class WibbleTests(unittest.TestCase): - def setUp(self): - from client import ServerProxy - self.client = ServerProxy('http://localhost:1337/', - username='testuser', - password='s3cr3t', - user_agent='wibble_unittest') - -class IgnoreClassNameTest(WibbleTests): - def runTest(self): - self.assertEqual(self.client.api.mymethod(),self.client.mymethod()) - -class ExceptionTest(WibbleTests): - def runTest(self): - with self.assertRaises(Exception): - self.client.raiseexception() - -class BadRequestTest(WibbleTests): - def runTest(self): - from client import BadRequestException - with self.assertRaises(BadRequestException): - self.client.FunctionDoesNotExist() - -class EchoTest(WibbleTests): - def runTest(self): - if platform.python_version().startswith('3'): - POUND = '\u00A3' - else: - POUND = unicode('\u00A3') - self.assertEqual(self.client.echo(POUND), 'ECHO: ' + POUND) - self.assertEqual(self.client.echo('hello mum!'), 'ECHO: hello mum!') - -class ReturnNothing(WibbleTests): - def runTest(self): - self.assertEqual(self.client.returnnothing(), None) -##### client ^^^ ##### - -finished = False -def suite(): - if platform.python_version().startswith('2'): - # create server - def test_wrapper(): - server = make_server() - while not finished: - server.handle_request() - thread = Thread(target=test_wrapper) - thread.start() - time.sleep(0.1) # wait for server thread to start - - # tests are as client - suite = unittest.TestSuite() - suite.addTest(AuthTest()) - suite.addTest(NotFoundTest()) - suite.addTest(NetworkSocketTest()) - suite.addTest(IgnoreClassNameTest()) - suite.addTest(ExceptionTest()) - suite.addTest(BadRequestTest()) - suite.addTest(EchoTest()) - suite.addTest(ReturnNothing()) - return suite - -if __name__ == '__main__': - import sys - if platform.python_version().startswith('2') and 'serve' in sys.argv: - print 'Listening on port 1337 (Ctrl-C qo quit)...' - server = make_server() - try: - server.serve_forever() - except KeyboardInterrupt: - sys.exit() - - unittest.TextTestRunner(verbosity=2).run(suite()) - finished = True - - # make a dummy request to get server thread out of loop - try: - import urllib - urllib.urlopen('http://localhost:1337/') - except: - pass -