# HG changeset patch # User Teemu Piippo # Date 1417230247 -7200 # Node ID da291d9426eaedcc19fb22793188082e5c91bfb0 # Parent 60ead38a61af93a8c6df17c1d16cc33179bd0a41 - first implementation of REST diff -r 60ead38a61af -r da291d9426ea cobalt.py --- a/cobalt.py Mon Nov 17 22:25:18 2014 +0200 +++ b/cobalt.py Sat Nov 29 05:04:07 2014 +0200 @@ -37,6 +37,7 @@ import hgpoll as HgPoll import bt as Bt import irc as Irc +import rest as Rest if __name__ != '__main__': raise ImportError ('cobalt may not be imported as a module') @@ -103,6 +104,9 @@ else: raise ValueError ("unknown autoconnect entry %s" % (aconn)) + # Start the REST server + Rest.RESTServer() + g_BotActive = True asyncore.loop() except KeyboardInterrupt: @@ -113,4 +117,4 @@ excepterm (e.message) if __name__ == '__main__': - main() \ No newline at end of file + main() diff -r 60ead38a61af -r da291d9426ea irc.py --- a/irc.py Mon Nov 17 22:25:18 2014 +0200 +++ b/irc.py Sat Nov 29 05:04:07 2014 +0200 @@ -120,7 +120,7 @@ self.write ("JOIN %s %s" % (channel.get_value ('name'), channel.get_value ('password', default=''))) umode = self.cfg.get_value ('umode', '') - + if umode != '': self.write ('MODE %s %s' % (self.mynick, self.cfg.get_value ('umode', ''))) elif words[1] == "PRIVMSG": @@ -151,17 +151,17 @@ def handle_privmsg (self, line): rex = re.compile (r'^:([^!]+)!([^@]+)@([^ ]+) PRIVMSG ([^ ]+) :(.+)$') match = rex.match (line) - + if not match: broadcast ("Recieved bad PRIVMSG: %s" % line) - + sender = match.group (1) user = match.group (2) host = match.group (3) channel = match.group (4) message = match.group (5) replyto = channel if channel != self.mynick else sender - + # Check for command. if len(message) >= 2 and message[0] == self.commandprefix and message[1] != self.commandprefix: stuff = message[1:].split(' ') @@ -169,20 +169,20 @@ args = stuff[1:] self.handle_command (sender, user, host, replyto, command, args, message) return - + Bt.process_message (self, line, replyto) def add_irc_channel (self, channame): for channel in self.channels: if channel.get_value ('name').upper() == channame.upper(): return - + channel = self.cfg.append_nodelist ('channels') channel.set_value ('name', channame) self.channels = self.cfg.get_nodelist ('channels') self.write ('JOIN ' + channame) self.cfg.save() - + def remove_irc_channel (self, channame): for channel in self.channels: if channel.get_value ('name') == channame: @@ -190,29 +190,29 @@ break else: return - + self.write ('PART ' + channame) self.cfg.save() - + def handle_command (self, sender, ident, host, replyto, command, args, message): kvargs = {'sender': sender, 'ident': ident, 'host': host, 'replyto': replyto, 'cmdname': command, 'message': message} - + try: result = ModuleCore.call_command (self, **kvargs) - + if result: return except ModuleCore.CommandError as e: lines = str (e).split ('\n') self.privmsg (replyto, 'error: %s' % lines[0]) - + for line in lines[1:]: self.privmsg (replyto, ' ' + line) return - + def handle_error(self): raise RestartError (traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback)) - + def restart(self): raise RestartError('') @@ -232,4 +232,4 @@ self.close_connection ('Caught exception') def keyboardinterrupt (self): - self.close_connection ('KeyboardInterrupt') \ No newline at end of file + self.close_connection ('KeyboardInterrupt') diff -r 60ead38a61af -r da291d9426ea rest.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rest.py Sat Nov 29 05:04:07 2014 +0200 @@ -0,0 +1,194 @@ +import ssl +import socket +import errno +import select +import asyncore +import base64 +import re +import json +import urllib +import irc +from datetime import datetime, timedelta +from configfile import Config + +g_credentials = None +g_portnumber = None +g_throttle = [] + +valid_repos = ['Torr_Samaho/zandronum', 'Torr_Samaho/zandronum-stable', + 'crimsondusk/zandronum-sandbox', 'crimsondusk/zandronum-sandbox-stable', + 'crimsondusk/testrepo'] + +def is_throttled (address): + i = 0 + + while i < len (g_throttle): + if g_throttle[i][1] <= datetime.utcnow(): + print 'Throttle of %s expired' % g_throttle[i][0][0] + item = g_throttle.pop (i) # expired + + if item[0][0] == address[0]: + return False # this address expired + + continue + + if g_throttle[i][0][0] == address[0]: + return True # is throttled + + i += 1 + + return False # not throttled + +def throttle (address): + tt = datetime.utcnow() + timedelta (0, 30) + + try: + # attempt to just update the item + g_throttle[g_throttle.index (address)][1] = tt + except ValueError: + # not in list + g_throttle.append ([address, tt]) + + Irc.broadcast ('Throttling %s:%s for 30 seconds' % address) + +def handle_rest_http (data, address): + global g_credentials + displayaddress = '%s:%s' % address + authrex = re.compile (r'^authorization: Basic (.+)$') + payloadrex = re.compile (r'^payload=(.+)$') + authenticated = False + payload = '' + + if not g_credentials: + g_credentials = base64.b64encode (Config.get_node ('rest').get_value ('credentials')) + + # Authenticate and find the payload + for line in data: + match = authrex.match (line) + + if match and match.group (1) == g_credentials) + authenticated = True + continue + + match = payloadrex.match (line) + if match: + payload = match.group (1) + + if not authenticated: + Irc.broadcast ('%s:%s failed to authenticate' % address) + throttle (address) + return + + jsonstring = urllib.unquote_plus (payload).decode ('utf-8') + + try: + jsondata = json.loads (jsonstring) + repodata = jsondata['repository'] + repopath = '%s/%s' % (repodata['owner'], repodata['name'] + + if repopath not in valid_repos: + raise ValueError ('unknown repository %s' % repopath) + + if 'commits' in jsondata: + for commit in jsondata['commits']: + Irc.broadcast ('%s: new commit %s' % (address, commit['node'])) + except Exception as e: + Irc.broadcast ('%s provided bad JSON: %s' % (address, e)) + + try: + with open ('rejected_json.txt', 'w') as fp: + fp.write (jsonstring) + Irc.broadcast ('bad json written to rejected_json.txt') + except: + Irc.broadcast ('failed to log json') + pass + + throttle (address) + return + +class RESTConnection (asyncore.dispatcher): + httpdata = '' + address=None + writebuffer = '' + + def __init__ (self, conn, address): + asyncore.dispatcher.__init__ (self, conn) + self.socket = ssl.wrap_socket (conn, server_side=True, keyfile='key.pem', + certfile='cert.pem', do_handshake_on_connect=False) + self.socket.setblocking (0) + self.address = address + + while True: + try: + self.socket.do_handshake() + break + except ssl.SSLError, err: + if err.args[0] == ssl.SSL_ERROR_WANT_READ: + select.select([self.socket], [], []) + elif err.args[0] == ssl.SSL_ERROR_WANT_WRITE: + select.select([], [self.socket], []) + else: + Irc.broadcast ('%s:%s: SSL error: %s' % (self.address[0], self.address[1], err)) + throttle (self.address) + self.close() + return + + def write (self, msg): + self.writebuffer += msg + + def readable (self): + return True + + def writable (self): + return self.writebuffer + + def handle_close (self): + self.finish() + + def handle_read (self): + while 1: + try: + data = self.recv (4096) + except ssl.SSLError, err: + # EOF + self.finish() + return + + self.httpdata += data.replace ('\r', '') + + def finish (self): + handle_rest_http (self.httpdata.split ('\n'), self.address) + self.close() + + def handle_write (self): + self.send (self.writebuffer) + self.writebuffer='' + + def handle_error (self): + raise + +class RESTServer (asyncore.dispatcher): + def __init__ (self): + global g_portnumber + + if g_portnumber == None: + g_portnumber = Config.get_node ('rest').get_value ('port') + + asyncore.dispatcher.__init__ (self) + self.create_socket (socket.AF_INET, socket.SOCK_STREAM) + self.bind (('', g_portnumber)) + self.listen (5) + + def handle_accept (self): + sock, address = self.accept() + + if is_throttled (address): + # throttled + sock.close() + return + + Irc.broadcast ('REST connection from %s:%s' % address) + RESTConnection (sock, address) + + def handle_error (self): + return