Mercurial sobre Nginx

Desde que empecé a utilizar Mercurial me ha parecido una herramienta excelente para el control de versiones.

Además de ser open source, la encuentro ágil, sencilla y multiplataforma y con una documentación (Mercurial Red Book) que ya le gustaría tener a otros. Basta con sólo leer este único manual para comprender el concepto detrás del control de versiones distribuído, los comandos de uso, la administración de los repositorios y la arquitectura interna del producto.

Pero, la Ley de Metcalfe (que otros quieren rebautizar como Teoría del bar lleno) ha hecho que los usuarios de DVCS utilicen en forma masiva Git, quizás impulsados más por el éxito de GitHub que por el sistema de control de versiones sobre el que se basa.

El procedimiento que se describe a continuación es con el cual monté el repositorio web de Cross Forests basado en Mercurial hasta que, siguiendo la corriente, lo terminé migrando finalmente a Git.
Esta instalación funcionó correctamente durante varios años (desde el 2014 hasta finales de 2017) y, si bien puede servir de referencia para futuras instalaciones, debe tenerse en cuenta que los sistemas y productos en los que se basa puedan haber sufrido cambios que requieran actualizar el procedimiento aquí descripto.


Objetivo

El servicio gestiona dos repositorios:

  • Un repositorio público, accesible por http y que permite operaciones de lectura solamente.
  • Un repositorio privado, al que únicamente accederán usuarios autenticados accediendo por https y que permitirá operaciones tanto de lectura como de escritura.

En caso que se necesite un repositorio público que permita a la vez operaciones de escritura, siempre podrá ser creado en una de las dos áreas y enlazado simbólicamente a la otra o, mejor aún; creado en el área privada y clonado en el área pública. Así, se podrá controlar cuáles cambios son trasladados o no al área pública.

Como se generarán simultáneamente dos servicios distintos, en cada sección se tratará básicamente cómo configurar el área de repositorios pública y posteriormente se comentarán los cambios a realizar para implementar el área privada.

Requisitos

  • Se cuenta con un nombre de dominio (FQDN) específico para este servicio. En este ejemplo, el nombre utilizado es hg.example.org.
  • Se cuenta con certificados para el dominio. Ya sea auto-firmados como se explica en How to create SSL certificates u obtenidos por una CA.
  • Se cuenta con una distribución Linux con un servidor Nginx operacional donde se ha instalado Mercurial y los paquetes de gestión de fastcgi.
    En caso de tratarse de una distribución basada en Debian, el comando de intalación es:

    # apt-get install mercurial nginx apache2-utils python-flup spawn-fcgi
  • Se cuenta con un directorio donde alojar los scripts, archivos de configuración y repositorios. Este directorio debe contar con los permisos correctos de acceso y ejecución del servidor web.
    En este ejemplo, el directorio escogido es /var/local/hg, por lo que la secuencia de comandos será:

    # mkdir -p /var/local/hg/scripts
    # mkdir -p /var/local/hg/repos/public  
    # mkdir -p /var/local/hg/repos/private
    # chown -R www-data:www-data /var/local/hg/
    # chmod -R o-rwx /var/local/hg/
    

Scripts de publicación

En esta sección se definen los scripts fastcgi a ejecutarse para publicar los 2 servicios que se desean configurar.

Script de publicación del servicio público

Se edita un nuevo fichero de nombre /var/local/hg/scripts/hgwebpub.fcgi y se puebla con el siguiente contenido:

#!/usr/bin/env python
#
# An example CGI script to export multiple hgweb repos, edit as necessary

# adjust python path if not a system-wide install:
#import sys
#sys.path.insert(0, "/path/to/python/lib")

# enable demandloading to reduce startup time
from mercurial import demandimport; demandimport.enable()

# Uncomment to send python tracebacks to the browser if an error occurs:
#import cgitb
#cgitb.enable()

# If you'd like to serve pages with UTF-8 instead of your default
# locale charset, you can do so by uncommenting the following lines.
# Note that this will cause your .hgrc files to be interpreted in
# UTF-8 and all your repo files to be displayed using UTF-8.
#
import os
os.environ["HGENCODING"] = "UTF-8"

from mercurial.hgweb.hgwebdir_mod import hgwebdir  
from flup.server.fcgi import WSGIServer

# The config file looks like this.  You can have paths to individual
# repos, collections of repos in a directory tree, or both.
#
# [paths]
# virtual/path1 = /real/path1
# virtual/path2 = /real/path2
# virtual/root = /real/root/*
# / = /real/root2/*
#
# paths example:
#
# * First two lines mount one repository into one virtual path, like
# '/real/path1' into 'virtual/path1'.
#
# * The third entry tells every mercurial repository found in
# '/real/root', recursively, should be mounted in 'virtual/root'. This
# format is preferred over the [collections] one, using absolute paths
# as configuration keys is not supported on every platform (including
# Windows).
#
# * The last entry is a special case mounting all repositories in
# '/real/root2' in the root of the virtual directory.
#
# Alternatively you can pass a list of ('virtual/path', '/real/path') tuples
# or use a dictionary with entries like 'virtual/path': '/real/path'

