commit
32dd2d60fe
34 changed files with 1659 additions and 0 deletions
-
4.gitignore
-
391model.py
-
222pluto.py
-
BINstatic/add.jpg
-
60static/edit.svg
-
59static/home.svg
-
60static/minus.svg
-
60static/plus.svg
-
59static/search.svg
-
69static/site.js
-
195static/style.css
-
8templates/404.html
-
13templates/500.html
-
8templates/act.html
-
9templates/act_delete.html
-
9templates/act_edit.html
-
22templates/api_hit.txt
-
21templates/base.html
-
10templates/cond.html
-
8templates/cond_delete.html
-
10templates/cond_edit.html
-
4templates/cond_new.html
-
44templates/debuglogs.html
-
25templates/hook.html
-
8templates/hook_delete.html
-
10templates/hook_edit.html
-
10templates/hook_new_act.html
-
9templates/hook_new_cond.html
-
16templates/hooks.html
-
9templates/hooks_new.html
-
30templates/index.html
-
40templates/logs.html
-
123templates/macros.html
-
34util.py
@ -0,0 +1,4 @@ |
|||
*.pyc |
|||
secrets.* |
|||
*.db |
|||
*.swp |
@ -0,0 +1,391 @@ |
|||
import os, sqlite3, json, urllib2, ssl, urllib, time, subprocess, socket |
|||
|
|||
from flask import render_template_string |
|||
from util import * |
|||
import secrets |
|||
|
|||
#db = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'pluto.db'), check_same_thread = False) |
|||
db = sqlite3.connect('/var/www/pluto/pluto.db', check_same_thread = False) |
|||
cur = db.cursor() |
|||
|
|||
so = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
|||
so.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) |
|||
so6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) |
|||
so6.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) |
|||
|
|||
class DBError(Exception): |
|||
pass |
|||
|
|||
class NoSuchEntity(DBError): |
|||
pass |
|||
|
|||
class TooManyEntities(DBError): |
|||
pass |
|||
|
|||
class DBObject(object): |
|||
__FIELDS__ = () |
|||
__DEFAULTS__ = {} |
|||
__TABLE__ = '' |
|||
__TYPES__ = {} |
|||
AUTO_COMMIT = True |
|||
|
|||
def __init__(self, rowid, *data): |
|||
self.rowid = rowid |
|||
for idx, field in enumerate(self.__FIELDS__): |
|||
default = self.__DEFAULTS__.get(field) |
|||
if idx < len(data): |
|||
setattr(self, field, data[idx]) |
|||
else: |
|||
setattr(self, field, default) |
|||
|
|||
@classmethod |
|||
def create_table(cls): |
|||
cur.execute('CREATE TABLE IF NOT EXISTS %(table)s (%(columns)s)'%\ |
|||
{'table': cls.__TABLE__, |
|||
'columns': ', '.join('%s%s'%(field, ' '+cls.__TYPES__[field] if field in cls.__TYPES__ else '') for field in cls.__FIELDS__)} |
|||
) |
|||
|
|||
@classmethod |
|||
def create(cls, *data): |
|||
row = list(data) |
|||
for field in cls.__FIELDS__[len(data):]: |
|||
row.append(cls.__DEFAULTS__[field]) |
|||
cur.execute('INSERT INTO %(table)s VALUES (%(fields)s)'%{ |
|||
'table': cls.__TABLE__, |
|||
'fields': ', '.join(['?'] * len(cls.__FIELDS__)) |
|||
}, row) |
|||
if cls.AUTO_COMMIT: |
|||
db.commit() |
|||
return cls(cur.lastrowid, *row) |
|||
|
|||
def delete(self): |
|||
cur.execute('DELETE FROM %(table)s WHERE ROWID=?'%{'table': self.__TABLE__}, (self.rowid,)) |
|||
if self.AUTO_COMMIT: |
|||
db.commit() |
|||
|
|||
def update(self): |
|||
cur.execute('UPDATE %(table)s SET %(fields)s WHERE ROWID=?'%{ |
|||
'table': self.__TABLE__, |
|||
'fields': ', '.join('%s=?'%(field,) for field in self.__FIELDS__) |
|||
}, tuple(getattr(self, field) for field in self.__FIELDS__) + (self.rowid,)) |
|||
if self.AUTO_COMMIT: |
|||
db.commit() |
|||
|
|||
@classmethod |
|||
def get(cls, **criteria): |
|||
pairs = criteria.items() |
|||
keys = [pair[0] for pair in pairs] |
|||
values = [pair[1] for pair in pairs] |
|||
cur.execute('SELECT ROWID, %(fields)s FROM %(table)s WHERE %(criteria)s'%{ |
|||
'table': cls.__TABLE__, |
|||
'fields': ', '.join(cls.__FIELDS__), |
|||
'criteria': ' and '.join('%s=?'%(k,) for k in keys), |
|||
}, values) |
|||
return [cls(*row) for row in cur] |
|||
|
|||
@classmethod |
|||
def all(cls): |
|||
cur.execute('SELECT ROWID, %(fields)s FROM %(table)s'%{ |
|||
'table': cls.__TABLE__, |
|||
'fields': ', '.join(cls.__FIELDS__), |
|||
}) |
|||
return [cls(*row) for row in cur] |
|||
|
|||
@classmethod |
|||
def sorted(cls, by, limit=None): |
|||
cur.execute('SELECT ROWID, %(fields)s FROM %(table)s ORDER BY %(by)s %(limit)s'%{ |
|||
'table': cls.__TABLE__, |
|||
'fields': ', '.join(cls.__FIELDS__), |
|||
'by': by, |
|||
'limit': ('' if limit is None else 'LIMIT %d'%(limit,)), |
|||
}) |
|||
return [cls(*row) for row in cur] |
|||
|
|||
@classmethod |
|||
def get_one(cls, **criteria): |
|||
res = cls.get(**criteria) |
|||
if len(res) < 1: |
|||
raise NoSuchEntity(cls, criteria) |
|||
elif len(res) > 1: |
|||
raise TooManyEntities(cls, criteria) |
|||
return res[0] |
|||
|
|||
def __repr__(self): |
|||
return '<%(cls)s(%(table)s %(row)d %(items)s'%{ |
|||
'table': self.__TABLE__, |
|||
'cls': type(self).__name__, |
|||
'row': self.rowid, |
|||
'items': ' '.join('%s=%r'%(field, getattr(self, field)) for field in self.__FIELDS__), |
|||
} |
|||
|
|||
|
|||
class Log(DBObject): |
|||
__TABLE__ = 'log' |
|||
__FIELDS__ = ('time', 'path', 'headers', 'data', 'hooks') |
|||
|
|||
@classmethod |
|||
def most_recent(cls, n=None): |
|||
return cls.sorted('time DESC', n) |
|||
|
|||
class DebugLog(DBObject): |
|||
__TABLE__ = 'debuglog' |
|||
__FIELDS__ = ('time', 'path', 'headers', 'data', 'hook', 'cond', 'act', 'success', 'message') |
|||
|
|||
@classmethod |
|||
def most_recent(cls, n=None): |
|||
return cls.sorted('time DESC', n) |
|||
|
|||
class Hook(DBObject): |
|||
__TABLE__ = 'hooks' |
|||
__FIELDS__ = ('name', 'author', 'disabled', 'debugged') |
|||
__DEFAULTS__ = { |
|||
'disabled': 0, |
|||
'debugged': 0, |
|||
} |
|||
|
|||
def trigger(self, path, headers, data, response): |
|||
if self.disabled: |
|||
return False |
|||
conditions = Condition.for_hook(self) |
|||
actions = Action.for_hook(self) |
|||
for condition in conditions: |
|||
result = condition.test_select(path, headers, data, response) |
|||
if self.debugged: |
|||
DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), self.rowid, condition.rowid, None, result, None) |
|||
if not result: |
|||
break |
|||
else: |
|||
for act in actions: |
|||
result = act.actuate(path, headers, data, response) |
|||
if self.debugged: |
|||
DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), self.rowid, None, act.rowid, None, result) |
|||
if self.debugged: |
|||
DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), self.rowid, None, None, True, None) |
|||
return True |
|||
if self.debugged: |
|||
DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), self.rowid, None, None, False, None) |
|||
return False |
|||
|
|||
class Condition(DBObject): |
|||
__TABLE__ = 'conditions' |
|||
__FIELDS__ = ('hook', 'selector', 's1', 's2', 's3', 'test', 't1', 't2', 't3', 'invert') |
|||
|
|||
@classmethod |
|||
def for_hook(cls, hook): |
|||
return cls.get(hook=hook.rowid) |
|||
|
|||
def get_hook(self): |
|||
return Hook.get_one(rowid=self.hook) |
|||
|
|||
def select(self, path, headers, data, response): |
|||
return getattr(self, 'select_' + self.selector, self.no_select)(path, headers, data, response) |
|||
|
|||
def no_select(self, path, headers, data, response): |
|||
return None |
|||
|
|||
def select_header(self, path, headers, data, response): |
|||
return headers.get(self.s1, '') |
|||
|
|||
def select_JSON(self, path, headers, data, response): |
|||
if not isinstance(data, dict): |
|||
return False |
|||
cur = data |
|||
for part in self.s1.split('.'): |
|||
cur = cur.get(part) |
|||
if cur is None: |
|||
return False |
|||
return str(cur) |
|||
|
|||
def select_path(self, path, headers, data, response): |
|||
return path |
|||
|
|||
def test_value(self, val): |
|||
try: |
|||
result = getattr(self, 'test_' + self.test, self.no_test)(val) |
|||
except (ValueError, TypeError): |
|||
result = False |
|||
if self.invert: |
|||
result = not result |
|||
return result |
|||
|
|||
def no_test(self, val): |
|||
return False |
|||
|
|||
def test_equal(self, val): |
|||
return str(val) == self.t1 |
|||
|
|||
def test_inrange(self, val): |
|||
return float(self.t1) <= float(val) <= float(self.t2) |
|||
|
|||
def test_truthy(self, val): |
|||
return bool(val) |
|||
|
|||
def test_contains(self, val): |
|||
return self.t1 in val |
|||
|
|||
def test_select(self, path, headers, data, response): |
|||
return self.test_value(self.select(path, headers, data, response)) |
|||
|
|||
class Action(DBObject): |
|||
__TABLE__ = 'actions' |
|||
__FIELDS__ = ('hook', 'action', 'a1', 'a2', 'a3') |
|||
|
|||
GITLAB_API = 'https://gitlab.cosi.clarkson.edu/api/v3/' |
|||
GITLAB_TOKEN = secrets.GITLAB_TOKEN |
|||
PROTO = ssl.PROTOCOL_TLSv1_2 |
|||
|
|||
@classmethod |
|||
def for_hook(cls, hook): |
|||
return cls.get(hook=hook.rowid) |
|||
|
|||
def get_hook(self): |
|||
return Hook.get_one(rowid=self.hook) |
|||
|
|||
def actuate(self, path, headers, data, response): |
|||
try: |
|||
return getattr(self, 'act_' + self.action, self.no_act)(path, headers, data, response) |
|||
except (ValueError, TypeError): |
|||
pass |
|||
|
|||
def no_act(self, path, headers, data, response): |
|||
return 'INTERNAL ERROR: ACTION NOT FOUND' |
|||
|
|||
def act_post(self, path, headers, data, response): |
|||
args = {'path': path, 'headers': headers, 'data': data} |
|||
url = render_template_string(self.a1, **args) |
|||
postdata = render_template_string(self.a2, **args) |
|||
headers = json.loads(render_template_string(self.a3, **args)) |
|||
print 'Note: posting to', url, 'with data', postdata, 'and headers', headers, '...' |
|||
req = urllib2.Request(url, postdata, headers) |
|||
ctxt = ssl.SSLContext(self.PROTO) |
|||
result = urllib2.urlopen(req, context=ctxt) |
|||
out = result.read() |
|||
#out = None |
|||
print 'Complete, got', repr(out) |
|||
return out |
|||
|
|||
def act_gitlab(self, path, headers, data, response): |
|||
args = {'path': path, 'headers': headers, 'data': data} |
|||
url = self.GITLAB_API + render_template_string(self.a1, **args) |
|||
params = json.loads(render_template_string(self.a2, **args)) |
|||
headers = json.loads(render_template_string(self.a3, **args)) |
|||
headers.update({'PRIVATE-TOKEN': self.GITLAB_TOKEN}) |
|||
postdata = urllib.urlencode(params) |
|||
print 'Note: posting to', url, 'with data', postdata, 'and headers', headers, '...' |
|||
req = urllib2.Request(url, postdata, headers) |
|||
ctxt = ssl.SSLContext(self.PROTO) |
|||
result = urllib2.urlopen(req, context=ctxt) |
|||
out = result.read() |
|||
#out = None |
|||
print 'Complete, got', repr(out) |
|||
return out |
|||
|
|||
def act_system(self, path, headers, data, response): |
|||
args = {'path': path, 'headers': headers, 'data': data} |
|||
cmd = render_template_string(self.a1, **args) |
|||
if not self.a2: |
|||
proc = subprocess.Popen(cmd, shell=True) |
|||
return 'forked' |
|||
else: |
|||
try: |
|||
return subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True) |
|||
except subprocess.CalledProcessError as e: |
|||
return e.output |
|||
|
|||
def act_udp(self, path, headers, data, response): |
|||
args = {'path': path, 'headers': headers, 'data': data} |
|||
dest = render_template_string(self.a1, **args) |
|||
packet = render_template_string(self.a2, **args) |
|||
encoding = render_template_string(self.a3, **args) |
|||
try: |
|||
if encoding in (u'hex', u'base64'): |
|||
packet = packet.decode(encoding) |
|||
elif encoding == 'input': |
|||
packet = str(data) |
|||
elif encoding == 'json': |
|||
packet = jdumps(data) # XXX HACKS |
|||
else: |
|||
packet = packet.encode(encoding) |
|||
except Exception as e: |
|||
return 'failed to encode packet: ' + str(e) |
|||
host, _, port = dest.partition(':') |
|||
if not _: |
|||
return 'illegal specification: no port in destination' |
|||
try: |
|||
port = int(port) |
|||
except ValueError: |
|||
return 'illegal port value: ' + port |
|||
if port < 0 or port > 65535: |
|||
return 'illegal port value: ' + str(port) |
|||
try: |
|||
res = socket.getaddrinfo(host, port) |
|||
except socket.gaierror: |
|||
return 'bad hostname:' + host |
|||
for fam, tp, proto, canon, addr in res: |
|||
if tp == socket.SOCK_DGRAM: |
|||
try: |
|||
if fam == socket.AF_INET: |
|||
so.sendto(packet, addr) |
|||
return 'sent to {}: {}'.format(addr, packet.encode('hex')) |
|||
elif fam == socket.AF_INET6: |
|||
so6.sendto(packet, addr) |
|||
return 'sent to {}: {}'.format(addr, packet.encode('hex')) |
|||
except Exception: |
|||
pass |
|||
return 'no good address family found' |
|||
|
|||
def act_tcp(self, path, headers, data, response): |
|||
args = {'path': path, 'headers': headers, 'data': data} |
|||
dest = render_template_string(self.a1, **args) |
|||
packet = render_template_string(self.a2, **args) |
|||
encoding = render_template_string(self.a3, **args) |
|||
try: |
|||
if encoding in (u'hex', u'base64'): |
|||
packet = packet.decode(encoding) |
|||
elif encoding == 'input': |
|||
packet = str(data) |
|||
elif encoding == 'json': |
|||
packet = jdumps(data) # XXX HACKS |
|||
else: |
|||
packet = packet.encode(encoding) |
|||
except Exception as e: |
|||
return 'failed to encode packet: ' + str(e) |
|||
host, _, port = dest.partition(':') |
|||
if not _: |
|||
return 'illegal specification: no port in destination' |
|||
try: |
|||
port = int(port) |
|||
except ValueError: |
|||
return 'illegal port value: ' + port |
|||
if port < 0 or port > 65535: |
|||
return 'illegal port value: ' + str(port) |
|||
try: |
|||
res = socket.getaddrinfo(host, port) |
|||
except socket.gaierror: |
|||
return 'bad hostname:' + host |
|||
so = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|||
so.settimeout(0.1) |
|||
so6 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) |
|||
so6.settimeout(0.1) |
|||
for fam, tp, proto, canon, addr in res: |
|||
if tp == socket.SOCK_STREAM: |
|||
try: |
|||
if fam == socket.AF_INET: |
|||
so.connect(addr) |
|||
so.send(packet) |
|||
return 'sent to {}: {}'.format(addr, packet.encode('hex')) |
|||
elif fam == socket.AF_INET6: |
|||
so6.connect(addr) |
|||
so6.send(packet) |
|||
return 'sent to {}: {}'.format(addr, packet.encode('hex')) |
|||
except Exception: |
|||
pass |
|||
return 'no good address family found' |
|||
|
|||
def act_set_response(self, path, headers, data, response): |
|||
args = {'path': path, 'headers': headers, 'data': data} |
|||
content = render_template_string(self.a1, **args) |
|||
content_type = render_template_string(self.a2, **args) |
|||
response.set_data(content) |
|||
response.headers['Content-type'] = content_type |
|||
return 'response set to "' + content_type + '":\n' + content |
@ -0,0 +1,222 @@ |
|||
import time,string |
|||
|
|||
from flask import Flask, render_template, redirect, url_for, request, g, make_response |
|||
|
|||
from model import * |
|||
from util import * |
|||
|
|||
app = Flask('pluto') |
|||
app.debug = True |
|||
|
|||
@app.before_request |
|||
def init_globals(): |
|||
for cls in type.__subclasses__(DBObject): |
|||
setattr(g, cls.__name__, cls) |
|||
|
|||
app.jinja_env.filters['ctime'] = time.ctime |
|||
app.jinja_env.filters['jloads'] = jloads |
|||
app.jinja_env.filters['hloads'] = header_loads |
|||
app.jinja_env.filters['split'] = string.split |
|||
app.jinja_env.globals['safe_load'] = safe_load |
|||
|
|||
@app.errorhandler(404) |
|||
def error_404(error): |
|||
return render_template('404.html', entity='page'), 404 |
|||
|
|||
@app.route('/') |
|||
def root(): |
|||
return render_template('index.html') |
|||
|
|||
@app.route('/hook', methods=['GET', 'POST']) |
|||
def hook(): |
|||
response = make_response(render_template('api_hit.txt')) |
|||
response.headers['Content-type'] = 'text/json' |
|||
triggered = filter(lambda hook: hook.trigger(request.path, request.headers, jloads(request.data), response), Hook.all()) |
|||
Log.create(time.time(), request.path, header_dumps(request.headers), request.data, ','.join(str(hook.rowid) for hook in triggered)) |
|||
return response |
|||
|
|||
@app.route('/logs') |
|||
def logs(): |
|||
n = request.values.get('n', 10) |
|||
return render_template('logs.html', logs=Log.most_recent(n), n=n) |
|||
|
|||
@app.route('/debuglogs', methods=['GET', 'POST']) |
|||
def debuglogs(): |
|||
n = request.values.get('n', 10) |
|||
return render_template('debuglogs.html', logs=DebugLog.most_recent(n), n=n) |
|||
|
|||
@app.route('/hooks') |
|||
def hooks(): |
|||
return render_template('hooks.html', hooks=Hook.all()) |
|||
|
|||
@app.route('/hooks/new', methods=['GET', 'POST']) |
|||
def hooks_new(): |
|||
if request.method == 'POST': |
|||
hook = Hook.create( |
|||
request.values['name'], None, # FIXME no author yet |
|||
checkbox(request, 'disabled'), |
|||
checkbox(request, 'debugged'), |
|||
) |
|||
return redirect('/hook/%d'%(hook.rowid,)) |
|||
return render_template('hooks_new.html') |
|||
|
|||
@app.route('/hook/<int:hookid>') |
|||
def hook_obj(hookid): |
|||
try: |
|||
hook = Hook.get_one(rowid=hookid) |
|||
except NoSuchEntity: |
|||
return render_template('404.html', entity='hook'), 404 |
|||
except TooManyEntities: |
|||
return render_template('500.html', cause='too many hooks'), 500 |
|||
return render_template('hook.html', hook=hook) |
|||
|
|||
@app.route('/hook/<int:hookid>/edit', methods=['GET', 'POST']) |
|||
def hook_edit(hookid): |
|||
try: |
|||
hook = Hook.get_one(rowid=hookid) |
|||
except NoSuchEntity: |
|||
return render_template('404.html', entity='hook'), 404 |
|||
except TooManyEntities: |
|||
return render_template('500.html', cause='too many hooks'), 500 |
|||
if request.method == 'POST': |
|||
hook.name = request.values['name'] |
|||
hook.disabled = checkbox(request, 'disabled') |
|||
hook.debugged = checkbox(request, 'debugged') |
|||
hook.update() |
|||
return redirect('/hook/%d'%(hook.rowid,)) |
|||
return render_template('hook_edit.html', hook=hook) |
|||
|
|||
@app.route('/hook/<int:hookid>/newcond', methods=['GET', 'POST']) |
|||
def hook_new_cond(hookid): |
|||
try: |
|||
hook = Hook.get_one(rowid=hookid) |
|||
except NoSuchEntity: |
|||
return render_template('404.html', entity='hook'), 404 |
|||
except TooManyEntities: |
|||
return render_template('500.html', cause='too many hooks'), 500 |
|||
if request.method == 'POST': |
|||
cond = Condition.create( |
|||
hook.rowid, |
|||
request.values['selector'], request.values['s1'], request.values['s2'], request.values['s3'], |
|||
request.values['test'], request.values['t1'], request.values['t2'], request.values['t3'], |
|||
checkbox(request, 'invert'), |
|||
) |
|||
return redirect('/cond/%d'%(cond.rowid,)) |
|||
return render_template('hook_new_cond.html', hook=hook) |
|||
|
|||
@app.route('/hook/<int:hookid>/newact', methods=['GET', 'POST']) |
|||
def hook_new_act(hookid): |
|||
try: |
|||
hook = Hook.get_one(rowid=hookid) |
|||
except NoSuchEntity: |
|||
return render_template('404.html', entity='hook'), 404 |
|||
except TooManyEntities: |
|||
return render_template('500.html', cause='too many hooks'), 500 |
|||
if request.method == 'POST': |
|||
act = Action.create( |
|||
hook.rowid, |
|||
request.values['action'], request.values['a1'], request.values['a2'], request.values['a3'], |
|||
) |
|||
return redirect('/act/%d'%(act.rowid,)) |
|||
return render_template('hook_new_act.html', hook=hook) |
|||
|
|||
@app.route('/hook/<int:hookid>/delete', methods=['GET', 'POST']) |
|||
def hook_delete(hookid): |
|||
try: |
|||
hook = Hook.get_one(rowid=hookid) |
|||
except NoSuchEntity: |
|||
return render_template('404.html', entity='hook'), 404 |
|||
except TooManyEntities: |
|||
return render_template('500.html', cause='too many hooks'), 500 |
|||
if request.method == 'POST': |
|||
hook.delete() |
|||
return redirect(url_for('hooks')) |
|||
return render_template('hook_delete.html', hook=hook) |
|||
|
|||
@app.route('/cond/<int:condid>') |
|||
def cond_obj(condid): |
|||
try: |
|||
cond = Condition.get_one(rowid=condid) |
|||
except NoSuchEntity: |
|||
return render_template('404.html', entity='condition'), 404 |
|||
except TooManyEntities: |
|||
return render_template('500.html', cause='too many conditions'), 500 |
|||
return render_template('cond.html', cond=cond) |
|||
|
|||
@app.route('/cond/<int:condid>/edit', methods=['GET', 'POST']) |
|||
def cond_edit(condid): |
|||
try: |
|||
cond = Condition.get_one(rowid=condid) |
|||
except NoSuchEntity: |
|||
return render_template('404.html', entity='condition'), 404 |
|||
except TooManyEntities: |
|||
return render_template('500.html', cause='too many conditions'), 500 |
|||
if request.method == 'POST': |
|||
cond.selector = request.values['selector'] |
|||
cond.s1 = request.values['s1'] |
|||
cond.s2 = request.values['s2'] |
|||
cond.s3 = request.values['s3'] |
|||
cond.test = request.values['test'] |
|||
cond.t1 = request.values['t1'] |
|||
cond.t2 = request.values['t2'] |
|||
cond.t3 = request.values['t3'] |
|||
cond.invert = checkbox(request, 'invert') |
|||
cond.update() |
|||
return redirect('/cond/%d'%(cond.rowid,)) |
|||
return render_template('cond_edit.html', cond=cond) |
|||
|
|||
@app.route('/cond/<int:condid>/delete', methods=['GET', 'POST']) |
|||
def cond_delete(condid): |
|||
try: |
|||
cond = Condition.get_one(rowid=condid) |
|||
except NoSuchEntity: |
|||
return render_template('404.html', entity='condition'), 404 |
|||
except TooManyEntities: |
|||
return render_template('500.html', cause='too many conditions'), 500 |
|||
if request.method == 'POST': |
|||
cond.delete() |
|||
return redirect('/hook/%d'%(cond.hook,)) |
|||
return render_template('cond_delete.html', cond=cond) |
|||
|
|||
@app.route('/act/<int:actid>') |
|||
def act_obj(actid): |
|||
try: |
|||
act = Action.get_one(rowid=actid) |
|||
except NoSuchEntity: |
|||
return render_template('404.html', entity='action'), 404 |
|||
except TooManyEntities: |
|||
return render_template('500.html', cause='too many actions'), 500 |
|||
return render_template('act.html', act=act) |
|||
|
|||
@app.route('/act/<int:actid>/edit', methods=['GET', 'POST']) |
|||
def act_edit(actid): |
|||
try: |
|||
act = Action.get_one(rowid=actid) |
|||
except NoSuchEntity: |
|||
return render_template('404.html', entity='action'), 404 |
|||
except TooManyEntities: |
|||
return render_template('500.html', cause='too many actions'), 500 |
|||
if request.method == 'POST': |
|||
act.action = request.values['action'] |
|||
act.a1 = request.values['a1'] |
|||
act.a2 = request.values['a2'] |
|||
act.a3 = request.values['a3'] |
|||
act.update() |
|||
return redirect('/act/%d'%(act.rowid,)) |
|||
return render_template('act_edit.html', act=act) |
|||
|
|||
@app.route('/act/<int:actid>/delete', methods=['GET', 'POST']) |
|||
def act_delete(actid): |
|||
try: |
|||
act = Action.get_one(rowid=actid) |
|||
except NoSuchEntity: |
|||
return render_template('404.html', entity='action'), 404 |
|||
except TooManyEntities: |
|||
return render_template('500.html', cause='too many actions'), 500 |
|||
if request.method == 'POST': |
|||
act.delete() |
|||
return redirect('/hook/%d'%(act.hook,)) |
|||
return render_template('act_delete.html', act=act) |
|||
|
|||
if __name__ == '__main__': |
|||
app.run(host='0.0.0.0', port=8080) |
After Width: 480 | Height: 360 | Size: 18 KiB |
@ -0,0 +1,60 @@ |
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> |
|||
<svg |
|||
xmlns:dc="http://purl.org/dc/elements/1.1/" |
|||
xmlns:cc="http://creativecommons.org/ns#" |
|||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" |
|||
xmlns:svg="http://www.w3.org/2000/svg" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" |
|||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" |
|||
viewBox="0 -256 1792 1792" |
|||
id="svg3001" |
|||
version="1.1" |
|||
inkscape:version="0.48.3.1 r9886" |
|||
width="100%" |
|||
height="100%" |
|||
sodipodi:docname="pencil_font_awesome.svg"> |
|||
<metadata |
|||
id="metadata3011"> |
|||
<rdf:RDF> |
|||
<cc:Work |
|||
rdf:about=""> |
|||
<dc:format>image/svg+xml</dc:format> |
|||
<dc:type |
|||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> |
|||
</cc:Work> |
|||
</rdf:RDF> |
|||
</metadata> |
|||
<defs |
|||
id="defs3009" /> |
|||
<sodipodi:namedview |
|||
pagecolor="#ffffff" |
|||
bordercolor="#666666" |
|||
borderopacity="1" |
|||
objecttolerance="10" |
|||
gridtolerance="10" |
|||
guidetolerance="10" |
|||
inkscape:pageopacity="0" |
|||
inkscape:pageshadow="2" |
|||
inkscape:window-width="640" |
|||
inkscape:window-height="480" |
|||
id="namedview3007" |
|||
showgrid="false" |
|||
inkscape:zoom="0.13169643" |
|||
inkscape:cx="896" |
|||
inkscape:cy="896" |
|||
inkscape:window-x="0" |
|||
inkscape:window-y="25" |
|||
inkscape:window-maximized="0" |
|||
inkscape:current-layer="svg3001" /> |
|||
<g |
|||
transform="matrix(1,0,0,-1,121.49153,1270.2373)" |
|||
id="g3003" |
|||
style="color: #ddd;"> |
|||
<path |
|||
d="M 363,0 454,91 219,326 128,235 V 128 H 256 V 0 h 107 z m 523,928 q 0,22 -22,22 -10,0 -17,-7 L 305,401 q -7,-7 -7,-17 0,-22 22,-22 10,0 17,7 l 542,542 q 7,7 7,17 z M 832,1120 1248,704 416,-128 H 0 v 416 z m 683,-96 q 0,-53 -37,-90 l -166,-166 -416,416 166,165 q 36,38 90,38 53,0 91,-38 l 235,-234 q 37,-39 37,-91 z" |
|||
id="path3005" |
|||
inkscape:connector-curvature="0" |
|||
style="fill:currentColor" /> |
|||
</g> |
|||
</svg> |
@ -0,0 +1,59 @@ |
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> |
|||
<svg |
|||
xmlns:dc="http://purl.org/dc/elements/1.1/" |
|||
xmlns:cc="http://creativecommons.org/ns#" |
|||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" |
|||
xmlns:svg="http://www.w3.org/2000/svg" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" |
|||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" |
|||
viewBox="0 -256 1792 1792" |
|||
id="svg3013" |
|||
version="1.1" |
|||
inkscape:version="0.48.3.1 r9886" |
|||
width="100%" |
|||
height="100%" |
|||
sodipodi:docname="home_font_awesome.svg"> |
|||
<metadata |
|||
id="metadata3023"> |
|||
<rdf:RDF> |
|||
<cc:Work |
|||
rdf:about=""> |
|||
<dc:format>image/svg+xml</dc:format> |
|||
<dc:type |
|||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> |
|||
</cc:Work> |
|||
</rdf:RDF> |
|||
</metadata> |
|||
<defs |
|||
id="defs3021" /> |
|||
<sodipodi:namedview |
|||
pagecolor="#ffffff" |
|||
bordercolor="#666666" |
|||
borderopacity="1" |
|||
objecttolerance="10" |
|||
gridtolerance="10" |
|||
guidetolerance="10" |
|||
inkscape:pageopacity="0" |
|||
inkscape:pageshadow="2" |
|||
inkscape:window-width="640" |
|||
inkscape:window-height="480" |
|||
id="namedview3019" |
|||
showgrid="false" |
|||
inkscape:zoom="0.13169643" |
|||
inkscape:cx="896" |
|||
inkscape:cy="896" |
|||
inkscape:window-x="0" |
|||
inkscape:window-y="25" |
|||
inkscape:window-maximized="0" |
|||
inkscape:current-layer="svg3013" /> |
|||
<g |
|||
transform="matrix(1,0,0,-1,68.338983,1285.4237)" |
|||
id="g3015"> |
|||
<path |
|||
d="M 1408,544 V 64 Q 1408,38 1389,19 1370,0 1344,0 H 960 V 384 H 704 V 0 H 320 q -26,0 -45,19 -19,19 -19,45 v 480 q 0,1 0.5,3 0.5,2 0.5,3 l 575,474 575,-474 q 1,-2 1,-6 z m 223,69 -62,-74 q -8,-9 -21,-11 h -3 q -13,0 -21,7 L 832,1112 140,535 q -12,-8 -24,-7 -13,2 -21,11 l -62,74 q -8,10 -7,23.5 1,13.5 11,21.5 l 719,599 q 32,26 76,26 44,0 76,-26 l 244,-204 v 195 q 0,14 9,23 9,9 23,9 h 192 q 14,0 23,-9 9,-9 9,-23 V 840 l 219,-182 q 10,-8 11,-21.5 1,-13.5 -7,-23.5 z" |
|||
id="path3017" |
|||
inkscape:connector-curvature="0" |
|||
style="fill:currentColor" /> |
|||
</g> |
|||
</svg> |
@ -0,0 +1,60 @@ |
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> |
|||
<svg |
|||
xmlns:dc="http://purl.org/dc/elements/1.1/" |
|||
xmlns:cc="http://creativecommons.org/ns#" |
|||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" |
|||
xmlns:svg="http://www.w3.org/2000/svg" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" |
|||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" |
|||
viewBox="0 -256 1792 1792" |
|||
id="svg2989" |
|||
version="1.1" |
|||
inkscape:version="0.48.3.1 r9886" |
|||
width="100%" |
|||
height="100%" |
|||
sodipodi:docname="minus_sign_font_awesome.svg"> |
|||
<metadata |
|||
id="metadata2999"> |
|||
<rdf:RDF> |
|||
<cc:Work |
|||
rdf:about=""> |
|||
<dc:format>image/svg+xml</dc:format> |
|||
<dc:type |
|||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> |
|||
</cc:Work> |
|||
</rdf:RDF> |
|||
</metadata> |
|||
<defs |
|||
id="defs2997" /> |
|||
<sodipodi:namedview |
|||
pagecolor="#ffffff" |
|||
bordercolor="#666666" |
|||
borderopacity="1" |
|||
objecttolerance="10" |
|||
gridtolerance="10" |
|||
guidetolerance="10" |
|||
inkscape:pageopacity="0" |
|||
inkscape:pageshadow="2" |
|||
inkscape:window-width="640" |
|||
inkscape:window-height="480" |
|||
id="namedview2995" |
|||
showgrid="false" |
|||
inkscape:zoom="0.13169643" |
|||
inkscape:cx="896" |
|||
inkscape:cy="896" |
|||
inkscape:window-x="0" |
|||
inkscape:window-y="25" |
|||
inkscape:window-maximized="0" |
|||
inkscape:current-layer="svg2989" /> |
|||
<g |
|||
transform="matrix(1,0,0,-1,129.08475,1277.8305)" |
|||
id="g2991" |
|||
style="color: #ddd;"> |
|||
<path |
|||
d="m 1216,576 v 128 q 0,26 -19,45 -19,19 -45,19 H 384 q -26,0 -45,-19 -19,-19 -19,-45 V 576 q 0,-26 19,-45 19,-19 45,-19 h 768 q 26,0 45,19 19,19 19,45 z m 320,64 Q 1536,431 1433,254.5 1330,78 1153.5,-25 977,-128 768,-128 559,-128 382.5,-25 206,78 103,254.5 0,431 0,640 0,849 103,1025.5 206,1202 382.5,1305 559,1408 768,1408 977,1408 1153.5,1305 1330,1202 1433,1025.5 1536,849 1536,640 z" |
|||
id="path2993" |
|||
inkscape:connector-curvature="0" |
|||
style="fill:currentColor" /> |
|||
</g> |
|||
</svg> |
@ -0,0 +1,60 @@ |
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> |
|||
<svg |
|||
xmlns:dc="http://purl.org/dc/elements/1.1/" |
|||
xmlns:cc="http://creativecommons.org/ns#" |
|||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" |
|||
xmlns:svg="http://www.w3.org/2000/svg" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" |
|||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" |
|||
viewBox="0 -256 1792 1792" |
|||
id="svg3025" |
|||
version="1.1" |
|||
inkscape:version="0.48.3.1 r9886" |
|||
width="100%" |
|||
height="100%" |
|||
sodipodi:docname="plus_sign_font_awesome.svg"> |
|||
<metadata |
|||
id="metadata3035"> |
|||
<rdf:RDF> |
|||
<cc:Work |
|||
rdf:about=""> |
|||
<dc:format>image/svg+xml</dc:format> |
|||
<dc:type |
|||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> |
|||
</cc:Work> |
|||
</rdf:RDF> |
|||
</metadata> |
|||
<defs |
|||
id="defs3033" /> |
|||
<sodipodi:namedview |
|||
pagecolor="#ffffff" |
|||
bordercolor="#666666" |
|||
borderopacity="1" |
|||
objecttolerance="10" |
|||
gridtolerance="10" |
|||
guidetolerance="10" |
|||
inkscape:pageopacity="0" |
|||
inkscape:pageshadow="2" |
|||
inkscape:window-width="640" |
|||
inkscape:window-height="480" |
|||
id="namedview3031" |
|||
showgrid="false" |
|||
inkscape:zoom="0.13169643" |
|||
inkscape:cx="896" |
|||
inkscape:cy="896" |
|||
inkscape:window-x="0" |
|||
inkscape:window-y="25" |
|||
inkscape:window-maximized="0" |
|||
inkscape:current-layer="svg3025" /> |
|||
<g |
|||
transform="matrix(1,0,0,-1,113.89831,1277.8305)" |
|||
id="g3027" |
|||
style="color: #ddd;"> |
|||
<path |
|||
d="m 1216,576 v 128 q 0,26 -19,45 -19,19 -45,19 H 896 v 256 q 0,26 -19,45 -19,19 -45,19 H 704 q -26,0 -45,-19 -19,-19 -19,-45 V 768 H 384 q -26,0 -45,-19 -19,-19 -19,-45 V 576 q 0,-26 19,-45 19,-19 45,-19 H 640 V 256 q 0,-26 19,-45 19,-19 45,-19 h 128 q 26,0 45,19 19,19 19,45 v 256 h 256 q 26,0 45,19 19,19 19,45 z m 320,64 Q 1536,431 1433,254.5 1330,78 1153.5,-25 977,-128 768,-128 559,-128 382.5,-25 206,78 103,254.5 0,431 0,640 0,849 103,1025.5 206,1202 382.5,1305 559,1408 768,1408 977,1408 1153.5,1305 1330,1202 1433,1025.5 1536,849 1536,640 z" |
|||
id="path3029" |
|||
inkscape:connector-curvature="0" |
|||
style="fill:currentColor" /> |
|||
</g> |
|||
</svg> |
@ -0,0 +1,59 @@ |
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> |
|||
<svg |
|||
xmlns:dc="http://purl.org/dc/elements/1.1/" |
|||
xmlns:cc="http://creativecommons.org/ns#" |
|||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" |
|||
xmlns:svg="http://www.w3.org/2000/svg" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" |
|||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" |
|||
viewBox="0 -256 1792 1792" |
|||
id="svg3025" |
|||
version="1.1" |
|||
inkscape:version="0.48.3.1 r9886" |
|||
width="100%" |
|||
height="100%" |
|||
sodipodi:docname="search_font_awesome.svg"> |
|||
<metadata |
|||
id="metadata3035"> |
|||
<rdf:RDF> |
|||
<cc:Work |
|||
rdf:about=""> |
|||
<dc:format>image/svg+xml</dc:format> |
|||
<dc:type |
|||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> |
|||
</cc:Work> |
|||
</rdf:RDF> |
|||
</metadata> |
|||
<defs |
|||
id="defs3033" /> |
|||
<sodipodi:namedview |
|||
pagecolor="#ffffff" |
|||
bordercolor="#666666" |
|||
borderopacity="1" |
|||
objecttolerance="10" |
|||
gridtolerance="10" |
|||
guidetolerance="10" |
|||
inkscape:pageopacity="0" |
|||
inkscape:pageshadow="2" |
|||
inkscape:window-width="640" |
|||
inkscape:window-height="480" |
|||
id="namedview3031" |
|||
showgrid="false" |
|||
inkscape:zoom="0.13169643" |
|||
inkscape:cx="896" |
|||
inkscape:cy="896" |
|||
inkscape:window-x="0" |
|||
inkscape:window-y="25" |
|||
inkscape:window-maximized="0" |
|||
inkscape:current-layer="svg3025" /> |
|||
<g |
|||
transform="matrix(1,0,0,-1,60.745763,1201.8983)" |
|||
id="g3027"> |
|||
<path |
|||
d="m 1152,704 q 0,185 -131.5,316.5 Q 889,1152 704,1152 519,1152 387.5,1020.5 256,889 256,704 256,519 387.5,387.5 519,256 704,256 889,256 1020.5,387.5 1152,519 1152,704 z m 512,-832 q 0,-52 -38,-90 -38,-38 -90,-38 -54,0 -90,38 L 1103,124 Q 924,0 704,0 561,0 430.5,55.5 300,111 205.5,205.5 111,300 55.5,430.5 0,561 0,704 q 0,143 55.5,273.5 55.5,130.5 150,225 94.5,94.5 225,150 130.5,55.5 273.5,55.5 143,0 273.5,-55.5 130.5,-55.5 225,-150 94.5,-94.5 150,-225 Q 1408,847 1408,704 1408,484 1284,305 l 343,-343 q 37,-37 37,-90 z" |
|||
id="path3029" |
|||
inkscape:connector-curvature="0" |
|||
style="fill:currentColor" /> |
|||
</g> |
|||
</svg> |
@ -0,0 +1,69 @@ |
|||
const ACTION_SCHEMA = { |
|||
'post': ['URL template', 'POST data template', 'Headers JSON template'], |
|||
'gitlab': ['API subpath', 'POST data JSON template', 'Headers JSON template'], |
|||
'system': ['Command', "Don't fork"], |
|||
'udp': ['Destination', 'Packet Data', 'Codec'], |
|||
'tcp': ['Destination', 'Packet Data', 'Codec'], |
|||
'set_response': ['Content', 'Content Type'], |
|||
}; |
|||
|
|||
const SELECTOR_SCHEMA = { |
|||
'header': ['Header name'], |
|||
'JSON': ['Object path'], |
|||
'path': [], |
|||
}; |
|||
|
|||
const TEST_SCHEMA = { |
|||
'equal': ['String value'], |
|||
'inrange': ['Lower bound', 'Upper bound'], |
|||
'truthy': [], |
|||
'contains': ['String value'], |
|||
}; |
|||
|
|||
function reg_action_select(select, laba1, laba2, laba3) { |
|||
select.addEventListener("change", function() { |
|||
var schema = ACTION_SCHEMA[this.value]; |
|||
if(!schema) { |
|||
laba1.textContent = "(Unknown schema)"; |
|||
laba2.textContent = "(Unknown schema)"; |
|||
laba3.textContent = "(Unknown schema)"; |
|||
} else { |
|||
laba1.textContent = (schema.length > 0 ? schema[0] : "Unused"); |
|||
laba2.textContent = (schema.length > 1 ? schema[1] : "Unused"); |
|||
laba3.textContent = (schema.length > 2 ? schema[2] : "Unused"); |
|||
} |
|||
}); |
|||
select.dispatchEvent(new Event("change")); |
|||
} |
|||
|
|||
function reg_selector_select(select, labs1, labs2, labs3) { |
|||
select.addEventListener("change", function() { |
|||
var schema = SELECTOR_SCHEMA[this.value]; |
|||
if(!schema) { |
|||
labs1.textContent = "(Unknown schema)"; |
|||
labs2.textContent = "(Unknown schema)"; |
|||
labs3.textContent = "(Unknown schema)"; |
|||
} else { |
|||
labs1.textContent = (schema.length > 0 ? schema[0] : "Unused"); |
|||
labs2.textContent = (schema.length > 1 ? schema[1] : "Unused"); |
|||
labs3.textContent = (schema.length > 2 ? schema[2] : "Unused"); |
|||
} |
|||
}); |
|||
select.dispatchEvent(new Event("change")); |
|||
} |
|||
|
|||
function reg_test_select(select, labt1, labt2, labt3) { |
|||
select.addEventListener("change", function() { |
|||
var schema = TEST_SCHEMA[this.value]; |
|||
if(!schema) { |
|||
labt1.textContent = "(Unknown schema)"; |
|||
labt2.textContent = "(Unknown schema)"; |
|||
labt3.textContent = "(Unknown schema)"; |
|||
} else { |
|||
labt1.textContent = (schema.length > 0 ? schema[0] : "Unused"); |
|||
labt2.textContent = (schema.length > 1 ? schema[1] : "Unused"); |
|||
labt3.textContent = (schema.length > 2 ? schema[2] : "Unused"); |
|||
} |
|||
}); |
|||
select.dispatchEvent(new Event("change")); |
|||
} |
@ -0,0 +1,195 @@ |
|||
body { |
|||
background-color: #eee; |
|||
color: #222; |
|||
padding: 0 10%; |
|||
font-size: 125%; |
|||
} |
|||
|
|||
@media (max-width: 768px) { |
|||
body { |
|||
padding: 0 2%; |
|||
} |
|||
} |
|||
|
|||
table { |
|||
background-color: #ddd; |
|||
} |
|||
|
|||
span.monospace { |
|||
font-family: monospace; |
|||
border: 1px solid #777; |
|||
border-radius: 3px; |
|||
white-space: pre; |
|||
display: inline-block; |
|||
padding: 5px; |
|||
background-color: #fff; |
|||
} |
|||
|
|||
table { |
|||
border-collapse: collapse; |
|||
border: 1px solid #777; |
|||
} |
|||
|
|||
table td, table th { |
|||
padding: 10px; |
|||
border: 1px solid #777; |
|||
} |
|||
|
|||
#logs.debug { |
|||
font-size: 75%; |
|||
} |
|||
|
|||
#logs .headers, #logs .data { |
|||
white-space: pre-wrap; |
|||
font-family: monospace; |
|||
background-color: #eee; |
|||
} |
|||
|
|||
.nodata { |
|||
font-style: italic; |
|||
color: #700; |
|||
} |
|||
|
|||
.positive { |
|||
color: #0a0; |
|||
} |
|||
|
|||
.negative { |
|||
color: #a00; |
|||
} |
|||
|
|||
div.error { |
|||
background-color: #edd; |
|||
border: 1px solid #a77; |
|||
border-radius: 15px; |
|||
width: 100%; |
|||
padding: 25px; |
|||
} |
|||
|
|||
.posbutton a { |
|||
display: inline-block; |
|||
background-color: #080; |
|||
color: #ddd; |
|||
border: 2px solid #060; |
|||
border-radius: 5px; |
|||
padding: 10px; |
|||
margin: 5px; |
|||
font-weight: bolder; |
|||
text-decoration: none; |
|||
} |
|||
|
|||
.posbutton a:hover { |
|||
background-color: #0a0; |
|||
} |
|||
|
|||
.posbutton a:active { |
|||
background-color: #ddd; |
|||
color: #0a0; |
|||
} |
|||
|
|||
.negbutton a { |
|||
display: inline-block; |
|||
background-color: #800; |
|||
color: #ddd; |
|||
border: 2px solid #600; |
|||
border-radius: 5px; |
|||
padding: 10px; |
|||
margin: 5px; |
|||
font-weight: bolder; |
|||
text-decoration: none; |
|||
} |
|||
|
|||
.negbutton a:hover { |
|||
background-color: #a00; |
|||
} |
|||
|
|||
.negbutton a:active { |
|||
background-color: #ddd; |
|||
color: #a00; |
|||
} |
|||
|
|||
.neutbutton a { |
|||
display: inline-block; |
|||
background-color: #008; |
|||
color: #ddd; |
|||
border: 2px solid #006; |
|||
border-radius: 5px; |
|||
padding: 10px; |
|||
margin: 5px; |
|||
font-weight: bolder; |
|||
text-decoration: none; |
|||
} |
|||
|
|||
.neutbutton a:hover { |
|||
background-color: #00a; |
|||
} |
|||
|
|||
.neutbutton a:active { |
|||
background-color: #ddd; |
|||
color: #00a; |
|||
} |
|||
|
|||
.addbutton a::after { |
|||
margin-left: 10px; |
|||
margin-bottom: -3px; |
|||
background-image: url('/static/plus.svg'); |
|||
background-size: 25px 25px; |
|||
width: 25px; |
|||
height: 25px; |
|||
display: inline-block; |
|||
content: ""; |
|||
color: #f0f; |
|||
} |
|||
|
|||
.rembutton a::after { |
|||
margin-left: 10px; |
|||
margin-bottom: -5px; |
|||
background-image: url('/static/minus.svg'); |
|||
background-size: 25px 25px; |
|||
width: 25px; |
|||
height: 25px; |
|||
display: inline-block; |
|||
content: ""; |
|||
} |
|||
|
|||
.edbutton a::after { |
|||
margin-left: 10px; |
|||
margin-bottom: -5px; |
|||
background-image: url('/static/edit.svg'); |
|||
background-size: 25px 25px; |
|||
width: 25px; |
|||
height: 25px; |
|||
display: inline-block; |
|||
content: ""; |
|||
} |
|||
|
|||
.srchbutton a::after { |
|||
margin-left: 10px; |
|||
margin-bottom: -5px; |
|||
background-image: url('/static/search.svg'); |
|||
background-size: 25px 25px; |
|||
width: 25px; |
|||
height: 25px; |
|||
display: inline-block; |
|||
content: ""; |
|||
} |
|||
|
|||
.hacks-flr { |
|||
float: right; |
|||
} |
|||
|
|||
#home { |
|||
width: 46px; |
|||
height: 46px; |
|||
padding-right: 10px; |
|||
float: left; |
|||
} |
|||
|
|||
.flex-center { |
|||
display: flex; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.flex-center > div { |
|||
margin: 0 50px; |
|||
} |
@ -0,0 +1,8 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto - Error 404{% endblock %} |
|||
{% block content %} |
|||
<div class="error"> |
|||
There is no such {{ entity | default('entity') }} identified by your request. |
|||
</div> |
|||
{% endblock %} |
|||
|
@ -0,0 +1,13 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto - Error 500{% endblock %} |
|||
{% block content %} |
|||
<div class="error"> |
|||
An error occurred while processing your request.<br/> |
|||
The cause is: |
|||
{% if cause %} |
|||
<span class="monospace">{{ cause }}</span> |
|||
{% else %} |
|||
<span class="nodata">No cause given.</span> |
|||
{% endif %} |
|||
</div> |
|||
{% endblock %} |
@ -0,0 +1,8 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto - Action {{act.rowid}}{% endblock %} |
|||
{% block content %} |
|||
<p>This is action {{ act.rowid }} for {{ macros.hook(act.get_hook()) }}:</p> |
|||
<div class="neutbutton edbutton hacks-flr"><a href="/act/{{ act.rowid }}/edit">Edit</a></div> |
|||
<div class="negbutton rembutton hacks-flr"><a href="/act/{{ act.rowid }}/delete">Delete</a></div> |
|||
<p>Action: {{ act.action }} ({{ act.a1 | pprint }}, {{ act.a2 | pprint }}, {{ act.a3 | pprint }})</p> |
|||
{% endblock %} |
@ -0,0 +1,9 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto - Delete Action {{ act.rowid }}{% endblock %} |
|||
{% block content %} |
|||
<p>Are you sure you want to delete {{ macros.act(act) }} of {{ macros.hook(act.get_hook()) }}?</p> |
|||
<form action="?" method="POST"> |
|||
<button type="submit">Confirm</button> |
|||
</form> |
|||
{% endblock %} |
|||
|
@ -0,0 +1,9 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto - Edit Action {{ act.rowid }}{% endblock %} |
|||
{% block content %} |
|||
<p>Edit action {{ macros.act(act) }}:</p> |
|||
<form action="?" method="POST"> |
|||
{{ macros.act_params(act) }} |
|||
<button type="submit">Submit</button> |
|||
</form> |
|||
{% endblock %} |
@ -0,0 +1,22 @@ |
|||
{ |
|||
"version": "1.0", |
|||
"sessionAttributes": {}, |
|||
"response": { |
|||
"outputSpeech": { |
|||
"type": "PlainText", |
|||
"text": "Welcome to the Alexa Skills Kit sample, Please tell me your favorite color by saying, my favorite color is red" |
|||
}, |
|||
"card": { |
|||
"type": "Simple", |
|||
"title": "SessionSpeechlet - Welcome", |
|||
"content": "SessionSpeechlet - Welcome to the Alexa Skills Kit sample, Please tell me your favorite color by saying, my favorite color is red" |
|||
}, |
|||
"reprompt": { |
|||
"outputSpeech": { |
|||
"type": "PlainText", |
|||
"text": "Please tell me your favorite color by saying, my favorite color is red" |
|||
} |
|||
}, |
|||
"shouldEndSession": false |
|||
} |
|||
} |
@ -0,0 +1,21 @@ |
|||
{% import "macros.html" as macros %} |
|||
<!DOCTYPE HTML> |
|||
<html> |
|||
<head> |
|||
{% block head %} |
|||
<title>{% block title %}{% endblock %}</title> |
|||
<link rel="stylesheet" type="text/css" href="/static/style.css"/> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|||
<script type="text/javascript" src="/static/site.js"></script> |
|||
{% endblock %} |
|||
</head> |
|||
<body> |
|||
{% block header %} |
|||
<h1><a href="/"><img id="home" src="/static/home.svg"></a></h1> |
|||
<h1>{{ self.title() }}</h1> |
|||
{% endblock %} |
|||
<div style="clear: left;"></div> |
|||
{% block content %} |
|||
{% endblock %} |
|||
</body> |
|||
</html> |
@ -0,0 +1,10 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto - Condition {{cond.rowid}}{% endblock %} |
|||
{% block content %} |
|||
<p>This is condition {{ cond.rowid }} for {{ macros.hook(cond.get_hook()) }}:</p> |
|||
<div class="neutbutton edbutton hacks-flr"><a href="/cond/{{ cond.rowid }}/edit">Edit</a></div> |
|||
<div class="negbutton rembutton hacks-flr"><a href="/cond/{{ cond.rowid }}/delete">Delete</a></div> |
|||
<p>Selector: {{ cond.selector }} ({{ cond.s1 | pprint }}, {{ cond.s2 | pprint }}, {{ cond.s3 | pprint }})</p> |
|||
<p>Test: {{ cond.test }} ({{ cond.t1 | pprint }}, {{ cond.t2 | pprint }}, {{ cond.t3 | pprint }})</p> |
|||
<p>Inverted: {% if cond.invert %}Yes{% else %}No{% endif %}</p> |
|||
{% endblock %} |
@ -0,0 +1,8 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto - Delete Condition {{ cond.rowid }}{% endblock %} |
|||
{% block content %} |
|||
<p>Are you sure you want to delete {{ macros.cond(cond) }} of {{ macros.hook(cond.get_hook()) }}?</p> |
|||
<form action="?" method="POST"> |
|||
<button type="submit">Confirm</button> |
|||
</form> |
|||
{% endblock %} |
@ -0,0 +1,10 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto - Edit Condition {{ cond.rowid }}{% endblock %} |
|||
{% block content %} |
|||
<p>Edit condition {{ macros.cond(cond) }}:</p> |
|||
<form action="?" method="POST"> |
|||
{{ macros.cond_params(cond) }} |
|||
<button type="submit">Submit</button> |
|||
</form> |
|||
{% endblock %} |
|||
|
@ -0,0 +1,4 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto - New Condition{% endblock %} |
|||
{% block content %} |
|||
<p> |
@ -0,0 +1,44 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto - Logs{% endblock %} |
|||
{% block content %} |
|||
<p> |
|||
Here are the most recent {{ n }} entries in the debug log: |
|||
</p> |
|||
|
|||
<table id="logs" class="debug"> |
|||
<tr> |
|||
<th>Time</th> |
|||
<th>Path</th> |
|||
<th>Headers</th> |
|||
<th>Data</th> |
|||
<th>Hook</th> |
|||
<th>Actor</th> |
|||
<th>Success</th> |
|||
<th>Message</th> |
|||
</tr> |
|||
{% for log in logs %} |
|||
<tr> |
|||
<td class="time">{{ log.time | ctime }}</td> |
|||
<td class="path">{{ log.path }}</td> |
|||
<td class="headers">{{ log.headers | hloads | pprint(True) }}</td> |
|||
<td class="data">{{ log.data | jloads | pprint(True) }}</td> |
|||
<td class="hook">{{ macros.hook_id(log.hook) }}</td> |
|||
<td class="actor"> |
|||
{% if log.cond %} |
|||
{{ macros.cond_id(log.cond) }} |
|||
{% elif log.act %} |
|||
{{ macros.act_id(log.act) }} |
|||
{% else %} |
|||
{{ macros.hook_id(log.hook) }} |
|||
{% endif %} |
|||
</td> |
|||
<td class="success">{{ macros.yesno(log.success) }}</td> |
|||
<td class="message">{{ log.message }}</td> |
|||
</tr> |
|||
{% else %} |
|||
<tr> |
|||
<td colspan="8" class="nodata">No data.</td> |
|||
</tr> |
|||
{% endfor %} |
|||
</table> |
|||
{% endblock %} |
@ -0,0 +1,25 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto - Hook {{hook.name}}{% endblock %} |
|||
{% block content %} |
|||
<p>This is hook {{ hook.rowid }}, named "{{ hook.name }}", presently {% if hook.disabled %}<span class="negative">disabled</span>{% else %}<span class="positive">enabled</span>{% endif %}, presently {% if hook.debugged %}<span class="positive">debugged (<a href="/debuglogs">see entries</a>)</span>{% else %}<span class="negative">not debugged</span>{% endif %}.</p> |
|||
<div class="neutbutton edbutton hacks-flr"><a href="/hook/{{ hook.rowid }}/edit">Edit</a></div> |
|||
<div class="negbutton rembutton hacks-flr"><a href="/hook/{{ hook.rowid }}/delete">Delete</a></div> |
|||
<p>Conditions for activation:</p> |
|||
<ul> |
|||
{% for cond in g.Condition.for_hook(hook) %} |
|||
<li>{{ macros.cond(cond) }}</li> |
|||
{% else %} |
|||
<li class="nodata">No conditions</li> |
|||
{% endfor %} |
|||
<li class="posbutton addbutton"><a href="/hook/{{ hook.rowid }}/newcond">Add condition</a></li> |
|||
</ul> |
|||
<p>Actions upon activation:</p> |
|||
<ul> |
|||
{% for act in g.Action.for_hook(hook) %} |
|||
<li>{{ macros.act(act) }}</li> |
|||
{% else %} |
|||
<li class="nodata">No actions</li> |
|||
{% endfor %} |
|||
<li class="posbutton addbutton"><a href="/hook/{{ hook.rowid }}/newact">Add action</a></li> |
|||
</ul> |
|||
{% endblock %} |
@ -0,0 +1,8 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto - Delete Hook {{ hook.name }}{% endblock %} |
|||
{% block content %} |
|||
<p>Are you sure you want to delete {{ macros.hook(hook) }}?</p> |
|||
<form action="?" method="POST"> |
|||
<button type="submit">Confirm</button> |
|||
</form> |
|||
{% endblock %} |
@ -0,0 +1,10 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto - Edit Hook {{ hook.name }}{% endblock %} |
|||
{% block content %} |
|||
<p>Edit hook {{ macros.hook(hook) }}:</p> |
|||
<form action="?" method="POST"> |
|||
{{ macros.hook_params(hook) }} |
|||
<button type="submit">Submit</button> |
|||
</form> |
|||
{% endblock %} |
|||
|
@ -0,0 +1,10 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto - New Action{% endblock %} |
|||
{% block content %} |
|||
<p>Add an action to {{ macros.hook(hook) }}:</p> |
|||
<form action="?" method="POST"> |
|||
{{ macros.act_params(None) }} |
|||
<button type="submit">Add</button> |
|||
</form> |
|||
{% endblock %} |
|||
|
@ -0,0 +1,9 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto - New Condition{% endblock %} |
|||
{% block content %} |
|||
<p>Add a condition to {{ macros.hook(hook) }}:</p> |
|||
<form action="?" method="POST"> |
|||
{{ macros.cond_params(None) }} |
|||
<button type="submit">Add</button> |
|||
</form> |
|||
{% endblock %} |
@ -0,0 +1,16 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto - Hooks{% endblock %} |
|||
{% block content %} |
|||
<p> |
|||
Here are the active hooks: |
|||
</p> |
|||
|
|||
<ul> |
|||
{% for hook in hooks %} |
|||
<li>{{ macros.hook(hook) }}</li> |
|||
{% else %} |
|||
<li class="nodata">No data.</li> |
|||
{% endfor %} |
|||
<li class="posbutton addbutton"><a href="/hooks/new">Add hook</a></li> |
|||
</ul> |
|||
{% endblock %} |
@ -0,0 +1,9 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto - New Hook{% endblock %} |
|||
{% block content %} |
|||
<p>Add a new hook:</p> |
|||
<form action="?" method="POST"> |
|||
{{ macros.hook_params(None) }} |
|||
<button type="submit">Add</button> |
|||
</form> |
|||
{% endblock %} |
@ -0,0 +1,30 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto{% endblock %} |
|||
{% block content %} |
|||
<h2>A Webhook Hacking Server</h2> |
|||
<h3>Sponsored by <a href="https://gitlab.cosi.clarkson.edu/COSI/">COSI</a></h3> |
|||
|
|||
<p> |
|||
Welcome to Pluto! This server is a test server useful for deploying |
|||
quick, one-off scripts to integrate with "webhooks"—HTTP |
|||
requests initiated by services in response to events. |
|||
</p> |
|||
|
|||
<div class="flex-center"> |
|||
<div class="neutbutton srchbutton"><a href="/logs">Logs</a></div> |
|||
<div class="neutbutton srchbutton"><a href="/debuglogs">Debug Logs</a></div> |
|||
<div class="neutbutton srchbutton"><a href="/hooks">Hooks</a></div> |
|||
</div> |
|||
|
|||
<p> |
|||
If you'd like to use Pluto for one of your projects, point the URL |
|||
for a webhook at <span |
|||
class="monospace">http://pluto.cslabs.clarkson.edu/hook</span> or |
|||
<span |
|||
class="monospace">https://pluto.cslabs.clarkson.edu/hook</span>, |
|||
then actuate the hook a few times, and <a href="/logs">check the |
|||
logs</a> to see what kind of requests are being sent. Then, <a |
|||
href="/hooks">add a hook</a> |
|||
in the hooks table. |
|||
</p> |
|||
{% endblock %} |
@ -0,0 +1,40 @@ |
|||
{% extends "base.html" %} |
|||
{% block title %}Pluto - Logs{% endblock %} |
|||
{% block content %} |
|||
<p> |
|||
Here are the last {{ n }} requests observed to <span class="monospace">/hook</span>: |
|||
</p> |
|||
|
|||
<table id="logs"> |
|||
<tr> |
|||
<th>Time</th> |
|||
<th>Path</th> |
|||
<th>Headers</th> |
|||
<th>Data</th> |
|||