Un poco de C

Ya avise cuando comencé con esta pagina que no solo hablaría sobre de Delphi. Así que voy a poner por aquí un nuevo juguete con el que estoy entretenido desde hace unos días. Se trata de un pequeño servidor web realizado en C y pensado para ser utilizado en linux. Para hacernos una idea de lo pequeño que es basta decir que el código cuenta con solo unas 600 lineas.

Con ese tamaño no le podemos exigir demasiado, de todas formas ya dije que no aspira a ser mas que un juguete o como mucho una herramienta puntual para transferir archivos de un equipo a otro. Por ahora maneja correctamente los comandos Get y Head , puede enviar archivos, mensajes de texto, listar directorios, codificar/descodificar urls y además es multihilo. Faltan por implementar la autentificación de usuarios, pero eso ya lo haremos mas adelante si es necesario.

El funcionamiento es sencillo, solo hay que ejecutar pico en un terminal y luego en cualquier navegador utilizar la url http://127.0.0.1:1978/ si es en el mismo equipo o http://tuip:1978/ si es desde otro equipo de tu red. Se puede ejecutar sin usar el terminal, pero entonces el programa permanece oculto y no mostraría ni mensajes, ni errores (si los hubiera). Una vez que accedemos a esa url vemos un listado de todo el sistema de ficheros, aquí es importante recordar que el programa tiene los mismos permisos de lectura que el usuario que lo ejecuta (para mas adelante estoy pensando en utilizar algo como chroot para cambiar el directorio raíz del proceso).

Como siempre, se admiten sugerencias, consejos, criticas pero siempre recordando que se trata de un juguete y no de un servidor serio, yo al menos pienso utilizarlo como venia haciéndolo con su hermano gemelo en windows, para pasar archivos de un ordenador a otro rápidamente y sin tener que andar instalando nada, ni cambiando la configuración de las carpetas compartidas, etc ... un par de clicks y copiando, sin complicarse (muy útil cuando quieres pasar archivos al portátil de un amigo).

#include <getopt.h>
#include <dirent.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/select.h>
#include <sys/sendfile.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
 
const char hex[] = "0123456789ABCDEF";
 
// Configuracion
int buffersize = 8*1024;
int inetd = 0;
int puerto = 1978;
int timeout = 30;
// Variables globales
char *comando = NULL;
char *ruta = NULL;
 
// Imprime mensajes
void printmsg(char *str)
{
 if (!inetd)
   fprintf(stderr,"[%d] %s\n",(int) getpid(),str);
}
 
// Imprime mensajes de error
void printerror(char *str)
{
 if (!inetd)
   fprintf(stderr,"[%d] %s: %s\n",(int) getpid(),str,strerror(errno));
}
 
// Concatena dos cadenas
int stradd(char **dest, char *src)
{
 char *str;
 
 if (!src)
   return -1;  
 if (*dest)
 {
  str = realloc(*dest,strlen(*dest) + strlen(src) + 1);  
 } else 
 {  
  str = malloc(strlen(src) + 1);
  if (str)
    memset(str,0,strlen(src) + 1);
 }
 if (str)
 {
  strcat(str,src);
  *dest = str;  
  return 0;
 }
 return -1;
}
 
// Codifica una url
int urlencode(char **dest, char *src)
{ 
 char *str;
 
 if (!src)
   return -1;  
 str = malloc((strlen(src) * 3) + 1); 
 if (str)
 {
  *dest = str;
  memset(str,0,(strlen(src) * 3) + 1); 
  while (*src)
  {
   if (((*src >= 'a') && (*src <= 'z')) || 
       ((*src >= 'A') && (*src <= 'Z')) ||
       ((*src >= '0') && (*src <= '9')))
   {
    *str = *src;
   } else
   {
    *str = '%';
    str++;
    *str = hex[*src >> 4];
    str++; 
    *str = hex[*src & 0x0f];
   }
   str++;
   src++;
  } 
  return 0;  
 }
 return -1;
}
 
// Descodifica una url
int urldecode(char **dest, char *src)
{  
 char *str;
 char c;
 char esc = 0; 
 
 if (!src)
   return -1;  
 str = malloc(strlen(src) + 1);  
 if (str)
 {
  *dest = str;
  memset(str,0,strlen(src) + 1); 
  while (*src)
  {
   if (*src == '%')
   {
    esc = 2;
    c = 0;
   } else
   {
    switch(esc) 
    {
     case 2: 
       if (strchr(hex,toupper(*src)))
       {
        c = (char)((c + (strchr(hex,toupper(*src))-hex)) << 4);
        esc = 1;
       }
       break;
     case 1: 
       if (strchr(hex,toupper(*src)))
       {         
        *str = (char)(c + (strchr(hex,toupper(*src))-hex));
        str++;
        esc = 0;
       }
       break;
     default: 
       if (*src == '+')
         *str = ' ';
       else       
         *str = *src;
       str++;
    }
   }    
   src++;
  }   
  return 0;  
 }
 return -1;
}
 
