From 036c3f2810b2461198bffd80cc457b812fe301fe Mon Sep 17 00:00:00 2001 From: jrconlin Date: Thu, 29 Sep 2011 01:12:39 -0700 Subject: [PATCH] BROKEN --- client | 1 + etc/notifserver/notifserver.ini | 10 +- notifserver/auth/basic.py | 2 +- notifserver/auth/browserid.py | 51 +++++ notifserver/auth/jws.py | 252 +++++++++++++++++++++++++ notifserver/controllers/clientagent.py | 3 +- notifserver/ws_worker.py | 1 + notifserver/wsgiapp.py | 5 +- setup.py | 100 ++++++---- 9 files changed, 386 insertions(+), 39 deletions(-) create mode 160000 client create mode 100644 notifserver/auth/browserid.py create mode 100644 notifserver/auth/jws.py create mode 100644 notifserver/ws_worker.py diff --git a/client b/client new file mode 160000 index 0000000..f53dd10 --- /dev/null +++ b/client @@ -0,0 +1 @@ +Subproject commit f53dd10ad3049b8eed03c16396c22320d7900cc8 diff --git a/etc/notifserver/notifserver.ini b/etc/notifserver/notifserver.ini index b98a5aa..7f68d21 100644 --- a/etc/notifserver/notifserver.ini +++ b/etc/notifserver/notifserver.ini @@ -1,9 +1,15 @@ [auth] -backend=services.auth.dummy.DummyAuth +backend=notifserver.auth.browserid.NotifServerAuthentication [notifserver] templates = /home/jconlin/src/mozilla/notifications/server/notifserver/templates/ -backend = notifserver.storage.rabbitmq.RabbitMQStorage +backend = notifserver.storage.redis_file.RedisStorage username=admin password=admin host=push1.mtv1.dev.svc.mozilla.com + +[redis] +host = push1.mtv1.dev.svc.mozilla.com +data_path = /tmp/notif_msg +max_msgs_per_user = 200 + diff --git a/notifserver/auth/basic.py b/notifserver/auth/basic.py index 4e4319a..ff66929 100644 --- a/notifserver/auth/basic.py +++ b/notifserver/auth/basic.py @@ -6,7 +6,7 @@ # TODO: This needs to be integrated to the LDAP server (for end user control). -class NotifServerAuthentication(Authentication, BaseController): +class NotifServerAuthentication(Authentication): def get_session_uid(self, request): return "1" diff --git a/notifserver/auth/browserid.py b/notifserver/auth/browserid.py new file mode 100644 index 0000000..114b590 --- /dev/null +++ b/notifserver/auth/browserid.py @@ -0,0 +1,51 @@ +import json + +from cef import log_cef +from services.wsgiauth import Authentication +from webob.exc import (HTTPTemporaryRedirect, HTTPUnauthorized) +#from notifserver.controllers import BaseController +from notifserver.auth.jws import (JWS, JWSException) + +# TODO: This needs to be integrated to the LDAP server (for end user control). + +class NotifServerAuthentication(object): + """ for this class, username = user's email address, password = assertion """ + + def authenticate_user(self, user_name, password): + """ Return a validated user id """ + + try: + import pdb; pdb.set_trace() + raw_assertion = password; + jws = JWS(config = self.config, + environ = {} ) + assertion = jws.parse(raw_assertion) + return assertion + + except JWSException, e: + log_cef("Error parsing assertion, %s" % e, + 5, + environ = self.environ, + config = self.config) + raise HTTPUnauthorized("Invalid assertion") + except (ValueError, KeyError), e: + log_cef("Unparsable request body %s" % str(e), + 5, {}, {}) + raise HTTPUnauthorized("Invalid token") + + def get_user_id(self, user_name): + return user_name + + def get_session_uid(self, request): + return "1" + + def check(self, request, match): + if self.get_session_uid(request) is not None: + return +# user_id = self.authenticate_user(request, self.config) + user_id = "1" + if user_id is None: + data = request.method, request.path_info, {} + request.environ['beaker.session']['redirect'] = data + raise HTTPTemporaryRedirect(location='/login') + match['user_id'] = user_id diff --git a/notifserver/auth/jws.py b/notifserver/auth/jws.py new file mode 100644 index 0000000..2ae6762 --- /dev/null +++ b/notifserver/auth/jws.py @@ -0,0 +1,252 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is Firefox Identity Server. +# +# The Initial Developer of the Original Code is JR Conlin +# Portions created by the Initial Developer are Copyright (C) 2011 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** +from M2Crypto import RSA, BIO, ASN1 +from hashlib import sha256, sha384, sha512 + + +import base64 +import cef +import hmac +import json + + +class JWSException (Exception): + + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + +class JWS: + _header = None # header information + _crypto = None # usually the signature + _claim = None # the payload of the JWS + _config = {} # App specific configuration values + + + def __init__(self, config = None, environ = None, **kw): + if config: + self._config = config + else: + config = {} + self.environ = environ + self._sign = {'HS': self._sign_HS, + 'RS': self._sign_RS, + 'NO': self._sign_NONE} + self._verify = {'HS': self._verify_HS, + 'RS': self._verify_RS, + 'NO': self._verify_NONE} + + def sign(self, payload, header = None, alg = None, **kw): + if payload is None: + raise (JWSException("Cannot encode empty payload")) + header = self.header(alg = alg) + if alg is None: + alg = header.get('alg', 'NONE') + try: + signer = self._sign.get(alg[:2].upper()) + except KeyError, ex: + cef.log_cef("Invalid JWS Sign method specified %s", str(ex), + 5, + self.environ, + self.config) + raise(JWSException("Unsupported encoding method specified")) + header_str = base64.urlsafe_b64encode(json.dumps(header)) + payload_str = base64.urlsafe_b64encode(json.dumps(payload)) + sbs = "%s.%s" % (header_str, payload_str) + signed = signer(alg, header, sbs) + if signed: + return signed + else: + return sbs + + def parse(self, jws, **kw): + if not jws: + raise(JWSException("Cannot verify empty JWS")) + if self.verify(jws): + (head, payload_str, signature) = jws.split('.') + payload = json.parse(base64.b64decode(payload_str)) + return payload + else: + raise(JWSException("Invalid JWS")) + + def verify(self, jws, alg = None, **kw): + if not jws: + raise (JWSException("Cannot verify empty JWS")) + try: + (header_str, payload_str, signature) = jws.split('.') + header = json.parse(base64.b64decode(header_str)) + if alg is None: + alg = header.get('alg', 'NONE') + try: + sigcheck = self._verify.get(alg[:2].upper()) + except KeyError, ex: + cef.log_cef("Invalid JWS Sign method specified %s", str(ex), + 5, + self.environ, + self.config) + raise(JWSException("Unsupported encoding method specified")) + return sigcheck(alg, + header, + '%s.%s' % (header_str, payload_str), + signature) + except ValueError, ex: + cef.log_cef("JWS Verification error: %s" % ex, + 5, + self.environ, + self.config) + raise(JWSException("JWS has invalid format")) + + def _sign_NONE(self, alg, header, sbs): + """ No encryption has no encryption. + duh. + """ + return None; + + def _get_sha(self, depth): + depths = {'256': sha256, + '384': sha384, + '512': sha512} + if depth not in depths: + raise(JWSException('Invalid Depth specified for HS')) + return depths.get(depth) + + def _sign_HS(self, alg, header, sbs): + server_secret = self._config.get('jws.server_secret', '') + signature = hmac.new(server_secret, sbs, + self._get_sha(alg[2:])).digest() + return '%s.%s' % (sbs, base64.urlsafe_b64encode(signature)) + + def _sign_RS(self, alg, header, sbs): + priv_key_u = self._config.get('jws.rsa_key_path', None) + if priv_key_u is None: + raise(JWSException("No private key found for RSA signature")) + bio = BIO.openfile(priv_key_u) + rsa = RSA.load_key_bio(bio) + if not rsa.check_key(): + raise(JWSException("Invalid key specified")) + digest = self._get_sha(alg[2:])(sbs).digest() + signature = rsa.sign_rsassa_pss(digest) + return '%s.%s' % (sbs, base64.urlsafe_b64encode(signature)) + + def _verify_NONE(self, alg, header, sbs, signature): + """ There's really no way to verify this. """ + return len(sbs) != 0 + + def _verify_HS(self, alg, header, sbs, signature): + server_secret = self._config.get('jws.server_secret', '') + tsignature = hmac.new(server_secret, + sbs, + self._get_sha(alg[2:])).digest() + return (base64.urlsafe_b64encode(tsignature)) == signature + + def _verify_RS(self, alg, header, sbs, signature, testKey=None): + #rsa.verify(sbs, pubic_key) + ## fetch the public key + if testKey: + pub = testKey + else: + pub = fetch_rsa_pub_key(header) + rsa = RSA.new_pub_key((pub.get('e'), pub.get('n'))) + digest = self._get_sha(alg[2:])(sbs).digest() + return rsa.verify_rsassa_pss(digest, + base64.urlsafe_b64decode(signature)) + + def header(self, header = None, alg = None, **kw): + """ return the stored header or generate one from scratch. """ + if header: + self._header = header + if self._header: + return self._header + if not alg: + alg = self._config.get('jws.default_alg', 'HS256') + self._header = { + 'alg': alg, + 'typ': self._config.get('jws.default_typ', 'IDAssertion'), + 'jku': kw.get('jku', ''), + 'kid': self._config.get('jws.default_kid', ''), + 'pem': kw.get('pem', ''), + 'x5t': self._config.get('jws.default_x5t', ''), + 'x5u': self._config.get('jws.default_x5u', '') + + } + return self._header + + +def create_rsa_jki_entry(pubKey, keyid=None): + keys = json.parse(base64.b64decode(pubKey)) + vinz = {'algorithm': 'RSA', + 'modulus': keys.get('n'), + 'exponent': keys.get('e')} + if keyid is not None: + vinz['keyid'] = keyid + return vinz + +##REDO +def fetch_rsa_pub_key(header, **kw): + ## if 'test' defined, use that value for the returned pub key (blech) + if kw.get('test'): + return kw.get('test') + ## extract the target machine from the header. + if kw.get('keytype', None) is None and kw.get('keyname') is None: + raise JWSException('Must specify either keytype or keyname') + try: + if 'pem' in header and header.get('pem', None): + key = base64.urlsafe_b64decode(header.get('pem')).strip() + bio = BIO.MemoryBuffer(key) + pubbits = RSA.load_key_bio(bio).pub() + pub = { + 'n': int(pubbits[0].encode('hex'), 16), + 'e': int(pubbits[1].encode('hex'), 16) + } + elif 'jku' in header and header.get('jku', None): + key = header['jku'] + if key.lower().startswith('data:'): + pub = json.parse(key[key.index('base64,')+7:]) + return pub + "" + pub = { + 'n': key.get('modulus', None), + 'e': key.get('exponent', None) + } + "" + except (AttributeError, KeyError), ex: + cef.log_cef("Internal RSA error: %s" % str(ex), + 5, + environ = kw.get('environ', None), + config = kw.get('config', None)) + raise(JWSException("Could not extract key")) diff --git a/notifserver/controllers/clientagent.py b/notifserver/controllers/clientagent.py index 4ce96aa..79d7012 100644 --- a/notifserver/controllers/clientagent.py +++ b/notifserver/controllers/clientagent.py @@ -62,7 +62,7 @@ def _init(self, config): def new_queue(self, request): """ Create a new queue for the user. (queues hold subscriptions) """ - username = request.environ['REMOTE_USER'] + import pdb; pdb.set_trace(); self._init(self.app.config) try: @@ -75,6 +75,7 @@ def new_queue(self, request): def new_subscription(self, request): """ Generate a new subscription ID for the user's queue. """ + import pdb; pdb.set_trace(); username = request.environ['REMOTE_USER'] self._init(self.app.config) diff --git a/notifserver/ws_worker.py b/notifserver/ws_worker.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/notifserver/ws_worker.py @@ -0,0 +1 @@ + diff --git a/notifserver/wsgiapp.py b/notifserver/wsgiapp.py index 1eff5de..040bd2e 100644 --- a/notifserver/wsgiapp.py +++ b/notifserver/wsgiapp.py @@ -41,7 +41,7 @@ from notifserver.controllers.postoffice import PostOfficeController from notifserver.controllers.sse import ServerEventController from notifserver.controllers.clientagent import ClientAgent -from notifserver.auth.basic import NotifServerAuthentication +from services.wsgiauth import Authentication from services.baseapp import set_app, SyncServerApp from beaker.middleware import SessionMiddleware @@ -74,6 +74,7 @@ class NotificationServerApp(SyncServerApp): def __init__(self, urls, controllers, config, auth_class): """ Main storage """ + import pdb; pdb.set_trace(); super(NotificationServerApp, self).__init__(urls = urls, controllers = controllers, config = config, @@ -90,4 +91,4 @@ def _wrap(app, config = {}, **kw): controllers, klass=NotificationServerApp, wrapper=_wrap, - auth_class=NotifServerAuthentication) + auth_class=Authentication) diff --git a/setup.py b/setup.py index 4b45d3a..fa4d25e 100644 --- a/setup.py +++ b/setup.py @@ -1,38 +1,72 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is Push Notifications Server +# +# The Initial Developer of the Original Code is the Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2011 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# JR Conlin (jrconlin@mozilla.com) +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** from setuptools import setup, find_packages -setup( - name='NotifServer', - version=0.8, - packages=find_packages(), +entry_points = """ +[paste.app_factory] +main = notifserver.wsgiapp:make_app - install_requires=[ - 'PasteDeploy', - 'PasteScript', - 'distribute', - 'gunicorn', - 'pymongo', - 'mako', - 'beaker', - 'nose', - 'redis', - 'pika', - 'webob', - 'webtest', - ], +[paste.app_install] +main = paste.script.appinstall:Installer +""" - entry_points=""" - [paste.app_factory] - client_agent = notifserver.clientagent:make_client_agent - post_office = notifserver.postoffice:make_post_office - post_office_router = notifserver.postoffice:make_post_office_router - [paste.filter_app_factory] - basic_auth = notifserver.auth:make_basic_auth - """, +#TODO: limit these based on the preferred config +requires = [ + 'Beaker', + 'Distribute', + 'Gunicorn', + 'M2Crypto', + 'Mako', + 'Nose', + 'Paste', + 'PasteDeploy', + 'PasteScript', + 'Pika', + 'PyCrypto', + 'Pymongo', + 'Redis', + 'WebOb', + 'WebTest', + ] - test_suite = 'nose.collector', - - author="Shane da Silva", - author_email="sdasilva@mozilla.com", - description="Push notification server", - keywords="push notifications server real-time messaging", -) +setup(name='NotifServer', author='Mozilla Services Group', + url='http://hg.mozilla.org/services/', + description='Mozilla Push Notification Server', + long_description=open('README.txt').read(), + author_email='dev_services@mozilla.com', + version=0.1, packages=find_packages(), + entry_points=entry_points, install_requires=requires, + license='MPL')