]> sjero.net Git - wget/blobdiff - src/cookies.c
[svn] Update the license to include the OpenSSL exception.
[wget] / src / cookies.c
index 65ee3f1cddd13249cd425a078366f82df132cff2..c31a286524da6aa6532714cc9395a1bf1f5ef1f7 100644 (file)
@@ -1,24 +1,40 @@
 /* Support for cookies.
-   Copyright (C) 2001 Free Software Foundation, Inc.
+   Copyright (C) 2001, 2002 Free Software Foundation, Inc.
 
-This file is part of Wget.
+This file is part of GNU Wget.
 
-This program is free software; you can redistribute it and/or modify
+GNU Wget is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at
 your option) any later version.
 
-This program is distributed in the hope that it will be useful, but
+GNU Wget is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 General Public License for more details.
 
 You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  */
+along with Wget; if not, write to the Free Software
+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;
 }
 
@@ -104,98 +134,35 @@ delete_cookie (struct cookie *cookie)
   xfree (cookie);
 }
 \f
-/* Functions for cookie-specific hash tables.  These are regular hash
-   tables, but with case-insensitive test and hash functions.  */
-
-/* Like string_hash, but produces the same results regardless of the
-   case.  */
-
-static unsigned long
-unsigned_string_hash (const void *key)
-{
-  const char *p = key;
-  unsigned int h = TOLOWER (*p);
-  
-  if (h)
-    for (p += 1; *p != '\0'; p++)
-      h = (h << 5) - h + TOLOWER (*p);
-  
-  return h;
-}
-
-/* Front-end to strcasecmp. */
-
-static int
-unsigned_string_cmp (const void *s1, const void *s2)
-{
-  return !strcasecmp ((const char *)s1, (const char *)s2);
-}
-
-/* Like make_string_hash_table, but uses unsigned_string_hash and
-   unsigned_string_cmp.  */
-
-static struct hash_table *
-make_unsigned_string_hash_table (int initial_size)
-{
-  return hash_table_new (initial_size,
-                        unsigned_string_hash, unsigned_string_cmp);
-}
-
-/* Write "HOST:PORT" to RESULT.  RESULT should be a pointer, and the
-  memory for the contents is allocated on the stack.  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)
-
-/* 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);
-}
-\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.  */
+   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 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;
@@ -206,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
@@ -216,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_unsigned_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)
        {
@@ -257,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
@@ -269,34 +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);
-
-  DEBUGP (("\nStored cookie %s %d %s %d %s %s %s\n",
-          cookie->domain, cookie->port, cookie->path, cookie->secure,
-          asctime (localtime ((time_t *)&cookie->expiry_time)),
+  hash_table_put (jar->chains_by_domain, chain_key, cookie);
+  ++jar->cookie_count;
+
+  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,
+          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)
@@ -306,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"));
@@ -400,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
@@ -427,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.  */
@@ -453,9 +411,16 @@ update_cookie_field (struct cookie *cookie,
 #undef NAME_IS
 
 /* Returns non-zero for characters that are legal in the name of an
-   attribute.  */
+   attribute.  This used to allow only alphanumerics, '-', and '_',
+   but we need to be more lenient because a number of sites wants to
+   use weirder attribute names.  rfc2965 "informally specifies"
+   attribute name (token) as "a sequence of non-special, non-white
+   space characters".  So we allow everything except the stuff we know
+   could harm us.  */
 
-#define ATTR_NAME_CHAR(c) (ISALNUM (c) || (c) == '-' || (c) == '_')
+#define ATTR_NAME_CHAR(c) ((c) > 32 && (c) < 127       \
+                          && (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.)  */
@@ -698,80 +663,132 @@ 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 i, 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);
 
-  /* 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 ((" 2"));
 
   /* For the sake of efficiency, check for exact match first. */
   if (!strcasecmp (cookie_domain, host))
     return 1;
 
-  /* 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 != '.')
-    return 0;
-
-  /* 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:
+  DEBUGP ((" 3"));
 
-       HHHHTTTTTTTTTTTTTTT  <- host
-           DDDDDDDDDDDDDDD  <- domain
+  /* HOST must match the tail of cookie_domain. */
+  if (!match_tail (host, cookie_domain, 1))
+    return 0;
 
-     That is, "head" is the part of the host before (dlen - hlen), and
-     "tail" is what follows.
+  /* 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;
+       }
 
-     For the domain to match, two conditions need to be true:
+    DEBUGP ((" 5"));
 
-     1. Tail must equal DOMAIN.
-     2. Head must not contain an embedded dot.  */
+    if (dccount < 2)
+      return 0;
 
-  headlen = strlen (host) - strlen (cookie_domain);
+    DEBUGP ((" 6"));
 
-  if (headlen <= 0)
-    /* DOMAIN must be a proper subset of HOST. */
-    return 0;
-  tail = host + headlen;
+    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 < ARRAY_SIZE (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;
+      }
+  }
 
-  /* (1) */
-  if (strcasecmp (tail, cookie_domain))
-    return 0;
+  DEBUGP ((" 7"));
 
-  /* Test (2) is not part of the "domain-match" itself, but is
-     recommended by rfc2109 for reasons of privacy.  */
+  /* 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;
+    }
 
-  /* (2) */
-  if (memchr (host, '.', headlen))
-    return 0;
+  DEBUGP ((" 8"));
 
   return 1;
 }
@@ -786,82 +803,70 @@ 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 urlinfo *u = (struct urlinfo *)closure;
   struct cookie *cookie;
-
   cookies_now = time (NULL);
 
-  cookie = parse_set_cookies (hdr);
+  cookie = parse_set_cookies (set_cookie);
   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
    previously stored cookies.  Entry point is
    `build_cookies_request'.  */
 
-
-/* Count how many times CHR occurs in STRING. */
-
-static int
-count_char (const char *string, char chr)
-{
-  const char *p;
-  int count = 0;
-  for (p = string; *p; p++)
-    if (*p == chr)
-      ++count;
-  return count;
-}
-
 /* Store CHAIN to STORE if there is room in STORE.  If not, inrecement
    COUNT anyway, so that when the function is done, we end up with the
    exact count of how much place we actually need.  */
@@ -872,13 +877,13 @@ count_char (const char *string, char chr)
   ++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;
@@ -886,13 +891,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);
 
@@ -907,7 +912,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;
@@ -921,29 +926,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;
@@ -978,6 +1006,43 @@ equality_comparator (const void *p1, const void *p2)
   return namecmp ? namecmp : valuecmp;
 }
 
+/* Eliminate duplicate cookies.  "Duplicate cookies" are any two
+   cookies whose name and value are the same.  Whenever a duplicate
+   pair is found, one of the cookies is removed.  */
+
+static int
+eliminate_dups (struct weighed_cookie *outgoing, int count)
+{
+  int i;
+
+  /* We deploy a simple uniquify algorithm: first sort the array
+     according to our sort criterion, then uniquify it by comparing
+     each cookie with its neighbor.  */
+
+  qsort (outgoing, count, sizeof (struct weighed_cookie), equality_comparator);
+
+  for (i = 0; i < count - 1; i++)
+    {
+      struct cookie *c1 = outgoing[i].cookie;
+      struct cookie *c2 = outgoing[i + 1].cookie;
+      if (!strcmp (c1->attr, c2->attr) && !strcmp (c1->value, c2->value))
+       {
+         /* c1 and c2 are the same; get rid of c2. */
+         if (count > i + 1)
+           /* move all ptrs from positions [i + 1, count) to i. */
+           memmove (outgoing + i, outgoing + i + 1,
+                    (count - (i + 1)) * sizeof (struct weighed_cookie));
+         /* We decrement i to counter the ++i above.  Remember that
+            we've just removed the element in front of us; we need to
+            remain in place to check whether outgoing[i] matches what
+            used to be outgoing[i + 2].  */
+         --i;
+         --count;
+       }
+    }
+  return count;
+}
+
 /* Comparator used for sorting by quality. */
 
 static int
@@ -997,15 +1062,16 @@ goodness_comparator (const void *p1, const void *p2)
   return dgdiff ? dgdiff : pgdiff;
 }
 
-/* Build a `Cookies' header for a request that goes to HOST:PORT and
-   requests PATH from the server.  Memory is allocated by `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.  */
+/* 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;
@@ -1019,13 +1085,15 @@ 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
-        match.  But in this case it's easy to not have the
-        limitation, so we don't.  */
+        match.  But since find_matching_chains reports the exact size
+        it needs, it's easy to not have the limitation, so we
+        don't.  */
       all_chains = alloca (chain_count * sizeof (struct cookie *));
+      chain_store_size = chain_count;
       goto again;
     }
 
@@ -1038,7 +1106,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. */
@@ -1047,12 +1115,14 @@ build_cookies_request (const char *host, int port, const char *path,
   /* Allocate the array. */
   outgoing = alloca (count * sizeof (struct weighed_cookie));
 
+  /* Fill the array with all the matching cookies from all the
+     matching chains. */
   ocnt = 0;
   for (i = 0; i < chain_count; i++)
     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);
@@ -1062,28 +1132,8 @@ build_cookies_request (const char *host, int port, const char *path,
   assert (ocnt == count);
 
   /* Eliminate duplicate cookies; that is, those whose name and value
-     are the same.  We do it by first sorting the array, and then
-     uniq'ing it.  */
-  qsort (outgoing, count, sizeof (struct weighed_cookie), equality_comparator);
-  for (i = 0; i < count - 1; i++)
-    {
-      struct cookie *c1 = outgoing[i].cookie;
-      struct cookie *c2 = outgoing[i + 1].cookie;
-      if (!strcmp (c1->attr, c2->attr) && !strcmp (c1->value, c2->value))
-       {
-         /* c1 and c2 are the same; get rid of c2. */
-         if (count > i + 1)
-           /* move all ptrs from positions [i + 1, count) to i. */
-           memmove (outgoing + i, outgoing + i + 1,
-                    (count - (i + 1)) * sizeof (struct weighed_cookie));
-         /* We decrement i to counter the ++i above.  Remember that
-            we've just removed the element in front of us; we need to
-            remain in place to check whether outgoing[i] what used to
-            be outgoing[i + 2].  */
-         --i;
-         --count;
-       }
-    }
+     are the same.  */
+  count = eliminate_dups (outgoing, count);
 
   /* Sort the array so that best-matching domains come first, and
      that, within one domain, best-matching paths come first. */
@@ -1192,7 +1242,7 @@ domain_port (const char *domain_b, const char *domain_e,
     ++p;                                       \
 } while (0)
 
-#define MARK_WORD(p, b, e) do {                        \
+#define SET_WORD_BOUNDARIES(p, b, e) do {      \
   SKIP_WS (p);                                 \
   b = p;                                       \
   /* skip non-ws */                            \
@@ -1206,7 +1256,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");
@@ -1223,6 +1273,7 @@ load_cookies (const char *file)
       struct cookie *cookie;
       char *p = line;
 
+      double expiry;
       int port;
 
       char *domain_b  = NULL, *domain_e  = NULL;
@@ -1239,16 +1290,25 @@ load_cookies (const char *file)
        /* empty line */
        continue;
 
-      MARK_WORD (p, domain_b,  domain_e);
-      MARK_WORD (p, ignore_b,  ignore_e);
-      MARK_WORD (p, path_b,    path_e);
-      MARK_WORD (p, secure_b,  secure_e);
-      MARK_WORD (p, expires_b, expires_e);
-      MARK_WORD (p, name_b,    name_e);
+      SET_WORD_BOUNDARIES (p, domain_b,  domain_e);
+      SET_WORD_BOUNDARIES (p, ignore_b,  ignore_e);
+      SET_WORD_BOUNDARIES (p, path_b,    path_e);
+      SET_WORD_BOUNDARIES (p, secure_b,  secure_e);
+      SET_WORD_BOUNDARIES (p, expires_b, expires_e);
+      SET_WORD_BOUNDARIES (p, name_b,    name_e);
 
-      /* Don't use MARK_WORD for value because it may contain
-        whitespace itself.  Instead, . */
-      MARK_WORD (p, value_b,   value_e);
+      /* Don't use SET_WORD_BOUNDARIES for value because it may
+        contain whitespace.  Instead, set value_e to the end of line,
+        modulo trailing space (this will skip the line separator.) */
+      SKIP_WS (p);
+      value_b = p;
+      value_e = p + strlen (p);
+      while (value_e > value_b && ISSPACE (*(value_e - 1)))
+       --value_e;
+      if (value_b == value_e)
+       /* Hmm, should we check for empty value?  I guess that's
+          legal, so I leave it.  */
+       ;
 
       cookie = cookie_new ();
 
@@ -1264,37 +1324,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);
 
-      /* Don't use MARK_WORD for value because it may contain
-        whitespace itself.  Instead, set name_e to the end of line,
-        modulo trailing space (which includes the NL separator.) */
-      SKIP_WS (p);
-      name_b = p;
-      name_e = p + strlen (p);
-      while (name_e >= name_b && ISSPACE (*name_e))
-       --name_e;
-      if (name_b == name_e)
-       /* Hmm, should we check for empty value?  I guess that's
-          legal, so I leave it.  */
-       ;
-
       /* 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;
@@ -1319,12 +1367,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 */
@@ -1335,15 +1386,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);
@@ -1360,7 +1406,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"),
@@ -1370,17 +1416,23 @@ save_cookies (const char *file)
     logprintf (LOG_NOTQUIET, _("Error closing `%s': %s\n"),
               file, strerror (errno));
 
-  DEBUGP (("Done saving cookies.\n", 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. */
@@ -1398,11 +1450,9 @@ 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);
 }