2017-03-03 00:04:12 +00:00
|
|
|
# Captiveportal
|
|
|
|
# This is the web API the portal website speaks to and requests new clients
|
|
|
|
# to be added by running jobs in rq.
|
2016-04-15 17:22:16 +00:00
|
|
|
|
|
|
|
import json
|
2016-12-14 15:37:32 +00:00
|
|
|
from pprint import pprint as pp
|
2016-04-15 20:12:17 +00:00
|
|
|
from uuid import UUID
|
2016-04-15 17:22:16 +00:00
|
|
|
from importlib import import_module
|
2016-12-14 23:17:18 +00:00
|
|
|
from logging import Formatter, getLogger, DEBUG, INFO, WARN
|
|
|
|
from logging.handlers import SysLogHandler, RotatingFileHandler
|
2016-04-16 14:50:35 +00:00
|
|
|
|
2016-04-16 16:48:39 +00:00
|
|
|
# Until pyvenv-3.4 is fixed on centos 7 support python 2.
|
2016-04-16 14:50:35 +00:00
|
|
|
try:
|
|
|
|
from configparser import RawConfigParser
|
|
|
|
except ImportError:
|
|
|
|
from ConfigParser import RawConfigParser
|
|
|
|
|
2016-04-15 17:22:16 +00:00
|
|
|
from redis import Redis
|
|
|
|
from rq import Queue
|
2016-12-17 22:16:19 +00:00
|
|
|
from bottle import Bottle, default_app, debug
|
2016-04-15 17:22:16 +00:00
|
|
|
from bottle import request, response, template, static_file
|
2016-04-05 14:40:37 +00:00
|
|
|
|
|
|
|
config = RawConfigParser()
|
|
|
|
config.readfp(open('./portal.cfg'))
|
|
|
|
config.read(['/etc/captiveportal/portal.cfg', './portal_local.cfg'])
|
|
|
|
|
2016-04-15 17:22:16 +00:00
|
|
|
# Plugins configuration is separate so plugins can be disabled by having
|
|
|
|
# their section removed/commented from the config file.
|
2016-04-18 19:33:54 +00:00
|
|
|
plugin_defaults = {
|
|
|
|
'enabled': 'False',
|
|
|
|
'mandatory': 'True',
|
|
|
|
'debug': 'False'
|
|
|
|
}
|
|
|
|
plugin_config = RawConfigParser(defaults=plugin_defaults)
|
2016-04-15 17:22:16 +00:00
|
|
|
plugin_config.readfp(open('./plugins.cfg'))
|
2016-04-18 16:13:57 +00:00
|
|
|
plugin_config.read(['/etc/captiveportal/plugins.cfg', './plugins_local.cfg'])
|
2016-04-15 17:22:16 +00:00
|
|
|
|
2016-04-05 14:40:37 +00:00
|
|
|
# Setup logging
|
2016-04-15 17:22:16 +00:00
|
|
|
logFormatter = Formatter(config.get('logging', 'log_format'))
|
2016-04-05 14:40:37 +00:00
|
|
|
l = getLogger('captiveportal')
|
|
|
|
if config.get('logging', 'log_handler') == 'syslog':
|
|
|
|
syslog_address = config.get('logging', 'syslog_address')
|
|
|
|
|
|
|
|
if syslog_address.startswith('/'):
|
2016-04-15 17:22:16 +00:00
|
|
|
logHandler = SysLogHandler(
|
2016-04-05 14:40:37 +00:00
|
|
|
address=syslog_address,
|
|
|
|
facility=SysLogHandler.LOG_LOCAL0
|
|
|
|
)
|
|
|
|
else:
|
2016-04-15 17:22:16 +00:00
|
|
|
logHandler = SysLogHandler(
|
2016-04-05 14:40:37 +00:00
|
|
|
address=(
|
|
|
|
config.get('logging', 'syslog_address'),
|
|
|
|
config.getint('logging', 'syslog_port')
|
|
|
|
),
|
|
|
|
facility=SysLogHandler.LOG_LOCAL0
|
|
|
|
)
|
|
|
|
else:
|
2016-04-15 17:22:16 +00:00
|
|
|
logHandler = RotatingFileHandler(
|
2016-04-05 14:40:37 +00:00
|
|
|
config.get('logging', 'log_file'),
|
|
|
|
maxBytes=config.getint('logging', 'log_max_bytes'),
|
|
|
|
backupCount=config.getint('logging', 'log_max_copies')
|
|
|
|
)
|
2016-04-15 17:22:16 +00:00
|
|
|
logHandler.setFormatter(logFormatter)
|
|
|
|
l.addHandler(logHandler)
|
2016-04-05 14:40:37 +00:00
|
|
|
|
|
|
|
if config.get('logging', 'log_debug'):
|
|
|
|
l.setLevel(DEBUG)
|
|
|
|
else:
|
|
|
|
l.setLevel(WARN)
|
|
|
|
|
2016-04-15 17:22:16 +00:00
|
|
|
# Redis Queue
|
|
|
|
R = Redis(
|
|
|
|
host=config.get('portal', 'redis_host'),
|
|
|
|
port=config.getint('portal', 'redis_port')
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2016-04-15 20:12:17 +00:00
|
|
|
# Custom UUID route filter for bottle.py
|
|
|
|
def uuid_filter(config):
|
2016-04-16 14:50:35 +00:00
|
|
|
# Should catch UUIDv4 type strings
|
2016-04-15 20:12:17 +00:00
|
|
|
regexp = r'[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}'
|
2016-04-05 14:40:37 +00:00
|
|
|
|
2016-04-15 20:12:17 +00:00
|
|
|
def to_python(match):
|
|
|
|
return UUID(match, version=4)
|
2016-04-15 17:22:16 +00:00
|
|
|
|
2016-04-15 20:12:17 +00:00
|
|
|
def to_url(uuid):
|
|
|
|
return str(uuid)
|
2016-04-05 14:40:37 +00:00
|
|
|
|
2016-04-15 20:12:17 +00:00
|
|
|
return regexp, to_python, to_url
|
2016-04-05 14:40:37 +00:00
|
|
|
|
2016-04-15 17:22:16 +00:00
|
|
|
|
|
|
|
# Add plugins to job queue
|
|
|
|
def dispatch_plugins():
|
|
|
|
Q = Queue(connection=R)
|
2016-04-18 19:33:54 +00:00
|
|
|
jobs = {}
|
2016-04-15 17:22:16 +00:00
|
|
|
|
|
|
|
for plugin in plugin_config.sections():
|
|
|
|
l.debug('Loading plugin {plugin}'.format(
|
|
|
|
plugin=plugin
|
2016-04-05 14:40:37 +00:00
|
|
|
))
|
|
|
|
|
2016-04-15 17:22:16 +00:00
|
|
|
arg = {}
|
2016-04-05 14:40:37 +00:00
|
|
|
|
2016-04-15 17:22:16 +00:00
|
|
|
# Import some values from WSGI environ
|
|
|
|
arg['environ'] = {}
|
|
|
|
for key in request.environ:
|
|
|
|
value = request.environ.get(key)
|
|
|
|
if isinstance(value, (int, str, float, dict, set, tuple)):
|
|
|
|
arg['environ'][key] = value
|
2016-04-05 14:40:37 +00:00
|
|
|
|
2016-04-16 17:09:22 +00:00
|
|
|
# Import all the plugin configuration values as OrderedDict
|
|
|
|
config_sections = plugin_config._sections
|
2016-04-18 16:13:57 +00:00
|
|
|
arg['config'] = config_sections[plugin]
|
2016-04-17 09:21:37 +00:00
|
|
|
|
|
|
|
# Is plugin enabled?
|
|
|
|
if not plugin_config.getboolean(plugin, 'enabled'):
|
2016-04-17 09:23:50 +00:00
|
|
|
l.debug('{plugin}: Not enabled, skipping'.format(
|
|
|
|
plugin=plugin
|
|
|
|
))
|
2016-04-17 09:21:37 +00:00
|
|
|
continue
|
2016-04-16 17:09:22 +00:00
|
|
|
|
2016-04-16 16:48:39 +00:00
|
|
|
# Import the plugin
|
|
|
|
try:
|
|
|
|
plugin_module = import_module('plugins.'+plugin)
|
|
|
|
except Exception as e:
|
|
|
|
l.warn('{plugin}: failed import: {error}'.format(
|
|
|
|
plugin=plugin,
|
|
|
|
error=str(e)
|
|
|
|
))
|
|
|
|
continue
|
|
|
|
|
2016-12-14 15:37:32 +00:00
|
|
|
# Let plugin run for 30 more seconds than the defined plugin_timeout
|
|
|
|
# because that value is also used by the JS code to poll the job
|
|
|
|
# status so we don't want rq to kill the job before JS has timed out.
|
|
|
|
plugin_timeout = config.getint('portal', 'plugin_timeout')+30
|
|
|
|
|
2016-12-22 14:40:25 +00:00
|
|
|
# Queue plugin.run()
|
2016-04-05 14:40:37 +00:00
|
|
|
try:
|
2016-04-15 17:22:16 +00:00
|
|
|
plugin_job = Q.enqueue(
|
|
|
|
plugin_module.run,
|
2016-04-16 14:50:35 +00:00
|
|
|
arg,
|
2016-12-14 15:37:32 +00:00
|
|
|
timeout=plugin_timeout
|
2016-04-05 14:40:37 +00:00
|
|
|
)
|
|
|
|
except Exception as e:
|
2016-04-16 16:48:39 +00:00
|
|
|
l.warn('{plugin}: {error}'.format(
|
2016-04-15 17:22:16 +00:00
|
|
|
error=str(e),
|
|
|
|
plugin=plugin
|
2016-04-05 14:40:37 +00:00
|
|
|
))
|
|
|
|
continue
|
|
|
|
|
2016-04-18 19:33:54 +00:00
|
|
|
plugin_meta = {}
|
|
|
|
plugin_meta['mandatory'] = plugin_config.getboolean(
|
|
|
|
plugin,
|
|
|
|
'mandatory'
|
|
|
|
)
|
|
|
|
plugin_job.meta = plugin_meta
|
|
|
|
plugin_job.save()
|
|
|
|
|
|
|
|
jobs[plugin] = {
|
|
|
|
'id': plugin_job.id
|
|
|
|
}
|
2016-04-15 20:12:17 +00:00
|
|
|
|
|
|
|
return jobs
|
|
|
|
|
|
|
|
|
2016-04-16 14:50:35 +00:00
|
|
|
# Define app so we can add a custom filter to app.router
|
2016-04-15 20:12:17 +00:00
|
|
|
app = Bottle()
|
|
|
|
app.router.add_filter('uuid', uuid_filter)
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/')
|
|
|
|
def portalindex():
|
2016-04-16 14:50:35 +00:00
|
|
|
return template(
|
2016-04-27 15:05:23 +00:00
|
|
|
config.get('portal', 'index_page'),
|
2016-12-14 15:37:32 +00:00
|
|
|
plugin_timeout=config.getint('portal', 'plugin_timeout')
|
2016-04-16 14:50:35 +00:00
|
|
|
)
|
2016-04-15 20:12:17 +00:00
|
|
|
|
|
|
|
|
|
|
|
@app.route('/static/<path:path>')
|
|
|
|
def server_static(path):
|
2016-04-16 14:50:35 +00:00
|
|
|
return static_file(path, root=config.get('portal', 'static_dir'))
|
2016-04-15 20:12:17 +00:00
|
|
|
|
|
|
|
|
2016-04-18 20:57:38 +00:00
|
|
|
@app.route('/jobs')
|
|
|
|
def list_jobs():
|
|
|
|
Q = Queue(connection=R)
|
|
|
|
jobs = Q.get_job_ids()
|
|
|
|
response.content_tye = 'application/json'
|
|
|
|
return json.dumps(jobs)
|
|
|
|
|
|
|
|
|
2016-04-15 20:12:17 +00:00
|
|
|
@app.route('/job/<job_id:uuid>')
|
|
|
|
def job_status(job_id):
|
|
|
|
Q = Queue(connection=R)
|
|
|
|
job = Q.fetch_job(str(job_id))
|
2016-04-16 14:50:35 +00:00
|
|
|
response.content_type = 'application/json'
|
|
|
|
if job is None:
|
|
|
|
response.status = 404
|
2016-04-15 20:12:17 +00:00
|
|
|
return json.dumps({'error': 'Job not found'})
|
|
|
|
|
|
|
|
# Get data on the job to return to the client
|
|
|
|
job_data = {
|
|
|
|
'id': job.id,
|
|
|
|
'is_failed': job.is_failed,
|
|
|
|
'is_finished': job.is_finished,
|
|
|
|
'is_queued': job.is_queued,
|
2016-04-16 14:50:35 +00:00
|
|
|
'is_started': job.is_started,
|
2016-04-18 19:33:54 +00:00
|
|
|
'result': job.result,
|
|
|
|
'meta': job.meta
|
2016-04-15 20:12:17 +00:00
|
|
|
}
|
|
|
|
|
2016-12-14 15:37:32 +00:00
|
|
|
|
2016-04-15 20:12:17 +00:00
|
|
|
return json.dumps(job_data)
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/approve', method='POST')
|
|
|
|
def approve_client():
|
|
|
|
response.content_type = 'application/json'
|
2016-12-07 13:52:58 +00:00
|
|
|
try:
|
|
|
|
jobs = dispatch_plugins()
|
|
|
|
except Exception as e:
|
|
|
|
response.status = 500
|
|
|
|
jobs = {
|
|
|
|
'result': {
|
|
|
|
'error': str(e)
|
|
|
|
}
|
|
|
|
}
|
2016-04-15 20:12:17 +00:00
|
|
|
|
|
|
|
return json.dumps(jobs)
|
2016-04-05 14:40:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2016-04-15 20:12:17 +00:00
|
|
|
app.run(
|
2016-04-05 14:40:37 +00:00
|
|
|
host=config.get('portal', 'listen_host'),
|
|
|
|
port=config.getint('portal', 'listen_port')
|
|
|
|
)
|
2016-12-17 22:16:19 +00:00
|
|
|
debug(config.getboolean('portal', 'debug'))
|
2016-04-05 14:40:37 +00:00
|
|
|
else:
|
2016-04-15 20:12:17 +00:00
|
|
|
application = app
|