]> sjero.net Git - wget/blobdiff - src/cookies.c
[svn] Avoid code repetition between time_str and datetime_str.
[wget] / src / cookies.c
index 07cfdc8ad14721b2353420424502cac6e041748c..e173c5bc381d6fb41193aa8b7b9d9c1059a5ef00 100644 (file)
@@ -1,5 +1,5 @@
 /* Support for cookies.
-   Copyright (C) 2001, 2002 Free Software Foundation, Inc.
+   Copyright (C) 2001-2006 Free Software Foundation, Inc.
 
 This file is part of GNU Wget.
 
@@ -14,8 +14,8 @@ 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 Wget; 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.,
+51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 
 In addition, as a special exception, the Free Software Foundation
 gives permission to link the code of its release of Wget with the
@@ -27,40 +27,35 @@ 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.
+/* Written by Hrvoje Niksic.  Parts are loosely inspired by the
+   cookie patch submitted by Tomasz Wegrzanowski.
 
-   Ideas for future work:
+   This implements the client-side cookie support, as specified
+   (loosely) by Netscape's "preliminary specification", currently
+   available at:
 
-   * Implement limits on cookie-related sizes, such as max. cookie
-     size, max. number of cookies, etc.
+       http://wp.netscape.com/newsref/std/cookie_spec.html
 
-   * Add more "cookie jar" methods, such as methods to iterate over
-     stored cookies, to clear temporary cookies, to perform
-     intelligent auto-saving, etc.
-
-   * Support `Set-Cookie2' and `Cookie2' headers?  Does anyone really
-     use them?  */
+   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>
-#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 <time.h>
 
 #include "wget.h"
 #include "utils.h"
 #include "hash.h"
 #include "cookies.h"
-
-/* This should *really* be in a .h file!  */
-time_t http_atotm PARAMS ((const char *));
+#include "http.h"              /* for http_atotm */
 \f
 /* Declarations of `struct cookie' and the most basic functions. */
 
