Usando la directiva RewriteRule para redirección 301

Antes de seguir veremos un poco del archivo httpd.conf del servidor:

# Dynamic Shared Object (DSO) Support
#
# To be able to use the functionality of a module which was built as a DSO you
# have to place corresponding `LoadModule' lines at this location so the
# directives contained in it are actually available _before_ they are used.
# Statically compiled modules (those listed by `httpd -l') do not need
# to be loaded here.
#
# Example:
# LoadModule foo_module modules/mod_foo.so
#
LoadModule actions_module modules/mod_actions.so
LoadModule alias_module modules/mod_alias.so
LoadModule asis_module modules/mod_asis.so
LoadModule auth_basic_module modules/mod_auth_basic.so
#LoadModule auth_digest_module modules/mod_auth_digest.so
...
#LoadModule proxy_ftp_module modules/mod_proxy_ftp.so
#LoadModule proxy_http_module modules/mod_proxy_http.so
#LoadModule reqtimeout_module modules/mod_reqtimeout.so
#LoadModule rewrite_module modules/mod_rewrite.so   
...

Esta era la situación de mi archivo httpd.conf antes de hacer algo en localhost. Resaltado en amarillo vemos que el módulo alias_module (cuyo archivo es mod_alias.so) está activado, es decir, no tiene el caracter # que marca la línea como comentario. Pero el módulo rewrite_module (archivo mod_rewrite.so) si está como comentario. Para poder usar la directiva RewriteRule hay que quitarle la marca de comentario para que se cargue este módulo (será necesario reiniciar el servidor).

La necesidad de hacer una redirección se debe a que he cambiado de sitio los documentos que antes estaban en la carpeta /xhtml-css y los he trasladado a la carpeta /temas/xhtml-css. Aunque no viene al caso, se trataba de mantener esos contenidos dentro de la carpeta /temas para conseguir una mejor estructura de las carpetas. Para mantener las referencias a los documentos de esa carpeta /xhtml-css haremos una redirección con RewriteRule, con lo que cualquier petición a http://www.wextensible.com/xhtml-css/... será redirigida a http://www.wextensible.com/temas/xhtml-css/.... Para ello crearemos en la carpeta raíz un archivo de texto cuyo nombre será .htaccess con un simple Notepad y con este contenido.

   
RewriteEngine on
#RewriteBase /
RewriteRule ^xhtml-css/(.*)$ /temas/xhtml-css/$1 [R=301,L]

La directiva RewriteEngine permite activar o desactivar todas las directivas de reescritura (del módulo mod_rewrite). Hay que decir que esta directiva no se hereda entre carpetas, por lo que no afecta en absoluto si el administrador del httpd.conf la tuviera activada o desactivada, pero como su valor por defecto es off (desactivada), hemos de incluirla necesariamente con el valor on (activada). Otra utilidad es que si tenemos un montón de directivas de reescritura en el .htaccess, podemos desactivar su ejecución simplemente poniéndola a off sin necesidad de marcar como comentarios todas las directivas.

La directiva RewriteBase está marcada como comentario. En siguientes apartados veremos cuando hay que usarla. En este ejemplo no es necesaria.

La directiva RewriteRule es la que nos permitirá aplicar la redirección 301. Esta directiva tiene 3 partes separadas por espacios: RewriteRule Patrón Sustitución.

El patrón en nuestro ejemplo es ^xhtml-css/(.*)$. Se trata de una cadena de expresión regular, lo cual da una potencia considerable a esta regla de reescritura. Las expresiones regulares son díficiles de aprender pero tienen una gran utilidad en muy diversos ámbitos. Por lo tanto conviene aprenderlas al menos un poco, pues como he dicho en alguna ocasión, nos sacarán de muchos apuros. En este sitio, en el tema PHP: formularios seguros hay un apartado donde se expone un uso de las mismas en ejemplos de patrones de expresiones regulares. No es un tutorial de expresiones regulares, por lo que aconsejo buscar otras fuentes de información sobre esto, pero queda patente su gran utilidad. Veámos algunos detalles de las expresiones regulares usadas en el patrón ^xhtml-css/(.*)$.

