#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ eventlogging-devserver ---------------------- Invoking this command-line tool will spawn a web server that can serve as EventLogging endpoint during MediaWiki development. Events logged to this server will be validated verbosely and pretty-printed to the terminal. Example: event : {"wiki": "devwiki", "schema": "TrackedPageContentSaveComplete", "revision": 7872558, "event": {"revId": 10, "token": "foobar"}} url : http://localhost:8080/event?%7B%22wiki%22%3A+%22devwiki%22%2C+%22schema%22%3A+%22TrackedPageContentSaveComplete%22%2C+%22revision%22%3A+7872558%2C+%22event%22%3A+%7B%22revId%22%3A+10%2C+%22token%22%3A+%22foobar%22%7D%7D usage: eventlogging-devserver [-h] [--host HOST] [--port PORT] optional arguments: -h, --help show this help message and exit --host HOST server host (default: 'localhost') --port PORT server port (default: 8080) -v, --verbose print pretty colors to stderr :copyright: (c) 2012 by Ori Livneh :license: GNU General Public Licence 2.0 or later """ import sys import argparse import itertools import json from wsgiref.simple_server import make_server, WSGIRequestHandler import eventlogging.parse import eventlogging.schema import jsonschema from pygments.console import ansiformat argparser = argparse.ArgumentParser(fromfile_prefix_chars='@') argparser.add_argument('--host', default='localhost', help='server host (default: localhost)') argparser.add_argument('--port', default=8080, type=int, help='server port (default: 8080)') argparser.add_argument('--verbose', '-v', action='store_true', help='print out events to stderr as they come in') args = argparser.parse_args() colorize = ansiformat seq_ids = itertools.count() parser = eventlogging.parse.LogParser('%q %{recvFrom}s %{seqId}d %t %h') log_fmt = ('?%(QUERY_STRING)s %(SERVER_NAME)s ' '%(SEQ_ID)d %(TIME)s %(REMOTE_ADDR)s') max_qs_size = 900 def heading(caption=None): if caption is None: return 77 * '-' return '-- {:-<95}'.format(colorize('*yellow*', caption) + ' ') class EventLoggingHandler(WSGIRequestHandler): """WSGI request handler; annotates environ dict with seq ID and timestamp in NCSA Common Log Format.""" def get_environ(self): environ = WSGIRequestHandler.get_environ(self) environ.update(SEQ_ID=next(seq_ids), TIME=eventlogging.parse.utcnow()) return environ def log_message(self, format, *args): pass # We'll handle logging in the WSGI app. def validate(log_line): """Parse and validate a log line containing an encapsulated event. Returns a tuple of (event, errors). If no object was decoded, 'event' will be None.""" try: event = parser.parse(log_line) except ValueError as err: return None, [err] try: scid = event['schema'], event['revision'] except KeyError as err: return event, [err] try: schema = eventlogging.schema.get_schema(scid, encapsulate=True) except jsonschema.SchemaError as err: return event, [err] validator = jsonschema.Draft3Validator(schema) return event, list(validator.iter_errors(event)) def validate_size(environ): """Check whether the query string respects the maximum size. Returns a list with a ValueError if the size exceeds the maximum. Returns an empty list otherwise.""" if 'QUERY_STRING' in environ: qs_size = len(environ['QUERY_STRING']) if qs_size > max_qs_size: return [ValueError( 'Query string size (%d) is greater than max size (%d)' % (qs_size, max_qs_size))] return [] def handle_event(environ, start_response): """WSGI app; parses, validates and pretty-prints incoming event requests.""" log_line = log_fmt % environ event, errors = validate(log_line) errors.extend(validate_size(environ)) headers = { 'Server': 'eventlogging-devserver', 'Requested-Event-Valid': str(int(not errors)) } for i, error in enumerate(errors): headers['Validation-Error-%d' % (i + 1)] = str(error) status = '204 No Content' headers = list(headers.items()) start_response(status, headers) sys.stdout.write(json.dumps(event) + "\n") if args.verbose: print(heading('request')) print(log_line) print(heading('event')) pretty_json = json.dumps(event, indent=2, sort_keys=True) print(pretty_json) print(heading('validation')) for error in errors: print(colorize('red', 'Error:'), error) if not errors: print(colorize('green', 'Valid.')) print(heading()) return [] httpd = make_server(args.host, args.port, handle_event, handler_class=EventLoggingHandler) sys.stderr.write(''' ___ _ / (_) \_|_) o \__ _ _ _ _|_ | __ __, __, _ _ __, / | |_|/ / |/ | | _| / \_/ | / | | / |/ | / | \___/ \/ |__/ | |_/|_/(/\___/\__/ \_/|/\_/|/|_/ | |_/\_/|/ -----------------------------------------/|---/|--------------/|---------- (C) Wikimedia Foundation, 2020 \| \| \| # Ensure the following values are set in LocalSettings.php:''' + colorize('green', ''' wfLoadExtension('EventLogging'); $wgEventLoggingBaseUri = 'http://localhost:8080/event'; ''')) sys.stderr.write(heading('Listening to events...') + "\n") try: httpd.serve_forever() except KeyboardInterrupt: pass