diff options
Diffstat (limited to 'dpi/cookies.c')
-rw-r--r-- | dpi/cookies.c | 1613 |
1 files changed, 859 insertions, 754 deletions
diff --git a/dpi/cookies.c b/dpi/cookies.c index b7641372..a5142224 100644 --- a/dpi/cookies.c +++ b/dpi/cookies.c @@ -13,19 +13,13 @@ * */ -/* Handling of cookies takes place here. - * This implementation aims to follow RFC 2965: - * http://www.ietf.org/rfc/rfc2965.txt - */ - -/* - * TODO: Cleanup this code. Shorten some functions, order things, - * add comments, remove leaks, etc. - */ - -/* TODO: this server is not assembling the received packets. - * This means it currently expects dillo to send full dpi tags - * within the socket; if that fails, everything stops. +/* This is written to follow the HTTP State Working Group's + * draft-ietf-httpstate-cookie-01.txt. + * + * Info on cookies in the wild: + * http://www.ietf.org/mail-archive/web/http-state/current/msg00078.html + * And dates specifically: + * http://www.ietf.org/mail-archive/web/http-state/current/msg00128.html */ #ifdef DISABLE_COOKIES @@ -42,6 +36,7 @@ int main(void) #include <sys/socket.h> #include <sys/stat.h> #include <sys/un.h> +#include <netinet/in.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> @@ -63,13 +58,6 @@ int main(void) #define _MSG(...) #define MSG(...) printf("[cookies dpi]: " __VA_ARGS__) - -/* This one is tricky, some sources state it should include the byte - * for the terminating NULL, and others say it shouldn't. */ -# define D_SUN_LEN(ptr) ((size_t) (((struct sockaddr_un *) 0)->sun_path) \ - + strlen ((ptr)->sun_path)) - - /* * a_List_add() * @@ -90,6 +78,9 @@ int main(void) /* The maximum length of a line in the cookie file */ #define LINE_MAXLEN 4096 +#define MAX_DOMAIN_COOKIES 20 +#define MAX_TOTAL_COOKIES 1200 + typedef enum { COOKIE_ACCEPT, COOKIE_ACCEPT_SESSION, @@ -103,8 +94,8 @@ typedef struct { typedef struct { char *domain; - Dlist *dlist; -} CookieNode; + Dlist *cookies; +} DomainNode; typedef struct { char *name; @@ -112,20 +103,25 @@ typedef struct { char *domain; char *path; time_t expires_at; - uint_t version; - char *comment; - char *comment_url; + bool_t host_only; bool_t secure; bool_t session_only; - Dlist *ports; + long last_used; } CookieData_t; +typedef struct { + Dsh *sh; + int status; +} ClientInfo; + /* * Local data */ -/* List of CookieNode. Each node holds a domain and its list of cookies */ -static Dlist *cookies; +static Dlist *all_cookies; + +/* List of DomainNode. Each node holds a domain and its list of cookies */ +static Dlist *domains; /* Variables for access control */ static CookieControl *ccontrol = NULL; @@ -133,13 +129,19 @@ static int num_ccontrol = 0; static int num_ccontrol_max = 1; static CookieControlAction default_action = COOKIE_DENY; +static long cookies_use_counter = 0; static bool_t disabled; static FILE *file_stream; -static char *cookies_txt_header_str = +static const char *const cookies_txt_header_str = "# HTTP Cookie File\n" -"# http://wp.netscape.com/newsref/std/cookie_spec.html\n" -"# This is a generated file! Do not edit.\n\n"; +"# This is a generated file! Do not edit.\n" +"# [domain subdomains path secure expiry_time name value]\n\n"; +/* The epoch is Jan 1, 1970. When there is difficulty in representing future + * dates, use the (by far) most likely last representable time in Jan 19, 2038. + */ +static struct tm cookies_epoch_tm = {0, 0, 0, 1, 0, 70, 0, 0, 0, 0, 0}; +static time_t cookies_epoch_time, cookies_future_time; /* * Forward declarations @@ -147,33 +149,39 @@ static char *cookies_txt_header_str = static CookieControlAction Cookies_control_check_domain(const char *domain); static int Cookie_control_init(void); -static void Cookies_parse_ports(int url_port, CookieData_t *cookie, - const char *port_str); -static char *Cookies_build_ports_str(CookieData_t *cookie); -static char *Cookies_strip_path(const char *path); static void Cookies_add_cookie(CookieData_t *cookie); -static void Cookies_remove_cookie(CookieData_t *cookie); static int Cookies_cmp(const void *a, const void *b); /* - * Compare function for searching a cookie node + * Compare function for searching a domain node */ -static int Cookie_node_cmp(const void *v1, const void *v2) +static int Domain_node_cmp(const void *v1, const void *v2) { - const CookieNode *n1 = v1, *n2 = v2; + const DomainNode *n1 = v1, *n2 = v2; - return strcmp(n1->domain, n2->domain); + return dStrcasecmp(n1->domain, n2->domain); } /* - * Compare function for searching a cookie node by domain + * Compare function for searching a domain node by domain */ -static int Cookie_node_by_domain_cmp(const void *v1, const void *v2) +static int Domain_node_by_domain_cmp(const void *v1, const void *v2) { - const CookieNode *node = v1; + const DomainNode *node = v1; const char *domain = v2; - return strcmp(node->domain, domain); + return dStrcasecmp(node->domain, domain); +} + +/* + * Delete node. This will not free any cookies that might be in node->cookies. + */ +static void Cookies_delete_node(DomainNode *node) +{ + dList_remove(domains, node); + dFree(node->domain); + dList_free(node->cookies); + dFree(node); } /* @@ -181,17 +189,22 @@ static int Cookie_node_by_domain_cmp(const void *v1, const void *v2) * with the optional 'init_str' as its content. */ static FILE *Cookies_fopen(const char *filename, const char *mode, - char *init_str) + const char *init_str) { FILE *F_in; - int fd; + int fd, rc; if ((F_in = fopen(filename, mode)) == NULL) { /* Create the file */ fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); if (fd != -1) { - if (init_str) - write(fd, init_str, strlen(init_str)); + if (init_str) { + rc = write(fd, init_str, strlen(init_str)); + if (rc == -1) { + MSG("Cookies: Could not write initial string to file %s: %s\n", + filename, dStrerror(errno)); + } + } close(fd); MSG("Created file: %s\n", filename); @@ -215,71 +228,37 @@ static void Cookies_free_cookie(CookieData_t *cookie) dFree(cookie->value); dFree(cookie->domain); dFree(cookie->path); - dFree(cookie->comment); - dFree(cookie->comment_url); - dList_free(cookie->ports); dFree(cookie); } +static void Cookies_tm_init(struct tm *tm) +{ + tm->tm_sec = cookies_epoch_tm.tm_sec; + tm->tm_min = cookies_epoch_tm.tm_min; + tm->tm_hour = cookies_epoch_tm.tm_hour; + tm->tm_mday = cookies_epoch_tm.tm_mday; + tm->tm_mon = cookies_epoch_tm.tm_mon; + tm->tm_year = cookies_epoch_tm.tm_year; + tm->tm_isdst = cookies_epoch_tm.tm_isdst; +} + /* - * Initialize the cookies module - * (The 'disabled' variable is writable only within Cookies_init) + * Read in cookies from 'stream' (cookies.txt) */ -static void Cookies_init() +static void Cookies_load_cookies(FILE *stream) { - CookieData_t *cookie; - char *filename; char line[LINE_MAXLEN]; -#ifndef HAVE_LOCKF - struct flock lck; -#endif - FILE *old_cookies_file_stream; - - /* Default setting */ - disabled = TRUE; - - /* Read and parse the cookie control file (cookiesrc) */ - if (Cookie_control_init() != 0) { - MSG("Disabling cookies.\n"); - return; - } - - /* Get a stream for the cookies file */ - filename = dStrconcat(dGethomedir(), "/.dillo/cookies.txt", NULL); - file_stream = Cookies_fopen(filename, "r+", cookies_txt_header_str); - - dFree(filename); - - if (!file_stream) { - MSG("ERROR: Can't open ~/.dillo/cookies.txt, disabling cookies\n"); - return; - } - - /* Try to get a lock from the file descriptor */ -#ifdef HAVE_LOCKF - disabled = (lockf(fileno(file_stream), F_TLOCK, 0) == -1); -#else /* POSIX lock */ - lck.l_start = 0; /* start at beginning of file */ - lck.l_len = 0; /* lock entire file */ - lck.l_type = F_WRLCK; - lck.l_whence = SEEK_SET; /* absolute offset */ - disabled = (fcntl(fileno(file_stream), F_SETLK, &lck) == -1); -#endif - if (disabled) { - MSG("The cookies file has a file lock: disabling cookies!\n"); - fclose(file_stream); - return; - } - - MSG("Enabling cookies as from cookiesrc...\n"); - - cookies = dList_new(32); + all_cookies = dList_new(32); + domains = dList_new(32); /* Get all lines in the file */ - while (!feof(file_stream)) { + while (!feof(stream)) { line[0] = '\0'; - fgets(line, LINE_MAXLEN, file_stream); + if ((fgets(line, LINE_MAXLEN, stream) == NULL) && ferror(stream)) { + MSG("Error while reading from cookies.txt: %s\n", dStrerror(errno)); + break; /* bail out */ + } /* Remove leading and trailing whitespaces */ dStrstrip(line); @@ -290,7 +269,7 @@ static void Cookies_init() * pieces[0] The domain name * pieces[1] TRUE/FALSE: is the domain a suffix, or a full domain? * pieces[2] The path - * pieces[3] Is the cookie unsecure or secure (TRUE/FALSE) + * pieces[3] TRUE/FALSE: is the cookie for secure use only? * pieces[4] Timestamp of expire date * pieces[5] Name of the cookie * pieces[6] Value of the cookie @@ -298,27 +277,37 @@ static void Cookies_init() CookieControlAction action; char *piece; char *line_marker = line; - - cookie = dNew0(CookieData_t, 1); + CookieData_t *cookie = dNew0(CookieData_t, 1); cookie->session_only = FALSE; - cookie->version = 0; cookie->domain = dStrdup(dStrsep(&line_marker, "\t")); - dStrsep(&line_marker, "\t"); /* we use domain always as sufix */ + piece = dStrsep(&line_marker, "\t"); + if (piece != NULL && piece[0] == 'F') + cookie->host_only = TRUE; cookie->path = dStrdup(dStrsep(&line_marker, "\t")); piece = dStrsep(&line_marker, "\t"); if (piece != NULL && piece[0] == 'T') cookie->secure = TRUE; piece = dStrsep(&line_marker, "\t"); - if (piece != NULL) - cookie->expires_at = (time_t) strtol(piece, NULL, 10); + if (piece != NULL) { + /* There is some problem with simply putting the maximum value + * into tm.tm_sec (although a value close to it works). + */ + long seconds = strtol(piece, NULL, 10); + struct tm tm; + Cookies_tm_init(&tm); + tm.tm_min += seconds / 60; + tm.tm_sec += seconds % 60; + cookie->expires_at = mktime(&tm); + } else { + cookie->expires_at = (time_t) -1; + } cookie->name = dStrdup(dStrsep(&line_marker, "\t")); - cookie->value = dStrdup(dStrsep(&line_marker, "\t")); + cookie->value = dStrdup(line_marker ? line_marker : ""); if (!cookie->domain || cookie->domain[0] == '\0' || !cookie->path || cookie->path[0] != '/' || - !cookie->name || cookie->name[0] == '\0' || - !cookie->value) { + !cookie->name || !cookie->value) { MSG("Malformed line in cookies.txt file!\n"); Cookies_free_cookie(cookie); continue; @@ -336,81 +325,63 @@ static void Cookies_init() Cookies_add_cookie(cookie); } } + MSG("Cookies loaded: %d.\n", dList_length(all_cookies)); +} - filename = dStrconcat(dGethomedir(), "/.dillo/cookies", NULL); - if ((old_cookies_file_stream = fopen(filename, "r")) != NULL) { - MSG("WARNING: Reading old cookies file ~/.dillo/cookies too\n"); - - /* Get all lines in the file */ - while (!feof(old_cookies_file_stream)) { - line[0] = '\0'; - fgets(line, LINE_MAXLEN, old_cookies_file_stream); - - /* Remove leading and trailing whitespaces */ - dStrstrip(line); - - if (line[0] != '\0') { - /* - * Split the row into pieces using a tab as the delimiter. - * pieces[0] The version this cookie was set as (0 / 1) - * pieces[1] The domain name - * pieces[2] A comma separated list of accepted ports - * pieces[3] The path - * pieces[4] Is the cookie unsecure or secure (0 / 1) - * pieces[5] Timestamp of expire date - * pieces[6] Name of the cookie - * pieces[7] Value of the cookie - * pieces[8] Comment - * pieces[9] Comment url - */ - CookieControlAction action; - char *piece; - char *line_marker = line; - - cookie = dNew0(CookieData_t, 1); - - cookie->session_only = FALSE; - piece = dStrsep(&line_marker, "\t"); - if (piece != NULL) - cookie->version = strtol(piece, NULL, 10); - cookie->domain = dStrdup(dStrsep(&line_marker, "\t")); - Cookies_parse_ports(0, cookie, dStrsep(&line_marker, "\t")); - cookie->path = dStrdup(dStrsep(&line_marker, "\t")); - piece = dStrsep(&line_marker, "\t"); - if (piece != NULL && piece[0] == '1') - cookie->secure = TRUE; - piece = dStrsep(&line_marker, "\t"); - if (piece != NULL) - cookie->expires_at = (time_t) strtol(piece, NULL, 10); - cookie->name = dStrdup(dStrsep(&line_marker, "\t")); - cookie->value = dStrdup(dStrsep(&line_marker, "\t")); - cookie->comment = dStrdup(dStrsep(&line_marker, "\t")); - cookie->comment_url = dStrdup(dStrsep(&line_marker, "\t")); - - if (!cookie->domain || cookie->domain[0] == '\0' || - !cookie->path || cookie->path[0] != '/' || - !cookie->name || cookie->name[0] == '\0' || - !cookie->value) { - MSG("Malformed line in cookies file!\n"); - Cookies_free_cookie(cookie); - continue; - } +/* + * Initialize the cookies module + * (The 'disabled' variable is writeable only within Cookies_init) + */ +static void Cookies_init() +{ + char *filename; +#ifndef HAVE_LOCKF + struct flock lck; +#endif + struct tm future_tm = {7, 14, 3, 19, 0, 138, 0, 0, 0, 0, 0}; - action = Cookies_control_check_domain(cookie->domain); - if (action == COOKIE_DENY) { - Cookies_free_cookie(cookie); - continue; - } else if (action == COOKIE_ACCEPT_SESSION) { - cookie->session_only = TRUE; - } + /* Default setting */ + disabled = TRUE; - /* Save cookie in memory */ - Cookies_add_cookie(cookie); - } - } - fclose(old_cookies_file_stream); + cookies_epoch_time = mktime(&cookies_epoch_tm); + cookies_future_time = mktime(&future_tm); + + /* Read and parse the cookie control file (cookiesrc) */ + if (Cookie_control_init() != 0) { + MSG("Disabling cookies.\n"); + return; } + + /* Get a stream for the cookies file */ + filename = dStrconcat(dGethomedir(), "/.dillo/cookies.txt", NULL); + file_stream = Cookies_fopen(filename, "r+", cookies_txt_header_str); + dFree(filename); + + if (!file_stream) { + MSG("ERROR: Can't open ~/.dillo/cookies.txt; disabling cookies\n"); + return; + } + + /* Try to get a lock from the file descriptor */ +#ifdef HAVE_LOCKF + disabled = (lockf(fileno(file_stream), F_TLOCK, 0) == -1); +#else /* POSIX lock */ + lck.l_start = 0; /* start at beginning of file */ + lck.l_len = 0; /* lock entire file */ + lck.l_type = F_WRLCK; + lck.l_whence = SEEK_SET; /* absolute offset */ + + disabled = (fcntl(fileno(file_stream), F_SETLK, &lck) == -1); +#endif + if (disabled) { + MSG("The cookies file has a file lock; disabling cookies!\n"); + fclose(file_stream); + return; + } + MSG("Enabling cookies as per cookiesrc...\n"); + + Cookies_load_cookies(file_stream); } /* @@ -418,9 +389,10 @@ static void Cookies_init() */ static void Cookies_save_and_free() { - int i, fd; - CookieNode *node; + int i, fd, saved = 0; + DomainNode *node; CookieData_t *cookie; + time_t now; #ifndef HAVE_LOCKF struct flock lck; @@ -429,33 +401,34 @@ static void Cookies_save_and_free() if (disabled) return; + now = time(NULL); + rewind(file_stream); fd = fileno(file_stream); - ftruncate(fd, 0); - fprintf(file_stream, cookies_txt_header_str); + if (ftruncate(fd, 0) == -1) + MSG("Cookies: Truncate file stream failed: %s\n", dStrerror(errno)); + fprintf(file_stream, "%s", cookies_txt_header_str); /* Iterate cookies per domain, saving and freeing */ - while ((node = dList_nth_data(cookies, 0))) { - for (i = 0; (cookie = dList_nth_data(node->dlist, i)); ++i) { - if (!cookie->session_only) { - /* char * ports_str = Cookies_build_ports_str(cookie); */ - fprintf(file_stream, "%s\tTRUE\t%s\t%s\t%ld\t%s\t%s\n", + while ((node = dList_nth_data(domains, 0))) { + for (i = 0; (cookie = dList_nth_data(node->cookies, i)); ++i) { + if (!cookie->session_only && difftime(cookie->expires_at, now) > 0) { + fprintf(file_stream, "%s\t%s\t%s\t%s\t%ld\t%s\t%s\n", cookie->domain, + cookie->host_only ? "FALSE" : "TRUE", cookie->path, cookie->secure ? "TRUE" : "FALSE", - (long)cookie->expires_at, + (long)difftime(cookie->expires_at, cookies_epoch_time), cookie->name, cookie->value); - /* dFree(ports_str); */ + saved++; } - Cookies_free_cookie(cookie); } - dList_remove(cookies, node); - dFree(node->domain); - dList_free(node->dlist); - dFree(node); + Cookies_delete_node(node); } + dList_free(domains); + dList_free(all_cookies); #ifdef HAVE_LOCKF lockf(fd, F_ULOCK, 0); @@ -468,34 +441,33 @@ static void Cookies_save_and_free() fcntl(fileno(file_stream), F_SETLKW, &lck); #endif fclose(file_stream); -} -static char *months[] = -{ "", - "Jan", "Feb", "Mar", - "Apr", "May", "Jun", - "Jul", "Aug", "Sep", - "Oct", "Nov", "Dec" -}; + MSG("Cookies saved: %d.\n", saved); +} /* - * Take a months name and return a number between 1-12. - * E.g. 'April' -> 4 + * Take a month's name and return a number between 0-11. + * E.g. 'April' -> 3 */ static int Cookies_get_month(const char *month_name) { + static const char *const months[] = + { "Jan", "Feb", "Mar", + "Apr", "May", "Jun", + "Jul", "Aug", "Sep", + "Oct", "Nov", "Dec" + }; int i; - for (i = 1; i <= 12; i++) { + for (i = 0; i < 12; i++) { if (!dStrncasecmp(months[i], month_name, 3)) return i; } - return 0; + return -1; } /* - * Return a local timestamp from a GMT date string - * Accept: RFC-1123 | RFC-850 | ANSI asctime | Old Netscape format. + * Accept: RFC-1123 | RFC-850 | ANSI asctime | Old Netscape format date string. * * Wdy, DD-Mon-YY HH:MM:SS GMT * Wdy, DD-Mon-YYYY HH:MM:SS GMT @@ -504,633 +476,734 @@ static int Cookies_get_month(const char *month_name) * Tue May 21 13:46:22 1991\n * Tue May 21 13:46:22 1991 * - * (return 0 on malformed date string syntax) + * Let's add: + * Mon Jan 11 08:00:00 2010 GMT + * + * Return a pointer to a struct tm, or NULL on error. + * + * NOTE that the draft spec wants user agents to be more flexible in what + * they accept. For now, let's hack in special cases when they're encountered. + * Why? Because this function is currently understandable, and I don't want to + * abandon that (or at best decrease that -- see section 5.1.1) until there + * is known to be good reason. */ -static time_t Cookies_create_timestamp(const char *expires) +static struct tm *Cookies_parse_date(const char *date) { - time_t ret; - int day, month, year, hour, minutes, seconds; - char *cp; - char *E_msg = - "Expire date is malformed!\n" - " (should be RFC-1123 | RFC-850 | ANSI asctime)\n" - " Ignoring cookie: "; - - cp = strchr(expires, ','); - if (!cp && (strlen(expires) == 24 || strlen(expires) == 25)) { + struct tm *tm; + char *cp = strchr(date, ','); + + if (!cp && strlen(date)>20 && date[13] == ':' && date[16] == ':') { /* Looks like ANSI asctime format... */ - cp = (char *)expires; - day = strtol(cp + 8, NULL, 10); /* day */ - month = Cookies_get_month(cp + 4); /* month */ - year = strtol(cp + 20, NULL, 10); /* year */ - hour = strtol(cp + 11, NULL, 10); /* hour */ - minutes = strtol(cp + 14, NULL, 10); /* minutes */ - seconds = strtol(cp + 17, NULL, 10); /* seconds */ - - } else if (cp && (cp - expires == 3 || cp - expires > 5) && + tm = dNew0(struct tm, 1); + + cp = (char *)date; + tm->tm_mon = Cookies_get_month(cp + 4); + tm->tm_mday = strtol(cp + 8, NULL, 10); + tm->tm_hour = strtol(cp + 11, NULL, 10); + tm->tm_min = strtol(cp + 14, NULL, 10); + tm->tm_sec = strtol(cp + 17, NULL, 10); + tm->tm_year = strtol(cp + 20, NULL, 10) - 1900; + + } else if (cp && (cp - date == 3 || cp - date > 5) && (strlen(cp) == 24 || strlen(cp) == 26)) { /* RFC-1123 | RFC-850 format | Old Netscape format */ - day = strtol(cp + 2, NULL, 10); - month = Cookies_get_month(cp + 5); - year = strtol(cp + 9, &cp, 10); - /* TODO: tricky, because two digits for year IS ambiguous! */ - year += (year < 70) ? 2000 : ((year < 100) ? 1900 : 0); - hour = strtol(cp + 1, NULL, 10); - minutes = strtol(cp + 4, NULL, 10); - seconds = strtol(cp + 7, NULL, 10); + tm = dNew0(struct tm, 1); + + tm->tm_mday = strtol(cp + 2, NULL, 10); + tm->tm_mon = Cookies_get_month(cp + 5); + tm->tm_year = strtol(cp + 9, &cp, 10); + /* tm_year is the number of years since 1900 */ + if (tm->tm_year < 70) + tm->tm_year += 100; + else if (tm->tm_year > 100) + tm->tm_year -= 1900; + tm->tm_hour = strtol(cp + 1, NULL, 10); + tm->tm_min = strtol(cp + 4, NULL, 10); + tm->tm_sec = strtol(cp + 7, NULL, 10); } else { - MSG("%s%s\n", E_msg, expires); - return (time_t) 0; + tm = NULL; + MSG("In date \"%s\", format not understood.\n", date); } - /* Error checks --this may be overkill */ - if (!(day > 0 && day < 32 && month > 0 && month < 13 && year > 1970 && - hour >= 0 && hour < 24 && minutes >= 0 && minutes < 60 && - seconds >= 0 && seconds < 60)) { - MSG("%s%s\n", E_msg, expires); - return (time_t) 0; + /* Error checks. This may be overkill. */ + if (tm && + !(tm->tm_mday > 0 && tm->tm_mday < 32 && tm->tm_mon >= 0 && + tm->tm_mon < 12 && tm->tm_year >= 70 && tm->tm_hour >= 0 && + tm->tm_hour < 24 && tm->tm_min >= 0 && tm->tm_min < 60 && + tm->tm_sec >= 0 && tm->tm_sec < 60)) { + MSG("Date \"%s\" values not in range.\n", date); + dFree(tm); + tm = NULL; } - /* Calculate local timestamp. - * [stolen from Lynx... (http://lynx.browser.org)] */ - month -= 3; - if (month < 0) { - month += 12; - year--; - } + return tm; +} - day += (year - 1968) * 1461 / 4; - day += ((((month * 153) + 2) / 5) - 672); - ret = (time_t)((day * 60 * 60 * 24) + - (hour * 60 * 60) + - (minutes * 60) + - seconds); +/* + * Find the least recently used cookie among those in the provided list. + */ +static CookieData_t *Cookies_get_LRU(Dlist *cookies) +{ + int i, n = dList_length(cookies); + CookieData_t *lru = dList_nth_data(cookies, 0); - MSG("Expires in %ld seconds, at %s", - (long)ret - time(NULL), ctime(&ret)); + for (i = 1; i < n; i++) { + CookieData_t *curr = dList_nth_data(cookies, i); - return ret; + if (curr->last_used < lru->last_used) + lru = curr; + } + return lru; } /* - * Parse a string containing a list of port numbers. + * Delete expired cookies. + * If node is given, only check those cookies. + * Note that nodes can disappear if all of their cookies were expired. + * + * Return the number of cookies that were expired. */ -static void Cookies_parse_ports(int url_port, CookieData_t *cookie, - const char *port_str) +static int Cookies_rm_expired_cookies(DomainNode *node) { - if ((!port_str || !port_str[0]) && url_port != 0) { - /* There was no list, so only the calling urls port should be allowed. */ - if (!cookie->ports) - cookie->ports = dList_new(1); - dList_append(cookie->ports, INT2VOIDP(url_port)); - } else if (port_str[0] == '"' && port_str[1] != '"') { - char *tok, *str; - int port; - - str = dStrdup(port_str + 1); - while ((tok = dStrsep(&str, ","))) { - port = strtol(tok, NULL, 10); - if (port > 0) { - if (!cookie->ports) - cookie->ports = dList_new(1); - dList_append(cookie->ports, INT2VOIDP(port)); - } + Dlist *cookies = node ? node->cookies : all_cookies; + int removed = 0; + int i = 0, n = dList_length(cookies); + time_t now = time(NULL); + + while (i < n) { + CookieData_t *c = dList_nth_data(cookies, i); + + if (difftime(c->expires_at, now) < 0) { + DomainNode *currnode = node ? node : + dList_find_sorted(domains, c->domain, Domain_node_by_domain_cmp); + dList_remove(currnode->cookies, c); + if (dList_length(currnode->cookies) == 0) + Cookies_delete_node(currnode); + dList_remove_fast(all_cookies, c); + Cookies_free_cookie(c); + n--; + removed++; + } else { + i++; } - dFree(str); } + return removed; } /* - * Build a string of the ports in 'cookie'. + * There are too many cookies. Choose one to remove and delete. + * If node is given, select from among its cookies only. */ -static char *Cookies_build_ports_str(CookieData_t *cookie) +static void Cookies_too_many(DomainNode *node) { - Dstr *dstr; - char *ret; - void *data; - int i; - - dstr = dStr_new("\""); - for (i = 0; (data = dList_nth_data(cookie->ports, i)); ++i) { - dStr_sprintfa(dstr, "%d,", VOIDP2INT(data)); - } - /* Remove any trailing comma */ - if (dstr->len > 1) - dStr_erase(dstr, dstr->len - 1, 1); - dStr_append(dstr, "\""); - - ret = dstr->str; - dStr_free(dstr, FALSE); - - return ret; + CookieData_t *lru = Cookies_get_LRU(node ? node->cookies : all_cookies); + + MSG("Too many cookies!\n" + "Removing LRU cookie for \'%s\': \'%s=%s\'\n", lru->domain, + lru->name, lru->value); + if (!node) + node = dList_find_sorted(domains, lru->domain,Domain_node_by_domain_cmp); + + dList_remove(node->cookies, lru); + dList_remove_fast(all_cookies, lru); + Cookies_free_cookie(lru); + if (dList_length(node->cookies) == 0) + Cookies_delete_node(node); } static void Cookies_add_cookie(CookieData_t *cookie) { Dlist *domain_cookies; CookieData_t *c; - CookieNode *node; - - /* Don't add an expired cookie */ - if (!cookie->session_only && cookie->expires_at < time(NULL)) { - Cookies_free_cookie(cookie); - return; - } + DomainNode *node; - node = dList_find_sorted(cookies, cookie->domain,Cookie_node_by_domain_cmp); - domain_cookies = (node) ? node->dlist : NULL; + node = dList_find_sorted(domains, cookie->domain,Domain_node_by_domain_cmp); + domain_cookies = (node) ? node->cookies : NULL; if (domain_cookies) { - /* Respect the limit of 20 cookies per domain */ - if (dList_length(domain_cookies) >= 20) { - MSG("There are too many cookies for this domain (%s)\n", - cookie->domain); - Cookies_free_cookie(cookie); - return; - } - - /* Remove any cookies with the same name and path */ - while ((c = dList_find_custom(domain_cookies, cookie, Cookies_cmp))){ - Cookies_remove_cookie(c); + /* Remove any cookies with the same name, path, and host-only values. */ + while ((c = dList_find_custom(domain_cookies, cookie, Cookies_cmp))) { + dList_remove(domain_cookies, c); + dList_remove_fast(all_cookies, c); + Cookies_free_cookie(c); } } - /* add the cookie into the respective domain list */ - node = dList_find_sorted(cookies, cookie->domain,Cookie_node_by_domain_cmp); - domain_cookies = (node) ? node->dlist : NULL; - if (!domain_cookies) { - domain_cookies = dList_new(5); - dList_append(domain_cookies, cookie); - node = dNew(CookieNode, 1); - node->domain = dStrdup(cookie->domain); - node->dlist = domain_cookies; - dList_insert_sorted(cookies, node, Cookie_node_cmp); + if ((cookie->expires_at == (time_t) -1) || + (difftime(cookie->expires_at, time(NULL)) <= 0)) { + /* + * Don't add an expired cookie. Whether expiring now == expired, exactly, + * is arguable, but we definitely do not want to add a Max-Age=0 cookie. + */ + _MSG("Goodbye, cookie %s=%s d:%s p:%s\n", cookie->name, + cookie->value, cookie->domain, cookie->path); + Cookies_free_cookie(cookie); } else { - dList_append(domain_cookies, cookie); + if (domain_cookies && dList_length(domain_cookies) >=MAX_DOMAIN_COOKIES){ + int removed = Cookies_rm_expired_cookies(node); + + if (removed == 0) { + Cookies_too_many(node); + } else if (removed >= MAX_DOMAIN_COOKIES) { + /* So many were removed that the node might have been deleted. */ + node = dList_find_sorted(domains, cookie->domain, + Domain_node_by_domain_cmp); + domain_cookies = (node) ? node->cookies : NULL; + } + } + if (dList_length(all_cookies) >= MAX_TOTAL_COOKIES) { + if (Cookies_rm_expired_cookies(NULL) == 0) { + Cookies_too_many(NULL); + } else if (domain_cookies) { + /* Our own node might have just been deleted. */ + node = dList_find_sorted(domains, cookie->domain, + Domain_node_by_domain_cmp); + domain_cookies = (node) ? node->cookies : NULL; + } + } + + cookie->last_used = cookies_use_counter++; + + /* Actually add the cookie! */ + dList_append(all_cookies, cookie); + + if (!domain_cookies) { + domain_cookies = dList_new(5); + dList_append(domain_cookies, cookie); + node = dNew(DomainNode, 1); + node->domain = dStrdup(cookie->domain); + node->cookies = domain_cookies; + dList_insert_sorted(domains, node, Domain_node_cmp); + } else { + dList_append(domain_cookies, cookie); + } } + if (domain_cookies && (dList_length(domain_cookies) == 0)) + Cookies_delete_node(node); } /* - * Remove the cookie from the domain list. - * If the domain list is empty, remove the node too. - * Free the cookie. + * Return the attribute that is present at *cookie_str. */ -static void Cookies_remove_cookie(CookieData_t *cookie) +static char *Cookies_parse_attr(char **cookie_str) { - CookieNode *node; + char *str; + uint_t len; - node = dList_find_sorted(cookies, cookie->domain,Cookie_node_by_domain_cmp); - if (node) { - dList_remove(node->dlist, cookie); - if (dList_length(node->dlist) == 0) { - dList_remove(cookies, node); - dFree(node->domain); - dList_free(node->dlist); - } - } else { - MSG("Attempting to remove a cookie that doesn't exist!\n"); - } + while (dIsspace(**cookie_str)) + (*cookie_str)++; - Cookies_free_cookie(cookie); + str = *cookie_str; + /* find '=' at end of attr, ';' after attr/val pair, '\0' end of string */ + len = strcspn(str, "=;"); + *cookie_str += len; + + while (len && (str[len - 1] == ' ' || str[len - 1] == '\t')) + len--; + return dStrndup(str, len); } /* - * Return the attribute that is present at *cookie_str. This function - * will also attempt to advance cookie_str past any equal-sign. + * Get the value in *cookie_str. */ -static char *Cookies_parse_attr(char **cookie_str) +static char *Cookies_parse_value(char **cookie_str) { - char *str = *cookie_str; - uint_t i, end = 0; - bool_t got_attr = FALSE; - - for (i = 0; ; i++) { - switch (str[i]) { - case ' ': - case '\t': - case '=': - case ';': - got_attr = TRUE; - if (end == 0) - end = i; - break; - case ',': - *cookie_str = str + i; - return dStrndup(str, i); - break; - case '\0': - if (!got_attr) { - end = i; - got_attr = TRUE; - } - /* fall through! */ - default: - if (got_attr) { - *cookie_str = str + i; - return dStrndup(str, end); - } - break; - } + uint_t len; + char *str; + + if (**cookie_str == '=') { + (*cookie_str)++; + while (dIsspace(**cookie_str)) + (*cookie_str)++; + + str = *cookie_str; + /* finds ';' after attr/val pair or '\0' at end of string */ + len = strcspn(str, ";"); + *cookie_str += len; + + while (len && (str[len - 1] == ' ' || str[len - 1] == '\t')) + len--; + } else { + str = *cookie_str; + len = 0; } + return dStrndup(str, len); +} - return NULL; +/* + * Advance past any value + */ +static void Cookies_eat_value(char **cookie_str) +{ + if (**cookie_str == '=') + *cookie_str += strcspn(*cookie_str, ";"); } /* - * Get the value starting at *cookie_str. - * broken_syntax: watch out for stupid syntax (comma in unquoted string...) + * Return the number of seconds by which our clock is ahead of the server's + * clock. */ -static char *Cookies_parse_value(char **cookie_str, - bool_t broken_syntax, - bool_t keep_quotes) +static double Cookies_server_timediff(const char *server_date) { - uint_t i, end; - char *str = *cookie_str; - - for (i = end = 0; !end; ++i) { - switch (str[i]) { - case ' ': - case '\t': - if (!broken_syntax && str[0] != '\'' && str[0] != '"') { - *cookie_str = str + i + 1; - end = 1; - } - break; - case '\'': - case '"': - if (i != 0 && str[i] == str[0]) { - char *tmp = str + i; + double ret = 0; - while (*tmp != '\0' && *tmp != ';' && *tmp != ',') - tmp++; + if (server_date) { + struct tm *server_tm = Cookies_parse_date(server_date); - *cookie_str = (*tmp == ';') ? tmp + 1 : tmp; + if (server_tm) { + time_t server_time = mktime(server_tm); - if (keep_quotes) - i++; - end = 1; - } - break; - case '\0': - *cookie_str = str + i; - end = 1; - break; - case ',': - if (str[0] != '\'' && str[0] != '"' && !broken_syntax) { - /* A new cookie starts here! */ - *cookie_str = str + i; - end = 1; - } - break; - case ';': - if (str[0] != '\'' && str[0] != '"') { - *cookie_str = str + i + 1; - end = 1; - } - break; - default: - break; + if (server_time != (time_t) -1) + ret = difftime(time(NULL), server_time); + dFree(server_tm); } } - /* keep i as an index to the last char */ - --i; + return ret; +} - if ((str[0] == '\'' || str[0] == '"') && !keep_quotes) { - return i > 1 ? dStrndup(str + 1, i - 1) : NULL; - } else { - return dStrndup(str, i); +static void Cookies_unquote_string(char *str) +{ + if (str && str[0] == '\"') { + uint_t len = strlen(str); + + if (len > 1 && str[len - 1] == '\"') { + str[len - 1] = '\0'; + while ((*str = str[1])) + str++; + } } } /* - * Parse one cookie... + * Parse cookie. A cookie might look something like: + * "Name=Val; Domain=example.com; Max-Age=3600; HttpOnly" */ -static CookieData_t *Cookies_parse_one(int url_port, char **cookie_str) +static CookieData_t *Cookies_parse(char *cookie_str, const char *server_date) { - CookieData_t *cookie; - char *str = *cookie_str; - char *attr; - char *value; - int num_attr = 0; + CookieData_t *cookie = NULL; + char *str = cookie_str; + bool_t first_attr = TRUE; bool_t max_age = FALSE; - bool_t discard = FALSE; - bool_t error = FALSE; - - cookie = dNew0(CookieData_t, 1); - cookie->session_only = TRUE; + bool_t expires = FALSE; - /* Iterate until there is nothing left of the string OR we come - * across a comma representing the start of another cookie */ - while (*str != '\0' && *str != ',') { - if (error) { - str++; - continue; - } - /* Skip whitespace */ - while (isspace(*str)) - str++; + /* Iterate until there is nothing left of the string */ + while (*str) { + char *attr; + char *value; /* Get attribute */ attr = Cookies_parse_attr(&str); - if (!attr) { - MSG("Cannot parse cookie attribute!\n"); - error = TRUE; - continue; - } /* Get the value for the attribute and store it */ - if (num_attr == 0) { - /* The first attr, which always is the user supplied attr, may - * have the same name as an ordinary attr. Hence this workaround. */ - cookie->name = dStrdup(attr); - cookie->value = Cookies_parse_value(&str, FALSE, TRUE); + if (first_attr) { + if (*str != '=' || *attr == '\0') { + /* disregard nameless cookie */ + dFree(attr); + return NULL; + } + cookie = dNew0(CookieData_t, 1); + cookie->name = attr; + cookie->value = Cookies_parse_value(&str); + + /* let's arbitrarily initialise with a year for now */ + time_t now = time(NULL); + struct tm *tm = gmtime(&now); + ++tm->tm_year; + cookie->expires_at = mktime(tm); + if (cookie->expires_at == (time_t) -1) + cookie->expires_at = cookies_future_time; } else if (dStrcasecmp(attr, "Path") == 0) { - value = Cookies_parse_value(&str, FALSE, FALSE); + value = Cookies_parse_value(&str); + dFree(cookie->path); cookie->path = value; } else if (dStrcasecmp(attr, "Domain") == 0) { - value = Cookies_parse_value(&str, FALSE, FALSE); + value = Cookies_parse_value(&str); + dFree(cookie->domain); cookie->domain = value; - } else if (dStrcasecmp(attr, "Discard") == 0) { - cookie->session_only = TRUE; - discard = TRUE; } else if (dStrcasecmp(attr, "Max-Age") == 0) { - if (!discard) { - value = Cookies_parse_value(&str, FALSE, FALSE); - - if (value) { - cookie->expires_at = time(NULL) + strtol(value, NULL, 10); - cookie->session_only = FALSE; - max_age = TRUE; - dFree(value); - } else { - MSG("Cannot parse cookie Max-Age value!\n"); - dFree(attr); - error = TRUE; - continue; + value = Cookies_parse_value(&str); + if (isdigit(*value) || *value == '-') { + time_t now = time(NULL); + long age = strtol(value, NULL, 10); + struct tm *tm = gmtime(&now); + + tm->tm_sec += age; + cookie->expires_at = mktime(tm); + if (age > 0 && cookie->expires_at == (time_t) -1) { + cookie->expires_at = cookies_future_time; } + _MSG("Cookie to expire at %s", ctime(&cookie->expires_at)); + expires = max_age = TRUE; } + dFree(value); } else if (dStrcasecmp(attr, "Expires") == 0) { - if (!max_age && !discard) { - MSG("Old netscape-style cookie...\n"); - value = Cookies_parse_value(&str, TRUE, FALSE); - if (value) { - cookie->expires_at = Cookies_create_timestamp(value); - cookie->session_only = FALSE; - dFree(value); + if (!max_age) { + value = Cookies_parse_value(&str); + Cookies_unquote_string(value); + _MSG("Expires attribute gives %s\n", value); + struct tm *tm = Cookies_parse_date(value); + if (tm) { + tm->tm_sec += Cookies_server_timediff(server_date); + cookie->expires_at = mktime(tm); + if (cookie->expires_at == (time_t) -1 && tm->tm_year >= 138) { + /* Just checking tm_year does not ensure that the problem was + * inability to represent a distant date... + */ + cookie->expires_at = cookies_future_time; + } + _MSG("Cookie to expire at %s", ctime(&cookie->expires_at)); + dFree(tm); } else { - MSG("Cannot parse cookie Expires value!\n"); - dFree(attr); - error = TRUE; - continue; + cookie->expires_at = (time_t) -1; } - } - } else if (dStrcasecmp(attr, "Port") == 0) { - value = Cookies_parse_value(&str, FALSE, TRUE); - Cookies_parse_ports(url_port, cookie, value); - dFree(value); - } else if (dStrcasecmp(attr, "Comment") == 0) { - value = Cookies_parse_value(&str, FALSE, FALSE); - cookie->comment = value; - } else if (dStrcasecmp(attr, "CommentURL") == 0) { - value = Cookies_parse_value(&str, FALSE, FALSE); - cookie->comment_url = value; - } else if (dStrcasecmp(attr, "Version") == 0) { - value = Cookies_parse_value(&str, FALSE, FALSE); - - if (value) { - cookie->version = strtol(value, NULL, 10); + expires = TRUE; dFree(value); } else { - MSG("Cannot parse cookie Version value!\n"); - dFree(attr); - error = TRUE; - continue; + Cookies_eat_value(&str); } } else if (dStrcasecmp(attr, "Secure") == 0) { cookie->secure = TRUE; + Cookies_eat_value(&str); + } else if (dStrcasecmp(attr, "HttpOnly") == 0) { + Cookies_eat_value(&str); } else { - /* Oops! this can't be good... */ MSG("Cookie contains unknown attribute: '%s'\n", attr); - dFree(attr); - error = TRUE; - continue; + Cookies_eat_value(&str); } - dFree(attr); - num_attr++; - } - - *cookie_str = (*str == ',') ? str + 1 : str; + if (first_attr) + first_attr = FALSE; + else + dFree(attr); - if (!error && (!cookie->name || !cookie->value)) { - MSG("Cookie missing name and/or value!\n"); - error = TRUE; - } - if (error) { - Cookies_free_cookie(cookie); - cookie = NULL; + if (*str == ';') + str++; } + cookie->session_only = expires == FALSE; return cookie; } /* - * Iterate the cookie string until we catch all cookies. - * Return Value: a list with all the cookies! (or NULL upon error) + * Compare cookies by host_only, name, and path. Return 0 if equal. */ -static Dlist *Cookies_parse_string(int url_port, char *cookie_string) +static int Cookies_cmp(const void *a, const void *b) { - CookieData_t *cookie; - Dlist *ret = NULL; - char *str = cookie_string; + const CookieData_t *ca = a, *cb = b; - /* The string may contain several cookies separated by comma. - * We'll iterate until we've caught them all */ - while (*str) { - cookie = Cookies_parse_one(url_port, &str); + return (ca->host_only != cb->host_only) || + (strcmp(ca->name, cb->name) != 0) || + (strcmp(ca->path, cb->path) != 0); +} - if (cookie) { - if (!ret) - ret = dList_new(4); - dList_append(ret, cookie); - } else { - MSG("Malformed cookie field, ignoring cookie: %s\n", cookie_string); - } - } +/* + * Is the domain an IP address? + */ +static bool_t Cookies_domain_is_ip(const char *domain) +{ + uint_t len; - return ret; + if (!domain) + return FALSE; + + len = strlen(domain); + + if (len == strspn(domain, "0123456789.")) { + _MSG("an IPv4 address\n"); + return TRUE; + } + if (*domain == '[' && + (len == strspn(domain, "0123456789abcdefABCDEF:.[]"))) { + /* The precise format is shown in section 3.2.2 of rfc 3986 */ + _MSG("an IPv6 address\n"); + return TRUE; + } + return FALSE; } /* - * Compare cookies by name and path (return 0 if equal) + * Check whether url_path path-matches cookie_path + * + * Note different user agents apparently vary in path-matching behaviour, + * but this is the recommended method at the moment. */ -static int Cookies_cmp(const void *a, const void *b) +static bool_t Cookies_path_matches(const char *url_path, + const char *cookie_path) { - const CookieData_t *ca = a, *cb = b; - int ret; + bool_t ret = TRUE; + + if (!url_path || !cookie_path) { + ret = FALSE; + } else { + uint_t c_len = strlen(cookie_path); + uint_t u_len = strlen(url_path); - if (!(ret = strcmp(ca->name, cb->name))) - ret = strcmp(ca->path, cb->path); + ret = (!strncmp(cookie_path, url_path, c_len) && + ((c_len == u_len) || + (c_len > 0 && cookie_path[c_len - 1] == '/') || + (url_path[c_len] == '/'))); + } return ret; } /* - * Validate cookies domain against some security checks. + * If cookie path is not properly set, remedy that. */ -static bool_t Cookies_validate_domain(CookieData_t *cookie, char *host, - char *url_path) +static void Cookies_validate_path(CookieData_t *cookie, const char *url_path) { - int dots, diff, i; - bool_t is_ip; - - /* Make sure that the path is set to something */ if (!cookie->path || cookie->path[0] != '/') { dFree(cookie->path); - cookie->path = Cookies_strip_path(url_path); + + if (url_path) { + uint_t len = strlen(url_path); + + while (len && url_path[len] != '/') + len--; + cookie->path = dStrndup(url_path, len ? len : 1); + } else { + cookie->path = dStrdup("/"); + } } +} - /* If the server never set a domain, or set one without a leading - * dot (which isn't allowed), we use the calling URL's hostname. */ - if (cookie->domain == NULL || cookie->domain[0] != '.') { - dFree(cookie->domain); - cookie->domain = dStrdup(host); +/* + * Check whether host name A domain-matches host name B. + */ +static bool_t Cookies_domain_matches(char *A, char *B) +{ + int diff; + + if (!A || !*A || !B || !*B) + return FALSE; + + if (*B == '.') + B++; + + /* Should we concern ourselves with trailing dots in matching (here or + * elsewhere)? The HTTP State people have found that most user agents + * don't, so: No. + */ + + if (!dStrcasecmp(A, B)) return TRUE; - } - /* Count the number of dots and also find out if it is an IP-address */ - is_ip = TRUE; - for (i = 0, dots = 0; cookie->domain[i] != '\0'; i++) { - if (cookie->domain[i] == '.') - dots++; - else if (!isdigit(cookie->domain[i])) - is_ip = FALSE; - } + if (Cookies_domain_is_ip(B)) + return FALSE; - /* A valid domain must have at least two dots in it */ - /* NOTE: this breaks cookies on localhost... */ - if (dots < 2) { + diff = strlen(A) - strlen(B); + + if (diff > 0) { + /* B is the tail of A, and the match is preceded by a '.' */ + return (dStrcasecmp(A + diff, B) == 0 && A[diff - 1] == '.'); + } else { return FALSE; } +} - /* Now see if the url matches the domain */ - diff = strlen(host) - i; - if (diff > 0) { - if (dStrcasecmp(host + diff, cookie->domain)) - return FALSE; - - if (!is_ip) { - /* "x.y.test.com" is not allowed to set cookies for ".test.com"; - * only an url of the form "y.test.com" would be. */ - while ( diff-- ) - if (host[diff] == '.') - return FALSE; +/* + * Based on the host, how many internal dots do we need in a cookie domain + * to make it valid? e.g., "org" is not on the list, so dillo.org is a safe + * cookie domain, but "uk" is on the list, so ac.uk is not safe. + * + * This is imperfect, but it's something. Specifically, checking for these + * TLDs is the solution that Konqueror used once upon a time, according to + * reports. + */ +static uint_t Cookies_internal_dots_required(const char *host) +{ + uint_t ret = 1; + + if (host) { + int start, after, tld_len; + + /* We may be able to trust the format of the host string more than + * I am here. Trailing dots and no dots are real possibilities, though. + */ + after = strlen(host); + if (after > 0 && host[after - 1] == '.') + after--; + start = after; + while (start > 0 && host[start - 1] != '.') + start--; + tld_len = after - start; + + if (tld_len > 0) { + /* These TLDs were chosen by examining the current publicsuffix list + * in January 2010 and picking out those where it was simplest for + * them to describe the situation by beginning with a "*.[tld]" rule. + */ + const char *const tlds[] = {"ar","au","bd","bn","bt","ck","cy","do", + "eg","er","et","fj","fk","gt","gu","id", + "il","jm","ke","kh","kw","ml","mm","mt", + "mz","ni","np","nz","om","pg","py","qa", + "sv","tr","uk","uy","ve","ye","yu","za", + "zm","zw"}; + uint_t i, tld_num = sizeof(tlds) / sizeof(tlds[0]); + + for (i = 0; i < tld_num; i++) { + if (strlen(tlds[i]) == (uint_t) tld_len && + !dStrncasecmp(tlds[i], host + start, tld_len)) { + _MSG("TLD code matched %s\n", tlds[i]); + ret++; + break; + } + } } } - - return TRUE; + return ret; } /* - * Strip of the filename from a full path + * Validate cookies domain against some security checks. */ -static char *Cookies_strip_path(const char *path) +static bool_t Cookies_validate_domain(CookieData_t *cookie, char *host) { - char *ret; - uint_t len; + uint_t i, internal_dots; + + if (!cookie->domain) { + cookie->domain = dStrdup(host); + cookie->host_only = TRUE; + return TRUE; + } - if (path) { - len = strlen(path); + if (!Cookies_domain_matches(host, cookie->domain)) + return FALSE; - while (len && path[len] != '/') - len--; - ret = dStrndup(path, len + 1); - } else { - ret = dStrdup("/"); + internal_dots = 0; + for (i = 1; i < strlen(cookie->domain) - 1; i++) { + if (cookie->domain[i] == '.') + internal_dots++; } - return ret; + /* All of this dots business is a weak hack. + * TODO: accept the publicsuffix.org list as an optional external file. + */ + if (internal_dots < Cookies_internal_dots_required(host)) { + MSG("not enough dots in %s\n", cookie->domain); + return FALSE; + } + + _MSG("host %s and domain %s is all right\n", host, cookie->domain); + return TRUE; } /* * Set the value corresponding to the cookie string + * Return value: 0 set OK, -1 disabled, -2 denied, -3 rejected. */ -static void Cookies_set(char *cookie_string, char *url_host, - char *url_path, int url_port) +static int Cookies_set(char *cookie_string, char *url_host, + char *url_path, char *server_date) { CookieControlAction action; CookieData_t *cookie; - Dlist *list; - int i; + int ret = -1; if (disabled) - return; + return ret; action = Cookies_control_check_domain(url_host); if (action == COOKIE_DENY) { MSG("denied SET for %s\n", url_host); - return; - } + ret = -2; - if ((list = Cookies_parse_string(url_port, cookie_string))) { - for (i = 0; (cookie = dList_nth_data(list, i)); ++i) { - if (Cookies_validate_domain(cookie, url_host, url_path)) { + } else { + MSG("%s SETTING: %s\n", url_host, cookie_string); + ret = -3; + if ((cookie = Cookies_parse(cookie_string, server_date))) { + if (Cookies_validate_domain(cookie, url_host)) { + Cookies_validate_path(cookie, url_path); if (action == COOKIE_ACCEPT_SESSION) cookie->session_only = TRUE; Cookies_add_cookie(cookie); + ret = 0; } else { - MSG("Rejecting cookie for %s from host %s path %s\n", + MSG("Rejecting cookie for domain %s from host %s path %s\n", cookie->domain, url_host, url_path); Cookies_free_cookie(cookie); } } - dList_free(list); } + + return ret; } /* - * Compare the cookie with the supplied data to see if it matches + * Compare the cookie with the supplied data to see whether it matches */ -static bool_t Cookies_match(CookieData_t *cookie, int port, - const char *path, bool_t is_ssl) +static bool_t Cookies_match(CookieData_t *cookie, const char *url_path, + bool_t host_only_val, bool_t is_ssl) { - void *data; - int i; + if (cookie->host_only != host_only_val) + return FALSE; /* Insecure cookies matches both secure and insecure urls, secure cookies matches only secure urls */ if (cookie->secure && !is_ssl) return FALSE; - /* Check that the cookie path is a subpath of the current path */ - if (strncmp(cookie->path, path, strlen(cookie->path)) != 0) - return FALSE; - - /* Check if the port of the request URL matches any - * of those set in the cookie */ - if (cookie->ports) { - for (i = 0; (data = dList_nth_data(cookie->ports, i)); ++i) { - if (VOIDP2INT(data) == port) - return TRUE; - } + if (!Cookies_path_matches(url_path, cookie->path)) return FALSE; - } /* It's a match */ return TRUE; } +static void Cookies_add_matching_cookies(const char *domain, + const char *url_path, + bool_t host_only_val, + Dlist *matching_cookies, + bool_t is_ssl) +{ + DomainNode *node = dList_find_sorted(domains, domain, + Domain_node_by_domain_cmp); + if (node) { + int i; + CookieData_t *cookie; + Dlist *domain_cookies = node->cookies; + + for (i = 0; (cookie = dList_nth_data(domain_cookies, i)); ++i) { + /* Remove expired cookie. */ + if (difftime(cookie->expires_at, time(NULL)) < 0) { + _MSG("Goodbye, expired cookie %s=%s d:%s p:%s\n", cookie->name, + cookie->value, cookie->domain, cookie->path); + dList_remove(domain_cookies, cookie); + dList_remove_fast(all_cookies, cookie); + Cookies_free_cookie(cookie); + --i; continue; + } + /* Check if the cookie matches the requesting URL */ + if (Cookies_match(cookie, url_path, host_only_val, is_ssl)) { + int j; + CookieData_t *curr; + uint_t path_length = strlen(cookie->path); + + cookie->last_used = cookies_use_counter; + + /* Longest cookies go first */ + for (j = 0; + (curr = dList_nth_data(matching_cookies, j)) && + strlen(curr->path) >= path_length; + j++) ; + dList_insert_pos(matching_cookies, cookie, j); + } + } + + if (dList_length(domain_cookies) == 0) + Cookies_delete_node(node); + } +} + /* * Return a string that contains all relevant cookies as headers. */ static char *Cookies_get(char *url_host, char *url_path, - char *url_scheme, int url_port) + char *url_scheme) { - char *domain_str, *q, *str, *path; + char *domain_str, *str; CookieData_t *cookie; Dlist *matching_cookies; - CookieNode *node; - Dlist *domain_cookies; - bool_t is_ssl; + bool_t is_ssl, is_ip_addr, host_only_val; + Dstr *cookie_dstring; int i; @@ -1139,27 +1212,47 @@ static char *Cookies_get(char *url_host, char *url_path, matching_cookies = dList_new(8); - path = Cookies_strip_path(url_path); - /* Check if the protocol is secure or not */ is_ssl = (!dStrcasecmp(url_scheme, "https")); - for (domain_str = (char *) url_host; - domain_str != NULL && *domain_str; - domain_str = strchr(domain_str+1, '.')) { - - node = dList_find_sorted(cookies, domain_str, Cookie_node_by_domain_cmp); - domain_cookies = (node) ? node->dlist : NULL; - - for (i = 0; (cookie = dList_nth_data(domain_cookies, i)); ++i) { - /* Remove expired cookie. */ - if (!cookie->session_only && cookie->expires_at < time(NULL)) { - Cookies_remove_cookie(cookie); - --i; continue; - } - /* Check if the cookie matches the requesting URL */ - if (Cookies_match(cookie, url_port, path, is_ssl)) { - dList_append(matching_cookies, cookie); + is_ip_addr = Cookies_domain_is_ip(url_host); + + /* If a cookie is set that lacks a Domain attribute, its domain is set to + * the server's host and the host_only flag is set for that cookie. Such a + * cookie can only be sent back to that host. Cookies with Domain attrs do + * not have the host_only flag set, and may be sent to subdomains. Domain + * attrs can have leading dots, which should be ignored for matching + * purposes. + */ + host_only_val = FALSE; + if (!is_ip_addr) { + /* e.g., sub.example.com set a cookie with domain ".sub.example.com". */ + domain_str = dStrconcat(".", url_host, NULL); + Cookies_add_matching_cookies(domain_str, url_path, host_only_val, + matching_cookies, is_ssl); + dFree(domain_str); + } + host_only_val = TRUE; + /* e.g., sub.example.com set a cookie with no domain attribute. */ + Cookies_add_matching_cookies(url_host, url_path, host_only_val, + matching_cookies, is_ssl); + host_only_val = FALSE; + /* e.g., sub.example.com set a cookie with domain "sub.example.com". */ + Cookies_add_matching_cookies(url_host, url_path, host_only_val, + matching_cookies, is_ssl); + + if (!is_ip_addr) { + for (domain_str = strchr(url_host+1, '.'); + domain_str != NULL && *domain_str; + domain_str = strchr(domain_str+1, '.')) { + /* e.g., sub.example.com set a cookie with domain ".example.com". */ + Cookies_add_matching_cookies(domain_str, url_path, host_only_val, + matching_cookies, is_ssl); + if (domain_str[1]) { + domain_str++; + /* e.g., sub.example.com set a cookie with domain "example.com".*/ + Cookies_add_matching_cookies(domain_str, url_path, host_only_val, + matching_cookies, is_ssl); } } } @@ -1167,36 +1260,24 @@ static char *Cookies_get(char *url_host, char *url_path, /* Found the cookies, now make the string */ cookie_dstring = dStr_new(""); if (dList_length(matching_cookies) > 0) { - CookieData_t *first_cookie = dList_nth_data(matching_cookies, 0); dStr_sprintfa(cookie_dstring, "Cookie: "); - if (first_cookie->version != 0) - dStr_sprintfa(cookie_dstring, "$Version=\"%d\"; ", - first_cookie->version); - - for (i = 0; (cookie = dList_nth_data(matching_cookies, i)); ++i) { - q = (cookie->version == 0 ? "" : "\""); - dStr_sprintfa(cookie_dstring, - "%s=%s; $Path=%s%s%s; $Domain=%s%s%s", - cookie->name, cookie->value, - q, cookie->path, q, q, cookie->domain, q); - if (cookie->ports) { - char *ports_str = Cookies_build_ports_str(cookie); - dStr_sprintfa(cookie_dstring, "; $Port=%s", ports_str); - dFree(ports_str); - } - + dStr_sprintfa(cookie_dstring, "%s=%s", cookie->name, cookie->value); dStr_append(cookie_dstring, dList_length(matching_cookies) > i + 1 ? "; " : "\r\n"); } } dList_free(matching_cookies); - dFree(path); str = cookie_dstring->str; dStr_free(cookie_dstring, FALSE); + + if (*str) + cookies_use_counter++; + + MSG("%s GETTING: %s\n", url_host, str); return str; } @@ -1216,11 +1297,10 @@ static int Cookie_control_init(void) { CookieControl cc; FILE *stream; - char *filename; + char *filename, *rc; char line[LINE_MAXLEN]; char domain[LINE_MAXLEN]; char rule[LINE_MAXLEN]; - int i, j; bool_t enabled = FALSE; /* Get a file pointer */ @@ -1234,28 +1314,31 @@ static int Cookie_control_init(void) /* Get all lines in the file */ while (!feof(stream)) { line[0] = '\0'; - fgets(line, LINE_MAXLEN, stream); + rc = fgets(line, LINE_MAXLEN, stream); + if (!rc && ferror(stream)) { + MSG("Error while reading rule from cookiesrc: %s\n", + dStrerror(errno)); + break; /* bail out */ + } /* Remove leading and trailing whitespaces */ dStrstrip(line); if (line[0] != '\0' && line[0] != '#') { - i = 0; - j = 0; + int i = 0, j = 0; /* Get the domain */ - while (!isspace(line[i])) + while (line[i] != '\0' && !dIsspace(line[i])) domain[j++] = line[i++]; domain[j] = '\0'; /* Skip past whitespaces */ - i++; - while (isspace(line[i])) + while (dIsspace(line[i])) i++; /* Get the rule */ j = 0; - while (line[i] != '\0' && !isspace(line[i])) + while (line[i] != '\0' && !dIsspace(line[i])) rule[j++] = line[i++]; rule[j] = '\0'; @@ -1277,8 +1360,17 @@ static int Cookie_control_init(void) default_action = cc.action; dFree(cc.domain); } else { + int i; + uint_t len = strlen(cc.domain); + + /* Insert into list such that longest rules come first. */ a_List_add(ccontrol, num_ccontrol, num_ccontrol_max); - ccontrol[num_ccontrol++] = cc; + for (i = num_ccontrol++; + i > 0 && (len > strlen(ccontrol[i-1].domain)); + i--) { + ccontrol[i] = ccontrol[i-1]; + } + ccontrol[i] = cc; } if (cc.action != COOKIE_DENY) @@ -1292,7 +1384,9 @@ static int Cookie_control_init(void) } /* - * Check the rules for an appropriate action for this domain + * Check the rules for an appropriate action for this domain. + * The rules are ordered by domain length, with longest first, so the + * first match is the most specific. */ static CookieControlAction Cookies_control_check_domain(const char *domain) { @@ -1326,69 +1420,74 @@ static CookieControlAction Cookies_control_check_domain(const char *domain) * Note: Buf is a zero terminated string * Return code: { 0:OK, 1:Abort, 2:Close } */ -static int srv_parse_buf(SockHandler *sh, char *Buf, size_t BufSize) +static int srv_parse_tok(Dsh *sh, ClientInfo *client, char *Buf) { - char *p, *cmd, *cookie, *host, *path, *scheme; - int port, ret; + char *cmd, *cookie, *host, *path; + int ret = 1; + size_t BufSize = strlen(Buf); + + cmd = a_Dpip_get_attr_l(Buf, BufSize, "cmd"); + + if (!cmd) { + /* abort */ + } else if (client->status == 0) { + /* authenticate */ + if (a_Dpip_check_auth(Buf) == 1) { + client->status = 1; + ret = 0; + } + } else if (strcmp(cmd, "DpiBye") == 0) { + dFree(cmd); + MSG("(pid %d): Got DpiBye.\n", (int)getpid()); + exit(0); - if (!(p = strchr(Buf, '>'))) { - /* Haven't got a full tag */ - MSG("Haven't got a full tag!\n"); - return 1; - } + } else if (strcmp(cmd, "set_cookie") == 0) { + int st; + char *date; - cmd = a_Dpip_get_attr(Buf, BufSize, "cmd"); + cookie = a_Dpip_get_attr_l(Buf, BufSize, "cookie"); + host = a_Dpip_get_attr_l(Buf, BufSize, "host"); + path = a_Dpip_get_attr_l(Buf, BufSize, "path"); + date = a_Dpip_get_attr_l(Buf, BufSize, "date"); - if (cmd && strcmp(cmd, "DpiBye") == 0) { - dFree(cmd); - MSG("Cookies dpi (pid %d): Got DpiBye.\n", (int)getpid()); - exit(0); + st = Cookies_set(cookie, host, path, date); - } else if (cmd && strcmp(cmd, "set_cookie") == 0) { dFree(cmd); - cookie = a_Dpip_get_attr(Buf, BufSize, "cookie"); - host = a_Dpip_get_attr(Buf, BufSize, "host"); - path = a_Dpip_get_attr(Buf, BufSize, "path"); - p = a_Dpip_get_attr(Buf, BufSize, "port"); - port = strtol(p, NULL, 10); - dFree(p); - - Cookies_set(cookie, host, path, port); + cmd = a_Dpip_build_cmd("cmd=%s msg=%s", "set_cookie_answer", + st == 0 ? "ok" : "not set"); + a_Dpip_dsh_write_str(sh, 1, cmd); + dFree(date); dFree(path); dFree(host); dFree(cookie); - return 2; + ret = 2; - } else if (cmd && strcmp(cmd, "get_cookie") == 0) { - dFree(cmd); - scheme = a_Dpip_get_attr(Buf, BufSize, "scheme"); - host = a_Dpip_get_attr(Buf, BufSize, "host"); - path = a_Dpip_get_attr(Buf, BufSize, "path"); - p = a_Dpip_get_attr(Buf, BufSize, "port"); - port = strtol(p, NULL, 10); - dFree(p); - - cookie = Cookies_get(host, path, scheme, port); + } else if (strcmp(cmd, "get_cookie") == 0) { + char *scheme = a_Dpip_get_attr_l(Buf, BufSize, "scheme"); + + host = a_Dpip_get_attr_l(Buf, BufSize, "host"); + path = a_Dpip_get_attr_l(Buf, BufSize, "path"); + + cookie = Cookies_get(host, path, scheme); dFree(scheme); dFree(path); dFree(host); + dFree(cmd); cmd = a_Dpip_build_cmd("cmd=%s cookie=%s", "get_cookie_answer", cookie); - if (sock_handler_write_str(sh, 1, cmd)) { + if (a_Dpip_dsh_write_str(sh, 1, cmd)) { ret = 1; } else { - _MSG("sock_handler_write_str: SUCCESS cmd={%s}\n", cmd); + _MSG("a_Dpip_dsh_write_str: SUCCESS cmd={%s}\n", cmd); ret = 2; } dFree(cookie); - dFree(cmd); - - return ret; } + dFree(cmd); - return 0; + return ret; } /* -- Termination handlers ----------------------------------------------- */ @@ -1415,13 +1514,13 @@ static void termination_handler(int signum) /* * -- MAIN ------------------------------------------------------------------- */ -int main (void) { - struct sockaddr_un spun; - int temp_sock_descriptor; +int main(void) { + struct sockaddr_in sin; socklen_t address_size; + ClientInfo *client; + int sock_fd, code; char *buf; - int code; - SockHandler *sh; + Dsh *sh; /* Arrange the cleanup function for terminations via exit() */ atexit(cleanup); @@ -1441,36 +1540,42 @@ int main (void) { exit(1); /* some OSes may need this... */ - address_size = sizeof(struct sockaddr_un); + address_size = sizeof(struct sockaddr_in); while (1) { - temp_sock_descriptor = - accept(STDIN_FILENO, (struct sockaddr *)&spun, &address_size); - if (temp_sock_descriptor == -1) { + sock_fd = accept(STDIN_FILENO, (struct sockaddr *)&sin, &address_size); + if (sock_fd == -1) { perror("[accept]"); exit(1); } - /* create the SockHandler structure */ - sh = sock_handler_new(temp_sock_descriptor,temp_sock_descriptor,8*1024); + /* create the Dsh structure */ + sh = a_Dpip_dsh_new(sock_fd, sock_fd, 8*1024); + client = dNew(ClientInfo,1); + client->sh = sh; + client->status = 0; while (1) { code = 1; - if ((buf = sock_handler_read(sh)) != NULL) { + if ((buf = a_Dpip_dsh_read_token(sh, 1)) != NULL) { /* Let's see what we fished... */ _MSG(" buf = {%s}\n", buf); - code = srv_parse_buf(sh, buf, strlen(buf)); + code = srv_parse_tok(sh, client, buf); + dFree(buf); } + _MSG(" code = %d %s\n", code, code == 1 ? "EXIT" : "BREAK"); - if (code == 1) + if (code == 1) { exit(1); - else if (code == 2) + } else if (code == 2) { break; + } } - _MSG("Closing SockHandler\n"); - sock_handler_close(sh); - sock_handler_free(sh); + _MSG("Closing Dsh\n"); + a_Dpip_dsh_close(sh); + a_Dpip_dsh_free(sh); + dFree(client); }/*while*/ |