/*
uri.c - parse a URI

Parse a URI of form scheme://host/path?search or
scheme:user@host;parameter?header=value&header=value.

Copyright 1998-1999 by Columbia University; all rights reserved 
*/

#include "uri.h"
#include "dstring.h"
#include "strdupn.h"
#include <ctype.h>     /* tolower() */
#include <stdlib.h>    /* atoi() */
#include <stdio.h>     /* sscanf() */
#include <string.h>    /* strcmp() */
#include <sys/types.h>
#include <netinet/in.h>  /* struct in_addr */
#include <arpa/inet.h>   /* inet_addr() */

#include "sysdep.h"

extern struct in_addr host2ip(char *host);

typedef enum {U_start, U_scheme, U_user, U_password, U_host, U_port, 
  U_path, U_pname, U_pvalue, U_search, U_hname, U_hvalue, U_done} state_t;

static struct {
  char *delimiter;
  state_t next[4];
} transition[] = {
  {" ",    {U_start}},                                  /* U_start */
  {":/",   {U_user, U_host}},                           /* U_scheme */
  {":@",   {U_password,U_host}},                        /* U_user */
  {"@;?",  {U_host,U_pname,U_hname}},                   /* U_password */
  {":/;?", {U_port,     U_path,   U_pname,  U_hname}},  /* U_host */
  {"/;?",  {U_path,     U_pname,  U_hname}},            /* U_port */
  {"?",    {U_search}},                                 /* U_path */
  {"=;?",  {U_pvalue,   U_pname,  U_hname}},            /* U_pname */
  {";?",   {U_pname,    U_hname}},                      /* U_pvalue */
  {"",     {U_done}},                                   /* U_search */
  {"=&",   {U_hvalue,   U_hname}},                      /* U_hname */
  {"&",    {U_hname}}                                   /* U_hvalue */
};

/*
* Convert hex-encoded character.
*/
static char encoded(unsigned char **s)
{
  int c_value = 0;

  sscanf((*s)+1, "%2x", &c_value);
  *s += 2;
  return c_value;
} /* encoded */


static char *w(char *s)
{
  return s ? s : "";
} /* w */


/*
* Compute the next state based on the current state, the delimiting
* character and the general URI delimiter.
*/
static state_t nextstate(state_t current, char c, char delimiter)
{
  char *s;
  int n;

  if (current > U_start && (c == delimiter || c == ' ')) {
    return U_done;
  }
  else {
    s = strchr(transition[current].delimiter, c); 
    n = s - transition[current].delimiter; 
    return transition[current].next[n];
  }
} /* nextstate */


/*
* Parse a generic URI and normalize it (remove %xx, convert scheme and
* host name to lower case).  Recognize * as valid. Recognize <URI>.
* Strip leading white space and convert user and host to lower case.
*/
uri_t URI_Parse(char *url)
{
  uri_t u;
  unsigned char *s;
  DString v;
  char c;
  int len = strlen(url);
  int parameters = 0;    /* number of parameters */
  int headers = 0;       /* number of headers */
  int delimiter = 0;     /* URI enclosed in <> or "" */
  state_t state = U_start;

  memset(&u, '\0', sizeof(uri_t));

  if (!strcmp(url, "*")) {
    u.scheme = strdup("*");
    return u;
  }

  DStringInit(&v);
  for (s = url; ((char *)s - url) <= len && state < U_done; s++) {
    c = *s;

    /* Check if URL ends; normally, terminated by space, but also
       handles URLs enclosed in <> or "". */
    if (isspace((int)c)) c = ' ';

    if (state == U_start && c != ' ') {
      if (c == '<' || c == '"') {
        delimiter = (c == '<') ? '>' : c;
      }
      else s--;
      state = U_scheme;
    }
    else if (strchr(transition[state].delimiter, c) || c == delimiter || c == ' ') {
      switch (state) {
        case U_start:
          break;

        case U_scheme:
          if (*s && *(s+1) && *(s+2) && *(s+1) == '/' && *(s+2) == '/') {
            s += 2;
            c = '/';  /* force transition to U_host */
          }
          u.scheme = DStringValue(&v);
          break;

        case U_user:
          u.user = DStringValue(&v);
          break;

        case U_password:
          u.password = DStringValue(&v);
          break;

        case U_host:
          u.host = DStringValue(&v);
          break;

        case U_port:
          if (DStringValue(&v)) u.port = atoi(DStringValue(&v));
          DStringFree(&v);
          break;

        case U_path:
          u.path = DStringValue(&v);
          break;

        case U_search:
          u.search = DStringValue(&v);
	  u.param = strdup(u.search);
          break;
       
        case U_pname:
          parameters++;
          if (parameters == 1) {
            u.parameters = calloc(2, sizeof(uri_nv_t));
          }
          else {
            u.parameters = realloc(u.parameters, (parameters+1)*sizeof(uri_nv_t));
            u.parameters[parameters].name  = NULL;
            u.parameters[parameters].value = NULL;
          }
          u.parameters[parameters-1].name  = DStringValue(&v);
          u.parameters[parameters-1].value = NULL;
          break;

        case U_pvalue:
          u.parameters[parameters-1].value = DStringValue(&v);
          break;

        case U_hname:
          headers++;
          if (headers == 1) {
            u.headers = calloc(2, sizeof(uri_nv_t));
          }
          else {
            u.headers = realloc(u.headers, (headers+1)*sizeof(uri_nv_t));
            u.headers[headers].name  = NULL;
            u.headers[headers].value = NULL;
          }
          u.headers[headers-1].name  = DStringValue(&v);
          u.headers[headers-1].value = NULL;
          break;

        case U_hvalue:
          u.headers[headers-1].value = DStringValue(&v);
          break;

        case U_done:
          break;
      } /* switch */
      state = nextstate(state, c, delimiter);
      DStringInit(&v);
    } /* if */
    else {
      if (c == '%') c = encoded(&s);
      DStringAppend(&v, &c, 1);
    }
  } /* for */

  /* If no host name, this was really 'scheme:host:port'. */
  if (!u.host) {
    u.host = u.user;
    u.user = NULL;
    if (u.password) {
      u.port = atoi(u.password);
      free(u.password);
      u.password = NULL;
    }
  }


  return u;
} /* URI_Parse */


