mirror of
https://github.com/FAUSheppy/simple-webhook-handler
synced 2025-12-06 04:11:35 +01:00
synology nas mode
This commit is contained in:
55
README.md
55
README.md
@@ -1,55 +0,0 @@
|
|||||||
# What is this
|
|
||||||
This is a simple webhook listener, primarily built for GitLab. It listens for request containing a secret token and executes scripts according to config file. The tool expects a request to be a **POST**-request, to be in a *JSON* format, to carry the correct type **application/json**, to contain a a header with **secret token** (see *$TOKEN_HEADER*) and to contain the json path **project/$PROJECT\_IDENTIFIER**.
|
|
||||||
|
|
||||||
# Config file structure
|
|
||||||
The config file uses *COMMA* as a separator, lines are comments if they start with a *#*. Each line must feature a web\_url of the project, the authorization token and the script to be executed. Scripts referenced in the config must be executable.
|
|
||||||
|
|
||||||
PROJECT,TOKEN,PATH_TO_SCRIPT
|
|
||||||
|
|
||||||
# Running standalone with flask-inbuild server
|
|
||||||
|
|
||||||
usage: webhook_listener.py [-h] [-i INTERFACE] [-p PORT] [-c C]
|
|
||||||
Simple Webhook listener
|
|
||||||
|
|
||||||
optional arguments:
|
|
||||||
-h, --help show this help message and exit
|
|
||||||
-i INTERFACE, --interface INTERFACE
|
|
||||||
Interface to listen on (default: 0.0.0.0)
|
|
||||||
-p PORT, --port PORT Port to listen on (default: 5000)
|
|
||||||
-c C Config for handling of webhooks (default:
|
|
||||||
webhook.config)
|
|
||||||
|
|
||||||
# Running with waitress (WSGI)
|
|
||||||
|
|
||||||
waitress-serve --host 127.0.0.1 --port 5000 --call 'app:createApp'
|
|
||||||
|
|
||||||
# Running behind NGINX for SSL
|
|
||||||
You can (and should) run this tool behind a reverse proxy handling SSL. I recommend nginx with this configuration. Note the *proxy_next_upstream*-directive which tells nginx, that it should only report a timeout as bad gateway, since the backend will respond with certain error codes to ease debugging.
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:5184;
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy_next_upstream timeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Response Codes
|
|
||||||
## 400 - project not identified in request
|
|
||||||
The field **project/$PROJECT\_IDENTIFIER** doesn't exist in the request.
|
|
||||||
|
|
||||||
## 401 - project not identified in config
|
|
||||||
The projects identification was found in the request, but not in the config file.
|
|
||||||
|
|
||||||
## 402 - secret token not found in request
|
|
||||||
The header with the name specified in *$TOKEN_HEADER* doesn't exist.
|
|
||||||
|
|
||||||
## 403 - secret token found but is mismatch
|
|
||||||
The project was found in the configuration and the correct header exists, but the header is either empty or the content (the token) of the header doesn't match the token specified in the configuration file.
|
|
||||||
|
|
||||||
# More Examples
|
|
||||||
Have a look at [my CI-Scripts and config](https://github.com/FAUSheppy/auto-redeploy-scripts) for more examples on how to work with this tool.
|
|
||||||
|
|
||||||
# Contribution & Feature-Requests
|
|
||||||
Contributions and feature requests are welcomed but must retain the spirit of this been a simple solution for simple problems.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# lines starting with '#' are comments
|
|
||||||
# web_url,SECRET_TOKEN,script_to_execute
|
|
||||||
# https://gitlab.com/Sheppy_/serien-ampel,test,scripts/example.sh
|
|
||||||
@@ -4,113 +4,28 @@ import sys
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import requests
|
||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
|
|
||||||
app = flask.Flask("webhook-listener")
|
app = flask.Flask("webhook-listener")
|
||||||
app.config["EXEC_CONFIG"] = "webhook.config"
|
|
||||||
TOKEN_HEADER = "X-Gitlab-Token"
|
|
||||||
PROJECT_IDENTIFIER = "web_url"
|
|
||||||
SEPERATOR = ","
|
|
||||||
COMMENT_INDICATOR = "#"
|
|
||||||
config = {}
|
|
||||||
|
|
||||||
HTTP_FORBIDDEN = 401
|
HTTP_FORBIDDEN = 401
|
||||||
HTTP_NOT_FOUND = 404
|
|
||||||
HTTP_UNPROCESSABLE = 422
|
|
||||||
HTTP_INTERNAL_ERR = 500
|
|
||||||
|
|
||||||
##### FRONTEND PATHS ########
|
##### FRONTEND PATHS ########
|
||||||
@app.route('/', methods=["GET","POST"])
|
@app.route('/', methods=["GET","POST"])
|
||||||
def rootPage():
|
def hook():
|
||||||
if flask.request.method == "GET":
|
if flask.request.args["token"] != app.config["TOKEN"]:
|
||||||
return "Webhook Listener ist running"
|
return ("Bad Token", HTTP_FORBIDDEN)
|
||||||
else:
|
|
||||||
data = flask.request.json
|
|
||||||
if data == None:
|
|
||||||
retString = "POST-request is missing payload."
|
|
||||||
print(retString)
|
|
||||||
return (retString, HTTP_UNPROCESSABLE)
|
|
||||||
|
|
||||||
print(json.dumps(flask.request.json, indent=4, sort_keys=True))
|
jsonFixed = flask.request.data.decode("utf-8").strip("'").replace("\n","")
|
||||||
|
jsonDict = json.loads(jsonFixed)
|
||||||
# check for project in request
|
jsonDict.update({"group" : "family"})
|
||||||
project = None
|
jsonDict.update({"message" : jsonDict["content"]})
|
||||||
githubMode = False
|
requests.post(app.config["SIGNAL_GATEWAY"], json=jsonDict)
|
||||||
try:
|
return ("", 204)
|
||||||
if "project" in data: # gitlab
|
|
||||||
project = data["project"][PROJECT_IDENTIFIER]
|
|
||||||
if "repository" in data: #github
|
|
||||||
project = data["repository"]["html_url"]
|
|
||||||
githubMode = True
|
|
||||||
except KeyError:
|
|
||||||
retString = "Rejected: missing project/{} json path".format(PROJECT_IDENTIFIER)
|
|
||||||
print(retString)
|
|
||||||
return (retString, HTTP_UNPROCESSABLE)
|
|
||||||
|
|
||||||
# check for project in config #
|
|
||||||
if not project or project not in config:
|
|
||||||
retString = "Rejected: project not identified in config"
|
|
||||||
print(retString)
|
|
||||||
return (retString, HTTP_NOT_FOUND)
|
|
||||||
|
|
||||||
token, scriptName = config[project]
|
|
||||||
|
|
||||||
# check authentification #
|
|
||||||
GITHUB_HEADER = "X-Hub-Signature"
|
|
||||||
if githubMode:
|
|
||||||
if GITHUB_HEADER not in flask.request.headers:
|
|
||||||
retString = "{} not found in headers".format(GITHUB_HEADER)
|
|
||||||
print(retString)
|
|
||||||
return (retString, HTTP_FORBIDDEN)
|
|
||||||
else:
|
|
||||||
hmacRemote = flask.request.headers[GITHUB_HEADER]
|
|
||||||
hmacLocal = hmac.new(token.encode(), flask.request.data, hashlib.sha1).hexdigest()
|
|
||||||
hmacLocal = "sha1=" + hmacLocal
|
|
||||||
if not hmacLocal == hmacRemote:
|
|
||||||
retString = "Rejected: Hash found but is mismatch"
|
|
||||||
print(retString)
|
|
||||||
return (retString, HTTP_FORBIDDEN)
|
|
||||||
|
|
||||||
elif TOKEN_HEADER not in flask.request.headers:
|
|
||||||
retString = "Rejected: secret token not found in request"
|
|
||||||
print(retString)
|
|
||||||
return (retString, HTTP_FORBIDDEN)
|
|
||||||
elif token != flask.request.headers[TOKEN_HEADER]:
|
|
||||||
retString = "Rejected: secret token found but is mismatch"
|
|
||||||
print(retString)
|
|
||||||
return (retString, HTTP_FORBIDDEN)
|
|
||||||
|
|
||||||
# try to execute script #
|
|
||||||
try:
|
|
||||||
executeScript(scriptName)
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
retString = "Failed: script execution on the server failed"
|
|
||||||
print(retString)
|
|
||||||
return (retString, HTTP_INTERNAL_ERR)
|
|
||||||
|
|
||||||
# signal successfull completion #
|
|
||||||
return ("Success", 200)
|
|
||||||
|
|
||||||
|
|
||||||
def executeScript(scriptName):
|
|
||||||
path = os.path.expanduser(scriptName)
|
|
||||||
subprocess.Popen(path)
|
|
||||||
|
|
||||||
def readExecutionConfig(configFile):
|
|
||||||
global config
|
|
||||||
with open(configFile, "r") as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.strip("\n")
|
|
||||||
if line.startswith(COMMENT_INDICATOR):
|
|
||||||
continue
|
|
||||||
projectIdent, token, scriptName = line.split(SEPERATOR)
|
|
||||||
config.update({projectIdent:(token, scriptName)})
|
|
||||||
|
|
||||||
@app.before_first_request
|
@app.before_first_request
|
||||||
def init():
|
def init():
|
||||||
readExecutionConfig(app.config["EXEC_CONFIG"])
|
pass
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
@@ -119,8 +34,10 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
parser.add_argument("-i", "--interface", default="0.0.0.0", help="Interface to listen on")
|
parser.add_argument("-i", "--interface", default="0.0.0.0", help="Interface to listen on")
|
||||||
parser.add_argument("-p", "--port", default="5000", help="Port to listen on")
|
parser.add_argument("-p", "--port", default="5000", help="Port to listen on")
|
||||||
parser.add_argument("-c", default="webhook.config", help="Config for handling of webhooks")
|
parser.add_argument("-t", "--token", required=True, help="Token in request for auth")
|
||||||
args = parser.parse_args()
|
parser.add_argument("-g", "--gateway", required=True, help="Gateway to forward message to")
|
||||||
|
|
||||||
app.config["EXEC_CONFIG"] = args.c
|
args = parser.parse_args()
|
||||||
|
app.config["TOKEN"] = args.token
|
||||||
|
app.config["SIGNAL_GATEWAY"] = args.gateway
|
||||||
app.run(host=args.interface, port=args.port)
|
app.run(host=args.interface, port=args.port)
|
||||||
|
|||||||
Reference in New Issue
Block a user