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
import markdown
import bson.json_util
import webapp2_extras.routes
import Crypto.PublicKey.RSA
import nimsutil
......@@ -37,7 +38,8 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler):
def get(self):
"""Return API documentation"""
resource = """Resource | Description
resources = """
Resource | Description
:-------------------------------------------------|:-----------------------
/nimsapi/download | download
/nimsapi/dump | dump
......@@ -69,7 +71,7 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler):
[(/nimsapi/epochs/listschema)] | schema for epoch list
[(/nimsapi/epochs/schema)] | schema for single epoch
/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.write('<html>\n')
self.response.write('<head>\n')
......@@ -88,7 +90,7 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler):
self.response.write('</style>\n')
self.response.write('</head>\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('</html>\n')
......@@ -390,7 +392,7 @@ 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 file')
self.add_argument('--privkey', help='path to private 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')
......@@ -438,14 +440,16 @@ app = webapp2.WSGIApplication(routes, debug=True)
if __name__ == '__main__':
args = ArgumentParser().parse_args()
nimsutil.configure_log(args.logfile, not args.quiet, args.loglevel)
if args.pubkey:
pubkey = open(args.pubkey).read() # failure raises a sensible IOError
log.debug('SSL pubkey loaded')
if args.privkey:
privkey = Crypto.PublicKey.RSA.importKey(open(args.privkey).read())
log.debug('SSL private key loaded')
else:
pubkey = None
log.warning('PUBKEY NOT SPECIFIED')
privkey = None
log.warning('PRIVKEY NOT SPECIFIED')
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()
httpserver.serve(app, host=httpserver.socket.gethostname(), port='8080')
......
......@@ -15,9 +15,10 @@ import pymongo
import webapp2
import nimsapi
import nimsutil
import Crypto.PublicKey.RSA
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'
stage_path = '/scratch/upload'
......@@ -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)
try:
pubkey = open(pubkey_file).read() # FIXME: don't read too much
# FIXME: verify that this is a valid public key
except IOError:
pubkey = None
privkey = Crypto.PublicKey.RSA.importKey(open(privkey_file).read())
except ValueError as e:
privkey = None
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()
......@@ -8,13 +8,15 @@ import webapp2
import datetime
import requests
import bson.json_util
import Crypto.Hash.HMAC
import Crypto.Random.random
import Crypto.Hash.SHA
import Crypto.PublicKey.RSA
import Crypto.Signature.PKCS1_v1_5
log = logging.getLogger('nimsapi')
requests_log = logging.getLogger('requests') # configure Requests logging
requests_log.setLevel(logging.WARNING) # set level to WARNING (default is INFO)
class NIMSRequestHandler(webapp2.RequestHandler):
"""fetches pubkey from own self.db.remotes. needs to be aware of OWN site uid"""
......@@ -62,86 +64,72 @@ class NIMSRequestHandler(webapp2.RequestHandler):
self.userid = self.request.remote_user or '@public'
self.user = self.app.db.users.find_one({'_id': self.userid})
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.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))
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']))
self.privkey = self.app.config.get('privkey') # is ALREADY 'None' if not specified in args, never empty
def dispatch(self):
"""dispatching and request forwarding"""
# dispatch to local instance
if self.target_id in [None, self.site_id]:
log.debug('{0} dispatching to local {1}'.format(socket.gethostname(), self.request.url))
super(NIMSRequestHandler, self).dispatch()
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))
log.debug(socket.gethostname() + ' dispatching to local ' + self.request.url)
# request originates from remote instance
if self.request.user_agent.startswith('NIMS Instance'):
# is the requester an authorized remote site
requester = self.request.user_agent.replace('NIMS Instance', '').strip()
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?
target = self.app.db.remotes.find_one({'_id': self.target_id}, {'_id':False, 'hostname':True})
if not target:
log.debug('remote host {0} not in auth list. DENIED'.format(self.target_id))
self.abort(403, 'forbidden: site is not registered with interNIMS')
self.cid = self.userid + ':' + self.site_id
log.debug('remote host ' + self.target_id + ' not in auth list. DENIED')
self.abort(403, self.target_id + 'is not authorized')
# disassemble the incoming request
reqparams = dict(self.request.params)
reqpayload = self.request.body # request payload, almost always empty
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}
# 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)
h = Crypto.Hash.HMAC.new(self.pubkey, challenge)
response = base64.b64encode('%s %s' % (self.cid, h.hexdigest()))
log.debug('response: %s %s' % (self.cid, h.hexdigest()))
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)
reqheaders['User-Agent'] = 'NIMS Instance ' + self.site_id
del reqheaders['Host'] # delete old host destination
# create a signature of the incoming request payload
h = Crypto.Hash.SHA.new(reqpayload)
signature = Crypto.Signature.PKCS1_v1_5.new(self.privkey).sign(h)
reqheaders['Authorization'] = base64.b64encode(signature)
# construct outgoing request
target_api = 'http://' + target['hostname'] + self.request.path # TODO: switch to https
# target_api = 'https://' + target['hostname'] + self.request.path)
r = requests.request(method=self.request.method, data=reqpayload, url=target_api, params=reqparams, headers=reqheaders, verify=False)
# return response content
# TODO: headers
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):
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
processes = 2
threads = 2
master = 1
lazy = 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