// Obtiene informacion sobre un archivo
int fileinfo(char *dir, char *nombre, struct stat *st)
{
 char *str;
 int len;
 int barra = 0;
 
 if (!nombre)
   return -1; 
 if (dir)
 {
  len = strlen(dir);
  if (len > 0)
    if (dir[len-1]!='/')
    {
     len += 1;
     barra = 1;
    } 
 } else len = 0;
 len = len + strlen(nombre) + 1; 
 str = malloc(len);
 if (str)
 {
  memset(str,0,len);
  if (dir)
  {  
   strcpy(str,dir);
   if (barra)
     strcat(str,"/");
  }
  // str = dir + "\" + nombre 
  strcat(str,nombre);  
  if (stat(str,st) == 0)
  {   
   free(str);
   return 0;
  } 
  free(str);
 }
 return -1;
}
 
// Escibe las cabeceras de la respuesta
void cabeceras(int codigo, char *res, int lon, char *tipo)
{ 
 printf("HTTP/1.1 %d %s\n",codigo,res);
 // Si la longitud es menor de cero la ignoramos
 if (lon > 0)
   printf("Content-Length: %d\n",lon);
 // Si se ha determinado el tipo, lo incluimos
 if (tipo)
   printf("Content-Type: %s\n",tipo);
 // Enviamos una linea en blanco
 printf("\n");
 fflush(NULL);
}
 
// Envia un mensaje de texto como respuesta
void mensaje(int codigo, char *res, char *str)
{
 // Escribimos las cabeceras de la respuesta
 cabeceras(codigo,res,strlen(str),"text/html");
 // Si el comando no es HEAD, escribimos en el socket el mensaje
 if (strcasecmp(comando,"HEAD") != 0)
 {  
  printf("%s",str);
  fflush(NULL);
 } 
 // Si el codigo es diferente de 200 (OK) lo mostramos
 if (codigo != 200)
   printmsg(res);
}
 
// Envia un listado con el contenido de un directorio
void indice()
{
 char *str;
 char *url;
 DIR *dir;
 struct dirent *ent;
 struct stat st; 
 
 // Abrimos el directorio
 dir = opendir(ruta);
 if (dir)
 {      
  str = NULL;
  if (stradd(&str,"<html>\n<h1>Indice de ") == 0)
  { 
   stradd(&str,ruta);
   stradd(&str,"</h1>\n<pre>\n");
   // Recorremos el directorio
   while (ent = readdir(dir)) 
   {  
    // Obtenemos informacion sobre el archivo  
    if (fileinfo(ruta,ent->d_name,&st) == 0)
    {
     // Lo agregamos al listado
     stradd(&str,"<a href=\"");
     urlencode(&url,ent->d_name);
     stradd(&str,url);
     free(url);
     // Si es un directorio agregamos una '/' al final
     if (S_ISDIR(st.st_mode))
       stradd(&str,"/");
     stradd(&str,"\">");
     stradd(&str,ent->d_name);
     stradd(&str,"</a>\n");
    }
   }
   stradd(&str,"</pre>\n</html>");
   // Enviamos el listado
   mensaje(200,"OK",str); 
   free(str);
  }
  closedir(dir);
 }
}
 
// Envia un archivo
void enviar()
{
 struct stat st;
 int f;
 off_t offset = 0;
 
 // Comprobamos si el archivo existe
 if (fileinfo(NULL,ruta,&st) == 0)
 {
  // Comprobamos si es un directorio
  if (S_ISDIR(st.st_mode))
  {
   // Si es un directorio enviamos su indice
   indice(ruta);	
  } else
   // Si es un archivo intentamos leerlo
   if ((f = open(ruta, O_RDONLY)) != -1)
   {
    // Escribimos las cabecera
    cabeceras(200,"OK",st.st_size,NULL);
    // Si el comando no es HEAD enviamos el fichero
    if (strcasecmp(comando,"HEAD") != 0)
    { 
     sendfile(STDOUT_FILENO,f,&offset,st.st_size);    
    }
    close(f);
   } else 
     // Respondemos que no encontramos el archivo
     mensaje(404,"Not Found","<html><h1>Not Found</h1></html>");
 } else 
   // Respondemos que no encontramos el archivo
   mensaje(404,"Not Found","<html><h1>Not Found</h1></html>");		
}
 
