2
0
mirror of https://github.com/xcat2/confluent.git synced 2025-01-18 21:53:18 +00:00
confluent/confluent/httpapi.py

362 lines
13 KiB
Python
Raw Normal View History

# Copyright (C) IBM 2013
# All rights reserved
# This SCGI server provides a http wrap to confluent api
# It additionally manages httprequest console sessions as supported by
# shillinabox javascript
import base64
import Cookie
import confluent.auth as auth
import confluent.consoleserver as consoleserver
import confluent.exceptions as exc
import confluent.messages
import confluent.pluginapi as pluginapi
import confluent.util as util
import eventlet
import json
import os
import string
import traceback
import time
2013-08-16 16:37:19 -04:00
import urlparse
import eventlet.wsgi
#scgi = eventlet.import_patched('flup.server.scgi')
consolesessions = {}
httpsessions = {}
opmap = {
'POST': 'create',
'GET': 'retrieve',
'PUT': 'update',
'DELETE': 'delete',
}
def _sessioncleaner():
while (1):
currtime = time.time()
for session in httpsessions.keys():
if httpsessions[session]['expiry'] < currtime:
del httpsessions[session]
for session in consolesessions.keys():
if consolesessions[session]['expiry'] < currtime:
del consolesessions[session]
eventlet.sleep(10)
def _get_query_dict(env, reqbody, reqtype):
qdict = {}
try:
qstring = env['QUERY_STRING']
except KeyError:
qstring = None
if qstring:
for qpair in qstring.split('&'):
qkey, qvalue = qpair.split('=')
qdict[qkey] = qvalue
2013-08-16 16:37:19 -04:00
if reqbody is not None:
if "application/x-www-form-urlencoded" in reqtype:
pbody = urlparse.parse_qs(reqbody, True)
for ky in pbody.iterkeys():
if len(pbody[ky]) > 1: # e.g. REST explorer
na = [i for i in pbody[ky] if i != '']
qdict[ky] = na
else:
qdict[ky] = pbody[ky][0]
if 'restexplorerhonorkey' in qdict:
nqdict = {}
for key in qdict:
if key == 'restexplorerop':
nqdict[key] = qdict['restexplorerop']
continue
if key in qdict['restexplorerhonorkey']:
nqdict[key] = qdict[key]
qdict = nqdict
return qdict
def _authorize_request(env):
"""Grant/Deny access based on data from wsgi env
"""
authdata = False
cookie = Cookie.SimpleCookie()
if 'HTTP_COOKIE' in env:
#attempt to use the cookie. If it matches
cc = Cookie.SimpleCookie()
cc.load(env['HTTP_COOKIE'])
if 'confluentsessionid' in cc:
sessionid = cc['confluentsessionid'].value
if sessionid in httpsessions:
httpsessions[sessionid]['expiry'] = time.time() + 90
name = httpsessions[sessionid]['name']
authdata = auth.authorize(name, element=None)
if authdata is False and 'HTTP_AUTHORIZATION' in env:
name, passphrase = base64.b64decode(
env['HTTP_AUTHORIZATION'].replace('Basic ','')).split(':',1)
authdata = auth.check_user_passphrase(name, passphrase, element=None)
sessid = util.randomstring(32)
while sessid in httpsessions:
sessid = util.randomstring(32)
httpsessions[sessid] = {'name': name, 'expiry': time.time() + 90}
cookie['confluentsessionid']=sessid
cookie['confluentsessionid']['secure'] = 1
cookie['confluentsessionid']['httponly'] = 1
cookie['confluentsessionid']['path'] = '/'
if authdata:
return {'code': 200,
'cookie': cookie,
'cfgmgr': authdata[1],
'userdata': authdata[0]}
else:
return {'code': 401}
# TODO(jbjohnso): actually evaluate the request for authorization
# In theory, the x509 or http auth stuff will get translated and then
# passed on to the core authorization function in an appropriate form
# expresses return in the form of http code
# 401 if there is no known identity
# 403 if valid identity, but no access
# going to run 200 just to get going for now
def _pick_mimetype(env):
"""Detect the http indicated mime to send back.
Note that as it gets into the ACCEPT header honoring, it only looks for
application/json and else gives up and assumes html. This is because
browsers are very chaotic about ACCEPT HEADER. It is assumed that
XMLHttpRequest.setRequestHeader will be used by clever javascript
if the '.json' scheme doesn't cut it.
"""
if env['PATH_INFO'].endswith('.json'):
return 'application/json; charset=utf-8'
elif env['PATH_INFO'].endswith('.html'):
return 'text/html'
elif 'application/json' in env['HTTP_ACCEPT']:
return 'application/json; charset=utf-8'
else:
return 'text/html'
def _assign_consessionid(consolesession):
sessid = util.randomstring(32)
while sessid in consolesessions.keys():
sessid = util.randomstring(32)
consolesessions[sessid] = {'session': consolesession,
'expiry': time.time() + 60}
return sessid
def resourcehandler(env, start_response):
"""Function to handle new wsgi requests
"""
authorized = _authorize_request(env)
mimetype = _pick_mimetype(env)
2013-08-16 16:37:19 -04:00
reqbody = None
reqtype = None
if 'CONTENT_LENGTH' in env and int(env['CONTENT_LENGTH']) > 0:
2013-08-16 16:37:19 -04:00
reqbody = env['wsgi.input'].read(int(env['CONTENT_LENGTH']))
reqtype = env['CONTENT_TYPE']
if authorized['code'] == 401:
start_response('401 Authentication Required',
[('Content-type', 'text/plain'),
('WWW-Authenticate', 'Basic realm="confluent"')])
yield 'authentication required'
return
if authorized['code'] == 403:
start_response('403 Forbidden',
[('Content-type', 'text/plain'),
('WWW-Authenticate', 'Basic realm="confluent"')])
yield 'authorization failed'
return
if authorized['code'] != 200:
raise Exception("Unrecognized code from auth engine")
headers = [('Content-Type', mimetype) ]
headers.extend(("Set-Cookie", m.OutputString())
for m in authorized['cookie'].values())
2013-09-06 16:15:59 -04:00
cfgmgr = authorized['cfgmgr']
operation = opmap[env['REQUEST_METHOD']]
querydict = _get_query_dict(env, reqbody, reqtype)
if 'restexplorerop' in querydict:
operation = querydict['restexplorerop']
del querydict['restexplorerop']
if '/console/session' in env['PATH_INFO']:
2013-08-16 16:37:19 -04:00
#hard bake JSON into this path, do not support other incarnations
prefix, _, _ = env['PATH_INFO'].partition('/console/session')
_, _, nodename = prefix.rpartition('/')
if 'session' not in querydict.keys() or not querydict['session']:
# Request for new session
consession = consoleserver.ConsoleSession(node=nodename,
configmanager=cfgmgr)
if not consession:
start_response("500 Internal Server Error", headers)
return
sessid = _assign_consessionid(consession)
start_response('200 OK', headers)
yield '{"session":"%s","data":""}' % sessid
return
elif 'keys' in querydict.keys():
# client wishes to push some keys into the remote console
input = ""
for idx in xrange(0, len(querydict['keys']), 2):
input += chr(int(querydict['keys'][idx:idx+2],16))
sessid = querydict['session']
consolesessions[sessid]['expiry'] = time.time() + 90
consolesessions[sessid]['session'].write(input)
start_response('200 OK', headers)
return # client has requests to send or receive, not both...
else: #no keys, but a session, means it's hooking to receive data
sessid = querydict['session']
consolesessions[sessid]['expiry'] = time.time() + 90
outdata = consolesessions[sessid]['session'].get_next_output(timeout=45)
try:
rsp = json.dumps({'session': querydict['session'], 'data': outdata})
except UnicodeDecodeError:
rsp = json.dumps({'session': querydict['session'], 'data': outdata}, encoding='cp437')
except UnicodeDecodeError:
rsp = json.dumps({'session': querydict['session'], 'data': 'DECODEERROR'})
start_response('200 OK', headers)
yield rsp
return
else:
# normal request
2013-11-03 00:02:13 -04:00
url = env['PATH_INFO']
url = url.replace('.json', '')
url = url.replace('.html', '')
resource = '.' + url[url.rindex('/'):]
2013-10-15 21:13:48 -04:00
try:
2013-11-03 00:02:13 -04:00
hdlr = pluginapi.handle_path(url, operation,
cfgmgr, querydict)
except exc.NotFoundException:
2013-10-15 21:13:48 -04:00
start_response('404 Not found', headers)
yield "404 - Request path not recognized"
return
except exc.InvalidArgumentException:
traceback.print_exc()
start_response('400 Bad Request', headers)
yield '400 - Bad Request'
return
start_response('200 OK', headers)
if mimetype == 'text/html':
for datum in _assemble_html(hdlr, resource, querydict, url):
yield datum
else:
for datum in _assemble_json(hdlr, resource, url):
yield datum
def _assemble_html(responses, resource, querydict, url):
yield '<html><head><title>'
yield 'Confluent REST Explorer: ' + resource + '</title></head>'
yield '<body><form action="' + resource + '" method="post">'
if querydict:
yield 'Response to input data:<br>'
yield json.dumps(querydict, separators=(',', ': '),
indent=4, sort_keys=True)
yield '<hr>'
yield 'Only fields that have their boxes checked will have their '
yield 'respective values honored by the confluent server.<hr>'
yield '<input type="hidden" name="restexplorerop" value="update">'
yield '<input type="hidden" name="restexplorerhonorkey" value="">'
yield '<a rel="self" href="%s">%s</a><br>' % (resource, resource)
if url == '/':
pass
elif resource[-1] == '/':
yield '<a rel="collection" href="../">../</a><br>'
else:
yield '<a rel="collection" href="./">./</a><br>'
pendingrsp = []
for rsp in responses:
if isinstance(rsp, confluent.messages.LinkRelation):
yield rsp.html()
yield "<br>"
else:
pendingrsp.append(rsp)
for rsp in pendingrsp:
yield rsp.html()
yield "<br>"
yield '<input value="PUT" type="submit"></form></body></html>'
def _assemble_json(responses, resource, url):
#NOTE(jbjohnso) I'm considering giving up on yielding bit by bit
#in json case over http. Notably, duplicate key values from plugin
#overwrite, but we'd want to preserve them into an array instead.
#the downside is that http would just always blurt it ll out at
#once and hold on to all the data in memory
docomma = False
links = {
'self': ['{"href":"%s"}' % resource],
}
if url == '/':
pass
elif resource[-1] == '/':
links['collection'] = ['{"href":"%s"}' % '../']
else:
links['collection'] = ['{"href":"%s"}' % './']
yield '{'
hadrsp = False
for rsp in responses:
if isinstance(rsp, confluent.messages.LinkRelation):
haldata = rsp.json_hal()
for hk in haldata.iterkeys():
if hk in links:
links[hk].append(haldata[hk])
else:
links[hk] = [haldata[hk]]
continue
hadrsp = True
if docomma:
yield ','
else:
docomma = True
yield rsp.json()
docomma = False
if hadrsp:
yield ','
yield '"_links": {'
groupcomma = False
for link in links.iterkeys():
if groupcomma:
yield ','
else:
groupcomma = True
yield json.dumps(link) + ":"
if len(links[link]) == 1:
yield links[link][0]
else:
yield '['
for lk in links[link]:
if docomma:
yield ','
else:
docomma = True
yield lk
yield ']'
yield '}'
yield '}'
def serve():
# TODO(jbjohnso): move to unix socket and explore
# either making apache deal with it
# or just supporting nginx or lighthttpd
# for now, http port access
#scgi.WSGIServer(resourcehandler, bindAddress=("localhost",4004)).run()
#based on a bakeoff perf wise, eventlet http support proxied actually did
#edge out patched flup. unpatched flup was about the same as eventlet http
#but deps are simpler without flup
#also, the potential for direct http can be handy
#todo remains unix domain socket for even http
eventlet.wsgi.server(eventlet.listen(("",4005)),resourcehandler)
class HttpApi(object):
def start(self):
self.server = eventlet.spawn(serve)
_cleaner = eventlet.spawn(_sessioncleaner)