/*
 * Copyright (c) 1997-2007, OpenFWTK Development Group
 * All rights reserved. See LICENSE.
 */

/* cookies.c */

/* Copyright 1997 by Eberhard Mattes <mattes@azu.informatik.uni-stuttgart.de>
   Donated to the public domain.  No warranty.

   1997-07-28 Initial version */

#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include "libemfw.h"
#include "emio.h"
#include "squid-gw.h"
#define SELECT_CTYPE_HTTP
#include "tables.h"

/* References:
   RFC 2068	HTTP/1.1
   RFC 2109	Cookies
   */

/* ============================== CHARACTERS =============================== */

#define HTTP_CHAR_TYPE(c,x) (http_char_type[(unsigned char)c] & (x))

#define TOKEN(c)	HTTP_CHAR_TYPE (c, CTYPE_TOKEN)
#define WHITE(c)	HTTP_CHAR_TYPE (c, CTYPE_WHITE)

extern const unsigned char http_char_type[UCHAR_MAX+1];

/* ==================== MANAGING ATTRIBUTE/VALUE PAIRS ===================== */

#define AV_PAIRS	64

struct av_pair
{
  const octet *name, *value;
  size_t name_length, value_length;
  octet has_value;
  octet quote;
  octet sep;
};

static int av_pairs;
static size_t av_size;
static struct av_pair av[AV_PAIRS];
static octet av_sep;
static const octet *av_verbatim;

static void av_pairs_init (void)
{
  av_pairs = 0; av_size = 0; av_sep = 0;
}

static const struct av_pair *parse_av_pair (const octet **sp)
{
  struct av_pair a;
  const octet *s = *sp;

  DEBUG_ASSERT (!TOKEN (0));
  DEBUG_ASSERT (!WHITE (0));
  DEBUG_ASSERT ((av_pairs == 0) == (av_sep == 0));

  /* Check if there's space for another attribute/value pair. */

  if (av_pairs >= AV_PAIRS)
    return NULL;

  /* Initialize the new pair. */

  a.name = a.value = s;
  a.name_length = a.value_length = 0;
  a.has_value = a.quote = 0;
  a.sep = av_sep;

  /* Parse the name. */

  while (TOKEN (*s))
    ++s;
  a.name_length = s - a.name;
  if (a.name_length == 0)
    return NULL;

  while (WHITE (*s))
    ++s;

  if (*s == '=')
    {
      /* Parse the value. */

      ++s; a.has_value = 1;
      while (WHITE (*s))
	++s;
      if (*s == '"')
	{
	  ++s; a.quote = 1;
	  a.value = s;
	  while (*s != 0 && *s != '"')
	    ++s;
	  if (*s != '"')
	    return NULL;
	  a.value_length = s - a.value;
	  ++s;
	}
      else
	{
	  a.value = s;

	  /* First apply RFC 2109 strictly. */

	  while (TOKEN (*s))
	    ++s;

	  /* Unfortunately, most HTTP servers don't care about RFC
	     2109 and don't quote the value of "path=/foobar" and
	     "expires=Wednesday, 15-Sep-99 12:00:00 GMT". */

	  if (cf_http.parse_cookies == PC_NETSCAPE
	      || cf_http.parse_cookies == PC_NETSCAPE_QUOTE)
	    {
	      /* White space may or may not be part of the value.
                 Note that we'll remove trailing white space later. */

	      while (WHITE (*s))
		++s;

	      /* If we're not at the end of the value, we'll mark the
                 value for quoting on output and continue scanning.
                 Note that we do not stop at "," (see example above)! */

	      if (*s != 0 && *s != ';')
		{
		  /* If PC_NETSCAPE_QUOTE is in effect, at least quote
                     the value. */

		  if (cf_http.parse_cookies == PC_NETSCAPE_QUOTE)
		    a.quote = 1;
		  while (*s != 0 && *s != ';')
		    ++s;
		}
	      while (s != a.value && WHITE (s[-1]))
		--s;
	    }
	  a.value_length = s - a.value;
	  if (a.value_length == 0)
	    return NULL;
	}
      while (WHITE (*s))
	++s;
    }

  /* Remember the separator for the next attribute/value pair. */

  if (*s ==';' || *s == ',')
    av_sep = *s;
  else
    av_sep = 0;
  *sp = s;

  /* Update av_size. */

  if (a.sep != 0)
    av_size += 2;		/* "; " or ", " */
  av_size += a.name_length + a.value_length;
  if (a.has_value)
    {
      av_size += 1;		/* "=" */
      if (a.quote)
	av_size += 2;		/* Two quotes */
    }

  /* Store the new pair. */

  av[av_pairs] = a;
  return &av[av_pairs++];
}

/* =========================== CHECKING COOKIES ============================ */

/* Return 0 if S points to a valid domain (w.r.t. the request URL).
   Return -1 otherwise. */

static int check_domain (const octet *s, size_t len)
{
  /* Check if the domain matches the request URL's host. */

  DEBUG_ASSERT (req_url_str != NULL);
  if (len == 0 || len > req_url.host_length)
    return -1;
  if (*s != '.' && len != req_url.host_length)
    return -1;
  if (memcmp (s, req_url_str + req_url.host_start + req_url.host_length - len,
	      len) != 0)
    return -1;

  /* Check for dots in the host part. */

  if (memchr (req_url_str + req_url.host_start, '.',
	      req_url.host_length - len) != NULL)
    return -1;

  /* Check for embedded dots. */

  if (*s == '.')
    {
      ++s; --len;
    }
  if (len != 0 && s[len-1] == '.')
    --len;
  if (memchr (s, '.', len) == NULL)
    return -1;

  return 0;
}

/* Return 0 if S points to a valid path (w.r.t. the request URL).
   Return -1 otherwise. */