// Lee una linea, utilizando un buffer
int readline(char *buf, char *line, int count)
{ 
 int tout;
 fd_set fds;
 struct timeval tv;
 int retval;
 char *c; 
 
 // 20 * 50000 us = 1 segundo
 tout = timeout * 20;
 // No salimos del bucle hasta encontrar '\n' o llenar el buffer
 while ((!strchr(buf,'\n')) && (strlen(buf) < count))
 {  
  FD_ZERO(&fds);
  FD_SET(STDIN_FILENO, &fds);
  tv.tv_sec = 0;
  tv.tv_usec = 50000;
  // Comprobamos si hay algo que leer
  retval = select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv);  
  if (retval == -1)
  {
   // Ignoramos la interrupciones
   if (errno = EINTR)
     continue;
   printerror("select()");
   return -1;
  }
  if (retval)
  {  
   // Reseteamos el timeout
   tout = timeout * 20;
   // Apuntamos al caracter nulo
   c = buf + strlen(buf);
   // Escribimos el texto nuevo a continuacion del anterior
   retval = read(STDIN_FILENO, c, count - strlen(buf));
   if (retval == -1)
   {
    printerror("read()");
    return -1;
   }
   if (retval == 0)
     return -1;  
   // Colocamos el caracter nulo al final del texto recibido
   c[retval] = '\0';
  } else
  {
   --tout;
   // Si se cumple el timeout
   if (tout < 1)
   {
    // Salimos 
    mensaje(408,"Request Timeout","<html><h1>Request Timeout</h1></html>");
    return -1;
   }
  }
 }
 // Si encontramos el caracter '\n'
 if (strchr(buf,'\n'))
 {
  // Comenzamos a copiar la linea
  c = buf;
  while (*c != '\n')
  {
    // Eliminamos el caracter '\r'
    if (*c != '\r')
    {
     *line = *c;
     line++;
    }
   c++;
  }
  // Colocamos el carcater nulo al final de la linea
  *line = '\0';
  // Desplazamos el resto del texto 
  c++;
  while (*c)
  {
   *buf = *c;
   buf++;
   c++;
  } 
  // Colocamos el caracter nulo al final del texto
  *buf = '\0';
  return 0;
 }
 // Si llegamos aqui, es que se ha llenado el buffer
 mensaje(414,"Request Too Long","<html><h1>Request Too Long</h1></html>");
 return -1; 
}
 
void conexion()
{
 char *buf;
 char *linea;
 char *saveptr;
 char *token;
 char *str;
 
 // Reservamos memoria para
 buf = malloc(buffersize);
 if (buf)
 {
  linea = malloc(buffersize);
  if (linea)
  {
   // Bucle de la conexion
   while (1)
   {
    if (readline(buf,linea,buffersize - 1) == -1) 
      break; 
    // Ignoramos las lineas en blanco
    if (strlen(linea) > 0)
    {
     // Mostramos la peticion 
     printmsg(linea);
     // Separamos el comando
     token = strtok_r(linea, " \t\n", &saveptr);
     if (token && (strcasecmp(token,"GET") == 0) || (strcasecmp(token,"HEAD") == 0))
     {
      comando = strdup(token);
      // Separamos el documento
      token = strtok_r(NULL, " ?\t\n", &saveptr);
      if (token)
      {
       ruta = strdup(token);
       // Ignoramos el resto de cabeceras (por ahora) 
       do {
         if (readline(buf,linea,buffersize - 1) == -1)
         {
          free(buf);
          free(linea);
          free(comando);
          free(ruta);
          return; 
         }          
       } while (strlen(linea)>0);       
       // Descodificamos el nombre del documento
       if (urldecode(&str,ruta) == 0)
       {
        // Intentamos enviar el documento
        free(ruta);
        ruta = str;
        enviar();
       } else
         mensaje(404,"Not Found","<html><h1>Not Found</h1></html>");
       free(ruta);
      }
      free(comando);      
     } 
    }    
   }
   free(linea); 
  }
  free(buf);
 }
} 
 
void signalhandler(int signal_numer)
{
 int retval;
 int status;
 
 switch(signal_numer)
 {
  case SIGCHLD:
    // Limpiamos los zombies
    do {
      retval = waitpid(-1,&status,WNOHANG);
    } while ((retval != 0) && (retval != -1));
    break;
 }
}
 
