Bad Daemons

Aplicaciones web simples con flask, uwsgi y nginx

Lo primero que tengo que dejar claro sobre el tema es que odio el dearrollo web. Solo hay que ver el estado de esta página web, si fuése más viejuna tendria que meterle gifs de bebes satánicos bailando. Supongo que es por que las veces que me he tenido que pelear con el tema, he tenido que tocar mucho de la parte visual, lo que seria el diseño y el CSS. Y yo tengo un gusto horrible.

Por ello, cuando quise hacer uso de los webhooks que ofrece gitea pensé que odiaría hacerlo posible. Por suerte me encontré con Flask, que viene a ser un django usable por gente que no quiere dedicarle mucho tiempo al desarrollo. Una vez instalado el paquete mediante un pip install flask, hacer un "Hola Mundo!" es tan simple como esto:

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hola Mundo!"

Cuando se ejecute el anterior script, dirá que hay un servidor web activo en el puerto 5000, si le hacemos un curl nos devolverá el querido "Hola Mundo".

La aplicación que hice es la siguiente. Está en python 2 por que queria usar xmpppy, que es más simple de usar que Sleekxmpp o Slixmpp. Estas librerias son mucho más robustas, pero mantener la conexión asíncrona era complicarse más de lo que queria. Así que una conexión por visita y ale:

#!/usr/bin/env python2
# -*- coding: utf-8 -*-

import xmpp
import json
from flask import Flask, request
import ConfigParser


app = Flask(__name__)


@app.route('/')
def root():
    return redirect("/webhook", code=302)

@app.route("/webhook", methods=['POST'])
def webhook():
    # read config file
    config = ConfigParser.ConfigParser()
    config.read('config.ini')
    username = config.get('xmpp', 'username', 0)
    password = config.get('xmpp', 'password', 0)
    server = config.get('xmpp', 'server', 0)
    room = config.get('xmpp', 'room', 0)
    nick = config.get('xmpp', 'nick', 0)
    secret = config.get('gitea', 'secret', 0)

    # TODO: comprobar si funciona sin secret
    if not secret:
        secret = ''

    if request.json['secret'] == secret:
        data = json.loads(request.data.decode('utf-8'))
        message = ''
        print(data)

        # commit
        if 'compare_url' in data.keys():
            message = data['pusher']['username'] + ' has pushed some changes:'\
                      + ' ' + data['compare_url']
        # pull request open
        elif data['action'] == 'opened':
            message = data['sender']['username'] + ' opened a new pull reques'\
                      + 't: ' + data['pull_request']['html_url']
        # close pull request
        elif data['action'] == 'closed':
            message = data['sender']['username'] + ' closed a pull request: ' \
                      + data['pull_request']['html_url']
        # reopen pull request
        elif data['action'] == 'reopened':
            message = data['sender']['username'] + ' reopened a pull request:'\
                      + ' ' + data['pull_request']['html_url']
        # add label
        elif data['action'] == 'label_updated':
            f_tag = ""
            for tag in data['pull_request']['labels']:
                f_tag += '[' + tag['name'] + '] '
            message = data['sender']['username'] + ' changed the labels ' \
                           'of a pull request: ' + f_tag + \
                           data['pull_request']['html_url']
        # delete label
        elif data['action'] == 'label_cleared':
            message = data['sender']['username'] + ' deleted all the labels ' \
                      'of a pull request: ' + data['pull_request']['html_url']

        if message:
            client = xmpp.Client(server, debug=[])
            client.connect()
            client.auth(username, password, 'gitea')

            # send join
            client.send(xmpp.Presence(to="%s/%s" % (room, nick)))

            msg = xmpp.protocol.Message(body=message)
            msg.setTo(room)
            msg.setType('groupchat')

            client.send(msg)
            presence = xmpp.Presence(to=room)
            presence.setAttr('type', 'unavailable')
            client.send(presence)

    return ":)"


if __name__ == "__main__":

    app.run()

Este fichero deberia guardarse como gitea-sendxmpp.py. No hay mucho que comentar. Las peticiones que se hagan a la raiz se re-envian a /webhook, por lo que se puede apuntar directamente ahí. Leerá el archivo de configuración (que veremos a continuación), y si el secreto cuadra con el de la configuración parseará el mensaje y enviará un mensaje a la sala de jabber que se le diga.

El archivo de configuración a rellenar es el siguiente:

[xmpp]
username =
password =
server   =
room     =
nick     =

[gitea]
secret   =

Y por último, la forma correcta de ejecutar aplicaciones web en python es usar uwsgi. Un ejemplo simple de configuración es el siguiente:

[uwsgi]

chdir = /var/www/gitea-sendxmpp/
module = gitea-sendxmpp:app

master = true
processes = 1
threads = 2

uid = www-data
gid = www-data
socket = /tmp/gitea-sendxmpp.sock
chmod-socket = 777

die-on-term = true

Ya solo queda configurar nginx para que actue como un proxy inverso (algo sumamente recomendable por temas de codificación, eficiencia, seguridad, …) del siguiente modo:

server {
    listen 80;
    server_name daemons.it;

    location / {
        include uwsgi_params;
        uwsgi_pass unix:/tmp/gitea-sendxmpp.sock;
    }
}

Lo más importante es que el socket uwsgi_pass sea el mismo tanto en la configuración de uwsgi como de nginx.

Para ver la versión más reciente, en caso de que la modifique, se puede visitar el repositorio.