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')
--- a/setup.py	Tue Feb 28 13:49:33 2012 +0000
+++ b/setup.py	Thu Apr 05 20:45:56 2012 +0100
@@ -18,7 +18,7 @@
     extra['use_2to3'] = True
 
 setup(name             = 'AuthRPC',
-      version          = '0.3.0a',
+      version          = '0.3.1a',
       packages         = find_packages(),
       install_requires = 'WebOb>=1.2b3',
       author           = 'Ben Croston',