@@ -85,7 +80,7 @@ struct cookie_jar {
 
 /* 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;
+static time_t cookies_now;
 
 struct cookie_jar *
 cookie_jar_new (void)
@@ -101,19 +96,20 @@ struct 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. */
-  int domain_exact;            /* whether DOMAIN must match as a
+  unsigned domain_exact :1;    /* whether DOMAIN must match as a
                                   whole. */
 
-  int permanent;               /* whether the cookie should outlive
-                                  the session */
-  time_t expiry_time;          /* time when the cookie expires */
-
-  int discard_requested;       /* whether cookie was created to
-                                  request discarding another
-                                  cookie */
+  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 */
@@ -123,7 +119,6 @@ struct cookie {
 };
 
 #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. */
 
@@ -132,23 +127,32 @@ cookie_new (void)
 {
   struct cookie *cookie = xnew0 (struct cookie);
 
-  /* 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.  */
+  /* 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;
 }
 
+/* Non-zero if the cookie has expired.  Assumes cookies_now has been
+   set by one of the entry point functions.  */
+
+static bool
+cookie_expired_p (const struct cookie *c)
+{
+  return c->expiry_time != 0 && c->expiry_time < cookies_now;
+}
+
 /* 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_null (cookie->domain);
+  xfree_null (cookie->path);
+  xfree_null (cookie->attr);
+  xfree_null (cookie->value);
   xfree (cookie);
 }
 \f
@@ -252,15 +256,18 @@ store_cookie (struct cookie_jar *jar, struct cookie *cookie)
   hash_table_put (jar->chains, 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));
+  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, port, path, and
@@ -312,139 +319,10 @@ discard_matching_cookie (struct cookie_jar *jar, struct cookie *cookie)
 /* 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 VALUE_EXISTS (value_b && value_e)
-
-#define VALUE_NON_EMPTY (VALUE_EXISTS && (value_b != value_e))
-
-/* Update the appropriate cookie field.  [name_b, name_e) are expected
-   to delimit the attribute name, while [value_b, value_e) (optional)
-   should delimit the attribute value.
-
-   When called the first time, it will set the cookie's attribute name
-   and value.  After that, it will check the attribute name for
-   special fields such as `domain', `path', etc.  Where appropriate,
-   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
-   found; such a cookie should be discarded.  */
-
-static int
-update_cookie_field (struct cookie *cookie,
-                    const char *name_b, const char *name_e,
-                    const char *value_b, const char *value_e)
-{
-  assert (name_b != NULL && name_e != NULL);
-
-  if (!cookie->attr)
-    {
-      if (!VALUE_EXISTS)
-       return 0;
-      cookie->attr = strdupdelim (name_b, name_e);
-      cookie->value = strdupdelim (value_b, value_e);
-      return 1;
-    }
-
-  if (NAME_IS ("domain"))
-    {
-      if (!VALUE_NON_EMPTY)
-       return 0;
-      FREE_MAYBE (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);
-      return 1;
-    }
-  else if (NAME_IS ("path"))
-    {
-      if (!VALUE_NON_EMPTY)
-       return 0;
-      FREE_MAYBE (cookie->path);
-      cookie->path = strdupdelim (value_b, value_e);
-      return 1;
-    }
-  else if (NAME_IS ("expires"))
-    {
-      char *value_copy;
-      time_t expires;
-
-      if (!VALUE_NON_EMPTY)
-       return 0;
-      BOUNDED_TO_ALLOCA (value_b, value_e, value_copy);
-
-      expires = http_atotm (value_copy);
-      if (expires != -1)
-       {
-         cookie->permanent = 1;
-         cookie->expiry_time = (time_t)expires;
-       }
-      else
-       /* Error in expiration spec.  Assume default (cookie valid for
-          this session.)  */
-       ;
+#define TOKEN_IS(token, string_literal)                                \
+  BOUNDED_EQUAL_NO_CASE (token.b, token.e, string_literal)
 
-      /* According to netscape's specification, expiry time in the
-        past means that discarding of a matching cookie is
-        requested.  */
-      if (cookie->expiry_time < cookies_now)
-       cookie->discard_requested = 1;
-
-      return 1;
-    }
-  else if (NAME_IS ("max-age"))
-    {
-      double maxage = -1;
-      char *value_copy;
-
-      if (!VALUE_NON_EMPTY)
-       return 0;
-      BOUNDED_TO_ALLOCA (value_b, value_e, value_copy);
-
-      sscanf (value_copy, "%lf", &maxage);
-      if (maxage == -1)
-       /* something went wrong. */
-       return 0;
-      cookie->permanent = 1;
-      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;
-
-      return 1;
-    }
-  else if (NAME_IS ("secure"))
-    {
-      /* ignore value completely */
-      cookie->secure = 1;
-      return 1;
-    }
-  else
-    /* Unrecognized attribute; ignore it. */
-    return 1;
-}
-
-#undef NAME_IS
-
-/* Returns non-zero 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.  */
-
-#define ATTR_NAME_CHAR(c) ((c) > 32 && (c) < 127       \
-                          && (c) != '"' && (c) != '='  \
-                          && (c) != ';' && (c) != ',')
+#define TOKEN_NON_EMPTY(token) (token.b != NULL && token.b != token.e)
 
 /* Parse the contents of the `Set-Cookie' header.  The header looks
    like this:
@@ -454,181 +332,124 @@ update_cookie_field (struct cookie *cookie,
    Trailing semicolon is optional; spaces are allowed between all
    tokens.  Additionally, values may be quoted.
 
-   A new cookie is returned upon success, NULL otherwise.  The
-   specified CALLBACK function (normally `update_cookie_field' is used
-   to update the fields of the newly created cookie structure.  */
+   A new cookie is returned upon success, NULL otherwise.
+
+   The first name-value pair will be used to set the cookie's
+   attribute name and value.  Subsequent parameters will be checked
+   against field names such as `domain', `path', etc.  Recognized
+   fields will be parsed and the corresponding members of COOKIE
+   filled.  */
 
 static struct cookie *
-parse_set_cookies (const char *sc,
-                  int (*callback) (struct cookie *,
-                                   const char *, const char *,
-                                   const char *, const char *),
-                  int silent)
+parse_set_cookie (const char *set_cookie, bool silent)
 {
+  const char *ptr = set_cookie;
   struct cookie *cookie = cookie_new ();
+  param_token name, value;
 
-  /* #### Hand-written DFAs are no fun to debug.  We'de be better off
-     to rewrite this as an inline parser.  */
+  if (!extract_param (&ptr, &name, &value, ';'))
+    goto error;
+  if (!value.b)
+    goto error;
+  cookie->attr = strdupdelim (name.b, name.e);
+  cookie->value = strdupdelim (value.b, value.e);
 
-  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 *name_b  = NULL, *name_e  = NULL;
-  const char *value_b = NULL, *value_e = NULL;
-
-  c = *p;
-
-  while (state != S_DONE && state != S_ERROR)
+  while (extract_param (&ptr, &name, &value, ';'))
     {
-      switch (state)
+      if (TOKEN_IS (name, "domain"))
        {
-       case S_START:
-         if (!c)
-           state = S_DONE;
-         else if (ISSPACE (c))
-           /* Strip all whitespace preceding the name. */
-           c = *++p;
-         else if (ATTR_NAME_CHAR (c))
-           {
-             name_b = p;
-             state = S_NAME;
-           }
-         else
-           /* empty attr name not allowed */
-           state = S_ERROR;
-         break;
-       case S_NAME:
-         if (!c || c == ';' || c == '=' || ISSPACE (c))
-           {
-             name_e = p;
-             state = S_NAME_POST;
-           }
-         else if (ATTR_NAME_CHAR (c))
-           c = *++p;
-         else
-           state = S_ERROR;
-         break;
-       case S_NAME_POST:
-         if (!c || c == ';')
-           {
-             value_b = value_e = NULL;
-             if (c == ';')
-               c = *++p;
-             state = S_ATTR_ACTION;
-           }
-         else if (c == '=')
-           {
-             c = *++p;
-             state = S_VALUE_PRE;
-           }
-         else if (ISSPACE (c))
-           /* Ignore space and keep the state. */
-           c = *++p;
-         else
-           state = S_ERROR;
-         break;
-       case S_VALUE_PRE:
-         if (!c || c == ';')
-           {
-             value_b = value_e = p;
-             if (c == ';')
-               c = *++p;
-             state = S_ATTR_ACTION;
-           }
-         else if (c == '"')
-           {
-             c = *++p;
-             value_b = p;
-             state = S_QUOTED_VALUE;
-           }
-         else if (ISSPACE (c))
-           c = *++p;
-         else
-           {
-             value_b = p;
-             value_e = NULL;
-             state = S_VALUE;
-           }
-         break;
-       case S_VALUE:
-         if (!c || c == ';' || ISSPACE (c))
-           {
-             value_e = p;
-             state = S_VALUE_TRAILSPACE;
-           }
-         else
-           {
-             value_e = NULL;   /* no trailing space */
-             c = *++p;
-           }
-         break;
-       case S_QUOTED_VALUE:
-         if (c == '"')
-           {
-             value_e = p;
-             c = *++p;
-             state = S_VALUE_TRAILSPACE;
-           }
-         else if (!c)
-           state = S_ERROR;
-         else
-           c = *++p;
-         break;
-       case S_VALUE_TRAILSPACE:
-         if (c == ';')
+         if (!TOKEN_NON_EMPTY (value))
+           goto error;
+         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 in browsers.  */
+         if (*value.b == '.')
+           ++value.b;
+         cookie->domain = strdupdelim (value.b, value.e);
+       }
+      else if (TOKEN_IS (name, "path"))
+       {
+         if (!TOKEN_NON_EMPTY (value))
+           goto error;
+         xfree_null (cookie->path);
+         cookie->path = strdupdelim (value.b, value.e);
+       }
+      else if (TOKEN_IS (name, "expires"))
+       {
+         char *value_copy;
+         time_t expires;
+
+         if (!TOKEN_NON_EMPTY (value))
+           goto error;
+         BOUNDED_TO_ALLOCA (value.b, value.e, value_copy);
+
+         expires = http_atotm (value_copy);
+         if (expires != (time_t) -1)
            {
-             c = *++p;
-             state = S_ATTR_ACTION;
+             cookie->permanent = 1;
+             cookie->expiry_time = expires;
            }
-         else if (!c)
-           state = S_ATTR_ACTION;
-         else if (ISSPACE (c))
-           c = *++p;
          else
-           state = S_VALUE;
-         break;
-       case S_ATTR_ACTION:
-         {
-           int legal = callback (cookie, name_b, name_e, value_b, value_e);
-           if (!legal)
-             {
-               if (!silent)
-                 {
-                   char *name;
-                   BOUNDED_TO_ALLOCA (name_b, name_e, name);
-                   logprintf (LOG_NOTQUIET,
-                              _("Error in Set-Cookie, field `%s'"), name);
-                 }
-               state = S_ERROR;
-               break;
-             }
-           state = S_START;
-         }
-         break;
-       case S_DONE:
-       case S_ERROR:
-         /* handled by loop condition */
-         break;
+           /* 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
+            past means that discarding of a matching cookie is
+            requested.  */
+         if (cookie->expiry_time < cookies_now)
+           cookie->discard_requested = 1;
+       }
+      else if (TOKEN_IS (name, "max-age"))
+       {
+         double maxage = -1;
+         char *value_copy;
+
+         if (!TOKEN_NON_EMPTY (value))
+           goto error;
+         BOUNDED_TO_ALLOCA (value.b, value.e, value_copy);
+
+         sscanf (value_copy, "%lf", &maxage);
+         if (maxage == -1)
+           /* something went wrong. */
+           goto error;
+         cookie->permanent = 1;
+         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;
+       }
+      else if (TOKEN_IS (name, "secure"))
+       {
+         /* ignore value completely */
+         cookie->secure = 1;
        }
+      else
+       /* Ignore unrecognized attribute. */
+       ;
     }
-  if (state == S_DONE)
-    return cookie;
+  if (*ptr)
+    /* extract_param has encountered a syntax error */
+    goto error;
 
-  delete_cookie (cookie);
-  if (state != S_ERROR)
-    abort ();
+  /* The cookie has been successfully constructed; return it. */
+  return cookie;
 
+ error:
   if (!silent)
     logprintf (LOG_NOTQUIET,
               _("Syntax error in Set-Cookie: %s at position %d.\n"),
-              sc, p - sc);
+              escnonprint (set_cookie), (int) (ptr - set_cookie));
+  delete_cookie (cookie);
   return NULL;
 }
