]> sjero.net Git - wget/blobdiff - src/cookies.c
[svn] Move extern declarations to .h files.
[wget] / src / cookies.c
index 94660b1b2fd04ab0bf79c88d21909e3346aa0aab..10710eb618f2d079b05c3c13da026897b7314a40 100644 (file)
 /* Support for cookies.
 /* 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.
 
 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
 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.  */
-
-/* Written by Hrvoje Niksic.  Parts are loosely inspired by cookie
-   code submitted by Tomasz Wegrzanowski.  */
+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 the
+   cookie patch submitted by Tomasz Wegrzanowski.
+
+   This implements the client-side cookie support, as specified
+   (loosely) by Netscape's "preliminary specification", currently
+   available at:
+
+       http://wp.netscape.com/newsref/std/cookie_spec.html
+
+   rfc2109 is not supported because of its incompatibilities with the
+   above widely-used specification.  rfc2965 is entirely ignored,
+   since popular client software doesn't implement it, and even the
+   sites that do send Set-Cookie2 also emit Set-Cookie for
+   compatibility.  */
 
 #include <config.h>
 
 #include <stdio.h>
 
 #include <config.h>
 
 #include <stdio.h>
-#ifdef HAVE_STRING_H
-# include <string.h>
-#else
-# include <strings.h>
-#endif
+#include <string.h>
 #include <stdlib.h>
 #include <assert.h>
 #include <errno.h>
 #include <stdlib.h>
 #include <assert.h>
 #include <errno.h>
+#include <time.h>
 
 #include "wget.h"
 #include "utils.h"
 #include "hash.h"
 
 #include "wget.h"
 #include "utils.h"
 #include "hash.h"
-#include "url.h"
 #include "cookies.h"
 #include "cookies.h"
+#include "http.h"              /* for http_atotm */
+\f
+/* Declarations of `struct cookie' and the most basic functions. */
 
 
-/* Hash table that maps domain names to cookie chains. */
+/* Cookie jar serves as cookie storage and a means of retrieving
+   cookies efficiently.  All cookies with the same domain are stored
+   in a linked list called "chain".  A cookie chain can be reached by
+   looking up the domain in the cookie jar's chains_by_domain table.
 
 
-static struct hash_table *cookies_hash_table;
+   For example, to reach all the cookies under google.com, one must
+   execute hash_table_get(jar->chains_by_domain, "google.com").  Of
+   course, when sending a cookie to `www.google.com', one must search
+   for cookies that belong to either `www.google.com' or `google.com'
+   -- but the point is that the code doesn't need to go through *all*
+   the cookies.  */
 
 
-/* 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 {
+  /* Cookie chains indexed by domain.  */
+  struct hash_table *chains;
+
+  int cookie_count;            /* number of cookies in the jar. */
+};
 
 
+/* Value set by entry point functions, so that the low-level
+   routines don't need to call time() all the time.  */
 static time_t cookies_now;
 static 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 = xnew (struct cookie_jar);
+  jar->chains = make_nocase_string_hash_table (0);
+  jar->cookie_count = 0;
+  return jar;
+}
 
 struct cookie {
   char *domain;                        /* domain of the cookie */
   int port;                    /* port number */
   char *path;                  /* path prefix of the cookie */
 
 struct cookie {
   char *domain;                        /* domain of the cookie */
   int port;                    /* port number */
   char *path;                  /* path prefix of the cookie */
-  int secure;                  /* whether cookie should be
+
+  unsigned discard_requested :1; /* whether cookie was created to
+                                  request discarding another
+                                  cookie. */
+
+  unsigned secure :1;          /* whether cookie should be
                                   transmitted over non-https
                                   connections. */
                                   transmitted over non-https
                                   connections. */
-  int permanent;               /* whether the cookie should outlive
-                                  the session */
-  unsigned long expiry_time;   /* time when the cookie expires */
-  int discard_requested;       /* whether cookie was created to
-                                  request discarding another
-                                  cookie */
+  unsigned domain_exact :1;    /* whether DOMAIN must match as a
+                                  whole. */
+
+  unsigned permanent :1;       /* whether the cookie should outlive
+                                  the session. */
+  time_t expiry_time;          /* time when the cookie expires, 0
+                                  means undetermined. */
 
   char *attr;                  /* cookie attribute name */
   char *value;                 /* cookie attribute value */
 
   char *attr;                  /* cookie attribute name */
   char *value;                 /* cookie attribute value */
@@ -70,134 +118,73 @@ struct cookie {
                                   same domain. */
 };
 
                                   same domain. */
 };
 
+#define PORT_ANY (-1)
+
 /* Allocate and return a new, empty cookie structure. */
 
 static struct cookie *
 cookie_new (void)
 {
 /* Allocate and return a new, empty cookie structure. */
 
 static struct cookie *
 cookie_new (void)
 {
-  struct cookie *cookie = xmalloc (sizeof (struct cookie));
-  memset (cookie, '\0', sizeof (struct cookie));
+  struct cookie *cookie = xnew0 (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.  This
+     means that the cookie doesn't expire, but is only valid for this
+     session (i.e. not written out to disk).  */
 
 
+  cookie->port = PORT_ANY;
   return cookie;
 }
 
   return cookie;
 }
 
-/* Deallocate COOKIE and its components. */
-
-static void
-delete_cookie (struct cookie *cookie)
-{
-  FREE_MAYBE (cookie->domain);
-  FREE_MAYBE (cookie->path);
-  FREE_MAYBE (cookie->attr);
-  FREE_MAYBE (cookie->value);
-  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 *sv)
-{
-  unsigned int h = 0;
-  unsigned const char *x = (unsigned const char *) sv;
-
-  while (*x)
-    {
-      unsigned int g;
-      unsigned char c = TOLOWER (*x);
-      h = (h << 4) + c;
-      if ((g = h & 0xf0000000) != 0)
-       h = (h ^ (g >> 24)) ^ g;
-      ++x;
-    }
-
-  return h;
-}
-
-/* Front-end to strcasecmp. */
+/* Non-zero if the cookie has expired.  Assumes cookies_now has been
+   set by one of the entry point functions.  */
 
 
-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)
+static bool
+cookie_expired_p (const struct cookie *c)
 {
 {
-  return hash_table_new (initial_size,
-                        unsigned_string_hash, unsigned_string_cmp);
+  return c->expiry_time != 0 && c->expiry_time < cookies_now;
 }
 
 }
 
-/* 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.  */
+/* Deallocate COOKIE and its components. */
 
 
-static struct cookie *
-find_cookie_chain_exact (const char *domain, int port)
+static void
+delete_cookie (struct cookie *cookie)
 {
 {
-  char *key;
-  if (!cookies_hash_table)
-    return NULL;
-  SET_HOSTPORT (domain, port, key);
-  return hash_table_get (cookies_hash_table, key);
+  xfree_null (cookie->domain);
+  xfree_null (cookie->path);
+  xfree_null (cookie->attr);
+  xfree_null (cookie->value);
+  xfree (cookie);
 }
 \f
 /* Functions for storing cookies.
 
 }
 \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 can be reached beginning with jar->chains.  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 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 *
 
    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;
 
 {
   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, cookie->domain);
   if (!chain)
     goto nomatch;
 
   prev = NULL;
   for (; chain; prev = chain, chain = chain->next)
   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;
       {
        *prevptr = prev;
        return chain;
@@ -208,7 +195,7 @@ find_matching_cookie (struct cookie *cookie, struct cookie **prevptr)
   return NULL;
 }
 
   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
 
    This is done by placing COOKIE at the head of its chain.  However,
    if COOKIE matches a cookie already in memory, as determined by
@@ -218,29 +205,19 @@ find_matching_cookie (struct cookie *cookie, struct cookie **prevptr)
    first time; next hash_table_put's reuse the same key.  */
 
 static void
    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;
 {
   struct cookie *chain_head;
-  char *hostport;
   char *chain_key;
 
   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, cookie->domain,
                           &chain_key, &chain_head))
     {
                           &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 *prev;
-      struct cookie *victim = find_matching_cookie (cookie, &prev);
+      struct cookie *victim = find_matching_cookie (jar, cookie, &prev);
 
       if (victim)
        {
 
       if (victim)
        {
@@ -259,6 +236,7 @@ store_cookie (struct cookie *cookie)
              cookie->next = victim->next;
            }
          delete_cookie (victim);
              cookie->next = victim->next;
            }
          delete_cookie (victim);
+         --jar->cookie_count;
          DEBUGP (("Deleted old cookie (to be replaced.)\n"));
        }
       else
          DEBUGP (("Deleted old cookie (to be replaced.)\n"));
        }
       else
@@ -266,39 +244,48 @@ store_cookie (struct cookie *cookie)
     }
   else
     {
     }
   else
     {
-      /* We are now creating the chain.  Allocate the string that will
-        be used as a key.  It is unsafe to use cookie->domain for
-        that, because it might get deallocated by the above code at
-        some point later.  */
+      /* We are now creating the chain.  Use a copy of cookie->domain
+        as the key for the life-time of the chain.  Using
+        cookie->domain would be unsafe because the life-time of the
+        chain may exceed the life-time of the cookie.  (Cookies may
+        be deleted from the chain by this very function.)  */
       cookie->next = NULL;
       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, chain_key, cookie);
+  ++jar->cookie_count;
 
 
-  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)),
-          cookie->attr, cookie->value));
+  IF_DEBUG
+    {
+      time_t exptime = cookie->expiry_time;
+      DEBUGP (("\nStored cookie %s %d%s %s <%s> <%s> [expiry %s] %s %s\n",
+              cookie->domain, cookie->port,
+              cookie->port == PORT_ANY ? " (ANY)" : "",
+              cookie->path,
+              cookie->permanent ? "permanent" : "session",
+              cookie->secure ? "secure" : "insecure",
+              cookie->expiry_time ? datetime_str (&exptime) : "none",
+              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
 
 static void
-discard_matching_cookie (struct cookie *cookie)
+discard_matching_cookie (struct cookie_jar *jar, struct cookie *cookie)
 {
   struct cookie *prev, *victim;
 
 {
   struct cookie *prev, *victim;
 
-  if (!cookies_hash_table
-      || !hash_table_count (cookies_hash_table))
+  if (!hash_table_count (jar->chains))
     /* No elements == nothing to discard. */
     return;
 
     /* No elements == nothing to discard. */
     return;
 
-  victim = find_matching_cookie (cookie, &prev);
+  victim = find_matching_cookie (jar, cookie, &prev);
   if (victim)
     {
       if (prev)
   if (victim)
     {
       if (prev)
@@ -308,25 +295,21 @@ discard_matching_cookie (struct cookie *cookie)
        {
          /* VICTIM was head of its chain.  We need to place a new
             cookie at the head.  */
        {
          /* VICTIM was head of its chain.  We need to place a new
             cookie at the head.  */
-
-         char *hostport;
          char *chain_key = NULL;
          int res;
 
          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, 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.  */
                                     &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, victim->domain);
              xfree (chain_key);
            }
          else
              xfree (chain_key);
            }
          else
-           hash_table_put (cookies_hash_table, chain_key, victim->next);
+           hash_table_put (jar->chains, chain_key, victim->next);
        }
       delete_cookie (victim);
       DEBUGP (("Discarded old cookie.\n"));
        }
       delete_cookie (victim);
       DEBUGP (("Discarded old cookie.\n"));
@@ -336,7 +319,6 @@ discard_matching_cookie (struct cookie *cookie)
 /* Functions for parsing the `Set-Cookie' header, and creating new
    cookies from the wire.  */
 
 /* Functions for parsing the `Set-Cookie' header, and creating new
    cookies from the wire.  */
 
-
 #define NAME_IS(string_literal)                                        \
   BOUNDED_EQUAL_NO_CASE (name_b, name_e, string_literal)
 
 #define NAME_IS(string_literal)                                        \
   BOUNDED_EQUAL_NO_CASE (name_b, name_e, string_literal)
 
@@ -354,10 +336,10 @@ discard_matching_cookie (struct cookie *cookie)
    it will parse the values of the fields it recognizes and fill the
    corresponding fields in COOKIE.
 
    it will parse the values of the fields it recognizes and fill the
    corresponding fields in COOKIE.
 
-   Returns 1 on success.  Returns zero in case a syntax error is
+   Returns true on success.  Returns false in case a syntax error is
    found; such a cookie should be discarded.  */
 
    found; such a cookie should be discarded.  */
 
-static int
+static bool
 update_cookie_field (struct cookie *cookie,
                     const char *name_b, const char *name_e,
                     const char *value_b, const char *value_e)
 update_cookie_field (struct cookie *cookie,
                     const char *name_b, const char *name_e,
                     const char *value_b, const char *value_e)
@@ -367,27 +349,33 @@ update_cookie_field (struct cookie *cookie,
   if (!cookie->attr)
     {
       if (!VALUE_EXISTS)
   if (!cookie->attr)
     {
       if (!VALUE_EXISTS)
-       return 0;
+       return false;
       cookie->attr = strdupdelim (name_b, name_e);
       cookie->value = strdupdelim (value_b, value_e);
       cookie->attr = strdupdelim (name_b, name_e);
       cookie->value = strdupdelim (value_b, value_e);
-      return 1;
+      return true;
     }
 
   if (NAME_IS ("domain"))
     {
       if (!VALUE_NON_EMPTY)
     }
 
   if (NAME_IS ("domain"))
     {
       if (!VALUE_NON_EMPTY)
-       return 0;
-      FREE_MAYBE (cookie->domain);
+       return false;
+      xfree_null (cookie->domain);
+      /* Strictly speaking, we should set cookie->domain_exact if the
+        domain doesn't begin with a dot.  But many sites set the
+        domain to "foo.com" and expect "subhost.foo.com" to get the
+        cookie, and it apparently works.  */
+      if (*value_b == '.')
+       ++value_b;
       cookie->domain = strdupdelim (value_b, value_e);
       cookie->domain = strdupdelim (value_b, value_e);
-      return 1;
+      return true;
     }
   else if (NAME_IS ("path"))
     {
       if (!VALUE_NON_EMPTY)
     }
   else if (NAME_IS ("path"))
     {
       if (!VALUE_NON_EMPTY)
-       return 0;
-      FREE_MAYBE (cookie->path);
+       return false;
+      xfree_null (cookie->path);
       cookie->path = strdupdelim (value_b, value_e);
       cookie->path = strdupdelim (value_b, value_e);
-      return 1;
+      return true;
     }
   else if (NAME_IS ("expires"))
     {
     }
   else if (NAME_IS ("expires"))
     {
@@ -395,19 +383,18 @@ update_cookie_field (struct cookie *cookie,
       time_t expires;
 
       if (!VALUE_NON_EMPTY)
       time_t expires;
 
       if (!VALUE_NON_EMPTY)
-       return 0;
+       return false;
       BOUNDED_TO_ALLOCA (value_b, value_e, value_copy);
 
       expires = http_atotm (value_copy);
       BOUNDED_TO_ALLOCA (value_b, value_e, value_copy);
 
       expires = http_atotm (value_copy);
-      if (expires != -1)
+      if (expires != (time_t) -1)
        {
          cookie->permanent = 1;
        {
          cookie->permanent = 1;
-         cookie->expiry_time = (unsigned long)expires;
+         cookie->expiry_time = expires;
        }
       else
        }
       else
-       /* Error in expiration spec.  Assume default (cookie valid for
-          this session.)  #### Should we return 0 and invalidate the
-          cookie?  */
+       /* Error in expiration spec.  Assume default (cookie doesn't
+          expire, but valid only for this session.)  */
        ;
 
       /* According to netscape's specification, expiry time in the
        ;
 
       /* According to netscape's specification, expiry time in the
@@ -416,7 +403,7 @@ update_cookie_field (struct cookie *cookie,
       if (cookie->expiry_time < cookies_now)
        cookie->discard_requested = 1;
 
       if (cookie->expiry_time < cookies_now)
        cookie->discard_requested = 1;
 
-      return 1;
+      return true;
     }
   else if (NAME_IS ("max-age"))
     {
     }
   else if (NAME_IS ("max-age"))
     {
@@ -424,55 +411,47 @@ update_cookie_field (struct cookie *cookie,
       char *value_copy;
 
       if (!VALUE_NON_EMPTY)
       char *value_copy;
 
       if (!VALUE_NON_EMPTY)
-       return 0;
+       return false;
       BOUNDED_TO_ALLOCA (value_b, value_e, value_copy);
 
       sscanf (value_copy, "%lf", &maxage);
       if (maxage == -1)
       BOUNDED_TO_ALLOCA (value_b, value_e, value_copy);
 
       sscanf (value_copy, "%lf", &maxage);
       if (maxage == -1)
-       /* something is wrong. */
-       return 0;
+       /* something went wrong. */
+       return false;
       cookie->permanent = 1;
       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.  */
       if (maxage == 0)
        cookie->discard_requested = 1;
 
 
       /* According to rfc2109, a cookie with max-age of 0 means that
         discarding of a matching cookie is requested.  */
       if (maxage == 0)
        cookie->discard_requested = 1;
 
-      return 1;
+      return true;
     }
   else if (NAME_IS ("secure"))
     {
       /* ignore value completely */
       cookie->secure = 1;
     }
   else if (NAME_IS ("secure"))
     {
       /* ignore value completely */
       cookie->secure = 1;
-      return 1;
+      return true;
     }
   else
     /* Unrecognized attribute; ignore it. */
     }
   else
     /* Unrecognized attribute; ignore it. */
-    return 1;
+    return true;
 }
 
 #undef NAME_IS
 
 }
 
 #undef NAME_IS
 
-/* Returns non-zero for characters that are legal in the name of an
-   attribute.  */
-
-#define ATTR_NAME_CHAR(c) (ISALNUM (c) || (c) == '-' || (c) == '_')
+/* Returns true for characters that are legal in the name of an
+   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.  */
 
 
-/* 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)
+#define ATTR_NAME_CHAR(c) ((c) > 32 && (c) < 127       \
+                          && (c) != '"' && (c) != '='  \
+                          && (c) != ';' && (c) != ',')
 
 /* Parse the contents of the `Set-Cookie' header.  The header looks
    like this:
 
 /* Parse the contents of the `Set-Cookie' header.  The header looks
    like this:
@@ -483,19 +462,25 @@ update_cookie_field (struct cookie *cookie,
    tokens.  Additionally, values may be quoted.
 
    A new cookie is returned upon success, NULL otherwise.  The
    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 *
 
 static struct cookie *
-parse_set_cookies (const char *sc)
+parse_set_cookies (const char *sc,
+                  bool (*callback) (struct cookie *,
+                                    const char *, const char *,
+                                    const char *, const char *),
+                  bool silent)
 {
   struct cookie *cookie = cookie_new ();
 
 {
   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;
 
   const char *p = sc;
   char c;
@@ -503,19 +488,21 @@ parse_set_cookies (const char *sc)
   const char *name_b  = NULL, *name_e  = NULL;
   const char *value_b = NULL, *value_e = NULL;
 
   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)
        {
 
   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))
            {
          else if (ATTR_NAME_CHAR (c))
            {
-             name_b = p - 1;
-             FETCH1 (c, p);
+             name_b = p;
              state = S_NAME;
            }
          else
              state = S_NAME;
            }
          else
@@ -523,116 +510,112 @@ parse_set_cookies (const char *sc)
            state = S_ERROR;
          break;
        case S_NAME:
            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;
            }
              state = S_NAME_POST;
            }
+         else if (ATTR_NAME_CHAR (c))
+           c = *++p;
          else
            state = S_ERROR;
          break;
        case S_NAME_POST:
          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;
            {
              value_b = value_e = NULL;
+             if (c == ';')
+               c = *++p;
              state = S_ATTR_ACTION;
            }
          else if (c == '=')
            {
              state = S_ATTR_ACTION;
            }
          else if (c == '=')
            {
-             FETCH1 (c, p);
+             c = *++p;
              state = S_VALUE_PRE;
            }
              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:
          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 == '"')
            {
          else if (c == '"')
            {
+             c = *++p;
              value_b = p;
              value_b = p;
-             FETCH (c, p);
              state = S_QUOTED_VALUE;
            }
              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
            {
          else
            {
-             value_b = p - 1;
+             value_b = p;
              value_e = NULL;
              state = S_VALUE;
            }
          break;
        case S_VALUE:
              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 */
            }
          else
            {
              value_e = NULL;   /* no trailing space */
-             FETCH1 (c, p);
+             c = *++p;
            }
          break;
            }
          break;
-       case S_VALUE_TRAILSPACE_MAYBE:
-         if (ISSPACE (c))
-           FETCH1 (c, p);
-         else
-           state = S_VALUE;
-         break;
        case S_QUOTED_VALUE:
          if (c == '"')
            {
        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
          else
-           FETCH (c, p);
+           c = *++p;
          break;
          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))
            state = S_ATTR_ACTION;
          else if (ISSPACE (c))
-           FETCH1 (c, p);
+           c = *++p;
          else
          else
-           state = S_ERROR;
+           state = S_VALUE;
          break;
        case S_ATTR_ACTION:
          {
          break;
        case S_ATTR_ACTION:
          {
-           int legal = update_cookie_field (cookie, name_b, name_e,
-                                            value_b, value_e);
+           bool legal = callback (cookie, name_b, name_e, value_b, value_e);
            if (!legal)
              {
            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'"),
+                              escnonprint (name));
+                 }
                state = S_ERROR;
                break;
              }
                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:
          }
          break;
        case S_DONE:
@@ -645,16 +628,13 @@ parse_set_cookies (const char *sc)
     return cookie;
 
   delete_cookie (cookie);
     return cookie;
 
   delete_cookie (cookie);
-  if (state == S_ERROR)
-    logprintf (LOG_NOTQUIET, _("Error in Set-Cookie, at character `%c'.\n"), c);
-  else
+  if (state != S_ERROR)
     abort ();
     abort ();
-  return NULL;
 
 
- eof:
-  delete_cookie (cookie);
-  logprintf (LOG_NOTQUIET,
-            _("Error in Set-Cookie: premature end of string.\n"));
+  if (!silent)
+    logprintf (LOG_NOTQUIET,
+              _("Syntax error in Set-Cookie: %s at position %d.\n"),
+              escnonprint (sc), (int) (p - sc));
   return NULL;
 }
 \f
   return NULL;
 }
 \f
@@ -665,23 +645,23 @@ parse_set_cookies (const char *sc)
 
 #define REQUIRE_DIGITS(p) do {                 \
   if (!ISDIGIT (*p))                           \
 
 #define REQUIRE_DIGITS(p) do {                 \
   if (!ISDIGIT (*p))                           \
-    return 0;                                  \
+    return false;                              \
   for (++p; ISDIGIT (*p); p++)                 \
     ;                                          \
 } while (0)
 
 #define REQUIRE_DOT(p) do {                    \
   if (*p++ != '.')                             \
   for (++p; ISDIGIT (*p); p++)                 \
     ;                                          \
 } while (0)
 
 #define REQUIRE_DOT(p) do {                    \
   if (*p++ != '.')                             \
-    return 0;                                  \
+    return false;                              \
 } while (0)
 
 /* Check whether ADDR matches <digits>.<digits>.<digits>.<digits>.
 
 } while (0)
 
 /* Check whether ADDR matches <digits>.<digits>.<digits>.<digits>.
 
-  We don't want to call network functions like inet_addr() because all
-  we need is a check, preferrably one that is small, fast, and
-  well-defined.  */
+   We don't want to call network functions like inet_addr() because
+   all we need is a check, preferrably one that is small, fast, and
+   well-defined.  */
 
 
-static int
+static bool
 numeric_address_p (const char *addr)
 {
   const char *p = addr;
 numeric_address_p (const char *addr)
 {
   const char *p = addr;
@@ -695,163 +675,253 @@ numeric_address_p (const char *addr)
   REQUIRE_DIGITS (p);          /* D */
 
   if (*p != '\0')
   REQUIRE_DIGITS (p);          /* D */
 
   if (*p != '\0')
-    return 0;
-  return 1;
+    return false;
+  return true;
 }
 
 /* Check whether COOKIE_DOMAIN is an appropriate domain for HOST.
 }
 
 /* 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
+static bool
 check_domain_match (const char *cookie_domain, const char *host)
 {
 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
 
   /* 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))
   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. */
 
   /* 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:
-
-       HHHHTTTTTTTTTTTTTTT  <- host
-           DDDDDDDDDDDDDDD  <- domain
-
-     That is, "head" is the part of the host before (dlen - hlen), and
-     "tail" is what follows.
+  if (0 == strcasecmp (cookie_domain, host))
+    return true;
+
+  DEBUGP ((" 3"));
+
+  /* HOST must match the tail of cookie_domain. */
+  if (!match_tail (host, cookie_domain, true))
+    return false;
+
+  /* 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 false;
+         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 false;
 
 
-  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 = false;
+       static const 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], true))
+           {
+             known_toplevel = true;
+             break;
+           }
+       if (!known_toplevel && nldcl <= 3)
+         return false;
+      }
+  }
 
 
-  /* (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 the host "foobar.com" to set a cookie for domain
+     "bar.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 false;
+    }
 
 
-  /* (2) */
-  if (memchr (host, '.', headlen))
-    return 0;
+  DEBUGP ((" 8"));
 
 
-  return 1;
+  return true;
 }
 
 }
 
-static int path_matches PARAMS ((const char *, const char *));
+static int path_matches (const char *, const char *);
 
 /* Check whether PATH begins with COOKIE_PATH. */
 
 
 /* Check whether PATH begins with COOKIE_PATH. */
 
-static int
+static bool
 check_path_match (const char *cookie_path, const char *path)
 {
 check_path_match (const char *cookie_path, const char *path)
 {
-  return path_matches (path, cookie_path);
+  return path_matches (path, cookie_path) != 0;
 }
 }
+
+/* Prepend '/' to string S.  S is copied to fresh stack-allocated
+   space and its value is modified to point to the new location.  */
+
+#define PREPEND_SLASH(s) do {                                  \
+  char *PS_newstr = (char *) alloca (1 + strlen (s) + 1);      \
+  *PS_newstr = '/';                                            \
+  strcpy (PS_newstr + 1, s);                                   \
+  s = PS_newstr;                                               \
+} while (0)
+
 \f
 \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_handle_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;
   struct cookie *cookie;
-
   cookies_now = time (NULL);
 
   cookies_now = time (NULL);
 
-  cookie = parse_set_cookies (hdr);
+  /* Wget's paths don't begin with '/' (blame rfc1808), but cookie
+     usage assumes /-prefixed paths.  Until the rest of Wget is fixed,
+     simply prepend slash to PATH.  */
+  PREPEND_SLASH (path);
+
+  cookie = parse_set_cookies (set_cookie, update_cookie_field, false);
   if (!cookie)
     goto out;
 
   /* Sanitize parts of cookie. */
 
   if (!cookie->domain)
   if (!cookie)
     goto out;
 
   /* Sanitize parts of cookie. */
 
   if (!cookie->domain)
-    cookie->domain = xstrdup (u->host);
+    {
+    copy_domain:
+      /* If the domain was not provided, we use the one we're talking
+        to, and set exact match.  */
+      cookie->domain = xstrdup (host);
+      cookie->domain_exact = 1;
+      /* Set the port, but only if it's non-default. */
+      if (port != 80 && port != 443)
+       cookie->port = port;
+    }
   else
     {
   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"),
+                    escnonprint (host), escnonprint (cookie->domain));
+         xfree (cookie->domain);
+         goto copy_domain;
        }
     }
        }
     }
+
   if (!cookie->path)
   if (!cookie->path)
-    cookie->path = xstrdup (u->path);
+    {
+      /* The cookie doesn't set path: set it to the URL path, sans the
+        file part ("/dir/file" truncated to "/dir/").  */
+      char *trailing_slash = strrchr (path, '/');
+      if (trailing_slash)
+       cookie->path = strdupdelim (path, trailing_slash + 1);
+      else
+       /* no slash in the string -- can this even happen? */
+       cookie->path = xstrdup (path);
+    }
   else
     {
   else
     {
-      if (!check_path_match (cookie->path, u->path))
+      /* The cookie sets its own path; verify that it is legal. */
+      if (!check_path_match (cookie->path, path))
        {
          DEBUGP (("Attempt to fake the path: %s, %s\n",
        {
          DEBUGP (("Attempt to fake the path: %s, %s\n",
-                  cookie->path, u->path));
+                  cookie->path, path));
          goto out;
        }
     }
 
          goto out;
        }
     }
 
-  cookie->port = u->port;
+  /* Now store the cookie, or discard an existing cookie, if
+     discarding was requested.  */
 
   if (cookie->discard_requested)
     {
 
   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);
 
  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'.  */
 }
 \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. */
+   
+/* Return a count of how many times CHR occurs in STRING. */
 
 static int
 count_char (const char *string, char chr)
 
 static int
 count_char (const char *string, char chr)
@@ -864,42 +934,55 @@ count_char (const char *string, char chr)
   return count;
 }
 
   return count;
 }
 
-/* Return the head of the cookie chain that matches HOST.  */
-
-static struct cookie *
-find_cookie_chain (const char *host, int port)
-{
-  int dot_count;
-  char *hash_key;
-  struct cookie *chain = NULL;
+/* Find the cookie chains whose domains match HOST and store them to
+   DEST.
 
 
-  if (!cookies_hash_table)
-    return NULL;
+   A cookie chain is the head of a list of cookies that belong to a
+   host/domain.  Given HOST "img.search.xemacs.org", this function
+   will return the chains for "img.search.xemacs.org",
+   "search.xemacs.org", and "xemacs.org" -- those of them that exist
+   (if any), that is.
 
 
-  SET_HOSTPORT (host, port, hash_key);
+   DEST should be large enough to accept (in the worst case) as many
+   elements as there are domain components of HOST.  */
 
 
-  /* Exact match. */
-  chain = hash_table_get (cookies_hash_table, hash_key);
-  if (chain)
-    return chain;
+static int
+find_chains_of_host (struct cookie_jar *jar, const char *host,
+                    struct cookie *dest[])
+{
+  int dest_count = 0;
+  int passes, passcnt;
 
 
-  dot_count = count_char (host, '.');
+  /* Bail out quickly if there are no cookies in the jar.  */
+  if (!hash_table_count (jar->chains))
+    return 0;
 
 
-  /* Match less and less specific domains.  For instance, given
-     fly.srk.fer.hr, we match .srk.fer.hr, then .fer.hr.  */
-  while (dot_count-- > 1)
+  if (numeric_address_p (host))
+    /* If host is an IP address, only check for the exact match. */
+    passes = 1;
+  else
+    /* Otherwise, check all the subdomains except the top-level (last)
+       one.  As a domain with N components has N-1 dots, the number of
+       passes equals the number of dots.  */
+    passes = count_char (host, '.');
+
+  passcnt = 0;
+
+  /* Find chains that match HOST, starting with exact match and
+     progressing to less specific domains.  For instance, given HOST
+     fly.srk.fer.hr, first look for fly.srk.fer.hr's chain, then
+     srk.fer.hr's, then fer.hr's.  */
+  while (1)
     {
     {
-      /* Note: we operate directly on hash_key (in form host:port)
-        because we don't want to allocate new hash keys in a
-        loop.  */
-      char *p = strchr (hash_key, '.');
-      assert (p != NULL);
-      chain = hash_table_get (cookies_hash_table, p);
+      struct cookie *chain = hash_table_get (jar->chains, host);
       if (chain)
       if (chain)
-       return chain;
-      hash_key = p + 1;
+       dest[dest_count++] = chain;
+      if (++passcnt >= passes)
+       break;
+      host = strchr (host, '.') + 1;
     }
     }
-  return NULL;
+
+  return dest_count;
 }
 
 /* If FULL_PATH begins with PREFIX, return the length of PREFIX, zero
 }
 
 /* If FULL_PATH begins with PREFIX, return the length of PREFIX, zero
@@ -909,114 +992,240 @@ static int
 path_matches (const char *full_path, const char *prefix)
 {
   int len = strlen (prefix);
 path_matches (const char *full_path, const char *prefix)
 {
   int len = strlen (prefix);
-  if (strncmp (full_path, prefix, len))
+
+  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. */
     /* FULL_PATH doesn't begin with PREFIX. */
     return 0;
 
   /* Length of PREFIX determines the quality of the match. */
-  return len;
+  return len + 1;
+}
+
+/* Return true iff COOKIE matches the provided parameters of the URL
+   being downloaded: HOST, PORT, PATH, and SECFLAG.
+
+   If PATH_GOODNESS is non-NULL, store the "path goodness" value
+   there.  That value is a measure of how closely COOKIE matches PATH,
+   used for ordering cookies.  */
+
+static bool
+cookie_matches_url (const struct cookie *cookie,
+                   const char *host, int port, const char *path,
+                   bool secflag, int *path_goodness)
+{
+  int pg;
+
+  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 false;
+
+  if (cookie->secure && !secflag)
+    /* Don't transmit secure cookies over insecure connections.  */
+    return false;
+  if (cookie->port != PORT_ANY && cookie->port != port)
+    return false;
+
+  /* If exact domain match is required, verify that cookie's domain is
+     equal to HOST.  If not, assume success on the grounds of the
+     cookie's chain having been found by find_chains_of_host.  */
+  if (cookie->domain_exact
+      && 0 != strcasecmp (host, cookie->domain))
+    return false;
+
+  pg = path_matches (path, cookie->path);
+  if (pg == 0)
+    return false;
+
+  if (path_goodness)
+    /* If the caller requested path_goodness, we return it.  This is
+       an optimization, so that the caller doesn't need to call
+       path_matches() again.  */
+    *path_goodness = pg;
+  return true;
 }
 
 }
 
+/* A structure that points to a cookie, along with the additional
+   information about the cookie's "goodness".  This allows us to sort
+   the cookies when returning them to the server, as required by the
+   spec.  */
+
 struct weighed_cookie {
   struct cookie *cookie;
 struct weighed_cookie {
   struct cookie *cookie;
+  int domain_goodness;
   int path_goodness;
 };
 
   int path_goodness;
 };
 
+/* Comparator used for uniquifying the list. */
+
+static int
+equality_comparator (const void *p1, const void *p2)
+{
+  struct weighed_cookie *wc1 = (struct weighed_cookie *)p1;
+  struct weighed_cookie *wc2 = (struct weighed_cookie *)p2;
+
+  int namecmp  = strcmp (wc1->cookie->attr, wc2->cookie->attr);
+  int valuecmp = strcmp (wc1->cookie->value, wc2->cookie->value);
+
+  /* We only really care whether both name and value are equal.  We
+     return them in this order only for consistency...  */
+  return namecmp ? namecmp : valuecmp;
+}
+
+/* Eliminate duplicate cookies.  "Duplicate cookies" are any two
+   cookies with the same attr name and value.  Whenever a duplicate
+   pair is found, one of the cookies is removed.  */
+
+static int
+eliminate_dups (struct weighed_cookie *outgoing, int count)
+{
+  struct weighed_cookie *h;    /* hare */
+  struct weighed_cookie *t;    /* tortoise */
+  struct weighed_cookie *end = outgoing + count;
+
+  /* We deploy a simple uniquify algorithm: first sort the array
+     according to our sort criteria, then copy it to itself, comparing
+     each cookie to its neighbor and ignoring the duplicates.  */
+
+  qsort (outgoing, count, sizeof (struct weighed_cookie), equality_comparator);
+
+  /* "Hare" runs through all the entries in the array, followed by
+     "tortoise".  If a duplicate is found, the hare skips it.
+     Non-duplicate entries are copied to the tortoise ptr.  */
+
+  for (h = t = outgoing; h < end; h++)
+    {
+      if (h != end - 1)
+       {
+         struct cookie *c0 = h[0].cookie;
+         struct cookie *c1 = h[1].cookie;
+         if (!strcmp (c0->attr, c1->attr) && !strcmp (c0->value, c1->value))
+           continue;           /* ignore the duplicate */
+       }
+
+      /* If the hare has advanced past the tortoise (because of
+        previous dups), make sure the values get copied.  Otherwise,
+        no copying is necessary.  */
+      if (h != t)
+       *t++ = *h;
+      else
+       t++;
+    }
+  return t - outgoing;
+}
+
+/* Comparator used for sorting by quality. */
+
 static int
 goodness_comparator (const void *p1, const void *p2)
 {
   struct weighed_cookie *wc1 = (struct weighed_cookie *)p1;
   struct weighed_cookie *wc2 = (struct weighed_cookie *)p2;
 static int
 goodness_comparator (const void *p1, const void *p2)
 {
   struct weighed_cookie *wc1 = (struct weighed_cookie *)p1;
   struct weighed_cookie *wc2 = (struct weighed_cookie *)p2;
-  /* It's goodness2-goodness1 because we want a sort in *decreasing*
-     order of goodness. */
-  return wc2->path_goodness - wc1->path_goodness;
+
+  /* Subtractions take `wc2' as the first argument becauase we want a
+     sort in *decreasing* order of goodness.  */
+  int dgdiff = wc2->domain_goodness - wc1->domain_goodness;
+  int pgdiff = wc2->path_goodness - wc1->path_goodness;
+
+  /* Sort by domain goodness; if these are the same, sort by path
+     goodness.  (The sorting order isn't really specified; maybe it
+     should be the other way around.)  */
+  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 *
 
 char *
-build_cookies_request (const char *host, int port, const char *path,
-                      int connection_secure_p)
+cookie_header (struct cookie_jar *jar, const char *host,
+              int port, const char *path, bool secflag)
 {
 {
-  struct cookie *chain = find_cookie_chain (host, port);
+  struct cookie **chains;
+  int chain_count;
+
   struct cookie *cookie;
   struct weighed_cookie *outgoing;
   struct cookie *cookie;
   struct weighed_cookie *outgoing;
-  int count, i;
+  int count, i, ocnt;
   char *result;
   int result_size, pos;
   char *result;
   int result_size, pos;
+  PREPEND_SLASH (path);                /* see cookie_handle_set_cookie */
 
 
-  if (!chain)
+  /* First, find the cookie chains whose domains match HOST. */
+
+  /* Allocate room for find_chains_of_host to write to.  The number of
+     chains can at most equal the number of subdomains, hence
+     1+<number of dots>.  */
+  chains = alloca_array (struct cookie *, 1 + count_char (host, '.'));
+  chain_count = find_chains_of_host (jar, host, chains);
+
+  /* No cookies for this host. */
+  if (!chain_count)
     return NULL;
 
   cookies_now = time (NULL);
 
     return NULL;
 
   cookies_now = time (NULL);
 
-  /* Count the number of cookies whose path matches. */
+  /* Now extract from the chains those cookies that match our host
+     (for domain_exact cookies), port (for cookies with port other
+     than PORT_ANY), etc.  See matching_cookie for details.  */
+
+  /* Count the number of matching cookies. */
   count = 0;
   count = 0;
-  result_size = 0;
-  for (cookie = chain; cookie; cookie = cookie->next)
-    {
-      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'.  */
-       continue;
-      if (cookie->secure && !connection_secure_p)
-       /* Don't transmit secure cookies over an insecure
-          connection.  */
-       continue;
-      if (path_matches (path, cookie->path))
-       {
-         ++count;
-         /* name=value */
-         result_size += strlen (cookie->attr) + 1 + strlen (cookie->value);
-       }
-    }
+  for (i = 0; i < chain_count; i++)
+    for (cookie = chains[i]; cookie; cookie = cookie->next)
+      if (cookie_matches_url (cookie, host, port, path, secflag, NULL))
+       ++count;
   if (!count)
   if (!count)
-    return NULL;
+    return NULL;               /* no cookies matched */
 
   /* Allocate the array. */
 
   /* Allocate the array. */
-  outgoing = alloca (count * sizeof (struct weighed_cookie));
-  i = 0;
-  for (cookie = chain; cookie; cookie = cookie->next)
-    {
-      int goodness;
-      /* #### These two if's are repeated verbatim from the loop
-        above.  Should I put them in a separate function?  */
-      if (cookie->expiry_time < cookies_now)
-       continue;
-      if (cookie->secure && !connection_secure_p)
-       /* Don't transmit secure cookies over an insecure
-          connection.  */
-       continue;
-      goodness = path_matches (path, cookie->path);
-      if (!goodness)
-       continue;
-      outgoing[i].cookie = cookie;
-      outgoing[i].path_goodness = goodness;
-      ++i;
-    }
-  assert (i == count);
+  outgoing = alloca_array (struct weighed_cookie, count);
+
+  /* Fill the array with all the matching cookies from the chains that
+     match HOST. */
+  ocnt = 0;
+  for (i = 0; i < chain_count; i++)
+    for (cookie = chains[i]; cookie; cookie = cookie->next)
+      {
+       int pg;
+       if (!cookie_matches_url (cookie, host, port, path, secflag, &pg))
+         continue;
+       outgoing[ocnt].cookie = cookie;
+       outgoing[ocnt].domain_goodness = strlen (cookie->domain);
+       outgoing[ocnt].path_goodness   = pg;
+       ++ocnt;
+      }
+  assert (ocnt == count);
 
 
-  /* Sort the array so that paths that match our path better come
-     first. */
+  /* Eliminate duplicate cookies; that is, those whose name and value
+     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. */
   qsort (outgoing, count, sizeof (struct weighed_cookie), goodness_comparator);
 
   qsort (outgoing, count, sizeof (struct weighed_cookie), goodness_comparator);
 
+  /* Count the space the name=value pairs will take. */
+  result_size = 0;
+  for (i = 0; i < count; i++)
+    {
+      struct cookie *c = outgoing[i].cookie;
+      /* name=value */
+      result_size += strlen (c->attr) + 1 + strlen (c->value);
+    }
+
   /* Allocate output buffer:
   /* Allocate output buffer:
-     "Cookie: "       -- 8
      name=value pairs -- result_size
      "; " separators  -- (count - 1) * 2
      name=value pairs -- result_size
      "; " separators  -- (count - 1) * 2
-     \r\n line ending -- 2
      \0 terminator    -- 1 */
      \0 terminator    -- 1 */
-  result_size = 8 + result_size + (count - 1) * 2 + 2 + 1;
+  result_size = result_size + (count - 1) * 2 + 1;
   result = xmalloc (result_size);
   pos = 0;
   result = xmalloc (result_size);
   pos = 0;
-  strcpy (result, "Cookie: ");
-  pos += 8;
   for (i = 0; i < count; i++)
     {
       struct cookie *c = outgoing[i].cookie;
   for (i = 0; i < count; i++)
     {
       struct cookie *c = outgoing[i].cookie;
@@ -1034,16 +1243,15 @@ build_cookies_request (const char *host, int port, const char *path,
          result[pos++] = ' ';
        }
     }
          result[pos++] = ' ';
        }
     }
-  result[pos++] = '\r';
-  result[pos++] = '\n';
   result[pos++] = '\0';
   assert (pos == result_size);
   return result;
 }
 \f
 /* Support for loading and saving cookies.  The format used for
   result[pos++] = '\0';
   assert (pos == result_size);
   return result;
 }
 \f
 /* Support for loading and saving cookies.  The format used for
-   loading and saving roughly matches the format of `cookies.txt' file
-   used by Netscape and Mozilla, at least the Unix versions.  The
+   loading and saving should be the format of the `cookies.txt' file
+   used by Netscape and Mozilla, at least the Unix versions.
+   (Apparently IE can export cookies in that format as well.)  The
    format goes like this:
 
        DOMAIN DOMAIN-FLAG PATH SECURE-FLAG TIMESTAMP ATTR-NAME ATTR-VALUE
    format goes like this:
 
        DOMAIN DOMAIN-FLAG PATH SECURE-FLAG TIMESTAMP ATTR-NAME ATTR-VALUE
@@ -1056,22 +1264,18 @@ build_cookies_request (const char *host, int port, const char *path,
      ATTR-NAME   -- name of the cookie attribute
      ATTR-VALUE  -- value of the cookie attribute (empty if absent)
 
      ATTR-NAME   -- name of the cookie attribute
      ATTR-VALUE  -- value of the cookie attribute (empty if absent)
 
-   The fields are separated by TABs (but Wget's loader recognizes any
-   whitespace).  All fields are mandatory, except for ATTR-VALUE.  The
-   `-FLAG' fields are boolean, their legal values being "TRUE" and
-   "FALSE'.  Empty lines, lines consisting of whitespace only, and
-   comment lines (beginning with # optionally preceded by whitespace)
-   are ignored.
+   The fields are separated by TABs.  All fields are mandatory, except
+   for ATTR-VALUE.  The `-FLAG' fields are boolean, their legal values
+   being "TRUE" and "FALSE'.  Empty lines, lines consisting of
+   whitespace only, and comment lines (beginning with # optionally
+   preceded by whitespace) are ignored.
 
    Example line from cookies.txt (split in two lines for readability):
 
        .google.com     TRUE    /       FALSE   2147368447      \
        PREF    ID=34bb47565bbcd47b:LD=en:NR=20:TM=985172580:LM=985739012
 
 
    Example line from cookies.txt (split in two lines for readability):
 
        .google.com     TRUE    /       FALSE   2147368447      \
        PREF    ID=34bb47565bbcd47b:LD=en:NR=20:TM=985172580:LM=985739012
 
-   DOMAIN-FLAG is currently not honored by Wget.  The cookies whose
-   domain begins with `.' are treated as if DOMAIN-FLAG were true,
-   while all other cookies are treated as if it were FALSE. */
-
+*/
 
 /* If the region [B, E) ends with :<digits>, parse the number, return
    it, and store new boundary (location of the `:') to DOMAIN_E_PTR.
 
 /* If the region [B, E) ends with :<digits>, parse the number, return
    it, and store new boundary (location of the `:') to DOMAIN_E_PTR.
@@ -1095,32 +1299,26 @@ domain_port (const char *domain_b, const char *domain_e,
   return port;
 }
 
   return port;
 }
 
-#define SKIP_WS(p) do {                                \
-  while (*p && ISSPACE (*p))                   \
-    ++p;                                       \
-} while (0)
-
-#define MARK_WORD(p, b, e) do {                        \
-  SKIP_WS (p);                                 \
+#define GET_WORD(p, b, e) do {                 \
   b = p;                                       \
   b = p;                                       \
-  /* skip non-ws */                            \
-  while (*p && !ISSPACE (*p))                  \
+  while (*p && *p != '\t')                     \
     ++p;                                       \
   e = p;                                       \
     ++p;                                       \
   e = p;                                       \
-  if (b == e)                                  \
+  if (b == e || !*p)                           \
     goto next;                                 \
     goto next;                                 \
+  ++p;                                         \
 } while (0)
 
 /* Load cookies from FILE.  */
 
 void
 } while (0)
 
 /* 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");
   if (!fp)
     {
 {
   char *line;
   FILE *fp = fopen (file, "r");
   if (!fp)
     {
-      logprintf (LOG_NOTQUIET, "Cannot open cookies file `%s': %s\n",
+      logprintf (LOG_NOTQUIET, _("Cannot open cookies file `%s': %s\n"),
                 file, strerror (errno));
       return;
     }
                 file, strerror (errno));
       return;
     }
@@ -1131,83 +1329,93 @@ load_cookies (const char *file)
       struct cookie *cookie;
       char *p = line;
 
       struct cookie *cookie;
       char *p = line;
 
+      double expiry;
       int port;
 
       char *domain_b  = NULL, *domain_e  = NULL;
       int port;
 
       char *domain_b  = NULL, *domain_e  = NULL;
-      char *ignore_b  = NULL, *ignore_e  = NULL;
+      char *domflag_b = NULL, *domflag_e = NULL;
       char *path_b    = NULL, *path_e    = NULL;
       char *secure_b  = NULL, *secure_e  = NULL;
       char *expires_b = NULL, *expires_e = NULL;
       char *name_b    = NULL, *name_e    = NULL;
       char *value_b   = NULL, *value_e   = NULL;
 
       char *path_b    = NULL, *path_e    = NULL;
       char *secure_b  = NULL, *secure_e  = NULL;
       char *expires_b = NULL, *expires_e = NULL;
       char *name_b    = NULL, *name_e    = NULL;
       char *value_b   = NULL, *value_e   = NULL;
 
-      SKIP_WS (p);
-
+      /* Skip leading white-space. */
+      while (*p && ISSPACE (*p))
+       ++p;
+      /* Ignore empty lines.  */
       if (!*p || *p == '#')
       if (!*p || *p == '#')
-       /* empty line */
        continue;
 
        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);
-
-      /* Don't use MARK_WORD for value because it may contain
-        whitespace itself.  Instead, . */
-      MARK_WORD (p, value_b,   value_e);
+      GET_WORD (p, domain_b,  domain_e);
+      GET_WORD (p, domflag_b, domflag_e);
+      GET_WORD (p, path_b,    path_e);
+      GET_WORD (p, secure_b,  secure_e);
+      GET_WORD (p, expires_b, expires_e);
+      GET_WORD (p, name_b,    name_e);
+
+      /* Don't use GET_WORD for value because it ends with newline,
+        not TAB.  */
+      value_b = p;
+      value_e = p + strlen (p);
+      if (value_e > value_b && value_e[-1] == '\n')
+       --value_e;
+      if (value_e > value_b && value_e[-1] == '\r')
+       --value_e;
+      /* Empty values are legal (I think), so don't bother checking. */
 
       cookie = cookie_new ();
 
       cookie->attr    = strdupdelim (name_b, name_e);
       cookie->value   = strdupdelim (value_b, value_e);
       cookie->path    = strdupdelim (path_b, path_e);
 
       cookie = cookie_new ();
 
       cookie->attr    = strdupdelim (name_b, name_e);
       cookie->value   = strdupdelim (value_b, value_e);
       cookie->path    = strdupdelim (path_b, path_e);
+      cookie->secure  = BOUNDED_EQUAL (secure_b, secure_e, "TRUE");
 
 
-      if (BOUNDED_EQUAL (secure_b, secure_e, "TRUE"))
-       cookie->secure = 1;
+      /* Curl source says, quoting Andre Garcia: "flag: A TRUE/FALSE
+        value indicating if all machines within a given domain can
+        access the variable.  This value is set automatically by the
+        browser, depending on the value set for the domain."  */
+      cookie->domain_exact = !BOUNDED_EQUAL (domflag_b, domflag_e, "TRUE");
 
       /* DOMAIN needs special treatment because we might need to
         extract the port.  */
       port = domain_port (domain_b, domain_e, (const char **)&domain_e);
       if (port)
        cookie->port = port;
 
       /* DOMAIN needs special treatment because we might need to
         extract the port.  */
       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;
 
 
+      if (*domain_b == '.')
+       ++domain_b;             /* remove leading dot internally */
       cookie->domain  = strdupdelim (domain_b, domain_e);
 
       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. */
       /* 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.)  */
+      /* I don't like changing the line, but it's safe here.  (line is
+        malloced.)  */
       *expires_e = '\0';
       *expires_e = '\0';
-      sscanf (expires_b, "%lu", &cookie->expiry_time);
-      if (cookie->expiry_time < cookies_now)
-       /* ignore stale cookie. */
-       goto abort;
-      cookie->permanent = 1;
+      sscanf (expires_b, "%lf", &expiry);
+
+      if (expiry == 0)
+       {
+         /* EXPIRY can be 0 for session cookies saved because the
+            user specified `--keep-session-cookies' in the past.
+            They remain session cookies, and will be saved only if
+            the user has specified `keep-session-cookies' again.  */
+       }
+      else
+       {
+         if (expiry < cookies_now)
+           goto abort_cookie;  /* ignore stale cookie. */
+         cookie->expiry_time = expiry;
+         cookie->permanent = 1;
+       }
 
 
-      store_cookie (cookie);
+      store_cookie (jar, cookie);
 
     next:
       continue;
 
 
     next:
       continue;
 
-    abort:
+    abort_cookie:
       delete_cookie (cookie);
     }
   fclose (fp);
       delete_cookie (cookie);
     }
   fclose (fp);
@@ -1222,18 +1430,23 @@ save_cookies_mapper (void *key, void *value, void *arg)
 {
   FILE *fp = (FILE *)arg;
   char *domain = (char *)key;
 {
   FILE *fp = (FILE *)arg;
   char *domain = (char *)key;
-  struct cookie *chain = (struct cookie *)value;
-  for (; chain; chain = chain->next)
+  struct cookie *cookie = (struct cookie *)value;
+  for (; cookie; cookie = cookie->next)
     {
     {
-      if (!chain->permanent)
+      if (!cookie->permanent && !opt.keep_session_cookies)
        continue;
        continue;
-      if (chain->expiry_time < cookies_now)
+      if (cookie_expired_p (cookie))
        continue;
        continue;
-      fprintf (fp, "%s\t%s\t%s\t%s\t%lu\t%s\t%s\n",
-              domain, *domain == '.' ? "TRUE" : "FALSE",
-              chain->path, chain->secure ? "TRUE" : "FALSE",
-              chain->expiry_time,
-              chain->attr, chain->value);
+      if (!cookie->domain_exact)
+       fputc ('.', fp);
+      fputs (domain, fp);
+      if (cookie->port != PORT_ANY)
+       fprintf (fp, ":%d", cookie->port);
+      fprintf (fp, "\t%s\t%s\t%s\t%.0f\t%s\t%s\n",
+              cookie->domain_exact ? "FALSE" : "TRUE",
+              cookie->path, cookie->secure ? "TRUE" : "FALSE",
+              (double)cookie->expiry_time,
+              cookie->attr, cookie->value);
       if (ferror (fp))
        return 1;               /* stop mapping */
     }
       if (ferror (fp))
        return 1;               /* stop mapping */
     }
@@ -1243,14 +1456,11 @@ save_cookies_mapper (void *key, void *value, void *arg)
 /* Save cookies, in format described above, to FILE. */
 
 void
 /* 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;
 
 {
   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);
 
 
   cookies_now = time (NULL);
 
@@ -1263,28 +1473,35 @@ save_cookies (const char *file)
     }
 
   fputs ("# HTTP cookie file.\n", fp);
     }
 
   fputs ("# HTTP cookie file.\n", fp);
-  fprintf (fp, "# Generated by Wget on %s.\n", datetime_str (NULL));
+  fprintf (fp, "# Generated by Wget on %s.\n", datetime_str (&cookies_now));
   fputs ("# Edit at your own risk.\n\n", fp);
 
   fputs ("# Edit at your own risk.\n\n", fp);
 
-  hash_table_map (cookies_hash_table, save_cookies_mapper, fp);
+  hash_table_map (jar->chains, save_cookies_mapper, fp);
 
   if (ferror (fp))
     logprintf (LOG_NOTQUIET, _("Error writing to `%s': %s\n"),
               file, strerror (errno));
 
   if (ferror (fp))
     logprintf (LOG_NOTQUIET, _("Error writing to `%s': %s\n"),
               file, strerror (errno));
-
   if (fclose (fp) < 0)
     logprintf (LOG_NOTQUIET, _("Error closing `%s': %s\n"),
               file, strerror (errno));
   if (fclose (fp) < 0)
     logprintf (LOG_NOTQUIET, _("Error closing `%s': %s\n"),
               file, strerror (errno));
+
+  DEBUGP (("Done saving cookies.\n"));
 }
 \f
 }
 \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
 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;
 {
   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. */
 
   /* Remove the chain from the table and free the key. */
-  hash_table_remove (cookies_hash_table, chain_key);
+  hash_table_remove (jar->chains, chain_key);
   xfree (chain_key);
 
   /* Then delete all the cookies in the chain. */
   xfree (chain_key);
 
   /* Then delete all the cookies in the chain. */
@@ -1302,11 +1519,97 @@ delete_cookie_chain_mapper (void *value, void *key, void *arg_ignored)
 /* Clean up cookie-related data. */
 
 void
 /* 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, nuke_cookie_chain, jar);
+  hash_table_destroy (jar->chains);
+  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 bool 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 true;
+}
+
+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, true);
+      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 */