Los caracteres ^ y $ se les denominan anclas, pues su cometido es literalmente "anclar" el patrón al inicio y final de la cadena. Es decir, con un patrón como ^abcdef$ lo que buscamos es que toda la cadena coincida con abcdef. Recordemos que lo que estamos analizando es una URL o petición que llegó al servidor.

Luego tenemos una subexpresión en paréntesis (.*). El punto es un metacaracter que significa buscar cualquier caracter, mientras que el asterisco es un cuantificador que se traduce por encontrar cero o más caracteres. La expresión encerrada entre paréntesis significa que será capturada para poder usarla más tarde.

En la parte de sustitución tenemos /temas/xhtml-css/$1. Aquí la expresión $1 significa que se sustituirá por lo encontrado en la subexpresión en paréntesis del patrón. No confundir este caracter $ con el anterior pues en la parte de sustituciones tiene otro cometido. El 1 a continuación indica que se trata de la primera subexpresión y única en este caso. Si hubiesen más se usarían los siguientes números correlativos según la posición de la subexpresión en el orden de lectura izquierda a derecha.

Por último y como parte de la sustitución, tenemos unos indicadores o flags que modifican el comportamiento de la directiva. Es la cadena [R=301,L]. La R=301 significa que hay que realizar una redirección 301. La L significa que cuando se ejecute esta directiva, el servidor ignorará el resto del archivo .htaccess. En este ejemplo esta L está puesta sólo a título informativo, pues el archivo no contiene más directivas después.

Probamos en nuestro localhost poniendo en la barra de direcciones del navegador una ruta como http://localhost/xhtml-css/formulario.html

rewriterule-1

Que deberá ser redirigida a http://localhost/temas/xhtml-css/formulario.html como se observa tras enviar esa petición:

rewriterule-2

En definitiva, si el servidor recibe una URL como /xhtml-css/abc/def/ghi.html entonces la cambiará por /temas/xhtml-css/abc/def/ghi.html. Además esto sucede sin que el usuario perciba nada al respecto en el navegador. Sólo si observa la barra de direcciones podrá ver que ha habido una redirección.

La directiva Alias para crear ubicaciones virtuales

En el apartado anterior nos asalta una duda: ¿qué pasa con la primera barra inicial de la URL de petición?, es decir, ¿por qué no la ponemos en el patrón ^xhtml-css/(.*)$ de esta forma ^/xhtml-css/(.*)$?. Como vimos antes, estábamos redirigiendo desde http://www.wextensible.com/xhtml-css/... hasta http://www.wextensible.com/temas/xhtml-css/..., por lo que es posible preguntarse porqué las rutas relativas al dominio no empiezan con una barra cuando usamos RewriteRule.

Pero creo que la cosa va según el contexto donde se use la reescritura. Si se usa dentro del archivo httpd.conf entonces hay que poner la barra inicial. Pero si se usa dentro de un archivo .htaccess entonces hay que tener en cuenta la directiva RewriteBase, que por defecto ya contiene la carpeta raíz del sitio, es decir, la barra / indicadora de la carpeta raíz. Por lo tanto no hay que poner esa barra. Al final del apartado de la documentación Apache sobre RewriteRule se ven ejemplos sobre ambos contextos de reescritura.

En el enlace anterior de la documentación Apache sobre RewriteBase se observa un ejemplo de uso. Vamos a hacer algo parecido en nuestro localhost. Pero vamos a usar nuestro servidor local como si fuese un alojamiento compartido. De hecho lo tengo configurado así con objeto de saber como se comporta. Tengo 3 dominios virtuales: localhost, localhost1 y localhost2. En el tema de Sesiones en PHP hay un apartado sobre alojamiento compartido en Apache Server que explica como configuré estos dominios virtuales. Usaré el sitio localhost1 para hacer estas pruebas. Ahora modificaremos la directiva del dominio virtual que se encuentra en el archivo de configuración .../Apache server/conf/extra/httpd-vhosts.conf, archivo donde se configuran los dominios virtuales:

<VirtualHost *:80>
    DocumentRoot "C:/.../Apache server/htdocs/home/sitio1"
    ServerName localhost1
    Alias /virtual "C:/.../Apache server/htdocs/home/alias-sitio1"    
</VirtualHost>

Este dominio virtual localhost1 tiene su carpeta raíz (DocumentRoot) en "C:/.../Apache server/htdocs/home/sitio1". Las comillas son necesarias pues hay espacios en la ruta (recuerde que esto es en Windows y lleva la C:/ del disco donde se ubican las carpetas). Hemos omitido carpetas intermedias con los puntos suspensivos pues no tienen mayor importancia y podemos leer mejor el ejemplo. En todo caso se trata de una ruta física real a la carpeta que contiene todos los documentos del dominio virtual localhost1.

En teoría ningún usuario de localhost1 usando el protocolo HTTP puede acceder fuera de la carpeta raíz. Aunque con script como PHP si es posible como podemos ver en el tema PHP sesiones expuestas, para lo cual se manejan opciones PHP de limitación de acceso como safe_mode y open_basedir que también se expone en ese tema.

Pero a veces es interesante situar documentos por fuera de la carpeta raíz y que al mismo tiempo se pueda acceder a ellos con el protocolo HTTP. Si tenemos acceso al servidor, esto puede ser de utilidad porque esa carpeta está fuera del acceso de cualquier usuario y sólo será posible acceder únicamente con alguna directiva del servidor o por medios de script PHP si la configuración lo permite. Para hacer un ejemplo creamos una carpeta en /home/alias-sitio1 al mismo nivel que /home/sitio1, con lo que está por fuera de la carpeta raíz de ese sitio localhost1. Luego aplicamos la directiva Alias declarando que cualquier petición a http://localhost1/virtual realmente estará apuntando a la carpeta real "C:/.../Apache server/htdocs/home/alias-sitio1" en lugar de "C:/.../Apache server/htdocs/home/sitio1/virtual". Es decir, físicamente no existe esa carpeta /virtual en el servidor (no hay una ruta como "C:/.../home/sitio1/virtual").

Después de modificar el httpd-vhosts.conf y hacer unas cuantas páginas HTML muy simples para poder visualizar el ejemplo, reiniciamos el servidor. Esto hay que hacerlo cada vez que modifiquemos algo en los archivos de configuración de la carpeta conf. En primer lugar tenemos el documento /home/sitio1/uso-alias.html que es llamado con http://localhost1/uso-alias.html:

alias-1

Es sólo una página de entrada que contiene un vínculo a http://localhost1/virtual/pagina-virtual.html. Si lo pulsamos, la directiva Alias declarada anteriormente nos presentará ese documento:

alias-2

donde observamos que presenta la página "virtual" de forma que el usuario no sabrá si esa carpeta /virtual realmente existe en el servidor, como que de hecho no existe.

Redirecciones 301 con ubicaciones virtuales realizadas con la directiva Alias

Ahora vamos a ver como se comporta un redireccionamiento con RewriteRule en una carpeta virtual, es decir en la real "C:/.../Apache server/htdocs/home/alias-sitio1" pero que se tiene acceso sólo con la directiva Alias. Para ello disponemos un archivo .htaccess en esa carpeta con este contenido:

RewriteEngine on
RewriteBase /virtual
RewriteRule ^pagina-red1\.html$ pagina-red2.html [R=301,L]
El patrón de la expresión regular es ^pagina-red1\.html$. En este caso se escapa el punto con una barra invertida para indicar que no se trata del metacaracter que busca cualquier caracter sino del punto literal.

