]> sjero.net Git - wget/blobdiff - src/cookies.c
[svn] Renamed wget.h XDIGIT-related macros to (hopefully) clearer names.
[wget] / src / cookies.c
index 5f3749512e3c8f3e1f8a7a9888af475d169d619b..d8c2faea178093e56b2c82ecf79974890042ac55 100644 (file)
@@ -1,5 +1,5 @@
 /* Support for cookies.
-   Copyright (C) 2001 Free Software Foundation, Inc.
+   Copyright (C) 2001, 2002 Free Software Foundation, Inc.
 
 This file is part of GNU Wget.
 
@@ -15,10 +15,26 @@ General Public License for more details.
 
 You should have received a copy of the GNU General Public License
 along with Wget; if not, write to the Free Software
-Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  */
+Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+In addition, as a special exception, the Free Software Foundation
+gives permission to link the code of its release of Wget with the
+OpenSSL project's "OpenSSL" library (or with modified versions of it
+that use the same license as the "OpenSSL" library), and distribute
+the linked executables.  You must obey the GNU General Public License
+in all respects for all of the code used other than "OpenSSL".  If you
+modify this file, you may extend this exception to your version of the
+file, but you are not obligated to do so.  If you do not wish to do
+so, delete this exception statement from your version.  */
 
 /* Written by Hrvoje Niksic.  Parts are loosely inspired by cookie
-   code submitted by Tomasz Wegrzanowski.  */
+   code submitted by Tomasz Wegrzanowski.
+
+   TODO: Implement limits on cookie-related sizes, such as max. cookie
+   size, max. number of cookies, etc.  Add more "cookie jar" methods,
+   such as methods to over stored cookies, to clear temporary cookies,
+   to perform intelligent auto-saving, etc.  Ultimately support
+   `Set-Cookie2' and `Cookie2' headers.  */
 
 #include <config.h>
 
@@ -35,23 +51,34 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  */
 #include "wget.h"
 #include "utils.h"
 #include "hash.h"
-#include "url.h"
 #include "cookies.h"
 
-/* Hash table that maps domain names to cookie chains. */
-
-static struct hash_table *cookies_hash_table;
+/* This should *really* be in a .h file!  */
+time_t http_atotm PARAMS ((const char *));
+\f
+/* Declarations of `struct cookie' and the most basic functions. */
 
-/* This should be set by entry points in this file, so the low-level
-   functions don't need to call time() all the time.  */
+struct cookie_jar {
+  /* Hash table that maps domain names to cookie chains.  A "cookie
+     chain" is a linked list of cookies that belong to the same
+     domain.  */
+  struct hash_table *chains_by_domain;
 
-static time_t cookies_now;
+  int cookie_count;            /* number of cookies in the jar. */
+};
 
-/* This should *really* be in a .h file!  */
-time_t http_atotm PARAMS ((char *));
+/* Value set by entry point functions, so that the low-level
+   routines don't need to call time() all the time.  */
+time_t cookies_now;
 
-\f
-/* Definition of `struct cookie' and the most basic functions. */
+struct cookie_jar *
+cookie_jar_new (void)
+{
+  struct cookie_jar *jar = xmalloc (sizeof (struct cookie_jar));
+  jar->chains_by_domain = make_nocase_string_hash_table (0);
+  jar->cookie_count = 0;
+  return jar;
+}
 
 struct cookie {
   char *domain;                        /* domain of the cookie */
@@ -62,7 +89,7 @@ struct cookie {
                                   connections. */
   int permanent;               /* whether the cookie should outlive
                                   the session */
-  unsigned long expiry_time;   /* time when the cookie expires */
+  time_t expiry_time;          /* time when the cookie expires */
   int discard_requested;       /* whether cookie was created to
                                   request discarding another
                                   cookie */
@@ -70,10 +97,15 @@ struct cookie {
   char *attr;                  /* cookie attribute name */
   char *value;                 /* cookie attribute value */
 
+  struct cookie_jar *jar;      /* pointer back to the cookie jar, for
+                                  convenience. */
   struct cookie *next;         /* used for chaining of cookies in the
                                   same domain. */
 };
 
+#define PORT_ANY (-1)
+#define COOKIE_EXPIRED_P(c) ((c)->expiry_time != 0 && (c)->expiry_time < cookies_now)
+
 /* Allocate and return a new, empty cookie structure. */
 
 static struct cookie *
@@ -82,13 +114,11 @@ cookie_new (void)
   struct cookie *cookie = xmalloc (sizeof (struct cookie));
   memset (cookie, '\0', sizeof (struct cookie));
 
-  /* If we don't know better, assume cookie is non-permanent and valid
-     for the entire session. */
-  cookie->expiry_time = ~0UL;
-
-  /* Assume default port. */
-  cookie->port = 80;
+  /* Both cookie->permanent and cookie->expiry_time are now 0.  By
+     default, we assume that the cookie is non-permanent and valid
+     until the end of the session.  */
 
+  cookie->port = PORT_ANY;
   return cookie;
 }
 
@@ -106,59 +136,33 @@ delete_cookie (struct cookie *cookie)
 \f
 /* Functions for storing cookies.
 
-   All cookies can be referenced through cookies_hash_table.  The key
-   in that table is the domain name, and the value is a linked list of
-   all cookies from that domain.  Every new cookie is placed on the
-   head of the list.  */
-
-/* Write "HOST:PORT" to a stack-allocated area and make RESULT point
-  to that area.  RESULT should be a character pointer.  Useful for
-  creating HOST:PORT strings, which are the keys in the hash
-  table.  */
-
-#define SET_HOSTPORT(host, port, result) do {          \
-  int HP_len = strlen (host);                          \
-  result = alloca (HP_len + 1 + numdigit (port) + 1);  \
-  memcpy (result, host, HP_len);                       \
-  result[HP_len] = ':';                                        \
-  long_to_string (result + HP_len + 1, port);          \
-} while (0)
+   All cookies can be reached beginning with jar->chains_by_domain.
+   The key in that table is the domain name, and the value is a linked
+   list of all cookies from that domain.  Every new cookie is placed
+   on the head of the list.  */
 
-/* Find cookie chain that corresponds to DOMAIN (exact) and PORT.  */
-
-static struct cookie *
-find_cookie_chain_exact (const char *domain, int port)
-{
-  char *key;
-  if (!cookies_hash_table)
-    return NULL;
-  SET_HOSTPORT (domain, port, key);
-  return hash_table_get (cookies_hash_table, key);
-}
-
-/* Find and return the cookie whose domain, path, and attribute name
-   correspond to COOKIE.  If found, PREVPTR will point to the location
-   of the cookie previous in chain, or NULL if the found cookie is the
-   head of a chain.
+/* Find and return a cookie in JAR whose domain, path, and attribute
+   name correspond to COOKIE.  If found, PREVPTR will point to the
+   location of the cookie previous in chain, or NULL if the found
+   cookie is the head of a chain.
 
    If no matching cookie is found, return NULL. */
 
 static struct cookie *
-find_matching_cookie (struct cookie *cookie, struct cookie **prevptr)
+find_matching_cookie (struct cookie_jar *jar, struct cookie *cookie,
+                     struct cookie **prevptr)
 {
   struct cookie *chain, *prev;
 
-  if (!cookies_hash_table)
-    goto nomatch;
-
-  chain = find_cookie_chain_exact (cookie->domain, cookie->port);
+  chain = hash_table_get (jar->chains_by_domain, cookie->domain);
   if (!chain)
     goto nomatch;
 
   prev = NULL;
   for (; chain; prev = chain, chain = chain->next)
-    if (!strcmp (cookie->path, chain->path)
-       && !strcmp (cookie->attr, chain->attr))
+    if (0 == strcmp (cookie->path, chain->path)
+       && 0 == strcmp (cookie->attr, chain->attr)
+       && cookie->port == chain->port)
       {
        *prevptr = prev;
        return chain;
@@ -169,7 +173,7 @@ find_matching_cookie (struct cookie *cookie, struct cookie **prevptr)
   return NULL;
 }
 
-/* Store COOKIE to memory.
+/* Store COOKIE to the jar.
 
    This is done by placing COOKIE at the head of its chain.  However,
    if COOKIE matches a cookie already in memory, as determined by
@@ -179,29 +183,19 @@ find_matching_cookie (struct cookie *cookie, struct cookie **prevptr)
    first time; next hash_table_put's reuse the same key.  */
 
 static void
-store_cookie (struct cookie *cookie)
+store_cookie (struct cookie_jar *jar, struct cookie *cookie)
 {
   struct cookie *chain_head;
-  char *hostport;
   char *chain_key;
 
-  if (!cookies_hash_table)
-    /* If the hash table is not initialized, do so now, because we'll
-       need to store things.  */
-    cookies_hash_table = make_nocase_string_hash_table (0);
-
-  /* Initialize hash table key.  */
-  SET_HOSTPORT (cookie->domain, cookie->port, hostport);
-
-  if (hash_table_get_pair (cookies_hash_table, hostport,
+  if (hash_table_get_pair (jar->chains_by_domain, cookie->domain,
                           &chain_key, &chain_head))
     {
-      /* There already exists a chain of cookies with this exact
-         domain.  We need to check for duplicates -- if an existing
-         cookie exactly matches our domain, path and name, we replace
-         it.  */
+      /* A chain of cookies in this domain already exists.  Check for
+         duplicates -- if an extant cookie exactly matches our domain,
+         port, path, and name, replace it.  */
       struct cookie *prev;
-      struct cookie *victim = find_matching_cookie (cookie, &prev);
+      struct cookie *victim = find_matching_cookie (jar, cookie, &prev);
 
       if (victim)
        {
@@ -220,6 +214,7 @@ store_cookie (struct cookie *cookie)
              cookie->next = victim->next;
            }
          delete_cookie (victim);
+         --jar->cookie_count;
          DEBUGP (("Deleted old cookie (to be replaced.)\n"));
        }
       else
@@ -232,36 +227,39 @@ store_cookie (struct cookie *cookie)
         that, because it might get deallocated by the above code at
         some point later.  */
       cookie->next = NULL;
-      chain_key = xstrdup (hostport);
+      chain_key = xstrdup (cookie->domain);
     }
 
-  hash_table_put (cookies_hash_table, chain_key, cookie);
+  hash_table_put (jar->chains_by_domain, chain_key, cookie);
+  ++jar->cookie_count;
 
-  DEBUGP (("\nStored cookie %s %d %s %s %d %s %s %s\n",
-          cookie->domain, cookie->port, cookie->path,
+  DEBUGP (("\nStored cookie %s %d%s %s %s %d %s %s %s\n",
+          cookie->domain, cookie->port,
+          cookie->port == PORT_ANY ? " (ANY)" : "",
+          cookie->path,
           cookie->permanent ? "permanent" : "nonpermanent",
           cookie->secure,
-          asctime (localtime ((time_t *)&cookie->expiry_time)),
+          cookie->expiry_time
+          ? asctime (localtime (&cookie->expiry_time)) : "<undefined>",
           cookie->attr, cookie->value));
 }
 
-/* Discard a cookie matching COOKIE's domain, path, and attribute
-   name.  This gets called when we encounter a cookie whose expiry
-   date is in the past, or whose max-age is set to 0.  The former
-   corresponds to netscape cookie spec, while the latter is specified
-   by rfc2109.  */
+/* Discard a cookie matching COOKIE's domain, port, path, and
+   attribute name.  This gets called when we encounter a cookie whose
+   expiry date is in the past, or whose max-age is set to 0.  The
+   former corresponds to netscape cookie spec, while the latter is
+   specified by rfc2109.  */
 
 static void
-discard_matching_cookie (struct cookie *cookie)
+discard_matching_cookie (struct cookie_jar *jar, struct cookie *cookie)
 {
   struct cookie *prev, *victim;
 
-  if (!cookies_hash_table
-      || !hash_table_count (cookies_hash_table))
+  if (!hash_table_count (jar->chains_by_domain))
     /* No elements == nothing to discard. */
     return;
 
-  victim = find_matching_cookie (cookie, &prev);
+  victim = find_matching_cookie (jar, cookie, &prev);
   if (victim)
     {
       if (prev)
@@ -271,25 +269,21 @@ discard_matching_cookie (struct cookie *cookie)
        {
          /* VICTIM was head of its chain.  We need to place a new
             cookie at the head.  */
-
-         char *hostport;
          char *chain_key = NULL;
          int res;
 
-         SET_HOSTPORT (victim->domain, victim->port, hostport);
-         res = hash_table_get_pair (cookies_hash_table, hostport,
+         res = hash_table_get_pair (jar->chains_by_domain, victim->domain,
                                     &chain_key, NULL);
          assert (res != 0);
          if (!victim->next)
            {
              /* VICTIM was the only cookie in the chain.  Destroy the
                 chain and deallocate the chain key.  */
-
-             hash_table_remove (cookies_hash_table, hostport);
+             hash_table_remove (jar->chains_by_domain, victim->domain);
              xfree (chain_key);
            }
          else
-           hash_table_put (cookies_hash_table, chain_key, victim->next);
+           hash_table_put (jar->chains_by_domain, chain_key, victim->next);
        }
       delete_cookie (victim);
       DEBUGP (("Discarded old cookie.\n"));
@@ -365,12 +359,11 @@ update_cookie_field (struct cookie *cookie,
       if (expires != -1)
        {
          cookie->permanent = 1;
-         cookie->expiry_time = (unsigned long)expires;
+         cookie->expiry_time = (time_t)expires;
        }
       else
        /* Error in expiration spec.  Assume default (cookie valid for
-          this session.)  #### Should we return 0 and invalidate the
-          cookie?  */
+          this session.)  */
        ;
 
       /* According to netscape's specification, expiry time in the
@@ -392,10 +385,10 @@ update_cookie_field (struct cookie *cookie,
 
       sscanf (value_copy, "%lf", &maxage);
       if (maxage == -1)
-       /* something is wrong. */
+       /* something went wrong. */
        return 0;
       cookie->permanent = 1;
-      cookie->expiry_time = (unsigned long)cookies_now + (unsigned long)maxage;
+      cookie->expiry_time = cookies_now + maxage;
 
       /* According to rfc2109, a cookie with max-age of 0 means that
         discarding of a matching cookie is requested.  */
@@ -429,21 +422,6 @@ update_cookie_field (struct cookie *cookie,
                           && (c) != '"' && (c) != '='  \
                           && (c) != ';' && (c) != ',')
 
-/* Fetch the next character without doing anything special if CH gets
-   set to 0.  (The code executed next is expected to handle it.)  */
-
-#define FETCH1(ch, ptr) do {                   \
-  ch = *ptr++;                                 \
-} while (0)
-
-/* Like FETCH1, but jumps to `eof' label if CH gets set to 0.  */
-
-#define FETCH(ch, ptr) do {            \
-  FETCH1 (ch, ptr);                    \
-  if (!ch)                             \
-    goto eof;                          \
-} while (0)
-
 /* Parse the contents of the `Set-Cookie' header.  The header looks
    like this:
 
@@ -453,19 +431,25 @@ update_cookie_field (struct cookie *cookie,
    tokens.  Additionally, values may be quoted.
 
    A new cookie is returned upon success, NULL otherwise.  The
-   function `update_cookie_field' is used to update the fields of the
-   newly created cookie structure.  */
+   specified CALLBACK function (normally `update_cookie_field' is used
+   to update the fields of the newly created cookie structure.  */
 
 static struct cookie *
-parse_set_cookies (const char *sc)
+parse_set_cookies (const char *sc,
+                  int (*callback) (struct cookie *,
+                                   const char *, const char *,
+                                   const char *, const char *),
+                  int silent)
 {
   struct cookie *cookie = cookie_new ();
 
-  enum { S_NAME_PRE, S_NAME, S_NAME_POST,
-        S_VALUE_PRE, S_VALUE, S_VALUE_TRAILSPACE_MAYBE,
-        S_QUOTED_VALUE, S_QUOTED_VALUE_POST,
-        S_ATTR_ACTION,
-        S_DONE, S_ERROR } state = S_NAME_PRE;
+  /* #### Hand-written DFAs are no fun to debug.  We'de be better off
+     to rewrite this as an inline parser.  */
+
+  enum { S_START, S_NAME, S_NAME_POST,
+        S_VALUE_PRE, S_VALUE, S_QUOTED_VALUE, S_VALUE_TRAILSPACE,
+        S_ATTR_ACTION, S_DONE, S_ERROR
+  } state = S_START;
 
   const char *p = sc;
   char c;
@@ -473,19 +457,21 @@ parse_set_cookies (const char *sc)
   const char *name_b  = NULL, *name_e  = NULL;
   const char *value_b = NULL, *value_e = NULL;
 
-  FETCH (c, p);
+  c = *p;
 
   while (state != S_DONE && state != S_ERROR)
     {
       switch (state)
        {
-       case S_NAME_PRE:
-         if (ISSPACE (c))
-           FETCH (c, p);
+       case S_START:
+         if (!c)
+           state = S_DONE;
+         else if (ISSPACE (c))
+           /* Strip all whitespace preceding the name. */
+           c = *++p;
          else if (ATTR_NAME_CHAR (c))
            {
-             name_b = p - 1;
-             FETCH1 (c, p);
+             name_b = p;
              state = S_NAME;
            }
          else
@@ -493,116 +479,111 @@ parse_set_cookies (const char *sc)
            state = S_ERROR;
          break;
        case S_NAME:
-         if (ATTR_NAME_CHAR (c))
-           FETCH1 (c, p);
-         else if (!c || c == ';' || c == '=' || ISSPACE (c))
+         if (!c || c == ';' || c == '=' || ISSPACE (c))
            {
-             name_e = p - 1;
+             name_e = p;
              state = S_NAME_POST;
            }
+         else if (ATTR_NAME_CHAR (c))
+           c = *++p;
          else
            state = S_ERROR;
          break;
        case S_NAME_POST:
-         if (ISSPACE (c))
-           FETCH1 (c, p);
-         else if (!c || c == ';')
+         if (!c || c == ';')
            {
              value_b = value_e = NULL;
+             if (c == ';')
+               c = *++p;
              state = S_ATTR_ACTION;
            }
          else if (c == '=')
            {
-             FETCH1 (c, p);
+             c = *++p;
              state = S_VALUE_PRE;
            }
+         else if (ISSPACE (c))
+           /* Ignore space and keep the state. */
+           c = *++p;
          else
            state = S_ERROR;
          break;
        case S_VALUE_PRE:
-         if (ISSPACE (c))
-           FETCH1 (c, p);
+         if (!c || c == ';')
+           {
+             value_b = value_e = p;
+             if (c == ';')
+               c = *++p;
+             state = S_ATTR_ACTION;
+           }
          else if (c == '"')
            {
+             c = *++p;
              value_b = p;
-             FETCH (c, p);
              state = S_QUOTED_VALUE;
            }
-         else if (c == ';' || c == '\0')
-           {
-             value_b = value_e = p - 1;
-             state = S_ATTR_ACTION;
-           }
+         else if (ISSPACE (c))
+           c = *++p;
          else
            {
-             value_b = p - 1;
+             value_b = p;
              value_e = NULL;
              state = S_VALUE;
            }
          break;
        case S_VALUE:
-         if (c == ';' || c == '\0')
-           {
-             if (!value_e)
-               value_e = p - 1;
-             state = S_ATTR_ACTION;
-           }
-         else if (ISSPACE (c))
+         if (!c || c == ';' || ISSPACE (c))
            {
-             value_e = p - 1;
-             FETCH1 (c, p);
-             state = S_VALUE_TRAILSPACE_MAYBE;
+             value_e = p;
+             state = S_VALUE_TRAILSPACE;
            }
          else
            {
              value_e = NULL;   /* no trailing space */
-             FETCH1 (c, p);
+             c = *++p;
            }
          break;
-       case S_VALUE_TRAILSPACE_MAYBE:
-         if (ISSPACE (c))
-           FETCH1 (c, p);
-         else
-           state = S_VALUE;
-         break;
        case S_QUOTED_VALUE:
          if (c == '"')
            {
-             value_e = p - 1;
-             FETCH1 (c, p);
-             state = S_QUOTED_VALUE_POST;
+             value_e = p;
+             c = *++p;
+             state = S_VALUE_TRAILSPACE;
            }
+         else if (!c)
+           state = S_ERROR;
          else
-           FETCH (c, p);
+           c = *++p;
          break;
-       case S_QUOTED_VALUE_POST:
-         if (c == ';' || !c)
+       case S_VALUE_TRAILSPACE:
+         if (c == ';')
+           {
+             c = *++p;
+             state = S_ATTR_ACTION;
+           }
+         else if (!c)
            state = S_ATTR_ACTION;
          else if (ISSPACE (c))
-           FETCH1 (c, p);
+           c = *++p;
          else
-           state = S_ERROR;
+           state = S_VALUE;
          break;
        case S_ATTR_ACTION:
          {
-           int legal = update_cookie_field (cookie, name_b, name_e,
-                                            value_b, value_e);
+           int legal = callback (cookie, name_b, name_e, value_b, value_e);
            if (!legal)
              {
-               char *name;
-               BOUNDED_TO_ALLOCA (name_b, name_e, name);
-               logprintf (LOG_NOTQUIET,
-                          _("Error in Set-Cookie, field `%s'"), name);
+               if (!silent)
+                 {
+                   char *name;
+                   BOUNDED_TO_ALLOCA (name_b, name_e, name);
+                   logprintf (LOG_NOTQUIET,
+                              _("Error in Set-Cookie, field `%s'"), name);
+                 }
                state = S_ERROR;
                break;
              }
-
-           if (c)
-             FETCH1 (c, p);
-           if (!c)
-             state = S_DONE;
-           else
-             state = S_NAME_PRE;
+           state = S_START;
          }
          break;
        case S_DONE:
@@ -615,16 +596,13 @@ parse_set_cookies (const char *sc)
     return cookie;
 
   delete_cookie (cookie);
-  if (state == S_ERROR)
-    logprintf (LOG_NOTQUIET, _("Syntax error in Set-Cookie at character `%c'.\n"), c);
-  else
+  if (state != S_ERROR)
     abort ();
-  return NULL;
 
- eof:
-  delete_cookie (cookie);
-  logprintf (LOG_NOTQUIET,
-            _("Syntax error in Set-Cookie: premature end of string.\n"));
+  if (!silent)
+    logprintf (LOG_NOTQUIET,
+              _("Syntax error in Set-Cookie: %s at position %d.\n"),
+              sc, p - sc);
   return NULL;
 }
 \f
@@ -670,94 +648,130 @@ numeric_address_p (const char *addr)
 }
 
 /* Check whether COOKIE_DOMAIN is an appropriate domain for HOST.
-   This check is compliant with rfc2109.  */
+   Originally I tried to make the check compliant with rfc2109, but
+   the sites deviated too often, so I had to fall back to "tail
+   matching", as defined by the original Netscape's cookie spec.  */
 
 static int
 check_domain_match (const char *cookie_domain, const char *host)
 {
-  int headlen;
-  const char *tail;
-
   DEBUGP (("cdm: 1"));
 
   /* Numeric address requires exact match.  It also requires HOST to
-     be an IP address.  I suppose we *could* resolve HOST with
-     store_hostaddress (it would hit the hash table), but rfc2109
-     doesn't require it, and it doesn't seem very useful, so we
-     don't.  */
+     be an IP address.  */
   if (numeric_address_p (cookie_domain))
-    return !strcmp (cookie_domain, host);
+    return 0 == strcmp (cookie_domain, host);
 
   DEBUGP ((" 2"));
 
-  /* The domain must contain at least one embedded dot. */
-  {
-    const char *rest = cookie_domain;
-    int len = strlen (rest);
-    if (*rest == '.')
-      ++rest, --len;           /* ignore first dot */
-    if (len <= 0)
-      return 0;
-    if (rest[len - 1] == '.')
-      --len;                   /* ignore last dot */
-
-    if (!memchr (rest, '.', len))
-      /* No dots. */
-      return 0;
-  }
-
-  DEBUGP ((" 3"));
-
   /* For the sake of efficiency, check for exact match first. */
   if (!strcasecmp (cookie_domain, host))
     return 1;
 
-  DEBUGP ((" 4"));
+  DEBUGP ((" 3"));
 
-  /* In rfc2109 terminology, HOST needs domain-match COOKIE_DOMAIN.
-     This means that COOKIE_DOMAIN needs to start with `.' and be an
-     FQDN, and that HOST must end with COOKIE_DOMAIN.  */
-  if (*cookie_domain != '.')
+  /* HOST must match the tail of cookie_domain. */
+  if (!match_tail (host, cookie_domain, 1))
     return 0;
 
-  DEBUGP ((" 5"));
-
-  /* Two proceed, we need to examine two parts of HOST: its head and
-     its tail.  Head and tail are defined in terms of the length of
-     the domain, like this:
-
-       HHHHTTTTTTTTTTTTTTT  <- host
-           DDDDDDDDDDDDDDD  <- domain
-
-     That is, "head" is the part of the host before (dlen - hlen), and
-     "tail" is what follows.
-
-     For the domain to match, two conditions need to be true:
-
-     1. Tail must equal DOMAIN.
-     2. Head must not contain an embedded dot.  */
+  /* We know that COOKIE_DOMAIN is a subset of HOST; however, we must
+     make sure that somebody is not trying to set the cookie for a
+     subdomain shared by many entities.  For example, "company.co.uk"
+     must not be allowed to set a cookie for ".co.uk".  On the other
+     hand, "sso.redhat.de" should be able to set a cookie for
+     ".redhat.de".
+
+     The only marginally sane way to handle this I can think of is to
+     reject on the basis of the length of the second-level domain name
+     (but when the top-level domain is unknown), with the assumption
+     that those of three or less characters could be reserved.  For
+     example:
+
+          .co.org -> works because the TLD is known
+           .co.uk -> doesn't work because "co" is only two chars long
+          .com.au -> doesn't work because "com" is only 3 chars long
+          .cnn.uk -> doesn't work because "cnn" is also only 3 chars long (ugh)
+          .cnn.de -> doesn't work for the same reason (ugh!!)
+         .abcd.de -> works because "abcd" is 4 chars long
+      .img.cnn.de -> works because it's not trying to set the 2nd level domain
+       .cnn.co.uk -> works for the same reason
+
+    That should prevent misuse, while allowing reasonable usage.  If
+    someone knows of a better way to handle this, please let me
+    know.  */
+  {
+    const char *p = cookie_domain;
+    int dccount = 1;           /* number of domain components */
+    int ldcl  = 0;             /* last domain component length */
+    int nldcl = 0;             /* next to last domain component length */
+    int out;
+    if (*p == '.')
+      /* Ignore leading period in this calculation. */
+      ++p;
+    DEBUGP ((" 4"));
+    for (out = 0; !out; p++)
+      switch (*p)
+       {
+       case '\0':
+         out = 1;
+         break;
+       case '.':
+         if (ldcl == 0)
+           /* Empty domain component found -- the domain is invalid. */
+           return 0;
+         if (*(p + 1) == '\0')
+           {
+             /* Tolerate trailing '.' by not treating the domain as
+                one ending with an empty domain component.  */
+             out = 1;
+             break;
+           }
+         nldcl = ldcl;
+         ldcl  = 0;
+         ++dccount;
+         break;
+       default:
+         ++ldcl;
+       }
 
-  headlen = strlen (host) - strlen (cookie_domain);
+    DEBUGP ((" 5"));
 
-  if (headlen <= 0)
-    /* DOMAIN must be a proper subset of HOST. */
-    return 0;
-  tail = host + headlen;
+    if (dccount < 2)
+      return 0;
 
-  DEBUGP ((" 6"));
+    DEBUGP ((" 6"));
 
-  /* (1) */
-  if (strcasecmp (tail, cookie_domain))
-    return 0;
+    if (dccount == 2)
+      {
+       int i;
+       int known_toplevel = 0;
+       static char *known_toplevel_domains[] = {
+         ".com", ".edu", ".net", ".org", ".gov", ".mil", ".int"
+       };
+       for (i = 0; i < countof (known_toplevel_domains); i++)
+         if (match_tail (cookie_domain, known_toplevel_domains[i], 1))
+           {
+             known_toplevel = 1;
+             break;
+           }
+       if (!known_toplevel && nldcl <= 3)
+         return 0;
+      }
+  }
 
   DEBUGP ((" 7"));
 
-  /* Test (2) is not part of the "domain-match" itself, but is
-     recommended by rfc2109 for reasons of privacy.  */
-
-  /* (2) */
-  if (memchr (host, '.', headlen))
-    return 0;
+  /* Don't allow domain "bar.com" to match host "foobar.com".  */
+  if (*cookie_domain != '.')
+    {
+      int dlen = strlen (cookie_domain);
+      int hlen = strlen (host);
+      /* cookie host:    hostname.foobar.com */
+      /* desired domain:             bar.com */
+      /* '.' must be here in host-> ^        */
+      if (hlen > dlen && host[hlen - dlen - 1] != '.')
+       return 0;
+    }
 
   DEBUGP ((" 8"));
 
@@ -774,62 +788,64 @@ check_path_match (const char *cookie_path, const char *path)
   return path_matches (path, cookie_path);
 }
 \f
-/* Parse the `Set-Cookie' header and, if the cookie is legal, store it
-   to memory.  */
+/* Process the HTTP `Set-Cookie' header.  This results in storing the
+   cookie or discarding a matching one, or ignoring it completely, all
+   depending on the contents.  */
 
-int
-set_cookie_header_cb (const char *hdr, void *closure)
+void
+cookie_jar_process_set_cookie (struct cookie_jar *jar,
+                              const char *host, int port,
+                              const char *path, const char *set_cookie)
 {
-  struct url *u = (struct url *)closure;
   struct cookie *cookie;
-
   cookies_now = time (NULL);
 
-  cookie = parse_set_cookies (hdr);
+  cookie = parse_set_cookies (set_cookie, update_cookie_field, 0);
   if (!cookie)
     goto out;
 
   /* Sanitize parts of cookie. */
 
   if (!cookie->domain)
-    cookie->domain = xstrdup (u->host);
+    {
+    copy_domain:
+      cookie->domain = xstrdup (host);
+      cookie->port = port;
+    }
   else
     {
-      if (!check_domain_match (cookie->domain, u->host))
+      if (!check_domain_match (cookie->domain, host))
        {
-         DEBUGP (("Attempt to fake the domain: %s, %s\n",
-                  cookie->domain, u->host));
-         goto out;
+         logprintf (LOG_NOTQUIET,
+                    "Cookie coming from %s attempted to set domain to %s\n",
+                    host, cookie->domain);
+         goto copy_domain;
        }
     }
   if (!cookie->path)
-    cookie->path = xstrdup (u->path);
+    cookie->path = xstrdup (path);
   else
     {
-      if (!check_path_match (cookie->path, u->path))
+      if (!check_path_match (cookie->path, path))
        {
          DEBUGP (("Attempt to fake the path: %s, %s\n",
-                  cookie->path, u->path));
+                  cookie->path, path));
          goto out;
        }
     }
 
-  cookie->port = u->port;
-
   if (cookie->discard_requested)
     {
-      discard_matching_cookie (cookie);
-      delete_cookie (cookie);
-      return 1;
+      discard_matching_cookie (jar, cookie);
+      goto out;
     }
 
-  store_cookie (cookie);
-  return 1;
+  store_cookie (jar, cookie);
+  return;
 
  out:
   if (cookie)
     delete_cookie (cookie);
-  return 1;
 }
 \f
 /* Support for sending out cookies in HTTP requests, based on
@@ -846,13 +862,13 @@ set_cookie_header_cb (const char *hdr, void *closure)
   ++st_count;                                                  \
 } while (0)
 
-/* Store cookie chains that match HOST, PORT.  Since more than one
-   chain can match, the matches are written to STORE.  No more than
-   SIZE matches are written; if more matches are present, return the
-   number of chains that would have been written.  */
+/* Store cookie chains that match HOST.  Since more than one chain can
+   match, the matches are written to STORE.  No more than SIZE matches
+   are written; if more matches are present, return the number of
+   chains that would have been written.  */
 
-int
-find_matching_chains (const char *host, int port,
+static int
+find_matching_chains (struct cookie_jar *jar, const char *host,
                      struct cookie *store[], int size)
 {
   struct cookie *chain;
@@ -860,13 +876,13 @@ find_matching_chains (const char *host, int port,
   char *hash_key;
   int count = 0;
 
-  if (!cookies_hash_table)
+  if (!hash_table_count (jar->chains_by_domain))
     return 0;
 
-  SET_HOSTPORT (host, port, hash_key);
+  STRDUP_ALLOCA (hash_key, host);
 
-  /* Exact match. */
-  chain = hash_table_get (cookies_hash_table, hash_key);
+  /* Look for an exact match. */
+  chain = hash_table_get (jar->chains_by_domain, hash_key);
   if (chain)
     STORE_CHAIN (chain, store, size, count);
 
@@ -881,7 +897,7 @@ find_matching_chains (const char *host, int port,
         loop.  */
       char *p = strchr (hash_key, '.');
       assert (p != NULL);
-      chain = hash_table_get (cookies_hash_table, p);
+      chain = hash_table_get (jar->chains_by_domain, p);
       if (chain)
        STORE_CHAIN (chain, store, size, count);
       hash_key = p + 1;
@@ -895,29 +911,52 @@ find_matching_chains (const char *host, int port,
 static int
 path_matches (const char *full_path, const char *prefix)
 {
-  int len = strlen (prefix);
-  if (strncmp (full_path, prefix, len))
+  int len;
+
+  if (*prefix != '/')
+    /* Wget's HTTP paths do not begin with '/' (the URL code treats it
+       as a separator), but the '/' is assumed when matching against
+       the cookie stuff.  */
+    return 0;
+
+  ++prefix;
+  len = strlen (prefix);
+
+  if (0 != strncmp (full_path, prefix, len))
     /* FULL_PATH doesn't begin with PREFIX. */
     return 0;
 
   /* Length of PREFIX determines the quality of the match. */
-  return len;
+  return len + 1;
 }
 
+/* Return non-zero iff COOKIE matches the given PATH, PORT, and
+   security flag.  HOST is not a flag because it is assumed that the
+   cookie comes from the correct chain.
+
+   If PATH_GOODNESS is non-NULL, store the "path goodness" there.  The
+   said goodness is a measure of how well COOKIE matches PATH.  It is
+   used for ordering cookies.  */
+
 static int
-matching_cookie (const struct cookie *cookie, const char *path,
+matching_cookie (const struct cookie *cookie, const char *path, int port,
                 int connection_secure_p, int *path_goodness)
 {
   int pg;
 
-  if (cookie->expiry_time < cookies_now)
-    /* Ignore stale cookies.  There is no need to unchain the cookie
-       at this point -- Wget is a relatively short-lived application,
-       and stale cookies will not be saved by `save_cookies'.  */
+  if (COOKIE_EXPIRED_P (cookie))
+    /* Ignore stale cookies.  Don't bother unchaining the cookie at
+       this point -- Wget is a relatively short-lived application, and
+       stale cookies will not be saved by `save_cookies'.  On the
+       other hand, this function should be as efficient as
+       possible.  */
     return 0;
+
   if (cookie->secure && !connection_secure_p)
     /* Don't transmit secure cookies over an insecure connection.  */
     return 0;
+  if (cookie->port != PORT_ANY && cookie->port != port)
+    return 0;
   pg = path_matches (path, cookie->path);
   if (!pg)
     return 0;
@@ -1008,19 +1047,20 @@ goodness_comparator (const void *p1, const void *p2)
   return dgdiff ? dgdiff : pgdiff;
 }
 
-/* Build a `Cookie' header for a request that goes to HOST:PORT and
+/* Generate a `Cookie' header for a request that goes to HOST:PORT and
    requests PATH from the server.  The resulting string is allocated
    with `malloc', and the caller is responsible for freeing it.  If no
    cookies pertain to this request, i.e. no cookie header should be
    generated, NULL is returned.  */
 
 char *
-build_cookies_request (const char *host, int port, const char *path,
-                      int connection_secure_p)
+cookie_jar_generate_cookie_header (struct cookie_jar *jar, const char *host,
+                                  int port, const char *path,
+                                  int connection_secure_p)
 {
   struct cookie *chain_default_store[20];
   struct cookie **all_chains = chain_default_store;
-  int chain_store_size = ARRAY_SIZE (chain_default_store);
+  int chain_store_size = countof (chain_default_store);
   int chain_count;
 
   struct cookie *cookie;
@@ -1030,7 +1070,7 @@ build_cookies_request (const char *host, int port, const char *path,
   int result_size, pos;
 
  again:
-  chain_count = find_matching_chains (host, port, all_chains, chain_store_size);
+  chain_count = find_matching_chains (jar, host, all_chains, chain_store_size);
   if (chain_count > chain_store_size)
     {
       /* It's extremely unlikely that more than 20 chains will ever
@@ -1051,7 +1091,7 @@ build_cookies_request (const char *host, int port, const char *path,
   count = 0;
   for (i = 0; i < chain_count; i++)
     for (cookie = all_chains[i]; cookie; cookie = cookie->next)
-      if (matching_cookie (cookie, path, connection_secure_p, NULL))
+      if (matching_cookie (cookie, path, port, connection_secure_p, NULL))
        ++count;
   if (!count)
     /* No matching cookies. */
@@ -1067,7 +1107,7 @@ build_cookies_request (const char *host, int port, const char *path,
     for (cookie = all_chains[i]; cookie; cookie = cookie->next)
       {
        int pg;
-       if (!matching_cookie (cookie, path, connection_secure_p, &pg))
+       if (!matching_cookie (cookie, path, port, connection_secure_p, &pg))
          continue;
        outgoing[ocnt].cookie = cookie;
        outgoing[ocnt].domain_goodness = strlen (cookie->domain);
@@ -1201,7 +1241,7 @@ domain_port (const char *domain_b, const char *domain_e,
 /* Load cookies from FILE.  */
 
 void
-load_cookies (const char *file)
+cookie_jar_load (struct cookie_jar *jar, const char *file)
 {
   char *line;
   FILE *fp = fopen (file, "r");
@@ -1218,6 +1258,7 @@ load_cookies (const char *file)
       struct cookie *cookie;
       char *p = line;
 
+      double expiry;
       int port;
 
       char *domain_b  = NULL, *domain_e  = NULL;
@@ -1268,24 +1309,25 @@ load_cookies (const char *file)
       port = domain_port (domain_b, domain_e, (const char **)&domain_e);
       if (port)
        cookie->port = port;
-      else
-       cookie->port = cookie->secure ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT;
-
       cookie->domain  = strdupdelim (domain_b, domain_e);
 
       /* safe default in case EXPIRES field is garbled. */
-      cookie->expiry_time = cookies_now - 1;
+      expiry = (double)cookies_now - 1;
 
       /* I don't like changing the line, but it's completely safe.
         (line is malloced.)  */
       *expires_e = '\0';
-      sscanf (expires_b, "%lu", &cookie->expiry_time);
-      if (cookie->expiry_time < cookies_now)
+      sscanf (expires_b, "%lf", &expiry);
+      if (expiry < cookies_now)
        /* ignore stale cookie. */
        goto abort;
+      cookie->expiry_time = expiry;
+
+      /* If the cookie has survived being saved into an external file,
+        it is obviously permanent.  */
       cookie->permanent = 1;
 
-      store_cookie (cookie);
+      store_cookie (jar, cookie);
 
     next:
       continue;
@@ -1310,12 +1352,15 @@ save_cookies_mapper (void *key, void *value, void *arg)
     {
       if (!chain->permanent)
        continue;
-      if (chain->expiry_time < cookies_now)
+      if (COOKIE_EXPIRED_P (chain))
        continue;
-      fprintf (fp, "%s\t%s\t%s\t%s\t%lu\t%s\t%s\n",
-              domain, *domain == '.' ? "TRUE" : "FALSE",
+      fputs (domain, fp);
+      if (chain->port != PORT_ANY)
+       fprintf (fp, ":%d", chain->port);
+      fprintf (fp, "\t%s\t%s\t%s\t%.0f\t%s\t%s\n",
+              *domain == '.' ? "TRUE" : "FALSE",
               chain->path, chain->secure ? "TRUE" : "FALSE",
-              chain->expiry_time,
+              (double)chain->expiry_time,
               chain->attr, chain->value);
       if (ferror (fp))
        return 1;               /* stop mapping */
@@ -1326,15 +1371,10 @@ save_cookies_mapper (void *key, void *value, void *arg)
 /* Save cookies, in format described above, to FILE. */
 
 void
-save_cookies (const char *file)
+cookie_jar_save (struct cookie_jar *jar, const char *file)
 {
   FILE *fp;
 
-  if (!cookies_hash_table
-      || !hash_table_count (cookies_hash_table))
-    /* no cookies stored; nothing to do. */
-    return;
-
   DEBUGP (("Saving cookies to %s.\n", file));
 
   cookies_now = time (NULL);
@@ -1351,7 +1391,7 @@ save_cookies (const char *file)
   fprintf (fp, "# Generated by Wget on %s.\n", datetime_str (NULL));
   fputs ("# Edit at your own risk.\n\n", fp);
 
-  hash_table_map (cookies_hash_table, save_cookies_mapper, fp);
+  hash_table_map (jar->chains_by_domain, save_cookies_mapper, fp);
 
   if (ferror (fp))
     logprintf (LOG_NOTQUIET, _("Error writing to `%s': %s\n"),
@@ -1364,14 +1404,20 @@ save_cookies (const char *file)
   DEBUGP (("Done saving cookies.\n"));
 }
 \f
+/* Destroy all the elements in the chain and unhook it from the cookie
+   jar.  This is written in the form of a callback to hash_table_map
+   and used by cookie_jar_delete to delete all the cookies in a
+   jar.  */
+
 static int
-delete_cookie_chain_mapper (void *value, void *key, void *arg_ignored)
+nuke_cookie_chain (void *value, void *key, void *arg)
 {
   char *chain_key = (char *)value;
   struct cookie *chain = (struct cookie *)key;
+  struct cookie_jar *jar = (struct cookie_jar *)arg;
 
   /* Remove the chain from the table and free the key. */
-  hash_table_remove (cookies_hash_table, chain_key);
+  hash_table_remove (jar->chains_by_domain, chain_key);
   xfree (chain_key);
 
   /* Then delete all the cookies in the chain. */
@@ -1389,11 +1435,97 @@ delete_cookie_chain_mapper (void *value, void *key, void *arg_ignored)
 /* Clean up cookie-related data. */
 
 void
-cookies_cleanup (void)
+cookie_jar_delete (struct cookie_jar *jar)
 {
-  if (!cookies_hash_table)
-    return;
-  hash_table_map (cookies_hash_table, delete_cookie_chain_mapper, NULL);
-  hash_table_destroy (cookies_hash_table);
-  cookies_hash_table = NULL;
+  hash_table_map (jar->chains_by_domain, nuke_cookie_chain, jar);
+  hash_table_destroy (jar->chains_by_domain);
+  xfree (jar);
+}
+\f
+/* Test cases.  Currently this is only tests parse_set_cookies.  To
+   use, recompile Wget with -DTEST_COOKIES and call test_cookies()
+   from main.  */
+
+#ifdef TEST_COOKIES
+int test_count;
+char *test_results[10];
+
+static int test_parse_cookies_callback (struct cookie *ignored,
+                                       const char *nb, const char *ne,
+                                       const char *vb, const char *ve)
+{
+  test_results[test_count++] = strdupdelim (nb, ne);
+  test_results[test_count++] = strdupdelim (vb, ve);
+  return 1;
+}
+
+void
+test_cookies (void)
+{
+  /* Tests expected to succeed: */
+  static struct {
+    char *data;
+    char *results[10];
+  } tests_succ[] = {
+    { "", {NULL} },
+    { "arg=value", {"arg", "value", NULL} },
+    { "arg1=value1;arg2=value2", {"arg1", "value1", "arg2", "value2", NULL} },
+    { "arg1=value1; arg2=value2", {"arg1", "value1", "arg2", "value2", NULL} },
+    { "arg1=value1;  arg2=value2;", {"arg1", "value1", "arg2", "value2", NULL} },
+    { "arg1=value1;  arg2=value2;  ", {"arg1", "value1", "arg2", "value2", NULL} },
+    { "arg1=\"value1\"; arg2=\"\"", {"arg1", "value1", "arg2", "", NULL} },
+    { "arg=", {"arg", "", NULL} },
+    { "arg1=; arg2=", {"arg1", "", "arg2", "", NULL} },
+    { "arg1 = ; arg2= ", {"arg1", "", "arg2", "", NULL} },
+  };
+
+  /* Tests expected to fail: */
+  static char *tests_fail[] = {
+    ";",
+    "arg=\"unterminated",
+    "=empty-name",
+    "arg1=;=another-empty-name",
+  };
+  int i;
+
+  for (i = 0; i < countof (tests_succ); i++)
+    {
+      int ind;
+      char *data = tests_succ[i].data;
+      char **expected = tests_succ[i].results;
+      struct cookie *c;
+
+      test_count = 0;
+      c = parse_set_cookies (data, test_parse_cookies_callback, 1);
+      if (!c)
+       {
+         printf ("NULL cookie returned for valid data: %s\n", data);
+         continue;
+       }
+
+      for (ind = 0; ind < test_count; ind += 2)
+       {
+         if (!expected[ind])
+           break;
+         if (0 != strcmp (expected[ind], test_results[ind]))
+           printf ("Invalid name %d for '%s' (expected '%s', got '%s')\n",
+                   ind / 2 + 1, data, expected[ind], test_results[ind]);
+         if (0 != strcmp (expected[ind + 1], test_results[ind + 1]))
+           printf ("Invalid value %d for '%s' (expected '%s', got '%s')\n",
+                   ind / 2 + 1, data, expected[ind + 1], test_results[ind + 1]);
+       }
+      if (ind < test_count || expected[ind])
+       printf ("Unmatched number of results: %s\n", data);
+    }
+
+  for (i = 0; i < countof (tests_fail); i++)
+    {
+      struct cookie *c;
+      char *data = tests_fail[i];
+      test_count = 0;
+      c = parse_set_cookies (data, test_parse_cookies_callback, 1);
+      if (c)
+       printf ("Failed to report error on invalid data: %s\n", data);
+    }
 }
+#endif /* TEST_COOKIES */