blob: 7fe534aafeac635d3623e85d6cd95969c487bb8a [file] [log] [blame]
#!/usr/bin/env python3
# Implements a Google pubsub v1 push listener, see:
# https://cloud.google.com/pubsub/docs/push
#
# In order to work, grok-pull must be running as a daemon service with
# the "socket" option enabled in the configuration.
#
# The pubsub message should contain two attributes:
# {
# "message": {
# "attributes": {
# "proj": "projname",
# "repo": "/path/to/repo.git"
# }
# }
# }
#
# "proj" value should map to a "$proj.conf" file in /etc/grokmirror
# (you can override that default via the GROKMIRROR_CONFIG_DIR env var).
# "repo" value should match a repo defined in the manifest file as understood
# by the running grok-pull daemon (it will ignore anything else)
#
# Any other attributes or the "data" field are ignored.
import falcon
import json
import os
import socket
import re
from configparser import ConfigParser, ExtendedInterpolation
# Some sanity defaults
MAX_PROJ_LEN = 32
MAX_REPO_LEN = 1024
# noinspection PyBroadException
class PubsubListener(object):
def on_get(self, req, resp):
resp.status = falcon.HTTP_200
resp.body = "We don't serve GETs here\n"
def on_post(self, req, resp):
if not req.content_length:
resp.status = falcon.HTTP_500
resp.body = 'Payload required\n'
return
try:
doc = json.load(req.stream)
except:
resp.status = falcon.HTTP_500
resp.body = 'Failed to parse payload as json\n'
return
try:
proj = doc['message']['attributes']['proj']
repo = doc['message']['attributes']['repo']
except (KeyError, TypeError):
resp.status = falcon.HTTP_500
resp.body = 'Not a pubsub v1 payload\n'
return
if len(proj) > MAX_PROJ_LEN or len(repo) > MAX_REPO_LEN:
resp.status = falcon.HTTP_500
resp.body = 'Repo or project value too long\n'
return
# Proj shouldn't contain slashes or whitespace
if re.search(r'[\s/]', proj):
resp.status = falcon.HTTP_500
resp.body = 'Invalid characters in project name\n'
return
# Repo shouldn't contain whitespace
if re.search(r'\s', proj):
resp.status = falcon.HTTP_500
resp.body = 'Invalid characters in repo name\n'
return
confdir = os.environ.get('GROKMIRROR_CONFIG_DIR', '/etc/grokmirror')
cfgfile = os.path.join(confdir, '{}.conf'.format(proj))
if not os.access(cfgfile, os.R_OK):
resp.status = falcon.HTTP_500
resp.body = 'Invalid project name\n'
return
config = ConfigParser(interpolation=ExtendedInterpolation())
config.read(cfgfile)
if 'pull' not in config or not config['pull'].get('socket'):
resp.status = falcon.HTTP_500
resp.body = 'Invalid project configuration (no socket defined)\n'
return
sockfile = config['pull'].get('socket')
if not os.access(sockfile, os.W_OK):
resp.status = falcon.HTTP_500
resp.body = 'Invalid project configuration (socket does not exist or is not writable)\n'
return
try:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
client.connect(sockfile)
client.send(repo.encode())
except:
resp.status = falcon.HTTP_500
resp.body = 'Unable to communicate with the socket\n'
return
resp.status = falcon.HTTP_204
app = falcon.API()
pl = PubsubListener()
app.add_route('/pubsub_v1', pl)