Tenemos en esa carpeta dos documentos, pagina-red1.html y pagina-red2.html. Si intentamos acceder al primer documento, la reescritura nos presentará el segundo, siempre y cuando hayamos dispuesto la directiva RewriteBase /virtual. Por ejemplo, partiendo de la última pantalla y escribiendo http://localhost1/virtual/pagina-red1.html en la barra de direcciones del navegador:

rewritebase-1

El redireccionamiento nos llevará simpre a pagina-red2.html

rewritebase-2

Y eso es lo que hemos conseguido, un redirección permanente a un documento virtual que nos lleva de /virtual/pagina-red1.html a /virtual/pagina-red2.html. Pero ¿cómo funciona la directiva RewriteBase /virtual?.

Los archivos de registro access.log y error.log de Apache

En la carpeta logs del servidor Apache encontraremos los archivos de registro access.log y error.log. Veámos que se ha registrado en ellos con las peticiones y respuestas del ejemplo del apartado anterior. El último contenido de access.log es:

...
127.0.0.1 - - [23/Apr/2011:10:18:42 +0100] "GET /uso-alias.html HTTP/1.1" 304 -
127.0.0.1 - - [23/Apr/2011:10:18:43 +0100] "GET /virtual/pagina-virtual.html HTTP/1.1" 304 -
127.0.0.1 - - [23/Apr/2011:10:18:51 +0100] "GET /virtual/pagina-red1.html HTTP/1.1" 301 250
127.0.0.1 - - [23/Apr/2011:10:18:51 +0100] "GET /virtual/pagina-red2.html HTTP/1.1" 304 - 