WSGIServer(hgwebdir('/var/local/hg/scripts/hgwebpub.config')).run()  

# EOF : hgwebpub.fcgi

Y se ajustan los permisos de acceso y ejecución al script con:

# chown www-data:www-data /var/local/hg/scripts/hgwebpub.fcgi 
# chmod 0750 /var/local/hg/scripts/hgwebpub.fcgi

Script de publicación del servicio privado

Dado que ambos servicios son muy parecidos, la solución más sencilla consiste en copiar el archivo:
/var/local/hg/scripts/hgwebpub.fcgi
como:
/var/local/hg/scripts/hgwebprv.fcgi
y editar este último modificando la línea:

WSGIServer(hgwebdir('/var/local/hg/scripts/hgwebpub.config')).run()  

por:

WSGIServer(hgwebdir('/var/local/hg/scripts/hgwebprv.config')).run()  

cambiando por lo tanto el archivo de configuración a utilizar por uno y otro script.

Como en el script anterior, se ajustan los permisos de acceso y ejecución con:

# chown www-data:www-data /var/local/hg/scripts/hgwebprv.fcgi 
# chmod 0750 /var/local/hg/scripts/hgwebprv.fcgi

Creación de archivos de configuración

Archivo de configuración del servicio público

Se edita un nuevo fichero de nombre /var/local/hg/scripts/hgwebpub.config y se puebla con el siguiente contenido:

[web]
baseurl = /
push_ssl = True
descend = True
collapse = True