void servidor()
{
 struct sigaction sa; 
 int servidor, cliente;
 struct sockaddr_in sin;
 fd_set fds;
 struct timeval tv;
 int retval; 
 
 // Registramos el manejador
 memset(&sa,0,sizeof(sa));
 sa.sa_handler = &signalhandler;
 if (sigaction(SIGCHLD, &sa, NULL) != 0)
 {
  printerror("sigaction()");
  return;
 } 
 // Creamos el socket
 if ((servidor = socket(AF_INET, SOCK_STREAM, 0)) != -1)
 { 
  sin.sin_family = AF_INET;
  sin.sin_port = htons(puerto);
  sin.sin_addr.s_addr = htonl(INADDR_ANY);
  // bind()
  if (bind(servidor,(struct sockaddr *) &sin,sizeof(struct sockaddr_in)) == 0)
  {
   // listen()
   if (listen(servidor, 16) == 0)
   {
    // Bucle principal
    while (1)
    {
     FD_ZERO(&fds);
     FD_SET(servidor, &fds);
     tv.tv_sec = 0;
     tv.tv_usec = 50000;
     // Comprobamos si hay conexiones esperando - select()
     retval = select(servidor + 1, &fds, NULL, NULL, &tv);
     if (retval == -1 )
     {
      // Ignoramos la interrupciones
      if (errno = EINTR)
        continue;
      printerror("select()");
      break;
     }
     // Si hay conexiones esperando, las aceptamos 
     if (retval)
     {  
      retval = sizeof(sin); 
      cliente = accept(servidor, (struct sockaddr *) &sin, &retval);
      if (cliente != -1)
      {
       // Creamos un proceso "hijo" - fork()
       retval = fork(); 
       if (retval == -1)
       {
        printerror("fork()");
        break;
       }
       if (retval == 0)
       {  
        // Estamos dentro del proceso hijo
        close(servidor); 
        if (dup2(cliente,STDOUT_FILENO) != -1)
 	  if (dup2(cliente,STDIN_FILENO) != -1)               
            conexion();
          else printerror("dup2()");
        else printerror("dup2()");          
        close(cliente); 
        // Aqui termina el proceso hijo       
        exit(0);               
       }
       close(cliente);
      } else printerror("accept()");
     }
    }
   } else printerror("listen()");
  } else printerror("bind()");
  // Cerramos el socket
  close(servidor);
 } else printerror("socket()");
}
 
void printhelp(int exitcode)
{
 fprintf(stderr,"Uso: pico [opciones]\n");
 fprintf(stderr,"  -b --buffer\n");
 fprintf(stderr,"    Tamaño en Kb del buffer [8..32]\n");
 fprintf(stderr,"  -h --help\n");
 fprintf(stderr,"    Muestra este mensaje de ayuda.\n");
 fprintf(stderr,"  -i --inetd\n");
 fprintf(stderr,"    Permite ejecutar el servidor usando xinetd.\n");
 fprintf(stderr,"  -p --puerto\n");
 fprintf(stderr,"    Numero del puerto del servidor [1..65535]\n");
 fprintf(stderr,"  -t --timeout\n");
 fprintf(stderr,"    Tiempo de espera antes de cerrar la conexion, en segundos [5..300]\n\n");
 exit(exitcode);
}
 
int main(int argc, char *argv[])
{
 int i;
 int nextopt;
 char *str; 
 
 const struct option options[] =
 {
  {"buffer", 1, NULL, 'b'},
  {"help", 0, NULL, 'h'},
  {"inetd", 0, NULL, 'i'},
  {"puerto", 1, NULL, 'p'},
  {"timeout", 1, NULL, 't'},
  {NULL, 0, NULL, 0}
 };
 do {
   nextopt = getopt_long(argc,argv,"b:hip:t:",options,NULL);
   switch (nextopt)
   {
    case 'b':
    case 'p':
    case 't':
      i = strtol(optarg,&str,10);
      if (*str != '\0')
      {
       fprintf(stderr,"Parametro incorrecto: %s\n",optarg);
       printhelp(1);
      }  
      switch (nextopt)
      {
       case 'b':
         if ((i < 8) || (i > 32))
         {
          fprintf(stderr,"El tamaño del buffer es incorrecto.\n");
          printhelp(1);
         }
         buffersize = 1024*i;
         break;  
       case 'p':
         if ((i < 1) || (i > 65535))
         {
          fprintf(stderr,"El numero del puerto es incorrecto.\n");
          printhelp(1);
         }
         puerto = i;
         break;  
       case 't':
         if ((i < 5) || (i > 5*60))
         {
          fprintf(stderr,"El timeout es incorrecto.\n");
          printhelp(1);          
         }
         timeout = i;
         break;         
      }    
      break;
    case 'h':
      printhelp(0);
    case 'i':
      inetd = 1;
      break;
    case '?':
      fprintf(stderr,"Parametro no valido\n");
      printhelp(1);
      break;
   }
 } while (nextopt != -1); 
 if (inetd)
   conexion();
 else
 {
  fprintf(stderr,"Pid: %d Puerto: %d Buffer: %d Bytes Timeout: %d sg\n\n",
   (int) getpid(),puerto,buffersize,timeout);
  servidor();
 }
 return 0;
}