diff --git a/nimsapi.py b/nimsapi.py index db4f62e1fb5ec9d037e9b1041fdf8e00ee70c7f2..3733da09fcc3a84af5923c7002ffb537e7379830 100755 --- a/nimsapi.py +++ b/nimsapi.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# @author: Gunnar Schaefer +# @author: Gunnar Schaefer, Kevin S. Hahn import os import json @@ -10,7 +10,6 @@ import logging import pymongo import tarfile import webapp2 -import requests import zipfile import argparse import bson.json_util @@ -65,7 +64,7 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler): self.response.write('</style>\n') self.response.write('</head>\n') self.response.write('<body>\n') - self.response.write('<h1>nimsapi - ' + self.app.config['site_id'] + '</h1>\n') + self.response.write('<h1>nimsapi - %s </h1>\n' % self.site_id) self.response.write('<table class="tftable" border="1">\n') self.response.write('<tr><th>Resource</th><th>Description</th></tr>\n') for r, d in resources: @@ -347,10 +346,11 @@ class Remotes(nimsapiutil.NIMSRequestHandler): # query, user in userlist, _id does not match this site _id query = {'users': {'$in': [self.user['_id']]}, '_id': {'$ne': self.app.config['site_id']}} projection = ['_id'] + # if app has no site-id or pubkey, cannot fetch peer registry, and db.remotes will be empty remotes = list(self.app.db.remotes.find(query, projection)) data_remotes = [] # for list buildup for remote in remotes: - # use own API to delegate requests (hacky usage of unit tests) + # use own API to dispatch requests (hacky) response = self.app.get_response('/nimsapi/experiments?user=' + self.user['_id'] + '&iid=' + remote['_id'], headers=[('User-Agent', 'remotes_requestor')]) xpcount = len(json.loads(response.body)) if xpcount > 0: @@ -358,7 +358,7 @@ class Remotes(nimsapiutil.NIMSRequestHandler): data_remotes.append(remote['_id']) # return json encoded list of remote site '_id's - self.response.write(json.dumps([data_remote for data_remote in data_remotes], indent=4, separators=(',', ': '))) + self.response.write(json.dumps(data_remotes, indent=4, separators=(',', ': '))) class ArgumentParser(argparse.ArgumentParser): @@ -367,8 +367,8 @@ class ArgumentParser(argparse.ArgumentParser): super(ArgumentParser, self).__init__() self.add_argument('uri', help='NIMS DB URI') self.add_argument('stage_path', help='path to staging area') - self.add_argument('-k', '--pubkey', help='path to public SSL key') - self.add_argument('-u', '--uid', default='local', help='site UID') + self.add_argument('-k', '--pubkey', help='path to public SSL key file') + self.add_argument('-u', '--uid', help='site UID') 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('-q', '--quiet', action='store_true', default=False, help='disable console logging') @@ -415,10 +415,14 @@ app = webapp2.WSGIApplication(routes, debug=True) if __name__ == '__main__': args = ArgumentParser().parse_args() nimsutil.configure_log(args.logfile, not args.quiet, args.loglevel) - # TODO: if pubkey not specified, throw log.warning - + if args.pubkey: + pubkey = open(args.pubkey).read() # failure raises a sensible IOError + log.debug('SSL pubkey loaded') + else: + pubkey = None + log.warning('PUBKEY NOT SPECIFIED') from paste import httpserver - app.config = dict(stage_path=args.stage_path, site_id=args.uid, pubkey=args.pubkey) + app.config = dict(stage_path=args.stage_path, site_id=args.uid, pubkey=pubkey) 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') @@ -426,4 +430,4 @@ if __name__ == '__main__': # nimsapi.app.db = pymongo.MongoClient('mongodb://nims:cnimr750@slice.stanford.edu/nims').get_default_database() # response = webapp2.Request.blank('/nimsapi/local/users').get_response(nimsapi.app) # response.status -# response.body +# response.body \ No newline at end of file diff --git a/nimsapiutil.py b/nimsapiutil.py index c45c41d66a462deaadb830bed5e5ee03b3cbf9d3..bc8f2827e617ac3f1062eafab3a938eb3c018dc7 100644 --- a/nimsapiutil.py +++ b/nimsapiutil.py @@ -64,34 +64,37 @@ class NIMSRequestHandler(webapp2.RequestHandler): self.user_is_superuser = self.user.get('superuser') self.response.headers['Content-Type'] = 'application/json' self.target_id = self.request.get('iid', None) - self.site_id = self.app.config['site_id'] + 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 # 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)) + 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(':') - # log.info('{0} {1} {2}'.format(user, remote_site, digest)) + # look up pubkey from db.remotes projection = {'_id': False, 'pubkey': True} remote_pubkey = self.app.db.remotes.find_one({'_id': remote_site}, projection)['pubkey'] - # get the challenge from db.challenges + # look up challenge from db.challenges projection = {'_id': False, 'challenge': True} challenge = self.app.db.challenges.find_one({'_id': challenge_id}, projection)['challenge'] - # purge challenge (challenges are single use) + # delete challenge from db.challenges self.app.db.challenges.remove({'_id': challenge_id}) - # verify + # 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, @@ -107,15 +110,12 @@ class NIMSRequestHandler(webapp2.RequestHandler): def dispatch(self): """dispatching and request forwarding""" if self.target_id in [None, self.site_id]: - log.debug('{0} delegating to local {1}'.format(socket.gethostname(), self.request.url)) + log.debug('{0} dispatching to local {1}'.format(socket.gethostname(), self.request.url)) super(NIMSRequestHandler, self).dispatch() - else: - # WORK ON THIS SPOT - # capture error, and log. - self.pubkey = open(self.app.config['pubkey']).read() - # check if pubkey specified, throw error - - log.debug('{0} delegating to remote {1}'.format(socket.gethostname(), self.target_id)) + elif self.pubkey is None and self.site_id is None: + log.warning('target is %s, but no site ID, and no pubkey. cannot dispatch') + elif self.pubkey is not None and self.site_id is not None: + log.debug('{0} dispatching to remote {1}'.format(socket.gethostname(), self.target_id)) # is target registered? target = self.app.db.remotes.find_one({'_id': self.target_id}, {'_id':False, 'hostname':True}) if not target: @@ -123,17 +123,15 @@ class NIMSRequestHandler(webapp2.RequestHandler): self.abort(403, 'forbidden: site is not registered with interNIMS') self.cid = self.userid + ':' + self.site_id reqheaders = dict(self.request.headers) - # adjust the request, pass as much of orig request as possible reqheaders['User-Agent'] = 'NIMS Instance {0}'.format(self.site_id) del reqheaders['Host'] target_api = 'http://{0}{1}?{2}'.format(target['hostname'], self.request.path, self.request.query_string) reqparams = {'cid': self.cid} - - # TODO: error handling for host-down/host-unreachable # first attempt, expect 401, send as little as possible... + # TODO: error handling for host-down/host-unreachable + # TODO: timeout? r = requests.request(method=self.request.method, url=target_api, params=reqparams, headers=reqheaders) - if r.status_code == 401: challenge = base64.b64decode(r.headers['www-authenticate']) log.debug('Authorization requested - challenge: %s' % challenge) @@ -143,8 +141,7 @@ class NIMSRequestHandler(webapp2.RequestHandler): 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) 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)) \ No newline at end of file