diff options
| author | drebs <drebs@leap.se> | 2013-03-07 17:23:05 -0300 | 
|---|---|---|
| committer | drebs <drebs@leap.se> | 2013-03-07 17:23:05 -0300 | 
| commit | d89c1849d551de68d26a1f56798ee5084dca6556 (patch) | |
| tree | 25a54c87b22fca5ac494621a0ccbcca9e181cf76 /src/leap/soledad/server.py | |
| parent | 1b1def113e6ed9b8af6897e16f0d9b4c96bbfa6b (diff) | |
Move source files to proper subdirectory.
Diffstat (limited to 'src/leap/soledad/server.py')
| -rw-r--r-- | src/leap/soledad/server.py | 151 | 
1 files changed, 151 insertions, 0 deletions
| diff --git a/src/leap/soledad/server.py b/src/leap/soledad/server.py new file mode 100644 index 00000000..eaa5e964 --- /dev/null +++ b/src/leap/soledad/server.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +""" +A U1DB server that stores data using couchdb. + +This should be run with: +    twistd -n web --wsgi=leap.soledad.server.application +""" + +import configparser +from wsgiref.util import shift_path_info +import httplib +try: +    import simplejson as json +except ImportError: +    import json  # noqa +from urlparse import parse_qs + +from twisted.web.wsgi import WSGIResource +from twisted.internet import reactor + +from u1db.remote import http_app + +from leap.soledad.backends.couch import CouchServerState + + +#----------------------------------------------------------------------------- +# Authentication +#----------------------------------------------------------------------------- + +class Unauthorized(Exception): +    """ +    User authentication failed. +    """ + + +class SoledadAuthMiddleware(object): +    """ +    Soledad Authentication WSGI middleware. + +    In general, databases are accessed using a token provided by the LEAP API. +    Some special databases can be read without authentication. +    """ + +    def __init__(self, app, prefix, public_dbs=None): +        self.app = app +        self.prefix = prefix +        self.public_dbs = public_dbs + +    def _error(self, start_response, status, description, message=None): +        start_response("%d %s" % (status, httplib.responses[status]), +                       [('content-type', 'application/json')]) +        err = {"error": description} +        if message: +            err['message'] = message +        return [json.dumps(err)] + +    def __call__(self, environ, start_response): +        if self.prefix and not environ['PATH_INFO'].startswith(self.prefix): +            return self._error(start_response, 400, "bad request") +        shift_path_info(environ) +        qs = parse_qs(environ.get('QUERY_STRING'), strict_parsing=True) +        if 'auth_token' not in qs: +            if self.need_auth(environ): +                return self._error(start_response, 401, "unauthorized", +                                   "Missing Authentication Token.") +        else: +            token = qs['auth_token'][0] +            try: +                self.verify_token(environ, token) +            except Unauthorized: +                return self._error( +                    start_response, 401, "unauthorized", +                    "Incorrect password or login.") +            # remove auth token from query string. +            del qs['auth_token'] +            qs_str = '' +            if qs: +                qs_str = reduce(lambda x, y: '&'.join([x, y]), +                                map(lambda (x, y): '='.join([x, str(y)]), +                                    qs.iteritems())) +            environ['QUERY_STRING'] = qs_str +        return self.app(environ, start_response) + +    def verify_token(self, environ, token): +        """ +        Verify if token is valid for authenticating this action. +        """ +        # TODO: implement token verification +        raise NotImplementedError(self.verify_token) + +    def need_auth(self, environ): +        """ +        Check if action can be performed on database without authentication. + +        For now, just allow access to /shared/*. +        """ +        # TODO: design unauth verification. +        return not environ.get('PATH_INFO').startswith('/shared/') + + +#----------------------------------------------------------------------------- +# Soledad WSGI application +#----------------------------------------------------------------------------- + +class SoledadApp(http_app.HTTPApp): +    """ +    Soledad WSGI application +    """ + +    def __call__(self, environ, start_response): +        return super(SoledadApp, self).__call__(environ, start_response) + + +#----------------------------------------------------------------------------- +# Auxiliary functions +#----------------------------------------------------------------------------- + +def load_configuration(file_path): +    conf = { +        'couch_url': 'http://localhost:5984', +        'working_dir': '/tmp', +        'public_dbs': 'keys', +        'prefix': '/soledad/', +    } +    config = configparser.ConfigParser() +    config.read(file_path) +    if 'soledad-server' in config: +        for key in conf: +            if key in config['soledad-server']: +                conf[key] = config['soledad-server'][key] +    # TODO: implement basic parsing/sanitization of options comming from +    # config file. +    return conf + + +#----------------------------------------------------------------------------- +# Run as Twisted WSGI Resource +#----------------------------------------------------------------------------- + +# TODO: create command-line option for choosing config file. +conf = load_configuration('/etc/leap/soledad-server.ini') +state = CouchServerState(conf['couch_url']) +# TODO: change working dir to something meaningful (maybe eliminate it) +state.set_workingdir(conf['working_dir']) + +application = SoledadAuthMiddleware( +    SoledadApp(state), +    conf['prefix'], +    conf['public_dbs'].split(',')) + +resource = WSGIResource(reactor, reactor.getThreadPool(), application) | 
