Mercurial > hg > AuthRPC
changeset 30:a99009a7013c 0.3.1a
- Use generator with __getfile__ (uses much less memory)
- Fixed security issue with __getfile__ - do not allow access to whole disk!
- Handle exceptions in auth function
- Fixed encrypting of no password
- Changed README code examples
author | Ben Croston <ben@croston.org> |
---|---|
date | Thu, 05 Apr 2012 20:45:56 +0100 |
parents | b631d13b252d |
children | 183a9b11c78f |
files | AuthRPC/client/__init__.py AuthRPC/server/__init__.py AuthRPC/tests.py CHANGELOG.txt INSTALL.txt LICENCE.txt README.txt setup.py |
diffstat | 8 files changed, 165 insertions(+), 59 deletions(-) [+] |
line wrap: on
line diff
--- a/AuthRPC/client/__init__.py Tue Feb 28 13:49:33 2012 +0000 +++ b/AuthRPC/client/__init__.py Thu Apr 05 20:45:56 2012 +0100 @@ -29,7 +29,7 @@ import hashlib def _encrypt_password(password): - return hashlib.md5(password.encode()).hexdigest() + return hashlib.md5(password.encode()).hexdigest() if password is not None else None class _Method(object): def __init__(self, call, name, username=None, password=None): @@ -60,7 +60,7 @@ request['password'] = _encrypt_password(self._password) if request['method'] == '__getfile__': - return self.call(json.dumps(request)) + return self.call(json.dumps(request), generator=True) resp = self.call(json.dumps(request)) if resp is None: @@ -165,14 +165,24 @@ self._username = username self._password = password - def _request(self, request): + def _response_generator(self, response): + while True: + data = response.read(262144) # 262144 == 256k + if not data: + break + yield data + + def _request(self, request, generator=False): # call a method on the remote server try: response = self.__transport.request(request) except socket.error: raise NetworkSocketError if response.status == 200: - return response.read() + if generator: + return self._response_generator(response) + else: + return response.read() elif response.status == 400: raise BadRequestError(response.read().decode()) elif response.status == 401: @@ -261,6 +271,10 @@ response = json.loads(self._server._request(req).decode()) + # catch error in auth + if type(response) == dict and response['error'] is not None: + raise RemoteException(response['error']['error']) ## btc fixme - also pass exception type (e.g. IntegrityError) + result = [] for i,r in enumerate(response): if r['id'] != self._queue[i].id:
--- a/AuthRPC/server/__init__.py Tue Feb 28 13:49:33 2012 +0000 +++ b/AuthRPC/server/__init__.py Thu Apr 05 20:45:56 2012 +0100 @@ -23,6 +23,7 @@ from json import loads, dumps import traceback import sys +import os from webob import Request, Response, exc class AuthRPCApp(object): @@ -30,13 +31,15 @@ Serve the given object via json-rpc (http://json-rpc.org/) """ - def __init__(self, obj, auth=None): + def __init__(self, obj, auth=None, filepath=None): """ - obj - a class containing functions available using jsonrpc - auth - an authentication function (optional) + obj - a class containing functions available using jsonrpc + auth - an authentication function (optional) + filepath - root pathname of where __getfile__ files are located """ self.obj = obj self.auth = auth + self.filepath = os.path.expandvars(os.path.expanduser(filepath)) if filepath is not None else None def __call__(self, environ, start_response): req = Request(environ) @@ -67,7 +70,13 @@ cmds = json[2:] except IndexError: raise exc.HTTPBadRequest('JSON body missing parameters') - self._check_auth(username, password, req.user_agent) + try: + self._check_auth(username, password, req.user_agent) + except exc.HTTPException as e: + raise e + except: + return self._ExceptionResponse() + result = [] for c in cmds: if 'method' in c and 'params' in c and 'id' in c: @@ -85,41 +94,65 @@ params = json['params'] except KeyError as e: raise exc.HTTPBadRequest("JSON body missing parameter: %s" % e) - self._check_auth(username, password, req.user_agent, id) + try: + self._check_auth(username, password, req.user_agent) + except exc.HTTPException as e: + raise e + except: + return self._ExceptionResponse(id) if method == '__getfile__': - try: - with open(params[0],'rb') as f: - body = f.read() - except IOError: - raise exc.HTTPNotFound('File not found: %s'%params[0]) - return Response(content_type='application/octet-stream', - body=body) + return self._getfile(params[0]) result = self._process_single(method, params, id) return Response(content_type='application/json', body=dumps(result).encode()) - def _check_auth(self, username, password, user_agent, id=None): + def _ExceptionResult(self, id=None): + text = traceback.format_exc() + exc_value = sys.exc_info()[1] + result = {} + result['id'] = id + result['result'] = None + result['error'] = dict(name='JSONRPCError', + code=100, + message=str(exc_value), + error=text) + return result + + def _ExceptionResponse(self, id=None): + return Response(content_type='application/json', + body=dumps(self._ExceptionResult(id)).encode()) + + def _getfile(self, filename): + def fileiter(filename): + with open(filename,'rb') as f: + while True: + data = f.read(262144) # 262144 == 256k + if not data: + break + yield data + if self.filepath == None or '..' in filename: + raise exc.HTTPForbidden() + + # remove whitespace and leading / or \ + filename.strip().lstrip(os.sep) + + # add pathname + filename = os.path.join(self.filepath, filename) + + if not os.path.exists(filename): + raise exc.HTTPNotFound('File not found: %s'%filename) + + return Response(content_type='application/octet-stream', + app_iter=fileiter(filename), + content_length=os.path.getsize(filename)) + + def _check_auth(self, username, password, user_agent): if self.auth is None: return - try: - auth_result = self.auth(username, password, 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))) + auth_result = self.auth(username, password, user_agent) if not auth_result: raise exc.HTTPUnauthorized() @@ -155,15 +188,7 @@ else: retval['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) - retval['result'] = None - retval['error'] = error_value + return self._ExceptionResult(id) return retval
--- a/AuthRPC/tests.py Tue Feb 28 13:49:33 2012 +0000 +++ b/AuthRPC/tests.py Thu Apr 05 20:45:56 2012 +0100 @@ -20,6 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import os import unittest import hashlib from threading import Thread @@ -58,13 +59,16 @@ password == hashlib.md5('s3cr3t'.encode()).hexdigest() and \ useragent == 'AuthRPC_unittest' -def make_server(): +def mybadauth(username, password, useragent=None): + return 1 / 0 # generate exception + +def make_server(api, auth, port): 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) + application = AuthRPCApp(api, auth, filepath='') + return simple_server.make_server('localhost', port, application, handler_class=myhandler) ##### server ^^^ ##### ##### client vvv ##### @@ -92,6 +96,22 @@ with self.assertRaises(UnauthorisedError): self.client.api.mymethod() +class BadAuthTest(unittest.TestCase): + def runTest(self): + from client import ServerProxy, RemoteException + self.client = ServerProxy('http://localhost:1338/') + with self.assertRaises(RemoteException): + self.client.mymethod() + +class BadBatchAuthTest(unittest.TestCase): + def runTest(self): + from client import ServerProxy, RemoteException, BatchCall + self.client = ServerProxy('http://localhost:1338/') + batch = BatchCall(self.client) + batch.api.echo('One') + batch.api.echo(mystring='Two') + with self.assertRaises(RemoteException): + batch() @unittest.skipIf(NO_INTERNET, 'http://www.wyre-it.co.uk/ not contactable') class NotFoundTest(unittest.TestCase): @@ -158,11 +178,25 @@ self.client.api.test(1, '2', three=3) class FileTest(AuthRPCTests): + def setUp(self): + AuthRPCTests.setUp(self) + + # create big (100Mb) file + self.tempfile = 'bigfile.tmp' + with open(self.tempfile,'w') as f: + for x in range(1024*100): + f.write('x'*1024) # 1k of data + def runTest(self): - filename = 'LICENCE.txt' - with open(filename,'rb') as f: - source = f.read() - self.assertEqual(self.client.__getfile__(filename), source) + with open(self.tempfile,'rb') as f: + servercopy = self.client.__getfile__(self.tempfile) + for data in servercopy: + source = f.read(len(data)) + self.assertEqual(data, source) + + def tearDown(self): + AuthRPCTests.tearDown(self) + os.remove(self.tempfile) class NonExistentFileTest(AuthRPCTests): def runTest(self): @@ -208,14 +242,24 @@ def suite(): global finished finished = False - # create server + + # create server 1 def test_wrapper(): - server = make_server() + server = make_server(api(), myauth, 1337) while not finished: server.handle_request() thread = Thread(target=test_wrapper) thread.start() - time.sleep(0.1) # wait for server thread to start + + # create server 2 + def test_wrapper(): + server = make_server(api(), mybadauth, 1338) + while not finished: + server.handle_request() + thread = Thread(target=test_wrapper) + thread.start() + + time.sleep(0.1) # wait for server threads to start # tests are as client suite = unittest.TestSuite() @@ -232,9 +276,11 @@ suite.addTest(FileTest()) suite.addTest(NonExistentFileTest()) suite.addTest(BadAuthFileTest()) + suite.addTest(BadAuthTest()) suite.addTest(BatchTest()) suite.addTest(SetFinishedFlag()) suite.addTest(BadBatchTest()) + suite.addTest(BadBatchAuthTest()) # btc fixme - test a list/tuple of api classes in another server return suite
--- a/CHANGELOG.txt Tue Feb 28 13:49:33 2012 +0000 +++ b/CHANGELOG.txt Thu Apr 05 20:45:56 2012 +0100 @@ -1,6 +1,14 @@ Change Log ========== +0.3.1a +------ +- Use generator with __getfile__ (uses much less memory) +- Fixed security issue with __getfile__ - do not allow access to whole disk! +- Handle exceptions in auth function +- Fixed encrypting of no password +- Changed README code examples + 0.3.0a ------ - Changed/renamed exceptions that are generated (client)
--- a/INSTALL.txt Tue Feb 28 13:49:33 2012 +0000 +++ b/INSTALL.txt Thu Apr 05 20:45:56 2012 +0100 @@ -1,4 +1,10 @@ -python setup.py install +To install system-wide: +$ sudo python setup.py install or -python3 setup.py install +$ sudo python3 setup.py install +To just install to your home directory: +$ python setup.py install --user + or +$ python3 setup.py install --user +
--- a/LICENCE.txt Tue Feb 28 13:49:33 2012 +0000 +++ b/LICENCE.txt Thu Apr 05 20:45:56 2012 +0100 @@ -1,4 +1,4 @@ -Copyright (c) 2011 Ben Croston +Copyright (c) 2011 - 2012 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
--- a/README.txt Tue Feb 28 13:49:33 2012 +0000 +++ b/README.txt Thu Apr 05 20:45:56 2012 +0100 @@ -20,8 +20,9 @@ """Your code placed here""" return 'Something', myvar - application = AuthRPCApp(api(), auth=myauth) - simple_server.make_server('localhost', 1234, application) + application = AuthRPCApp(api(), auth=myauth, filepath='/home/myapp/datadir') + server = simple_server.make_server('localhost', 1234, application) + server.serve_forever() Example Usage (Client): @@ -34,7 +35,13 @@ password='secret', user_agent='myprogram') retval = client.do_something('test') - file_contents = client.__getfile__('myfile.pdf') + + # get a file and save local copy + file_contents_generator = client.__getfile__('myfile.pdf') + with open('myfile_downloaded.pdf', 'wb') as f: + for data in file_contents_generator: + f.write(data) + batch = BatchCall(client) batch.do_something('call 1') batch.do_something('call 2')