+
+#undef TOKEN_IS
+#undef TOKEN_NON_EMPTY
 \f
 /* Sanity checks.  These are important, otherwise it is possible for
    mailcious attackers to destroy important cookie information and/or
@@ -637,23 +458,23 @@ parse_set_cookies (const char *sc,
 
 #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++ != '.')                             \
-    return 0;                                  \
+    return false;                              \
 } 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;
@@ -667,8 +488,8 @@ numeric_address_p (const char *addr)
   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.
@@ -676,7 +497,7 @@ numeric_address_p (const char *addr)
    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)
 {
   DEBUGP (("cdm: 1"));
@@ -690,13 +511,13 @@ check_domain_match (const char *cookie_domain, const char *host)
 
   /* For the sake of efficiency, check for exact match first. */
   if (0 == strcasecmp (cookie_domain, host))
-    return 1;
+    return true;
 
   DEBUGP ((" 3"));
 
   /* HOST must match the tail of cookie_domain. */
-  if (!match_tail (host, cookie_domain, 1))
-    return 0;
+  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
@@ -742,7 +563,7 @@ check_domain_match (const char *cookie_domain, const char *host)
        case '.':
          if (ldcl == 0)
            /* Empty domain component found -- the domain is invalid. */
-           return 0;
+           return false;
          if (*(p + 1) == '\0')
            {
              /* Tolerate trailing '.' by not treating the domain as
@@ -761,25 +582,25 @@ check_domain_match (const char *cookie_domain, const char *host)
     DEBUGP ((" 5"));
 
     if (dccount < 2)
-      return 0;
+      return false;
 
     DEBUGP ((" 6"));
 
     if (dccount == 2)
       {
        int i;
-       int known_toplevel = 0;
-       static char *known_toplevel_domains[] = {
+       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], 1))
+         if (match_tail (cookie_domain, known_toplevel_domains[i], true))
            {
-             known_toplevel = 1;
+             known_toplevel = true;
              break;
            }
        if (!known_toplevel && nldcl <= 3)
-         return 0;
+         return false;
       }
   }
 
@@ -795,37 +616,53 @@ check_domain_match (const char *cookie_domain, const char *host)
       /* desired domain:             bar.com */
       /* '.' must be here in host-> ^        */
       if (hlen > dlen && host[hlen - dlen - 1] != '.')
-       return 0;
+       return false;
     }
 
   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. */
 
