mirror of
https://codeberg.org/prof_x_pvt_ltd/captive.whump.shanti-portal
synced 2024-08-14 22:46:42 +00:00
changed plugin management to simple rq jobs instead of using pkg_resources.
This commit is contained in:
parent
fe5e85f96c
commit
1bf19c791b
13 changed files with 156 additions and 77 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
||||||
|
*.log
|
||||||
.*.swp
|
.*.swp
|
||||||
.*.swo
|
.*.swo
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
# Also an example that simply logs data to the logging handler provided.
|
|
||||||
|
|
||||||
class LoggingPlugin(object):
|
|
||||||
plugin_name = 'LoggingPlugin'
|
|
||||||
|
|
||||||
def __init__(self, **kw):
|
|
||||||
self.l = kw.['logging']
|
|
||||||
self.config = kw.['config']
|
|
||||||
self.request = kw['request']
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.l.info('Request params: {params}'.format(
|
|
||||||
params=self.request.params.keys()
|
|
||||||
))
|
|
|
@ -1,2 +1,4 @@
|
||||||
[portal.plugins]
|
# Lists all the plugins, or jobs, and whether they are enabled or not. Each
|
||||||
LogDispatch = logging_plugin:LoggingPlugin
|
# section name must correspond to a plugin name in the plugins dir.
|
||||||
|
[logging]
|
||||||
|
enabled = True
|
||||||
|
|
32
plugins/logging.py
Normal file
32
plugins/logging.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Demonstration plugin, only logs a message.
|
||||||
|
|
||||||
|
# Sets up logging by importing from the bottle app in the parent dir.
|
||||||
|
from logging import getLogger, DEBUG, WARN, INFO
|
||||||
|
from portal import logHandler, logFormatter
|
||||||
|
|
||||||
|
def run(arg):
|
||||||
|
# The WSGI environ dict should always be there, sans any special objects
|
||||||
|
# like io streams.
|
||||||
|
environ = arg['environ']
|
||||||
|
|
||||||
|
l = getLogger('plugin_logging')
|
||||||
|
l.addHandler(logHandler)
|
||||||
|
l.setLevel(DEBUG)
|
||||||
|
|
||||||
|
log_url = '{proto}://{server}:{port}{request}'.format(
|
||||||
|
proto=environ.get('wsgi.url_scheme'),
|
||||||
|
server=environ.get('SERVER_NAME'),
|
||||||
|
port=environ.get('SERVER_PORT'),
|
||||||
|
request=environ.get('PATH_INFO')
|
||||||
|
)
|
||||||
|
|
||||||
|
log_client = '{client_ip}'.format(
|
||||||
|
client_ip=environ.get('REMOTE_ADDR')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log a msg
|
||||||
|
l.info('{log_client} - {method} - {log_url}'.format(
|
||||||
|
log_client=log_client,
|
||||||
|
log_url=log_url,
|
||||||
|
method=environ.get('REQUEST_METHOD')
|
||||||
|
))
|
|
@ -1,8 +1,12 @@
|
||||||
[portal]
|
[portal]
|
||||||
|
plugins_dir=./plugins
|
||||||
listen_host=localhost
|
listen_host=localhost
|
||||||
listen_port=9080
|
listen_port=9080
|
||||||
debug=True
|
debug=True
|
||||||
|
|
||||||
|
redis_host=127.0.0.1
|
||||||
|
redis_port=6379
|
||||||
|
|
||||||
[logging]
|
[logging]
|
||||||
log_format = %(asctime)s %(name)s[%(process)s] %(levelname)s: %(message)s
|
log_format = %(asctime)s %(name)s[%(process)s] %(levelname)s: %(message)s
|
||||||
log_debug = False
|
log_debug = False
|
||||||
|
|
92
portal.py
92
portal.py
|
@ -1,28 +1,40 @@
|
||||||
|
# Captiveportal web application using Bottle.py
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pprint import pprint
|
||||||
|
from importlib import import_module
|
||||||
from configparser import RawConfigParser
|
from configparser import RawConfigParser
|
||||||
from logging import Formatter, getLogger, DEBUG, WARN, INFO
|
from logging import Formatter, getLogger, DEBUG, WARN, INFO
|
||||||
from logging.handlers import SysLogHandler, RotatingFileHandler
|
from logging.handlers import SysLogHandler, RotatingFileHandler
|
||||||
|
|
||||||
import pkg_resources
|
from redis import Redis
|
||||||
|
from rq import Queue
|
||||||
from bottle import route, run, default_app
|
from bottle import route, run, default_app
|
||||||
from bottle import request, template, static_file
|
from bottle import request, response, template, static_file
|
||||||
|
|
||||||
config = RawConfigParser()
|
config = RawConfigParser()
|
||||||
config.readfp(open('./portal.cfg'))
|
config.readfp(open('./portal.cfg'))
|
||||||
config.read(['/etc/captiveportal/portal.cfg', './portal_local.cfg'])
|
config.read(['/etc/captiveportal/portal.cfg', './portal_local.cfg'])
|
||||||
|
|
||||||
|
# Plugins configuration is separate so plugins can be disabled by having
|
||||||
|
# their section removed/commented from the config file.
|
||||||
|
plugin_config = RawConfigParser()
|
||||||
|
plugin_config.readfp(open('./plugins.cfg'))
|
||||||
|
plugin_config.read(['/etc/captiveportal/plugins.cfg'])
|
||||||
|
|
||||||
# Setup logging
|
# Setup logging
|
||||||
formatter = Formatter(config.get('logging', 'log_format'))
|
logFormatter = Formatter(config.get('logging', 'log_format'))
|
||||||
l = getLogger('captiveportal')
|
l = getLogger('captiveportal')
|
||||||
if config.get('logging', 'log_handler') == 'syslog':
|
if config.get('logging', 'log_handler') == 'syslog':
|
||||||
syslog_address = config.get('logging', 'syslog_address')
|
syslog_address = config.get('logging', 'syslog_address')
|
||||||
|
|
||||||
if syslog_address.startswith('/'):
|
if syslog_address.startswith('/'):
|
||||||
h = SysLogHandler(
|
logHandler = SysLogHandler(
|
||||||
address=syslog_address,
|
address=syslog_address,
|
||||||
facility=SysLogHandler.LOG_LOCAL0
|
facility=SysLogHandler.LOG_LOCAL0
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
h = SysLogHandler(
|
logHandler = SysLogHandler(
|
||||||
address=(
|
address=(
|
||||||
config.get('logging', 'syslog_address'),
|
config.get('logging', 'syslog_address'),
|
||||||
config.getint('logging', 'syslog_port')
|
config.getint('logging', 'syslog_port')
|
||||||
|
@ -30,72 +42,80 @@ if config.get('logging', 'log_handler') == 'syslog':
|
||||||
facility=SysLogHandler.LOG_LOCAL0
|
facility=SysLogHandler.LOG_LOCAL0
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
h = RotatingFileHandler(
|
logHandler = RotatingFileHandler(
|
||||||
config.get('logging', 'log_file'),
|
config.get('logging', 'log_file'),
|
||||||
maxBytes=config.getint('logging', 'log_max_bytes'),
|
maxBytes=config.getint('logging', 'log_max_bytes'),
|
||||||
backupCount=config.getint('logging', 'log_max_copies')
|
backupCount=config.getint('logging', 'log_max_copies')
|
||||||
)
|
)
|
||||||
h.setFormatter(formatter)
|
logHandler.setFormatter(logFormatter)
|
||||||
l.addHandler(h)
|
l.addHandler(logHandler)
|
||||||
|
|
||||||
if config.get('logging', 'log_debug'):
|
if config.get('logging', 'log_debug'):
|
||||||
l.setLevel(DEBUG)
|
l.setLevel(DEBUG)
|
||||||
else:
|
else:
|
||||||
l.setLevel(WARN)
|
l.setLevel(WARN)
|
||||||
|
|
||||||
|
# Redis Queue
|
||||||
|
R = Redis(
|
||||||
|
host=config.get('portal', 'redis_host'),
|
||||||
|
port=config.getint('portal', 'redis_port')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@route('/')
|
@route('/')
|
||||||
def portalindex():
|
def portalindex():
|
||||||
return template('portalindex')
|
return template('portalindex')
|
||||||
|
|
||||||
|
|
||||||
@route('/static/<path:path>')
|
@route('/static/<path:path>')
|
||||||
def server_static(path):
|
def server_static(path):
|
||||||
return static_file(path, root='./static')
|
return static_file(path, root='./static')
|
||||||
|
|
||||||
|
|
||||||
@route('/approve', method='POST')
|
@route('/approve', method='POST')
|
||||||
def approve_client():
|
def approve_client():
|
||||||
_dispatch_plugins(request)
|
response.content_type = 'application/json'
|
||||||
|
jobs = dispatch_plugins()
|
||||||
|
|
||||||
# TODO: return job ID
|
# TODO: return job ID
|
||||||
# Maybe use the client IP as job ID to enable easier lookups of the job
|
# Maybe use the client IP as job ID to enable easier lookups of the job
|
||||||
# status.
|
# status.
|
||||||
return
|
return json.dumps(jobs)
|
||||||
|
|
||||||
def _dispatch_plugins(request):
|
|
||||||
for entrypoint in pkg_resources.iter_entry_points('portal.plugins'):
|
# Add plugins to job queue
|
||||||
l.debug('Loading entry point {point}'.format(
|
def dispatch_plugins():
|
||||||
point=entrypoint.name
|
Q = Queue(connection=R)
|
||||||
|
jobs = []
|
||||||
|
|
||||||
|
for plugin in plugin_config.sections():
|
||||||
|
l.debug('Loading plugin {plugin}'.format(
|
||||||
|
plugin=plugin
|
||||||
))
|
))
|
||||||
|
|
||||||
plugin_class = entrypoint.load()
|
arg = {}
|
||||||
plugin_name = entrypoint.name
|
|
||||||
|
|
||||||
plugin_log = getLogger('portal_'+plugin_name)
|
# Import some values from WSGI environ
|
||||||
plugin_log.addHandler(h)
|
arg['environ'] = {}
|
||||||
plugin_log.setLevel(DEBUG)
|
for key in request.environ:
|
||||||
|
value = request.environ.get(key)
|
||||||
|
if isinstance(value, (int, str, float, dict, set, tuple)):
|
||||||
|
arg['environ'][key] = value
|
||||||
|
|
||||||
# Instantiate the plugin class
|
plugin_module = import_module('plugins.'+plugin)
|
||||||
try:
|
try:
|
||||||
inst = plugin_class(
|
plugin_job = Q.enqueue(
|
||||||
request=request,
|
plugin_module.run,
|
||||||
config=config,
|
arg
|
||||||
log=plugin_log
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
l.error('{plugin}: {exception}'.format(
|
l.error('{plugin}: {error}'.format(
|
||||||
plugin=plugin_name,
|
error=str(e),
|
||||||
exception=str(e)
|
plugin=plugin
|
||||||
))
|
))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Run plugin.run() method
|
jobs.append(plugin_job)
|
||||||
try:
|
|
||||||
inst.run()
|
|
||||||
except Exception as e:
|
|
||||||
l.error('{plugin}: {exception}'.format(
|
|
||||||
plugin=plugin_name,
|
|
||||||
exception=str(e)
|
|
||||||
))
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
rq
|
--index-url https://pypi.python.org/simple/
|
||||||
bottle
|
|
||||||
|
-e
|
||||||
|
|
10
setup.py
10
setup.py
|
@ -1,15 +1,13 @@
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
try:
|
|
||||||
plugins = open('/etc/captiveportal/plugins.cfg')
|
|
||||||
except:
|
|
||||||
plugins = open('./plugins.cfg')
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="CaptivePortal",
|
name="CaptivePortal",
|
||||||
version="0.1",
|
version="0.1",
|
||||||
description="Captive Portal webpage",
|
description="Captive Portal webpage",
|
||||||
author="Stefan Midjich",
|
author="Stefan Midjich",
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
entry_points=plugins.read()
|
install_requires=[
|
||||||
|
'rq',
|
||||||
|
'bottle'
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
9
static/css/captiveportal.css
Normal file
9
static/css/captiveportal.css
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.button-loading {
|
||||||
|
background: #80c8ff url('/static/images/loader.gif') no-repeat 80% 80%;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
#approveButtonDiv img {
|
||||||
|
height: 100px;
|
||||||
|
width: 100px;
|
||||||
|
}
|
1
static/images/radio.svg
Normal file
1
static/images/radio.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><svg width='38px' height='38px' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="uil-blank"><rect x="0" y="0" width="100" height="100" fill="none" class="bk"></rect><g transform="scale(0.55)"><circle cx="30" cy="150" r="30" fill="#eb8614"><animate attributeName="opacity" from="0" to="1" dur="1s" begin="0" repeatCount="indefinite" keyTimes="0;0.5;1" values="0;1;1"></animate></circle><path d="M90,150h30c0-49.7-40.3-90-90-90v30C63.1,90,90,116.9,90,150z" fill="#fb9610"><animate attributeName="opacity" from="0" to="1" dur="1s" begin="0.1" repeatCount="indefinite" keyTimes="0;0.5;1" values="0;1;1"></animate></path><path d="M150,150h30C180,67.2,112.8,0,30,0v30C96.3,30,150,83.7,150,150z" fill="#fb9610"><animate attributeName="opacity" from="0" to="1" dur="1s" begin="0.2" repeatCount="indefinite" keyTimes="0;0.5;1" values="0;1;1"></animate></path></g></svg>
|
After Width: | Height: | Size: 944 B |
23
static/js/captiveportal.js
Normal file
23
static/js/captiveportal.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
$('#approveForm').submit(function (event) {
|
||||||
|
var api_url = '/approve';
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Had some issues trying to set a background image on the button.
|
||||||
|
if ($('#approveCheckbox').is(':checked')) {
|
||||||
|
$('#approveButton').prop('disabled', true);
|
||||||
|
$('#approveButton').val('');
|
||||||
|
$('#approveButton').addClass('button-loading');
|
||||||
|
|
||||||
|
$('#approveButtonDiv').replaceWith('<img src="/static/images/radio.svg" alt="Loading, please wait..." />');
|
||||||
|
|
||||||
|
var ajaxReq = $.post(api_url);
|
||||||
|
|
||||||
|
ajaxReq.done(function(data) {
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
ajaxReq.fail(function(XMLHttpRequest, textStatus, errorThrown) {
|
||||||
|
console.log('Request Error: '+ XMLHttpRequest.responseText + ', status:' + XMLHttpRequest.status + ', status text: ' + XMLHttpRequest.statusText)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,5 +0,0 @@
|
||||||
$(document).ready(function () {
|
|
||||||
$('#resetForm').submit(function (event) {
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -13,34 +13,41 @@
|
||||||
|
|
||||||
<link rel="stylesheet" href="/static/css/normalize.css">
|
<link rel="stylesheet" href="/static/css/normalize.css">
|
||||||
<link rel="stylesheet" href="/static/css/skeleton.css">
|
<link rel="stylesheet" href="/static/css/skeleton.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/captiveportal.css">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="six columns" style="margin-top: 15%">
|
<div class="six columns" style="margin-top: 10%">
|
||||||
<h4>End User Agreement</h4>
|
<h4>End User Agreement</h4>
|
||||||
|
|
||||||
<p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.</p>
|
<p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.</p>
|
||||||
|
|
||||||
<p>It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).</p>
|
<p>It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form id="approveForm">
|
<form id="approveForm" method="post">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="three columns">
|
<div class="four columns">
|
||||||
<label for="approveSend"></label>
|
<label>
|
||||||
<input class="button-primary" value="Approve" type="submit">
|
<input type="checkbox" id="approveCheckbox" required> I approve this user agreement
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="approveButtonDiv" class="one column u-pull-left">
|
||||||
|
<label>
|
||||||
|
<input id="approveButton" class="button-primary" value="Approve" type="submit">
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://code.jquery.com/jquery-1.12.2.min.js"></script>
|
<script src="https://code.jquery.com/jquery-1.12.2.min.js"></script>
|
||||||
<script src="/static/js/portal.js"></script>
|
<script src="/static/js/captiveportal.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in a new issue