Se observan las peticiones GET /uso-alias.html HTTP/1.1" 304 y la siguiente GET /uso-alias.html HTTP/1.1" 304 previas a la que desencadena la redirección en amarillo. Veámos esto un poco, especialmente en lo relacionado con el código de estado HTTP(El protocolo HTTP-1.1 es la base para entender como se comunica un servidor con un navegador. Es bastante díficil llegar a entenderlo completamente, pero poco a poco hay que ir haciéndose con él. En este vínculo a http://www.ietf.org/rfc/rfc2616.txt puede ver un original en inglés.) 304. Con este código el servidor le dice al navegador que la página no ha cambiado desde la última vez que la solicitó. Esto sucede porque el navegador la almacena en su caché, pero si fuera la primera vez que se solicita, entonces se respondería con un codigo 200 que significa que el servidor entendió la solicitud, encontró el recurso y se lo devolvió correctamente al navegador. En definitiva ambos códigos son iguales, pero con el 304 el servidor no envía el documento pues el navegador lo puede coger de su caché y así el proceso es más rápido.

Y ahora vamos a entender la petición GET /virtual/pagina-red1.html HTTP/1.1" 301, cuyo código de estado 301 significa (mi traducción del protocolo original):

301 Movido permanentemente: El recurso solicitado ha sido asignado a a una nueva URI permanente y las futuras referencias a este recurso DEBERÁN usar una de las URIs que presente el servidor. Los clientes [navegadores] con capacidad para editar vínculos deberán revincular automáticamente las referencias a la solicitud a una o más de las nuevas referencias devueltas por el servidor en los sitios que sea posible. Esta respuesta se almacenará en caché a menos que se indique lo contrario.

La nueva y permanente URI DEBERÁ ser enviada [al navegador] en un campo Location de la cabecera. A menos que el método usado sea HEAD, el documento de respuesta DEBERÁ contener un hipervínculo [como un elemento HTML <a>] a la nueva URI(s).

Pero entonces ¿quién hizo la última petición GET /virtual/pagina-red2.html que aparece en el registro y que finalmente nos condujo a la página redirigida?. Para entenderlo usemos la herramienta Telnet.php para ver como es la petición GET /virtual/pagina-red1.html y la respuesta que da el servidor:

PETICIÓN
================================================================================
GET /virtual/pagina-red1.html HTTP/1.1
Host: localhost1
Connection: Close


--------------------------------------------------------------------------------
RESPUESTA
================================================================================
HTTP/1.1 301 Moved Permanently
Date: Sat, 23 Apr 2011 09:35:40 GMT
Server: Apache/2.2.15 (Win32) PHP/5.2.13
Location: http://localhost1/virtual/pagina-red2.html
Content-Length: 250
Connection: close
Content-Type: text/html; charset=iso-8859-1

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved 
    <a href="http://localhost1/virtual/pagina-red2.html">here</a>.</p>
</body></html>

--------------------------------------------------------------------------------

Vemos que el servidor responde con una cabecera HTTP/1.1 301 Moved Permanently. Además indica la nueva referencia Location: http://localhost1/virtual/pagina-red2.html para que el navegador haga una nueva petición a esa nueva ubicación. En este caso nuestro Telnet no tiene esa capacidad y por tanto se muestra la página con un elemento vínculo <a> que apunta a la nueva ubicación. Pero en un navegador el usuario no notará la recepción de esta página de estado 301 y que inmediatamente sin presentarla en la ventana, el navegador hace una petición de la nueva ubicación.

En esta tabla exponemos los movimientos producidos en la comunicación entre servidor y navegador en el caso del ejemplo con el navegador (no el del Telnet que no puede responder al Location):

AcciónRuta del documento
Petición GET inicial del navegador:http://localhost1/virtual/pagina-red1.html
El servidor aplica Alias al observar la carpeta /virtual y así encuentra la ruta real del recurso:C:/.../alias-sitio1/pagina-red1.html
Luego el servidor aplica RewriteRule pues esa página tiene una redirección y responde al navegador con una página de estado 301 que contiene un Location en su cabecera a la nueva ruta. El servidor sabe que hay que hacer la redirección a:C:/.../alias-sitio1/pagina-red2.html
Pero el servidor no envía Location: C:/.../alias-sitio1/pagina-red2.html sino que aplica RewriteBase y así convierte esa ruta física en la virtual y es la que enviará en el Location:http://localhost1/virtual/pagina-red2.html
El navegador responde a ese 301 con una nueva petición a la página del Location anterior. Entonces el servidor vuelve a aplicar el Alias al encontrar la carpeta /virtual con objeto de saber la ruta real:C:/.../alias-sitio1/pagina-red2.html

Error 403 Forbidden relacionado con RewriteBase

Si no hubiésemos declarado RewriteBase /virtual, lo que podemos hacer marcando como comentario la diretiva en el .htaccess:

RewriteEngine on
# RewriteBase /virtual
RewriteRule ^pagina-red1\.html$ pagina-red2\.html [R=301,L]

Entonces al llamar a http://localhost1/virtual/pagina-red1.html nos encontraremos con una página como esta:

rewritebase-3

El archivo de registro access.log contiene las siguientes últimas líneas relacionadas con estos accesos (acortamos las rutas y ponemos cada entrada en dos líneas para verlo mejor):

...
127.0.0.1 - - [23/Apr/2011:10:12:59 +0100] 
    "GET /uso-alias.html HTTP/1.1" 304 -
127.0.0.1 - - [23/Apr/2011:10:13:01 +0100] 
    "GET /virtual/pagina-virtual.html HTTP/1.1" 304 -
127.0.0.1 - - [23/Apr/2011:10:13:04 +0100] 
    "GET /virtual/pagina-red1.html HTTP/1.1" 301 348
127.0.0.1 - - [23/Apr/2011:10:13:04 +0100] 
    "GET /C:/.../home/alias-sitio1/pagina-red2.html HTTP/1.1" 403 316

Vemos que con la solicitud de /virtual/pagina-red1.html se hace una redirección 301, pero luego se obtiene un código de error 403. En el archivo error.log observamos la siguiente última línea relacionada con este acceso (acortamos ruta y ponemos en varias líneas, aunque en el archivo está todo en una única línea):

...
[Sat Apr 23 10:13:04 2011] [error] [client 127.0.0.1] (20023)
    The given path was above the root path: 
    Cannot map GET /C:/.../home/alias-sitio1/pagina-red2.html HTTP/1.1 
    to file, referer: http://localhost1/virtual/pagina-virtual.html

En este error se dice que La ruta dada se salió de la carpeta raíz. No se puede servir esa ruta. Se trata del código de estado 403, que traducido del protocolo nos dice:

403 Forbidden [Prohibido]: El servidor entendió la petición, pero rehusó completarla. Una autorización no será posible y esta petición NO DEBERÍA ser repetida. Si la petición no fue un HEAD y el servidor desea hacer saber porque la petición no ha sido completada, DEBERÁ describir la razón para tal rechazo en la página de error. Si el servidor no desea dar explicaciones al usuario, entonces usará un código de estado 404 (No encontrado).

Por lo tanto sin la directiva RewriteBase el navegador recibirá la ruta física real en la cabecera Location, como podemos comprobar con el Telnet:

PETICIÓN
================================================================================
GET /virtual/pagina-red1.html HTTP/1.1
Host: localhost1
Connection: Close


--------------------------------------------------------------------------------
RESPUESTA
================================================================================
HTTP/1.1 301 Moved Permanently
Date: Sat, 23 Apr 2011 10:55:59 GMT
Server: Apache/2.2.15 (Win32) PHP/5.2.13
Location: http://localhost1/C:/.../home/alias-sitio1/pagina-red2.html
Content-Length: 348
Connection: close
Content-Type: text/html; charset=iso-8859-1

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved 
    <a href="http://localhost1/C:/.../home/alias-sitio1/pagina-red2.html"
    >here</a>.</p>
</body></html>

--------------------------------------------------------------------------------

En la respuesta observamos que el servidor envió el Location real, es decir, la ruta física real del recurso redirigido (la hemos abreviado para que no ocupe mucho en pantalla). Este Telnet no puede responder a una redirección, pero si fuese un navegador si lo haría y solicitaría ese recurso. Pero como /alias-sitio1/pagina-red2.html está por fuera de la carpeta raíz del dominio localhost1, el servidor enviará un código de estado 403.

Podemos simular la petición del Location prohibido en nuestro Telnet:

PETICIÓN
================================================================================
GET /C:/.../htdocs/home/alias-sitio1/pagina-red2.html HTTP/1.1
Host: localhost1
Connection: Close


--------------------------------------------------------------------------------
RESPUESTA
================================================================================
HTTP/1.1 403 Forbidden
Date: Sat, 23 Apr 2011 11:37:45 GMT
Server: Apache/2.2.15 (Win32) PHP/5.2.13
Content-Length: 316
Connection: close
Content-Type: text/html; charset=iso-8859-1

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
</head><body>
<h1>Forbidden</h1>
<p>You don't have permission to access 
    /C:/.../htdocs/home/alias-sitio1/pagina-red2.html
on this server.</p>
</body></html>

--------------------------------------------------------------------------------

Vemos que el servidor responde con un código de estado 403 "prohibido".

En definitiva, la directiva RewriteBase es necesaria antes de una RewriteRule si vamos a redirigir entre rutas virtuales. En otro caso no tenemos que declararla pues por defecto contiene RewriteBase /, es decir, la carpeta raíz. En la redirección 301 que preparamos más arriba para el servidor real de este sitio, tenía la directiva RewriteBase marcada como comentario:

   
RewriteEngine on
RewriteBase /
RewriteRule ^xhtml-css/(.*)$ /temas/xhtml-css/$1 [R=301,L]

Si la desmarcamos veremos que tiene el mismo efecto que si estuviera marcada como comentario o si no existiera, siempre que la escribamos como RewriteBase /. Por lo tanto es esta barra que apunta a la carpeta raíz la que será agregada de forma automática a la RewriteRule al inicio de cada ruta a redirigir: ^/xhtml-css/(.*)$.