-static int
+static bool
 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
 /* 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.  */
 
 void
-cookie_jar_process_set_cookie (struct cookie_jar *jar,
-                              const char *host, int port,
-                              const char *path, const char *set_cookie)
+cookie_handle_set_cookie (struct cookie_jar *jar,
+                         const char *host, int port,
+                         const char *path, const char *set_cookie)
 {
   struct cookie *cookie;
   cookies_now = time (NULL);
 
-  cookie = parse_set_cookies (set_cookie, update_cookie_field, 0);
+  /* 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_cookie (set_cookie, false);
   if (!cookie)
     goto out;
 
@@ -834,25 +671,40 @@ cookie_jar_process_set_cookie (struct cookie_jar *jar,
   if (!cookie->domain)
     {
     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->port = port;
+      cookie->domain_exact = 1;
+      /* Set the port, but only if it's non-default. */
+      if (port != 80 && port != 443)
+       cookie->port = port;
     }
   else
     {
       if (!check_domain_match (cookie->domain, host))
        {
          logprintf (LOG_NOTQUIET,
-                    "Cookie coming from %s attempted to set domain to %s\n",
-                    host, cookie->domain);
+                    _("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)
-    cookie->path = xstrdup (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
     {
+      /* 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",
@@ -861,6 +713,9 @@ cookie_jar_process_set_cookie (struct cookie_jar *jar,
        }
     }
 
+  /* Now store the cookie, or discard an existing cookie, if
+     discarding was requested.  */
+
   if (cookie->discard_requested)
     {
       discard_matching_cookie (jar, cookie);
@@ -878,6 +733,19 @@ cookie_jar_process_set_cookie (struct cookie_jar *jar,
 /* Support for sending out cookies in HTTP requests, based on
    previously stored cookies.  Entry point is
    `build_cookies_request'.  */
+   
+/* Return a count of 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;
+}
 
 /* Find the cookie chains whose domains match HOST and store them to
    DEST.
@@ -936,16 +804,7 @@ find_chains_of_host (struct cookie_jar *jar, const char *host,
 static int
 path_matches (const char *full_path, const char *prefix)
 {
-  int len;
-
-  if (*prefix != '/')
-    /* Wget's HTTP paths do not begin with '/' (the URL code treats it
-       as a mere separator, inspired by rfc1808), but the '/' is
-       assumed when matching against the cookie stuff.  */
-    return 0;
-
-  ++prefix;
-  len = strlen (prefix);
+  int len = strlen (prefix);
 
   if (0 != strncmp (full_path, prefix, len))
     /* FULL_PATH doesn't begin with PREFIX. */
@@ -955,51 +814,51 @@ path_matches (const char *full_path, const char *prefix)
   return len + 1;
 }
 
-/* Return non-zero iff COOKIE matches the provided parameters of the
-   URL being downloaded: HOST, PORT, PATH, and SECFLAG.
+/* 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 int
+static bool
 cookie_matches_url (const struct cookie *cookie,
                    const char *host, int port, const char *path,
-                   int secflag, int *path_goodness)
+                   bool secflag, int *path_goodness)
 {
   int pg;
 
-  if (COOKIE_EXPIRED_P (cookie))
+  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;
+    return false;
 
   if (cookie->secure && !secflag)
     /* Don't transmit secure cookies over insecure connections.  */
-    return 0;
+    return false;
   if (cookie->port != PORT_ANY && cookie->port != port)
-    return 0;
+    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 0;
+    return false;
 
   pg = path_matches (path, cookie->path);
-  if (!pg)
-    return 0;
+  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 1;
+  return true;
 }
 
 /* A structure that points to a cookie, along with the additional
@@ -1097,9 +956,8 @@ goodness_comparator (const void *p1, const void *p2)
    generated, NULL is returned.  */
 
 char *
-cookie_jar_generate_cookie_header (struct cookie_jar *jar, 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 **chains;
   int chain_count;
@@ -1109,6 +967,7 @@ cookie_jar_generate_cookie_header (struct cookie_jar *jar, const char *host,
   int count, i, ocnt;
   char *result;
   int result_size, pos;
+  PREPEND_SLASH (path);                /* see cookie_handle_set_cookie */
 
   /* First, find the cookie chains whose domains match HOST. */
 
@@ -1132,8 +991,7 @@ cookie_jar_generate_cookie_header (struct cookie_jar *jar, const char *host,
   count = 0;
   for (i = 0; i < chain_count; i++)
     for (cookie = chains[i]; cookie; cookie = cookie->next)
-      if (cookie_matches_url (cookie, host, port, path, connection_secure_p,
-                             NULL))
+      if (cookie_matches_url (cookie, host, port, path, secflag, NULL))
        ++count;
   if (!count)
     return NULL;               /* no cookies matched */
@@ -1148,8 +1006,7 @@ cookie_jar_generate_cookie_header (struct cookie_jar *jar, const char *host,
     for (cookie = chains[i]; cookie; cookie = cookie->next)
       {
        int pg;
-       if (!cookie_matches_url (cookie, host, port, path,
-                                connection_secure_p, &pg))
+       if (!cookie_matches_url (cookie, host, port, path, secflag, &pg))
          continue;
        outgoing[ocnt].cookie = cookie;
        outgoing[ocnt].domain_goodness = strlen (cookie->domain);
@@ -1176,16 +1033,12 @@ cookie_jar_generate_cookie_header (struct cookie_jar *jar, const char *host,
     }
 
   /* Allocate output buffer:
-     "Cookie: "       -- 8
      name=value pairs -- result_size
      "; " separators  -- (count - 1) * 2
-     \r\n line ending -- 2
      \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;
-  strcpy (result, "Cookie: ");
-  pos += 8;
   for (i = 0; i < count; i++)
     {
       struct cookie *c = outgoing[i].cookie;
@@ -1203,8 +1056,6 @@ cookie_jar_generate_cookie_header (struct cookie_jar *jar, const char *host,
          result[pos++] = ' ';
        }
     }
-  result[pos++] = '\r';
-  result[pos++] = '\n';
   result[pos++] = '\0';
   assert (pos == result_size);
   return result;
@@ -1280,7 +1131,7 @@ cookie_jar_load (struct cookie_jar *jar, const char *file)
   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;
     }
@@ -1356,64 +1207,40 @@ cookie_jar_load (struct cookie_jar *jar, const char *file)
         malloced.)  */
       *expires_e = '\0';
       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;
+      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 (jar, cookie);
 
     next:
       continue;
 
-    abort:
+    abort_cookie:
       delete_cookie (cookie);
     }
   fclose (fp);
 }
 
-/* Mapper for save_cookies callable by hash_table_map.  VALUE points
-   to the head in a chain of cookies.  The function prints the entire
-   chain.  */
-
-static int
-save_cookies_mapper (void *key, void *value, void *arg)
-{
-  FILE *fp = (FILE *)arg;
-  char *domain = (char *)key;
-  struct cookie *cookie = (struct cookie *)value;
-  for (; cookie; cookie = cookie->next)
-    {
-      if (!cookie->permanent)
-       continue;
-      if (COOKIE_EXPIRED_P (cookie))
-       continue;
-      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 */
-    }
-  return 0;
-}
-
 /* Save cookies, in format described above, to FILE. */
 
 void
 cookie_jar_save (struct cookie_jar *jar, const char *file)
 {
   FILE *fp;
+  hash_table_iterator iter;
 
   DEBUGP (("Saving cookies to %s.\n", file));
 
@@ -1428,15 +1255,39 @@ cookie_jar_save (struct cookie_jar *jar, const char *file)
     }
 
   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);
 
-  hash_table_map (jar->chains, save_cookies_mapper, fp);
-
+  for (hash_table_iterate (jar->chains, &iter);
+       hash_table_iter_next (&iter);
+       )
+    {
+      const char *domain = iter.key;
+      struct cookie *cookie = iter.value;
+      for (; cookie; cookie = cookie->next)
+       {
+         if (!cookie->permanent && !opt.keep_session_cookies)
+           continue;
+         if (cookie_expired_p (cookie))
+           continue;
+         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))
+           goto out;
+       }
+    }
+ out:
   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));
@@ -1444,40 +1295,25 @@ cookie_jar_save (struct cookie_jar *jar, const char *file)
   DEBUGP (("Done saving cookies.\n"));
 }
 \f
-/* Destroy all the elements in the chain and unhook it from the cookie
-   jar.  This is written in the form of a callback to hash_table_map
-   and used by cookie_jar_delete to delete all the cookies in a
-   jar.  */
-
-static int
-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 (jar->chains, chain_key);
-  xfree (chain_key);
-
-  /* Then delete all the cookies in the chain. */
-  while (chain)
-    {
-      struct cookie *next = chain->next;
-      delete_cookie (chain);
-      chain = next;
-    }
-
-  /* Keep mapping. */
-  return 0;
-}
-
 /* Clean up cookie-related data. */
 
 void
 cookie_jar_delete (struct cookie_jar *jar)
 {
-  hash_table_map (jar->chains, nuke_cookie_chain, jar);
+  /* Iterate over chains (indexed by domain) and free them. */
+  hash_table_iterator iter;
+  for (hash_table_iterate (jar->chains, &iter); hash_table_iter_next (&iter); )
+    {
+      struct cookie *chain = iter.value;
+      xfree (iter.key);
+      /* Then all cookies in this chain. */
+      while (chain)
+       {
+         struct cookie *next = chain->next;
+         delete_cookie (chain);
+         chain = next;
+       }
+    }
   hash_table_destroy (jar->chains);
   xfree (jar);
 }
@@ -1487,27 +1323,14 @@ cookie_jar_delete (struct cookie_jar *jar)
    from main.  */
 
 #ifdef TEST_COOKIES
-int test_count;
-char *test_results[10];
-
-static int test_parse_cookies_callback (struct cookie *ignored,
-                                       const char *nb, const char *ne,
-                                       const char *vb, const char *ve)
-{
-  test_results[test_count++] = strdupdelim (nb, ne);
-  test_results[test_count++] = strdupdelim (vb, ve);
-  return 1;
-}
-
 void
 test_cookies (void)
 {
   /* Tests expected to succeed: */
   static struct {
-    char *data;
-    char *results[10];
+    const char *data;
+    const 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} },
@@ -1531,39 +1354,51 @@ test_cookies (void)
   for (i = 0; i < countof (tests_succ); i++)
     {
       int ind;
-      char *data = tests_succ[i].data;
-      char **expected = tests_succ[i].results;
+      const char *data = tests_succ[i].data;
+      const char **expected = tests_succ[i].results;
       struct cookie *c;
 
-      test_count = 0;
-      c = parse_set_cookies (data, test_parse_cookies_callback, 1);
+      c = parse_set_cookie (data, 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);
+      /* Test whether extract_param handles these cases correctly. */
+      {
+       param_token name, value;
+       const char *ptr = data;
+       int j = 0;
+       while (extract_param (&ptr, &name, &value, ';'))
+         {
+           char *n = strdupdelim (name.b, name.e);
+           char *v = strdupdelim (value.b, value.e);
+           if (!expected[j])
+             {
+               printf ("Too many parameters for '%s'\n", data);
+               break;
+             }
+           if (0 != strcmp (expected[j], n))
+             printf ("Invalid name %d for '%s' (expected '%s', got '%s')\n",
+                     j / 2 + 1, data, expected[j], n);
+           if (0 != strcmp (expected[j + 1], v))
+             printf ("Invalid value %d for '%s' (expected '%s', got '%s')\n",
+                     j / 2 + 1, data, expected[j + 1], v);
+           j += 2;
+           free (n);
+           free (v);
+         }
+       if (expected[j])
+         printf ("Too few parameters for '%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);
+      c = parse_set_cookie (data, true);
       if (c)
        printf ("Failed to report error on invalid data: %s\n", data);
     }