2013-08-09 16:59:08 -04:00
|
|
|
# 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
|
2013-10-09 16:17:37 -04:00
|
|
|
import Cookie
|
2013-09-04 15:40:35 -04:00
|
|
|
import confluent.auth as auth
|
2013-10-12 18:04:27 -04:00
|
|
|
import confluent.console as console
|
|
|
|
import confluent.pluginapi as pluginapi
|
2013-08-09 16:59:08 -04:00
|
|
|
import confluent.util as util
|
|
|
|
import eventlet
|
2013-09-13 16:07:39 -04:00
|
|
|
import json
|
2013-08-09 16:59:08 -04:00
|
|
|
import os
|
|
|
|
import string
|
2013-10-09 16:17:37 -04:00
|
|
|
import time
|
2013-08-16 16:37:19 -04:00
|
|
|
import urlparse
|
2013-10-03 17:05:40 -04:00
|
|
|
import eventlet.wsgi
|
|
|
|
#scgi = eventlet.import_patched('flup.server.scgi')
|
2013-08-09 16:59:08 -04:00
|
|
|
|
|
|
|
|
|
|
|
consolesessions = {}
|
2013-10-09 16:17:37 -04:00
|
|
|
httpsessions = {}
|
2013-08-09 16:59:08 -04:00
|
|
|
|
|
|
|
|
2013-10-09 16:17:37 -04:00
|
|
|
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):
|
2013-08-09 16:59:08 -04:00
|
|
|
qdict = {}
|
2013-10-09 16:17:37 -04:00
|
|
|
try:
|
|
|
|
qstring = env['QUERY_STRING']
|
|
|
|
except KeyError:
|
|
|
|
qstring = None
|
2013-09-12 16:54:39 -04:00
|
|
|
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:
|
2013-09-12 16:54:39 -04:00
|
|
|
if "application/x-www-form-urlencoded" in reqtype:
|
2013-09-13 11:45:17 -04:00
|
|
|
pbody = urlparse.parse_qs(reqbody)
|
|
|
|
for ky in pbody.iterkeys():
|
|
|
|
qdict[ky] = pbody[ky][0]
|
2013-08-09 16:59:08 -04:00
|
|
|
return qdict
|
|
|
|
|
|
|
|
|
|
|
|
def _authorize_request(env):
|
|
|
|
"""Grant/Deny access based on data from wsgi env
|
|
|
|
|
|
|
|
"""
|
2013-10-09 16:17:37 -04:00
|
|
|
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}
|
2013-08-09 16:59:08 -04:00
|
|
|
# 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
|
2013-08-18 19:11:49 -04:00
|
|
|
browsers are very chaotic about ACCEPT HEADER. It is assumed that
|
2013-08-09 16:59:08 -04:00
|
|
|
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'
|
|
|
|
elif env['PATH_INFO'].endswith('.html'):
|
|
|
|
return 'text/html'
|
|
|
|
elif 'application/json' in env['HTTP_ACCEPT']:
|
|
|
|
return 'application/json'
|
|
|
|
else:
|
|
|
|
return 'text/html'
|
|
|
|
|
|
|
|
|
|
|
|
def _assign_consessionid(consolesession):
|
2013-10-09 16:17:37 -04:00
|
|
|
sessid = util.randomstring(32)
|
2013-08-09 16:59:08 -04:00
|
|
|
while sessid in consolesessions.keys():
|
2013-10-09 16:17:37 -04:00
|
|
|
sessid = util.randomstring(32)
|
|
|
|
consolesessions[sessid] = {'session': consolesession,
|
|
|
|
'expiry': time.time() + 60}
|
2013-08-09 16:59:08 -04:00
|
|
|
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
|
2013-09-04 15:40:35 -04:00
|
|
|
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']
|
2013-09-04 15:40:35 -04:00
|
|
|
if authorized['code'] == 401:
|
|
|
|
start_response('401 Authentication Required',
|
|
|
|
[('Content-type', 'text/plain'),
|
2013-10-09 16:17:37 -04:00
|
|
|
('WWW-Authenticate', 'Basic realm="confluent"')])
|
2013-10-13 20:51:30 -04:00
|
|
|
yield 'authentication required'
|
|
|
|
return
|
2013-09-04 15:40:35 -04:00
|
|
|
if authorized['code'] == 403:
|
|
|
|
start_response('403 Forbidden',
|
|
|
|
[('Content-type', 'text/plain'),
|
2013-10-09 16:17:37 -04:00
|
|
|
('WWW-Authenticate', 'Basic realm="confluent"')])
|
2013-10-13 20:51:30 -04:00
|
|
|
yield 'authorization failed'
|
|
|
|
return
|
2013-09-04 15:40:35 -04:00
|
|
|
if authorized['code'] != 200:
|
|
|
|
raise Exception("Unrecognized code from auth engine")
|
2013-10-09 16:17:37 -04:00
|
|
|
headers = [('Content-Type', 'application/json; charset=utf-8')]
|
|
|
|
headers.extend(("Set-Cookie", m.OutputString())
|
|
|
|
for m in authorized['cookie'].values())
|
2013-09-06 16:15:59 -04:00
|
|
|
cfgmgr = authorized['cfgmgr']
|
2013-10-09 16:17:37 -04:00
|
|
|
querydict = _get_query_dict(env, reqbody, reqtype)
|
2013-08-09 16:59:08 -04:00
|
|
|
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
|
2013-08-09 16:59:08 -04:00
|
|
|
prefix, _, _ = env['PATH_INFO'].partition('/console/session')
|
|
|
|
_, _, nodename = prefix.rpartition('/')
|
|
|
|
if 'session' not in querydict.keys() or not querydict['session']:
|
|
|
|
# Request for new session
|
2013-09-04 15:40:35 -04:00
|
|
|
consession = console.ConsoleSession(node=nodename,
|
|
|
|
configmanager=cfgmgr)
|
2013-08-09 16:59:08 -04:00
|
|
|
if not consession:
|
2013-10-09 16:17:37 -04:00
|
|
|
start_response("500 Internal Server Error", headers)
|
2013-08-09 16:59:08 -04:00
|
|
|
return
|
|
|
|
sessid = _assign_consessionid(consession)
|
2013-10-09 16:17:37 -04:00
|
|
|
start_response('200 OK', headers)
|
2013-10-13 20:51:30 -04:00
|
|
|
yield '{"session":"%s","data":""}' % sessid
|
|
|
|
return
|
2013-09-12 16:54:39 -04:00
|
|
|
elif 'keys' in querydict.keys():
|
|
|
|
# client wishes to push some keys into the remote console
|
|
|
|
input = ""
|
2013-09-13 16:07:39 -04:00
|
|
|
for idx in xrange(0, len(querydict['keys']), 2):
|
|
|
|
input += chr(int(querydict['keys'][idx:idx+2],16))
|
2013-09-12 16:54:39 -04:00
|
|
|
sessid = querydict['session']
|
2013-10-09 16:17:37 -04:00
|
|
|
consolesessions[sessid]['expiry'] = time.time() + 90
|
|
|
|
consolesessions[sessid]['session'].write(input)
|
|
|
|
start_response('200 OK', headers)
|
2013-10-13 20:51:30 -04:00
|
|
|
return # client has requests to send or receive, not both...
|
2013-09-12 16:54:39 -04:00
|
|
|
else: #no keys, but a session, means it's hooking to receive data
|
2013-09-13 11:45:17 -04:00
|
|
|
sessid = querydict['session']
|
2013-10-09 16:17:37 -04:00
|
|
|
consolesessions[sessid]['expiry'] = time.time() + 90
|
|
|
|
outdata = consolesessions[sessid]['session'].get_next_output(timeout=45)
|
2013-09-15 11:19:04 -04:00
|
|
|
try:
|
|
|
|
rsp = json.dumps({'session': querydict['session'], 'data': outdata})
|
|
|
|
except UnicodeDecodeError:
|
2013-09-20 14:36:55 -04:00
|
|
|
rsp = json.dumps({'session': querydict['session'], 'data': outdata}, encoding='cp437')
|
2013-09-15 11:19:04 -04:00
|
|
|
except UnicodeDecodeError:
|
|
|
|
rsp = json.dumps({'session': querydict['session'], 'data': 'DECODEERROR'})
|
2013-10-09 16:17:37 -04:00
|
|
|
start_response('200 OK', headers)
|
2013-10-13 20:51:30 -04:00
|
|
|
yield rsp
|
|
|
|
return
|
2013-10-12 18:04:27 -04:00
|
|
|
else:
|
2013-10-13 20:51:30 -04:00
|
|
|
start_response('200 OK', headers)
|
2013-10-15 21:13:48 -04:00
|
|
|
try:
|
|
|
|
hdlr = pluginapi.handle_path(env['PATH_INFO'], 'retrieve', cfgmgr)
|
|
|
|
except:
|
|
|
|
start_response('404 Not found', headers)
|
|
|
|
yield "404 - Request path not recognized"
|
|
|
|
return
|
|
|
|
for rsp in hdlr:
|
2013-11-02 13:06:48 -04:00
|
|
|
yield rsp.json()
|
2013-08-09 16:59:08 -04:00
|
|
|
|
2013-09-14 20:21:58 -04:00
|
|
|
|
|
|
|
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
|
2013-10-03 17:05:40 -04:00
|
|
|
#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)
|
2013-09-14 20:21:58 -04:00
|
|
|
|
|
|
|
|
2013-08-09 16:59:08 -04:00
|
|
|
class HttpApi(object):
|
|
|
|
def start(self):
|
2013-09-14 20:21:58 -04:00
|
|
|
self.server = eventlet.spawn(serve)
|
2013-08-09 16:59:08 -04:00
|
|
|
|
2013-10-09 16:17:37 -04:00
|
|
|
_cleaner = eventlet.spawn(_sessioncleaner)
|
|
|
|
|
2013-08-09 16:59:08 -04:00
|
|
|
|
|
|
|
|
|
|
|
|