[paths]
/public = /var/local/hg/repos/public/**

Al no incluirse en este archivo la declaración allow_push se garangiza que ningún usuario tendrá permisos de escritura sobre los repositorios de este servicio. Lo cual es irrelevante dado que la declaración push_ssl = True no permite operaciones de escritura salvo que se acceda vía https, y el protocolo https no será configurado en el servidor web para esta área.

Se ajustan los permisos de acceso y ejecución al archivo con:

# chown www-data:www-data /var/local/hg/scripts/hgwebpub.config 
# chmod 0640 /var/local/hg/scripts/hgwebpub.config

Archivo de configuración del servicio privado

Se edita un nuevo fichero de nombre /var/local/hg/scripts/hgwebprv.config y se puebla con el siguiente contenido:

[web]
baseurl = /
allow_push = *
push_ssl = True
descend = True
collapse = True

[paths]
/public = /var/local/hg/repos/public/**
/private = /var/local/hg/repos/private/**

Dado que la autenticación de usuarios ya se realiza en el servidor web, es seguro permitir el acceso de escritura irrestricto con la declaración allow_push = *. Si se desea realizar un control de acceso individualizado para cada repositorio en particular, se tiene que editar el fichero .hg/hgrc en ese repositorio especificando cuáles serán los usuarios con permisos de escritura.

Se ajustan los permisos de acceso y ejecución al script con:

# chown www-data:www-data /var/local/hg/scripts/hgwebprv.config 
# chmod 0640 /var/local/hg/scripts/hgwebprv.config

Creación de scripts de inicio automático

En esta sección se definen los scripts que permiten el arranque y apagado automático de los scripts de servicio cada vez que se encienda o apague el equipo donde se alojan los servicios de publicación.

NOTA: Estos scripts están realizados para una distribución que utiliza el mecanismo de arranque de servicios basado en System V y deberían ser adaptados a sistemas basados en systemd.

Script de inicio del servicio público

Se edita un nuevo fichero de nombre /etc/init.d/hgwebpub y se puebla con el siguiente contenido:

#! /bin/sh
#
# hgwebpub.init Startup script for the fast cgi Mercurial Server
#
# description: Loading hgwebpub.fcgi using spawn-cgi
#
# Author: Ryan Norbauer 
# Modified: Geoffrey Grosenbach http://topfunky.com
# Modified: David Krmpotic http://davidhq.com
# Modified: Kun Xi http://kunxi.org
# Modified: Jose Ignacio Croce http://crossforests.com
PATH=/var/local/hg/scripts:$PATH
DAEMON=/usr/bin/spawn-fcgi  
FCGISOCK=/var/run/hgwebpub.sock
FCGIUSER=www-data  
FCGIGROUP=www-data  
FCGIAPP=/var/local/hg/scripts/hgwebpub.fcgi  
PIDFILE=/var/run/hgwebpub.pid  
DESC="HG (public) in FastCGI mode"

# Gracefully exit if the package has been removed.
test -x $DAEMON || exit 0  
test -x $FCGIAPP || exit 0

start() {  
    $DAEMON -s $FCGISOCK -u $FCGIUSER -g $FCGIGROUP \
    -f $FCGIAPP -P $PIDFILE 2> /dev/null || echo -en "already running"
}

stop() {  
    kill -QUIT `cat $PIDFILE` || echo -en "not running"
}

restart() {  
    kill -HUP `cat $PIDFILE` || echo -en "can't reload"
}

case "$1" in  
    start)
        echo -n "Starting $DESC: "
        start
        ;;
    stop)
        echo -n "Stopping $DESC: "
        stop
        ;;
    restart|reload)
        echo -n "Restarting $DESC: "
        stop
        # One second might not be time enough for a daemon to stop,
        # if this happens, d_start will fail (and dpkg will break if
        # the package is being upgraded). Change the timeout if needed
        # be, or change d_stop to have start-stop-daemon use --retry.
        # Notice that using --retry slows down the shutdown process somewhat.
        sleep 1 
        start
        ;;
    *)
        echo "Usage: $SCRIPTNAME {start|stop|restart|reload}" >&2
        exit 3
        ;;
esac
exit $?

Se ajustan los permisos de acceso y ejecución al script y se habilita el inicio automático al encender el sistema con:

# chown root:root /etc/init.d/hgwebpub 
# chmod 0750 /etc/init.d/hgwebpub
# update-rc.d hgwebpub defaults

A partir de este momento, el servicio podrá ser iniciado con comandos del tipo:

# service hgwebpub start

y detenido con comandos como:

# service hgwebpub stop

Script de inicio del servicio privado

Se edita un nuevo fichero de nombre /etc/init.d/hgwebprv y se puebla con el siguiente contenido:

#! /bin/sh
#
# hgwebprv.init Startup script for the fast cgi Mercurial Server
#
# description: Loading hgwebprv.fcgi using spawn-cgi
#
# Author: Ryan Norbauer 
# Modified: Geoffrey Grosenbach http://topfunky.com
# Modified: David Krmpotic http://davidhq.com
# Modified: Kun Xi http://kunxi.org
# Modified: Jose Ignacio Croce http://crossforests.com
PATH=/var/local/hg/scripts:$PATH
DAEMON=/usr/bin/spawn-fcgi  
FCGISOCK=/var/run/hgwebprv.sock
FCGIUSER=www-data  
FCGIGROUP=www-data  
FCGIAPP=/var/local/hg/scripts/hgwebprv.fcgi  
PIDFILE=/var/run/hgwebprv.pid  
DESC="HG (private) in FastCGI mode"

# Gracefully exit if the package has been removed.
test -x $DAEMON || exit 0  
test -x $FCGIAPP || exit 0

start() {  
    $DAEMON -s $FCGISOCK -u $FCGIUSER -g $FCGIGROUP \
    -f $FCGIAPP -P $PIDFILE 2> /dev/null || echo -en "already running"
}

stop() {  
    kill -QUIT `cat $PIDFILE` || echo -en "not running"
}

restart() {  
    kill -HUP `cat $PIDFILE` || echo -en "can't reload"
}

case "$1" in  
    start)
        echo -n "Starting $DESC: "
        start
        ;;
    stop)
        echo -n "Stopping $DESC: "
        stop
        ;;
    restart|reload)
        echo -n "Restarting $DESC: "
        stop
        # One second might not be time enough for a daemon to stop,
        # if this happens, d_start will fail (and dpkg will break if
        # the package is being upgraded). Change the timeout if needed
        # be, or change d_stop to have start-stop-daemon use --retry.
        # Notice that using --retry slows down the shutdown process somewhat.
        sleep 1
        start
        ;;
    *)
        echo "Usage: $SCRIPTNAME {start|stop|restart|reload}" >&2
        exit 3
        ;;
esac

exit $?

Se ajustan los permisos de acceso y ejecución al script y se habilita el inicio automático al encender el sistema con:

# chown root:root /etc/init.d/hgwebprv 
# chmod 0750 /etc/init.d/hgwebprv
# update-rc.d hgwebprv defaults

A partir de este momento, el servicio podrá ser iniciado con comandos del tipo:

# service hgwebprv start

y detenido con comandos como:

# service hgwebprv stop

Creación de la base de datos de usuarios

La base de datos de los usuarios habilitados para acceder al sistema privado se ubica en /var/local/hg/scripts/hgusers y se crea con la opción ‑c del comando htpasswd como se muestra en el siguiente ejemplo:

# htpasswd -c /var/local/hg/scripts/hgusers user1

Al ejecutar este comando, se solicita que se ingrese dos veces la clave con la que el usuario se autenticará en el sistema.

La creación de usuarios adicionales también se realiza con el comando htpasswd user_name pero cuidando de nunca incluir la opción ‑c ya que, de hacerlo, la base de datos actual sería truncada a cero y reiniciada; sólo existiendo en su lugar el nuevo usuario creado.

# htpasswd /var/local/hg/scripts/hgusers user2

Una vez creado el archivo, se ajustan los permisos de acceso y ejecución con:

# chown www-data:www-data /var/local/hg/scripts/hgusers 
# chmod 0640 /var/local/hg/scripts/hgusers

Configuración del servidor Nginx

Se crea un site específico para el servicio. Para ello, se crea el fichero /etc/nginx/sites-available/hg y se puebla con el siguiente contenido:

#
# hg : Mercurial web server configuration
#
# 15DEC2014 - Jose Ignacio Croce
#

server {  
    listen 80;
    server_name hg.example.org;
    root /var/local/hg/repos;

    location / {
        include fastcgi_params;
        fastcgi_pass unix:/var/run/hgwebpub.sock;
        fastcgi_param DOCUMENT_ROOT /var/local/hg/repos;
        fastcgi_param QUERY_STRING $query_string;
        fastcgi_param REQUEST_METHOD $request_method;
        fastcgi_param CONTENT_TYPE $content_type;
        fastcgi_param CONTENT_LENGTH $content_length;
        fastcgi_param SCRIPT_FILENAME /var/local/hg/scripts/hgwebpub.fcgi;
    }
}

server {  
    listen 443 ssl;
    server_name hg.example.org;
    ssl_certificate /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;
    root /var/local/hg/repos;

    location / {
        include fastcgi_params;
        fastcgi_pass unix:/var/run/hgwebprv.sock;
        fastcgi_param DOCUMENT_ROOT /var/local/hg/repos;
        fastcgi_param QUERY_STRING $query_string;
        fastcgi_param REQUEST_METHOD $request_method;
        fastcgi_param CONTENT_TYPE $content_type;
        fastcgi_param CONTENT_LENGTH $content_length;
        fastcgi_param SCRIPT_FILENAME /var/local/hg/scripts/hgwebprv.fcgi;
        auth_basic 'hgwebprv';
        auth_basic_user_file /var/local/hg/scripts/hgusers;
    }
}

# EOF : hg

Se ajustan los permisos del archivo y se activa el site con la siguiente secuencia de comandos:

# chown www-data:www-data /etc/nginx/sites-available/hg 
# chmod 0640 /etc/nginx/sites-available/hg 
# ln -s /etc/nginx/sites-available/hg /etc/nginx/sites-enabled/hg

Validación

Reiniciamos el servidor con:

# /etc/init.d/hgwebpub restart
# /etc/init.d/hgwebprv restart
# /etc/init.d/nginx restart

Y comprobamos que el servicio funciona correctamente con el siguiente procedimiento.

Creación de repositorio de pruebas privado

Se accede al directorio de repositorios privado y se genera un repositorio de pruebas mediante el comando hg init y se ajustan los permisos para garantizar el acceso desde el servidor web:

# cd /var/local/hg/repos/private/
# hg init testprv
# chown -R www-data:www-data testprv/
# chmod -R o-rwx testprv/

Creación de repositorio de pruebas público

Se accede al directorio de repositorios público y se genera un repositorio de pruebas mediante el comando hg init y se ajustan los permisos para garantizar el acceso exclusivamente de lectura desde el servidor web:

# cd /var/local/hg/repos/public/
# hg init testpub
# chown -R root:www-data testpub/
# chmod -R o-rwx testpub/
# chmod -R g-w testpub/

En este ejemplo sólo el usuario root tendrá permiso para realizar operaciones de escritura para el repositorio, lo cual da un grado de seguridad adicional frente al acceso público.

Pruebas de acceso web

Desde un navegador web se accede a la URL http://hg.example.org. El resultado esperado es una página como la siguiente:

Pantalla inicial al servidor público
Pantalla inicial al servidor público

Haciendo click en el nombre public, se debería ver el repositorio generado en el área pública:

Lista de proyectos en el directorio público
Lista de proyectos en el directorio público

Y haciendo click en el nombre del repositorio, la ventana esperada es:

Detalle del proyecto de pruebas en el servidor público
Detalle del proyecto de pruebas en el servidor público

Ahora, si en lugar de utilizar el acceso http, se intenta el acceso seguro a https://hg.example.org, el servidor solicitará autenticación con nombre y clave. Una vez superada la fase de autenticación la página a viusalizar debería ser la siguiente:

Página inicial de acceso al servidor privado
Página inicial de acceso al servidor privado

José Administrator
Apasionado por el software, la electrónica y la fotografía.
follow me

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *