07. Go web: expresiones regulares (regex) en Golang

Publicado por

El servidor que hemos desarrollado hasta esta entrada tiene un grave problema de seguridad: el cliente puede hacer peticiones a cualquier ruta de lectura o escritura sin ninguna clase de filtro. En esta entrada vamos a solucionar ese problema.

Expresiones regulares

Una expresión regular, expresión racional, regex o regexp es una secuencia de caracteres que definen un patrón de búsqueda. Con ellas es posible agilizar y estandarizar los procesos de búsqueda de texto.

Golang cuenta con un paquete dedicado a las regex: regexp.

En esta entrada vamos a utilizar la siguiente expresión regular:

^/(edit|save|view)/([a-zA-Z0-9]+)$

De forma general esa expresión se puede leer como: verifica la coincidencia de las rutas de edición (edit), guardado (save) y visualización (view), que vayan acompañadas del nombre de una página que esté conformado por mínimo una letra mayúscula, o minúscula o un número, o bien, una combinación de esos caracteres.

Ruta válida: /view/Ejemplo
Ruta inválida: /view

Comenzamos

Importamos los paquetes regexp y errors al servidor; el primero de ellos ya hemos explicado para qué nos servirá. Por otro lado, el paquete errors incluye las definiciones de varios métodos para trabajar con errores, en nuestro caso nos servirá para avisar a los manejadores (handlers) cuando haya algo malo con la ruta de la petición. Los paquetes del servidor quedarían de la siguiente forma:

import (
  "fmt"
  "io/ioutil"
  "net/http"
  "html/template"
  "regexp"
  "errors"
)

En seguida, vamos a crear una variable global que almacenará la expresión regular:

var regex_ruta = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")

El método regex.MustCompile() es el encargado de analizar y compilar la expresión regular para generar una variable tipo regexp. Existe otro método para compilar expresiones regulares, regexp.Compile(); este último, además de regresar la variable regexp, también devuelve un dato tipo error que señala si hay algún problema con la expresión que se suministró. Si usamos regexp.MustCompile(), podemos prescindir de algunas líneas de código puesto que el método automáticamente hará que el programa entre en estado de pánico y termine su ejecución si no puede compilar la regex.

En seguida, creamos la función que retornará el título y la variable de error (de ser necesario):

func dameTitulo(w http.ResponseWriter, r *http.Request) (string, error) {
  m := regex_ruta.FindStringSubmatch(r.URL.Path)
  if m == nil {
    http.NotFound(w, r)
    return "", errors.New("Ruta inválida")
  }
  return m[2], nil
}

El método FindStringSubmatch() regresa un slice con la primera ocurrencia de la expresión buscada (de izquierda a derecha) y sus subgrupos. Si imprimimos el slice generado al intentar acceder a /view/Ejemplo, obtenemos:

[“/view/Ejemplo”, “view”, “Ejemplo”]

Cuando la URL de la petición del cliente no concuerda con el conjunto que acepta la regex, la variable m será nil y la función dameTitulo() regresará una cadena vacía junto con un error. En cambio, si es correcta, significa que el nombre de la wiki estará en el índice 2 del slice, es por eso que regresamos m[2] junto con nil para indicar que no hubo ningún problema.

Finalmente, agregamos el llamado a la función dameTitulo() en los 3 manejadores que necesitan hacer uso de ella: manejadorMostrar(), manejadorEditar() y manejadorGuardar().

Servidor funcional

El siguiente es el código fuente del servidor con los cambios que hemos hecho hasta esta entrada:

package main
import (
  "fmt"
  "io/ioutil"
  "net/http"
  "html/template"
  "regexp"
  "errors"
)

type Pagina struct{
  Titulo string
  Cuerpo []byte
}

var plantillas = template.Must(template.ParseFiles("edit.html", "view.html"))
var regex_ruta = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")

func main(){
  //Creamos y guardamos una página para que el cliente la pida
  pag1 := &Pagina{Titulo: "Ejemplo", Cuerpo:[]byte(
    "¡Hola personita! Este es el cuerpo de tu página.")}
  pag1.guardar()

  http.HandleFunc("/view/", manejadorMostrar)
  http.HandleFunc("/edit/", manejadorEditar)
  http.HandleFunc("/save/", manejadorGuardar)
  fmt.Println("El servidor se encuentra en ejecución.");
  http.ListenAndServe(":8080", nil)
}

//Método para guardar página
func ( p* Pagina ) guardar() error {
  nombre := p.Titulo + ".txt"
  return ioutil.WriteFile( "./view/" + nombre, p.Cuerpo, 0600)
}

//Función para cargar página
func cargarPagina( titulo string ) (*Pagina, error) {
  nombre_archivo := titulo + ".txt"
  fmt.Println("El cliente ha pedido: " + nombre_archivo)
  cuerpo, err := ioutil.ReadFile( "./view/" + nombre_archivo )
  if err != nil {
    return nil, err
  }
  return &Pagina{Titulo: titulo, Cuerpo: cuerpo}, nil
}

//Función para validar ruta y regresar nombre de la página solicitada
func dameTitulo(w http.ResponseWriter, r *http.Request) (string, error) {
  m := regex_ruta.FindStringSubmatch(r.URL.Path)
  if m == nil {
    http.NotFound(w, r)
     return "", errors.New("Ruta inválida")
  }
  return m[2], nil
}

//Función para cargar las plantillas HTML
func cargarPlantilla(w http.ResponseWriter, nombre_plantilla string, pagina *Pagina){
  plantillas.ExecuteTemplate(w, nombre_plantilla + ".html", pagina)
}

//Manejador para visualizar wikis
func manejadorMostrar(w http.ResponseWriter, r *http.Request){
  titulo, err := dameTitulo(w, r)
  if err != nil{
    return
  }
  p, err := cargarPagina(titulo)

  if err != nil {
    http.Redirect(w, r, "/edit/" + titulo, http.StatusFound)
    fmt.Println("La página solicitada no existía. Llamando al editor...")
    return
  }
  cargarPlantilla(w, "view", p)
}

//Manejador para editar wikis
func manejadorEditar(w http.ResponseWriter, r *http.Request){
  titulo, err := dameTitulo(w, r)

  if err != nil{
    return
  }

  p, err := cargarPagina(titulo)
  if err != nil{
    p = &Pagina{Titulo: titulo}
  }
  cargarPlantilla(w, "edit", p)
}

//Manejador para guardar wikis
func manejadorGuardar(w http.ResponseWriter, r * http.Request) {
  titulo, err := dameTitulo(w, r)

  if err != nil {
    return
  }

  cuerpo := r.FormValue("body")
  p := &Pagina{Titulo: titulo, Cuerpo: []byte(cuerpo)}
  fmt.Println("Guardando " + titulo + ".txt...")
  p.guardar()
  http.Redirect(w, r, "/view/" + titulo, http.StatusFound)
}

Nota: he renombrado la función manejadorMostrarPagina() a manejadorMostrar() para que concuerde con el nombre de los otros 2 manejadores.

Finalizando…

En esta entrada aprendiste a solucionar un grave problema de seguridad que tenía tu servidor. Cualquier duda que tengas puedes dejarla en los comentarios y con gusto te ayudaré a solucionarla. Hasta la próxima. See ya!

Deja una respuesta

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