From 23d851aff066a39e3ceb01f85c30539e467a70ea Mon Sep 17 00:00:00 2001 From: kongr45gpen Date: Tue, 15 Mar 2016 16:41:58 +0200 Subject: Add support for github secrets --- local/globals.py | 2 ++ local/handler/GithubHandler.py | 37 ++++++++++++++++++------ local/utility.py | 22 +++++++++++++++ plugin.py | 64 ++++++++++++++++++++++++++++++++++++++++++ request.sh | 11 +++++++- 5 files changed, 126 insertions(+), 10 deletions(-) diff --git a/local/globals.py b/local/globals.py index 6cc8c7b..b389cab 100644 --- a/local/globals.py +++ b/local/globals.py @@ -2,7 +2,9 @@ def init(): global messageList global configOverrides global travisStatuses + global secretDB messageList = [] configOverrides = {} travisStatuses = {} + secretDB = None diff --git a/local/handler/GithubHandler.py b/local/handler/GithubHandler.py index 4d420e3..53ea740 100644 --- a/local/handler/GithubHandler.py +++ b/local/handler/GithubHandler.py @@ -1,10 +1,12 @@ import os import re +import hmac import json import time import random import urllib import urllib2 +import hashlib import urlparse import threading import BaseHTTPServer @@ -37,17 +39,12 @@ class GithubHandler(BaseHTTPServer.BaseHTTPRequestHandler): def do_POST(s): """Respond to a POST request.""" length = int(s.headers['Content-Length']) + payload = s.rfile.read(length).decode('utf-8') if 'content-type' not in s.headers or s.headers['content-type'] == 'application/x-www-form-urlencoded': - post_data = urlparse.parse_qs(s.rfile.read(length).decode('utf-8')) + post_data = urlparse.parse_qs(payload) data = json.loads(post_data['payload'][0]) else: - data = json.loads(s.rfile.read(length).decode('utf-8')) - - s.send_response(200) - s.send_header('Content-type', 'text/html') - s.end_headers() - s.wfile.write("Thanks, you're awesome.\n") - s.wfile.write(s.path.split('/')) + data = json.loads(payload) if 'X-GitHub-Event' in s.headers: eventType = s.headers['X-GitHub-Event'] @@ -58,7 +55,7 @@ class GithubHandler(BaseHTTPServer.BaseHTTPRequestHandler): if not os.path.exists('requests/'): os.makedirs('requests') - f = open('requests/' + eventType + strftime("%Y-%m-%d %H:%M:%S") + '.json', 'w') + f = open('requests/' + eventType.replace('/','_') + strftime("%Y-%m-%d %H:%M:%S") + '.json', 'w') f.write(json.dumps(data, sort_keys=True, indent=4, separators=(',', ': '))) f.close() @@ -92,11 +89,33 @@ class GithubHandler(BaseHTTPServer.BaseHTTPRequestHandler): i+=1 + s.send_response(200) + s.send_header('Content-type', 'text/html') + s.end_headers() + s.wfile.write("Thanks, you're awesome.\n") + s.wfile.write(s.path.split('/')) + if requireCode and receivedcode != configValue('passcode'): # The password is wrong s.wfile.write("The password is wrong") return + secret = getChannelSecret(channel) + if secret is not None: + if not 'X-Hub-Signature' in s.headers: + s.wfile.write("This channel requires a secret") + return + + digest = "sha1=%s" % (hmac.new(secret, payload, hashlib.sha1).hexdigest(),) + log.debug("expected digest: %s", digest) + + provided = s.headers['X-Hub-Signature'] + log.debug("provided digest: %s", provided) + + if not secureCompare(digest, provided): + s.wfile.write("Invalid secret key") + return + brackets = parseBrackets(configValue('brackets')) themeName = configValue('theme') diff --git a/local/utility.py b/local/utility.py index de540a6..2594b61 100644 --- a/local/utility.py +++ b/local/utility.py @@ -1,5 +1,7 @@ import re import math +import random +import string import urllib2 import supybot.conf as conf @@ -134,6 +136,26 @@ def isStatusVisible(repo, status): globals.travisStatuses[repo] = status return changed +def randomString(length): + """Returns a securely generated random string of a specific length""" + return ''.join(random.SystemRandom().choice( + string.ascii_uppercase + string.ascii_lowercase + string.digits + ) for _ in range(length)) + +def secureCompare(s1, s2): + """Securely compare two strings""" + return sum(i != j for i, j in zip(s1, s2)) is 0 + +def getChannelSecret(channel): + """Returns a secret for a channel, or None if that channel has no secret""" + if globals.secretDB is None: + return None + try: + record = globals.secretDB.get(channel, 1) + return record.secret + except KeyError: + return None + def hexToMirc(hash): colors = { 'white': (255, 255, 255), diff --git a/plugin.py b/plugin.py index 6153d62..89a4832 100644 --- a/plugin.py +++ b/plugin.py @@ -34,6 +34,7 @@ import urlparse import threading import BaseHTTPServer +import supybot.dbi as dbi import supybot.log as log import supybot.conf as conf import supybot.utils as utils @@ -61,6 +62,11 @@ class Github(callbacks.Plugin): messages = [] pass + add = None + search = None + stats = None + change = None + def ServerStart(self, httpd): try: log.info('Server Starts - %s:%s' % ('', self.port)) @@ -68,6 +74,7 @@ class Github(callbacks.Plugin): except: return + def __init__(self, irc): self.__parent = super(Github, self) self.__parent.__init__(irc) @@ -117,6 +124,63 @@ class Github(callbacks.Plugin): # Debug command get = wrap(get, ['lowered', optional('lowered'), optional('text')]) if world.testing else False + def abs_val(self,irc,msg,args): + pass + abs_val = wrap(abs_val,[]) + + class secret(callbacks.Commands): + class DB(plugins.DbiChannelDB): + class DB(dbi.DB): + class Record(dbi.Record): + __fields__ = [ + 'secret' + ] + def add(self, secret, **kwargs): + record = self.Record(secret = secret, **kwargs) + return super(self.__class__, self).add(record) + def set(self, id, secret, **kwargs): + record = self.Record(secret = secret, **kwargs) + return super(self.__class__, self).set(id, record) + + def __init__(self, irc): + # self.db = self.DB(("%s-secret") % (self.name(),)) + print(conf.supybot.databases()) + super(Github.secret, self).__init__(irc) + self.db = plugins.DB(("github-secret"), {'flat': self.DB})() + globals.secretDB = self.db + + def set(self, irc, msg, args, channel, secret): + """[] secret + + Sets a Github secret for a channel to a specific value. + is only necessary if the message isn't sent in the channel itself. + """ + self.db.set(channel, 1, secret) + # record = Github.secret.DB.DB.Record(secret = sec) + # self.db.set(channel, 1, secret) + irc.replySuccess() + set = wrap(set, ['channel', 'text']) + + def reset(self, irc, msg, args, channel): + """[] + + Removes a Github secret for a channel. + is only necessary if the message isn't sent in the channel itself. + """ + self.db.remove(channel, 1) + irc.reply("Channel %s no longer has a secret." % channel) + reset = wrap(reset, ['channel']) + + def generate(self, irc, msg, args, channel): + """ + + Generates a Github secret for a channel. + is only necessary if the message isn't sent in the channel itself. + """ + secret = Utility.randomString(40) + irc.reply("Setting secret for %s to: %s" % (channel, secret)) + self.db.set(channel, 1, secret) + generate = wrap(generate, ['channel']) Class = Github diff --git a/request.sh b/request.sh index f118034..61864e7 100755 --- a/request.sh +++ b/request.sh @@ -1,3 +1,12 @@ #!/usr/bin/env bash -curl --data "payload=`cat $1`" http://localhost:8093/ +DATA="payload=`cat $1`" + +echo $DATA + +hash=$(echo -n "$DATA" | openssl dgst -sha1 -hmac "$2" | awk '{print $2}') +echo $hash + +echo -n "$DATA" > ~/repos/supybot-github/cmpr + +curl --header "X-GitHub-Event: $1" --header "X-Hub-Signature: sha1=$hash" --data "$DATA" http://localhost:8093/ -- cgit v1.2.3