/*
* URI_Equal()
* Return 1 if URI's u1 and u2 are equal.  Matches if URI 'u2' has a null
* search string and 'u1' has a non-null search string.  Does not handle
* default ports.
*/
int URI_Equal(uri_t u1, uri_t u2)
{
  return (!strcmp(w(u1.scheme),   w(u2.scheme))
         && !strcasecmp(w(u1.host),   w(u2.host))
         && !strcasecmp(w(u1.user),   w(u2.user))
         && !strcmp(w(u1.path),   w(u2.path))
         && (!strcmp(w(u1.search), w(u2.search)) || !u2.search)   
         && u1.port == u2.port);
} /* URI_Equal */


/*
* Create a copy of the URI content.
*/
uri_t URI_Copy(uri_t u)
{
  uri_t uu;
  int i;
  int parameters, headers;

  memset(&uu, '\0', sizeof(uri_t));

  if (u.scheme)   uu.scheme   = strdup(u.scheme);
  if (u.user)     uu.user     = strdup(u.user);
  if (u.password) uu.password = strdup(u.password);
  if (u.host)     uu.host     = strdup(u.host);
  uu.port = u.port;
  if (u.path)     uu.path     = strdup(u.path);
  if (u.param)    uu.param    = strdup(u.param);
  if (u.search)   uu.search   = strdup(u.search);
  if (u.parameters) {
    for (i = 0; u.parameters[i].name != NULL; i++) {
      ; /* Do nothing */
    }
    parameters = i;
    uu.parameters = (uri_nv_t *) calloc(parameters + 1, sizeof(uri_nv_t));
    for (i = 0; u.parameters[i].name != NULL; i++) {
      strassign(&uu.parameters[i].name, u.parameters[i].name);
      strassign(&uu.parameters[i].value, u.parameters[i].value);
    }
  }
  if (u.headers) {
    for (i = 0; u.headers[i].name != NULL; i++) {
      ; /* Do nothing */
    }
    headers = i;
    uu.headers = (uri_nv_t *) calloc(headers + 1, sizeof(uri_nv_t));
    for (i = 0; u.headers[i].name != NULL; i++) {
      strassign(&uu.headers[i].name, u.headers[i].name);
      strassign(&uu.headers[i].value, u.headers[i].value);
    }
  }
  return uu;
} /* URI_Copy */


/*
* URI_Free()
*/
void URI_Free(uri_t *u)
{
  int i;

  if (u->scheme) {
    free(u->scheme);
    u->scheme = NULL;
  }
  if (u->host) {
    free(u->host);
    u->host = NULL;
  }
  if (u->user) {
    free(u->user);
    u->user = NULL;
  }
  if (u->password) {
    free(u->password);
    u->password = NULL;
  }
  if (u->path) {
    free(u->path);
    u->path = NULL;
  }
  if (u->param) {
    free(u->param);
    u->param = NULL;
  }
  if (u->search) {
    free(u->search);
    u->search = NULL;
  }
  if (u->headers) {
    for (i = 0; u->headers[i].name; u++) {
      free(u->headers[i].name);
      if (u->headers[i].value) free(u->headers[i].value);
    }
    free(u->headers);
    u->headers = NULL;
  }
  if (u->parameters) {
    for (i = 0; u->parameters[i].name; i++) {
      free(u->parameters[i].name);
      if (u->parameters[i].value) free(u->parameters[i].value);
    }
    free(u->parameters);
    u->headers = NULL;
  }
} /* URI_Free */


/*
* URI_Print()
*/
void URI_Print(uri_t u)
{
  int i;

  if (u.path) {
    printf("%s://%s:%d/%s?%s\n", w(u.scheme), w(u.host), u.port, w(u.path),
     w(u.search));
  }
  else {
    printf("%s:%s:%s@%s:%d",
      w(u.scheme), w(u.user), w(u.password), w(u.host), u.port);
    for (i = 0; u.parameters && u.parameters[i].name; i++) {
      printf(";%s=%s", u.parameters[i].name, w(u.parameters[i].value));
    }
    for (i = 0; u.headers && u.headers[i].name; i++) {
      printf("%c%s=%s", (i==0)?'?':'&', 
        u.headers[i].name, w(u.headers[i].value));
    }
    printf("\n");
  }
} /* URI_Print */


/*
* URI_SPrint() 
* Returns a dynamic string with the URI in u_dstr, performing
* DNS-to-dotted quad translation if parameter 'ip' is true.  Assumes
* u_dstr has been properly initialized.
*/
void URI_SPrint(uri_t *u, DString *u_dstr, int ip)
{
  int i;

  DStringTrunc(u_dstr); /* truncate, so we don't append to previous content */

  if (u->scheme) {
    DStringPrintf(u_dstr, "%s:", u->scheme);
  }
  else {
    /* Error: a URI without a scheme?  XXX Log an error. */
    DStringAppend(u_dstr, "xxx-error-no-scheme:", -1);
  }

  if (u->path) {
    DStringAppend(u_dstr, "//", -1);
  }

  if (u->user) {
    DStringAppend(u_dstr, u->user, -1);
    if (u->password) {
      DStringPrintf(u_dstr, ":%s", u->password);
    }
    DStringAppend(u_dstr, "@", -1);
  }

  if (u->host) {
    char *host_str;
    host_str = w(u->host);
    if (ip) {
      struct in_addr host_addr;
      host_addr = host2ip(u->host);
      if (host_addr.s_addr != INADDR_ANY) {
        host_str = inet_ntoa(host_addr);
      }
    }
    DStringAppend(u_dstr, host_str, -1);

    if (u->port) {
      DStringPrintf(u_dstr, ":%hd", u->port);
    }
  }

  if (u->path) {
    DStringPrintf(u_dstr, "/%s", u->path);
    if (u->search) {
      DStringPrintf(u_dstr, "?%s", u->search);
    }
  }
  else {
    for (i = 0; u->parameters && u->parameters[i].name; i++) {
      DStringPrintf(u_dstr, ";%s=%s", u->parameters[i].name,
		    w(u->parameters[i].value));
    }
    for (i = 0; u->headers && u->headers[i].name; i++) {
      DStringPrintf(u_dstr, "%c%s=%s", (i==0)?'?':'&', 
		    u->headers[i].name, w(u->headers[i].value));
    }
  }

  return;
} /* URI_SPrint */


/* Takes a URI string u_orig, perform DNS lookups if necessary, and
 * put the result in u_dstr. Assume u_dstr is properly initialized */
void URI_Convert(const char *u_orig, DString *u_dstr, int ip)
{
  char u_temp[128];
  uri_t u;

  u_temp[127] = '\0'; /* just in case src data is 128 bytes */
  /* URI_Parse() may modify input arg, so we make a copy */
  strncpy(u_temp, u_orig, sizeof(u_temp)-1);
  u = URI_Parse(u_temp);
  URI_SPrint(&u, u_dstr, 1);
  URI_Free(&u);
} /* URI_Convert */



/*
* URI_Parameter()
* Return NULL if no match, otherwise value.
*/
char *URI_Parameter(uri_t u, char *pname)
{
  int i;
  uri_nv_t *p = u.parameters;

  if (p == NULL)
    return NULL;

  for (i = 0; p[i].name; i++) {
    if (strcasecmp(p[i].name, pname) == 0) return p[i].value;
  }
  return NULL;
} /* URI_Parameter */