static int check_path (const octet *s, size_t len)
{
  const char *upath;
  size_t ulen;

  DEBUG_ASSERT (req_url_str != NULL);
  if (req_url.path_length == 0)
    {
      upath = "/"; ulen = 1;
    }
  else
    {
      upath = req_url_str + req_url.path_start;
      ulen = req_url.path_length;
    }
  if (len == 0 || len > ulen)
    return -1;
  if (memcmp (s, upath, len) != 0)
    return -1;
  return 0;
}

/* ============================ PARSING COOKIES ============================ */

/* Parse the value of a "Cookie" HTTP header field.  Return -1 if it's
   invalid, 1 if it is to be rejected, 0 if it is to be kept. */

int cookies_parse_cookie (const octet *s)
{
  const struct av_pair *a;
  int r;

  if (cf_http.parse_cookies == PC_NOCHECK)
    {
      av_verbatim = s;
      av_size = strlen ((char*) s);
      return cf_http.block_cookies.v ? 1 : 0;
    }
  else
    av_verbatim = NULL;

  av_pairs_init ();
  DEBUG_ASSERT (!WHITE (0));
  while (WHITE (*s))
    ++s;
  while (*s != 0)
    {
      a = parse_av_pair (&s);
      if (a == NULL)
	return -1;

      /* Check the attribute/value pair.  We check the domain and the
         path because we don't trust the browser -- it might send
         cookies to a wrong server. */

      if ((a->name_length == 7 && lower_cmpn ((char*)a->name,"$domain", 7) == 0)
	  || (a->name_length == 6 && lower_cmpn ((char*)a->name, "domain", 6) 
	  == 0))
	{
	  r = check_domain (a->value, a->value_length);
	  if (r != 0)
	    return r;
	}
      else if ((a->name_length==5 && lower_cmpn ((char*)a->name,"$path",5) == 0)
	       || (a->name_length == 4 && lower_cmpn ((char*)a->name, "path", 4)
		== 0))
	{
	  r = check_path (a->value, a->value_length);
	  if (r != 0)
	    return r;
	}

      if (*s == 0)
	break;
      if (*s != ';' && *s != ',') /* Backwards compatibility */
	return -1;
      ++s;
      while (WHITE (*s))
	++s;
      if (*s == 0 && cf_http.parse_cookies == PC_RFC2109)
	return -1;
    }

  return cf_http.block_cookies.v ? 1 : 0;
}

/* Parse the value of a "Set-Cookie" HTTP header field.  Return -1 if
   it's invalid, 1 if it is to be rejected, 0 if it is to be kept. */

int cookies_parse_set_cookie (const octet *s)
{
  const struct av_pair *a;
  int r;

  if (cf_http.parse_cookies == PC_NOCHECK)
    {
      av_verbatim = s;
      av_size = strlen ((char*) s);
      return cf_http.block_cookies.v ? 1 : 0;
    }
  else
    av_verbatim = NULL;

  av_pairs_init ();
  DEBUG_ASSERT (!WHITE (0));
  while (WHITE (*s))
    ++s;
  while (*s != 0)
    {
      a = parse_av_pair (&s);
      if (a == NULL)
	return -1;

      /* Check the attribute/value pair.  We check the domain and the
         path to be able to log attempts to set cookies for other
         servers. */

      if (a->name_length == 6 && lower_cmpn ((char*)a->name, "domain", 6) == 0)
	{
	  r = check_domain (a->value, a->value_length);
	  if (r != 0)
	    return r;
	}
      else if (a->name_length == 4 && lower_cmpn ((char*)a->name, "path", 4) 
		== 0)
	{
	  r = check_path (a->value, a->value_length);
	  if (r != 0)
	    return r;
	}

      if (*s == 0)
	break;
      if (*s != ';')
	return -1;
      ++s;
      while (WHITE (*s))
	++s;
      if (*s == 0 && cf_http.parse_cookies == PC_RFC2109)
	return -1;
    }

  return cf_http.block_cookies.v ? 1 : 0;
}

/* ========================== REBUILDING COOKIES =========================== */

size_t cookies_rebuild_length (void)
{
  return av_size;
}

/* Return 0 on success, -1 on failure (buffer overflow). */

int cookies_rebuild (octet *dst, size_t size)
{
  int i;
  const struct av_pair *a;
  octet *d = dst;

  if (cf_http.parse_cookies == PC_NOCHECK)
    {
      DEBUG_ASSERT (av_verbatim != NULL);
      if (av_size > size)
	return -1;
      memcpy (dst, av_verbatim, av_size);
      return 0;
    }

  DEBUG_ASSERT (av_verbatim == NULL);
  for (i = 0; i < av_pairs; ++i)
    {
      a = &av[i];
      DEBUG_ASSERT ((i == 0) == (a->sep == 0));
      if (a->sep != 0)
	{
	  if (size < 2)
	    return -1;
	  *d++ = a->sep; *d++ = ' '; size -= 2;
	}
      if (a->name_length > size)
	return -1;
      memcpy (d, a->name, a->name_length);
      d += a->name_length; size -= a->name_length;
      if (a->has_value)
	{
	  if (size < 1)
	    return -1;
	  *d++ = '='; size -= 1;
	  if (a->quote)
	    {
	      if (size < 2)	/* Take the closing quote into account */
		return -1;
	      *d++ = '"'; size -= 2;
	    }
	  if (size < a->value_length)
	    return -1;
	  memcpy (d, a->value, a->value_length);
	  d += a->value_length; size -= a->value_length;
	  if (a->quote)
	    *d++ = '"';		/* size already checked and adjusted above */
	}
    }
  DEBUG_ASSERT (d - dst == av_size);
  return 0;
}
