Skip to content
Snippets Groups Projects
Commit 3f0655f9 authored by Kevin S. Hahn's avatar Kevin S. Hahn
Browse files

GAE and internims secured with https and signature.

parent e922ff6a
No related branches found
No related tags found
No related merge requests found
...@@ -16,6 +16,7 @@ import argparse ...@@ -16,6 +16,7 @@ import argparse
import markdown import markdown
import bson.json_util import bson.json_util
import webapp2_extras.routes import webapp2_extras.routes
import Crypto.PublicKey.RSA
import nimsutil import nimsutil
...@@ -37,7 +38,8 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler): ...@@ -37,7 +38,8 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler):
def get(self): def get(self):
"""Return API documentation""" """Return API documentation"""
resource = """Resource | Description resources = """
Resource | Description
:-------------------------------------------------|:----------------------- :-------------------------------------------------|:-----------------------
/nimsapi/download | download /nimsapi/download | download
/nimsapi/dump | dump /nimsapi/dump | dump
...@@ -69,7 +71,7 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler): ...@@ -69,7 +71,7 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler):
[(/nimsapi/epochs/listschema)] | schema for epoch list [(/nimsapi/epochs/listschema)] | schema for epoch list
[(/nimsapi/epochs/schema)] | schema for single epoch [(/nimsapi/epochs/schema)] | schema for single epoch
/nimsapi/epochs/*<eid>* | details for one epoch, *<eid>*""" /nimsapi/epochs/*<eid>* | details for one epoch, *<eid>*"""
resource = re.sub(r'\[\((.*)\)\]', r'[\1](\1)', resource).replace('<', '&lt;').replace('>', '&gt;') resources = re.sub(r'\[\((.*)\)\]', r'[\1](\1)', resources).replace('<', '&lt;').replace('>', '&gt;').strip()
self.response.headers['Content-Type'] = 'text/html; charset=utf-8' self.response.headers['Content-Type'] = 'text/html; charset=utf-8'
self.response.write('<html>\n') self.response.write('<html>\n')
self.response.write('<head>\n') self.response.write('<head>\n')
...@@ -88,7 +90,7 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler): ...@@ -88,7 +90,7 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler):
self.response.write('</style>\n') self.response.write('</style>\n')
self.response.write('</head>\n') self.response.write('</head>\n')
self.response.write('<body style="min-width:900px">\n') self.response.write('<body style="min-width:900px">\n')
self.response.write(markdown.markdown(resource, ['extra'])) self.response.write(markdown.markdown(resources, ['extra']))
self.response.write('</body>\n') self.response.write('</body>\n')
self.response.write('</html>\n') self.response.write('</html>\n')
...@@ -390,7 +392,7 @@ class ArgumentParser(argparse.ArgumentParser): ...@@ -390,7 +392,7 @@ class ArgumentParser(argparse.ArgumentParser):
super(ArgumentParser, self).__init__() super(ArgumentParser, self).__init__()
self.add_argument('uri', help='NIMS DB URI') self.add_argument('uri', help='NIMS DB URI')
self.add_argument('stage_path', help='path to staging area') self.add_argument('stage_path', help='path to staging area')
self.add_argument('-k', '--pubkey', help='path to public SSL key file') self.add_argument('--privkey', help='path to private SSL key file')
self.add_argument('-u', '--uid', help='site UID') self.add_argument('-u', '--uid', help='site UID')
self.add_argument('-f', '--logfile', help='path to log file') self.add_argument('-f', '--logfile', help='path to log file')
self.add_argument('-l', '--loglevel', default='info', help='path to log file') self.add_argument('-l', '--loglevel', default='info', help='path to log file')
...@@ -438,14 +440,16 @@ app = webapp2.WSGIApplication(routes, debug=True) ...@@ -438,14 +440,16 @@ app = webapp2.WSGIApplication(routes, debug=True)
if __name__ == '__main__': if __name__ == '__main__':
args = ArgumentParser().parse_args() args = ArgumentParser().parse_args()
nimsutil.configure_log(args.logfile, not args.quiet, args.loglevel) nimsutil.configure_log(args.logfile, not args.quiet, args.loglevel)
if args.pubkey:
pubkey = open(args.pubkey).read() # failure raises a sensible IOError if args.privkey:
log.debug('SSL pubkey loaded') privkey = Crypto.PublicKey.RSA.importKey(open(args.privkey).read())
log.debug('SSL private key loaded')
else: else:
pubkey = None privkey = None
log.warning('PUBKEY NOT SPECIFIED') log.warning('PRIVKEY NOT SPECIFIED')
from paste import httpserver from paste import httpserver
app.config = dict(stage_path=args.stage_path, site_id=args.uid, pubkey=pubkey) app.config = dict(stage_path=args.stage_path, site_id=args.uid, privkey=privkey)
app.db = (pymongo.MongoReplicaSetClient(args.uri) if 'replicaSet' in args.uri else pymongo.MongoClient(args.uri)).get_default_database() app.db = (pymongo.MongoReplicaSetClient(args.uri) if 'replicaSet' in args.uri else pymongo.MongoClient(args.uri)).get_default_database()
httpserver.serve(app, host=httpserver.socket.gethostname(), port='8080') httpserver.serve(app, host=httpserver.socket.gethostname(), port='8080')
......
...@@ -15,9 +15,10 @@ import pymongo ...@@ -15,9 +15,10 @@ import pymongo
import webapp2 import webapp2
import nimsapi import nimsapi
import nimsutil import nimsutil
import Crypto.PublicKey.RSA
log_file = '/var/local/log/nimsapi.log' log_file = '/var/local/log/nimsapi.log'
pubkey_file = '/var/local/nims/internims/internims.pub' privkey_file = '/var/local/nims/internims/internims.pem'
db_uri = 'mongodb://nims:cnimr750@cnifs.stanford.edu,cnibk.stanford.edu/nims?replicaSet=cni' db_uri = 'mongodb://nims:cnimr750@cnifs.stanford.edu,cnibk.stanford.edu/nims?replicaSet=cni'
stage_path = '/scratch/upload' stage_path = '/scratch/upload'
...@@ -25,11 +26,10 @@ nimsutil.configure_log(log_file, False) ...@@ -25,11 +26,10 @@ nimsutil.configure_log(log_file, False)
db_client = pymongo.MongoReplicaSetClient(db_uri) if 'replicaSet' in db_uri else pymongo.MongoClient(db_uri) db_client = pymongo.MongoReplicaSetClient(db_uri) if 'replicaSet' in db_uri else pymongo.MongoClient(db_uri)
try: try:
pubkey = open(pubkey_file).read() # FIXME: don't read too much privkey = Crypto.PublicKey.RSA.importKey(open(privkey_file).read())
# FIXME: verify that this is a valid public key except ValueError as e:
except IOError: privkey = None
pubkey = None
application = nimsapi.app application = nimsapi.app
application.config = dict(stage_path=stage_path, site_id='stanford-cni', pubkey=pubkey) application.config = dict(stage_path=stage_path, site_id='stanford_cni', privkey=privkey)
application.db = db_client.get_default_database() application.db = db_client.get_default_database()
...@@ -8,13 +8,15 @@ import webapp2 ...@@ -8,13 +8,15 @@ import webapp2
import datetime import datetime
import requests import requests
import bson.json_util import bson.json_util
import Crypto.Hash.HMAC import Crypto.Hash.SHA
import Crypto.Random.random import Crypto.PublicKey.RSA
import Crypto.Signature.PKCS1_v1_5
log = logging.getLogger('nimsapi') log = logging.getLogger('nimsapi')
requests_log = logging.getLogger('requests') # configure Requests logging requests_log = logging.getLogger('requests') # configure Requests logging
requests_log.setLevel(logging.WARNING) # set level to WARNING (default is INFO) requests_log.setLevel(logging.WARNING) # set level to WARNING (default is INFO)
class NIMSRequestHandler(webapp2.RequestHandler): class NIMSRequestHandler(webapp2.RequestHandler):
"""fetches pubkey from own self.db.remotes. needs to be aware of OWN site uid""" """fetches pubkey from own self.db.remotes. needs to be aware of OWN site uid"""
...@@ -62,86 +64,72 @@ class NIMSRequestHandler(webapp2.RequestHandler): ...@@ -62,86 +64,72 @@ class NIMSRequestHandler(webapp2.RequestHandler):
self.userid = self.request.remote_user or '@public' self.userid = self.request.remote_user or '@public'
self.user = self.app.db.users.find_one({'_id': self.userid}) self.user = self.app.db.users.find_one({'_id': self.userid})
self.user_is_superuser = self.user.get('superuser') self.user_is_superuser = self.user.get('superuser')
self.response.headers['Content-Type'] = 'application/json'
self.target_id = self.request.get('iid', None) self.target_id = self.request.get('iid', None)
self.site_id = self.app.config.get('site_id') # is ALREADY 'None' if not specified in args, never empty self.site_id = self.app.config.get('site_id') # is ALREADY 'None' if not specified in args, never empty
self.pubkey = self.app.config.get('pubkey') # is ALREADY 'None' if not specified in args, never empty self.privkey = self.app.config.get('privkey') # is ALREADY 'None' if not specified in args, never empty
# requests coming from another NIMS instance are dealt with differently
if self.request.user_agent.startswith('NIMS Instance'):
log.debug('request from "{0}", interNIMS p2p initiated'.format(self.request.user_agent))
try:
authinfo = self.request.headers['authorization']
challenge_id, digest = base64.b64decode(authinfo).split()
user, remote_site = challenge_id.split(':')
# look up pubkey from db.remotes
projection = {'_id': False, 'pubkey': True}
remote_pubkey = self.app.db.remotes.find_one({'_id': remote_site}, projection)['pubkey']
# look up challenge from db.challenges
projection = {'_id': False, 'challenge': True}
challenge = self.app.db.challenges.find_one({'_id': challenge_id}, projection)['challenge']
# delete challenge from db.challenges
self.app.db.challenges.remove({'_id': challenge_id})
# calculate expected response
h = Crypto.Hash.HMAC.new(remote_pubkey, challenge)
self.expected = base64.b64encode('%s %s' % (challenge_id, h.hexdigest()))
log.debug('recieved: %s' % authinfo)
log.debug('expected: %s' % self.expected)
# verify
if self.expected == authinfo:
log.debug('CRAM response accepted - %s authenticated' % challenge_id)
else:
self.abort(403, 'Not Authorized: cram failed')
except KeyError as e:
# send a 401 with a fresh challenge
# challenge associated with a challenge-id, cid, to ease lookup
cid = self.request.get('cid')
if not cid: self.abort(403, 'cid, challenge_id, required')
challenge = {'_id': cid,
'challenge': str(Crypto.Random.random.getrandbits(128)),
'timestamp': datetime.datetime.now()}
# upsert challenge with time of creation
self.app.db.challenges.find_and_modify(query={'_id': cid}, update=challenge, upsert=True, new=True)
# send 401 + challenge in 'www-authenticate' header
self.response.headers['www-authenticate'] = base64.b64encode(challenge['challenge'])
self.response.set_status(401)
log.debug('issued challenge to %s; %s' % (cid, challenge['challenge']))
def dispatch(self): def dispatch(self):
"""dispatching and request forwarding""" """dispatching and request forwarding"""
# dispatch to local instance
if self.target_id in [None, self.site_id]: if self.target_id in [None, self.site_id]:
log.debug('{0} dispatching to local {1}'.format(socket.gethostname(), self.request.url)) log.debug(socket.gethostname() + ' dispatching to local ' + self.request.url)
super(NIMSRequestHandler, self).dispatch() # request originates from remote instance
elif self.pubkey is None and self.site_id is None: if self.request.user_agent.startswith('NIMS Instance'):
log.warning('target is %s, but no site ID, and no pubkey. cannot dispatch') # is the requester an authorized remote site
elif self.pubkey is not None and self.site_id is not None: requester = self.request.user_agent.replace('NIMS Instance', '').strip()
log.debug('{0} dispatching to remote {1}'.format(socket.gethostname(), self.target_id)) target = self.app.db.remotes.find_one({'_id':requester})
if not target:
log.debug('remote host ' + requester + ' not in auth list. DENIED')
self.abort(403, requester + ' is not authorized')
log.debug('request from ' + self.request.user_agent + ', interNIMS p2p initiated')
# verify signature
self.signature = base64.b64decode(self.request.headers.get('Authorization'))
payload = self.request.body
key = Crypto.PublicKey.RSA.importKey(target['pubkey'])
h = Crypto.Hash.SHA.new(payload)
verifier = Crypto.Signature.PKCS1_v1_5.new(key)
if verifier.verify(h, self.signature):
log.debug('message/signature is authentic')
super(NIMSRequestHandler, self).dispatch()
else:
log.debug('message/signature is not authentic')
self.abort(403, 'authentication failed')
# request originates from self
else:
super(NIMSRequestHandler, self).dispatch()
# dispatch to remote instance
elif self.privkey is not None and self.site_id is not None:
log.debug(socket.gethostname() + ' dispatching to remote ' + self.target_id)
# is target registered? # is target registered?
target = self.app.db.remotes.find_one({'_id': self.target_id}, {'_id':False, 'hostname':True}) target = self.app.db.remotes.find_one({'_id': self.target_id}, {'_id':False, 'hostname':True})
if not target: if not target:
log.debug('remote host {0} not in auth list. DENIED'.format(self.target_id)) log.debug('remote host ' + self.target_id + ' not in auth list. DENIED')
self.abort(403, 'forbidden: site is not registered with interNIMS') self.abort(403, self.target_id + 'is not authorized')
self.cid = self.userid + ':' + self.site_id
# disassemble the incoming request
reqparams = dict(self.request.params)
reqpayload = self.request.body # request payload, almost always empty
reqheaders = dict(self.request.headers) reqheaders = dict(self.request.headers)
# adjust the request, pass as much of orig request as possible reqheaders['User-Agent'] = 'NIMS Instance ' + self.site_id
reqheaders['User-Agent'] = 'NIMS Instance {0}'.format(self.site_id) del reqheaders['Host'] # delete old host destination
del reqheaders['Host']
target_api = 'http://{0}{1}?{2}'.format(target['hostname'], self.request.path, self.request.query_string) # create a signature of the incoming request payload
reqparams = {'cid': self.cid} h = Crypto.Hash.SHA.new(reqpayload)
# first attempt, expect 401, send as little as possible... signature = Crypto.Signature.PKCS1_v1_5.new(self.privkey).sign(h)
# TODO: error handling for host-down/host-unreachable reqheaders['Authorization'] = base64.b64encode(signature)
# TODO: timeout?
r = requests.request(method=self.request.method, url=target_api, params=reqparams, headers=reqheaders) # construct outgoing request
if r.status_code == 401: target_api = 'http://' + target['hostname'] + self.request.path # TODO: switch to https
challenge = base64.b64decode(r.headers['www-authenticate']) # target_api = 'https://' + target['hostname'] + self.request.path)
log.debug('Authorization requested - challenge: %s' % challenge) r = requests.request(method=self.request.method, data=reqpayload, url=target_api, params=reqparams, headers=reqheaders, verify=False)
h = Crypto.Hash.HMAC.new(self.pubkey, challenge)
response = base64.b64encode('%s %s' % (self.cid, h.hexdigest())) # return response content
log.debug('response: %s %s' % (self.cid, h.hexdigest())) # TODO: headers
log.debug('b4encoded: %s' % response)
reqheaders['authorization'] = response
r = requests.request(method=self.request.method, url=target_api, params=reqparams, data=self.request.body, headers=reqheaders, cookies=self.request.cookies)
self.response.write(r.content) self.response.write(r.content)
elif self.privkey is None or self.site_id is None:
log.debug('no private key (privkey), or local instance id (iid). cannot dispatch to remote')
def schema(self, *args, **kwargs): def schema(self, *args, **kwargs):
self.response.write(json.dumps(self.json_schema, default=bson.json_util.default)) self.response.write(json.dumps(self.json_schema, default=bson.json_util.default))
...@@ -7,4 +7,5 @@ wsgi-file = /var/local/nims/nimsapi/nimsapi.wsgi ...@@ -7,4 +7,5 @@ wsgi-file = /var/local/nims/nimsapi/nimsapi.wsgi
processes = 2 processes = 2
threads = 2 threads = 2
master = 1 master = 1
lazy = 1
# vacuum = 1 # vacuum = 1
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment