diff options
Diffstat (limited to 'dpi/cookies.c')
-rw-r--r-- | dpi/cookies.c | 1466 |
1 files changed, 1466 insertions, 0 deletions
diff --git a/dpi/cookies.c b/dpi/cookies.c new file mode 100644 index 00000000..f669eb69 --- /dev/null +++ b/dpi/cookies.c @@ -0,0 +1,1466 @@ +/* + * File: cookies.c + * Cookies server. + * + * Copyright 2001 Lars Clausen <lrclause@cs.uiuc.edu> + * Jörgen Viksell <jorgen.viksell@telia.com> + * Copyright 2002-2006 Jorge Arellano Cid <jcid@dillo.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + */ + +/* Handling of cookies takes place here. + * This implementation aims to follow RFC 2965: + * http://www.cis.ohio-state.edu/cs/Services/rfc/rfc-text/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. + */ + +#ifdef DISABLE_COOKIES + +int main(void) +{ + return 0; /* never called */ +} + +#else + + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/un.h> +#include <fcntl.h> +#include <unistd.h> +#include <errno.h> +#include <stddef.h> +#include <string.h> +#include <stdlib.h> +#include <stdio.h> +#include <time.h> /* for time() and time_t */ +#include <ctype.h> +#include <netdb.h> +#include <signal.h> +#include "dpiutil.h" +#include "../dpip/dpip.h" + + +/* + * Debugging macros + */ +#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() + * + * Make sure there's space for 'num_items' items within the list + * (First, allocate an 'alloc_step' sized chunk, after that, double the + * list size --to make it faster) + */ +#define a_List_add(list,num_items,alloc_step) \ + if (!list) { \ + list = dMalloc(alloc_step * sizeof((*list))); \ + } \ + if (num_items >= alloc_step){ \ + while ( num_items >= alloc_step ) \ + alloc_step <<= 1; \ + list = dRealloc(list, alloc_step * sizeof((*list))); \ + } + +/* The maximum length of a line in the cookie file */ +#define LINE_MAXLEN 4096 + +typedef enum { + COOKIE_ACCEPT, + COOKIE_ACCEPT_SESSION, + COOKIE_DENY +} CookieControlAction; + +typedef struct { + char *domain; + CookieControlAction action; +} CookieControl; + +typedef struct { + char *domain; + Dlist *dlist; +} CookieNode; + +typedef struct { + char *name; + char *value; + char *domain; + char *path; + time_t expires_at; + uint_t version; + char *comment; + char *comment_url; + bool_t secure; + bool_t session_only; + Dlist *ports; +} CookieData_t; + +/* + * Local data + */ + +/* List of CookieNode. Each node holds a domain and its list of cookies */ +static Dlist *cookies; + +/* Variables for access control */ +static CookieControl *ccontrol = NULL; +static int num_ccontrol = 0; +static int num_ccontrol_max = 1; +static CookieControlAction default_action = COOKIE_DENY; + +static bool_t disabled; +static FILE *file_stream; +static char *cookies_txt_header_str = +"# HTTP Cookie File\n" +"# http://www.netscape.com/newsref/std/cookie_spec.html\n" +"# This is a generated file! Do not edit.\n\n"; + + +/* + * Forward declarations + */ + +static FILE *Cookies_fopen(const char *file, char *init_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 + */ +int Cookie_node_cmp(const void *v1, const void *v2) +{ + const CookieNode *n1 = v1, *n2 = v2; + + return strcmp(n1->domain, n2->domain); +} + +/* + * Compare function for searching a cookie node by domain + */ +int Cookie_node_by_domain_cmp(const void *v1, const void *v2) +{ + const CookieNode *node = v1; + const char *domain = v2; + + return strcmp(node->domain, domain); +} + +/* + * Return a file pointer. If the file doesn't exist, try to create it, + * with the optional 'init_str' as its content. + */ +static FILE *Cookies_fopen(const char *filename, char *init_str) +{ + FILE *F_in; + int fd; + + if ((F_in = fopen(filename, "r+")) == 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)); + close(fd); + + MSG("Created file: %s\n", filename); + F_in = Cookies_fopen(filename, NULL); + } else { + MSG("Could not create file: %s!\n", filename); + } + } + + /* set close on exec */ + fcntl(fileno(F_in), F_SETFD, FD_CLOEXEC | fcntl(fileno(F_in), F_GETFD)); + + return F_in; +} + +static void Cookies_free_cookie(CookieData_t *cookie) +{ + dFree(cookie->name); + dFree(cookie->value); + dFree(cookie->domain); + dFree(cookie->path); + dFree(cookie->comment); + dFree(cookie->comment_url); + dList_free(cookie->ports); + dFree(cookie); +} + +/* + * Initialize the cookies module + * (The 'disabled' variable is writable only within Cookies_init) + */ +void Cookies_init() +{ + 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, 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); + + /* Get all lines in the file */ + while (!feof(file_stream)) { + line[0] = '\0'; + fgets(line, LINE_MAXLEN, file_stream); + + /* Remove leading and trailing whitespaces */ + dStrstrip(line); + + if ((line[0] != '\0') && (line[0] != '#')) { + /* + * Split the row into pieces using a tab as the delimiter. + * 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[4] Timestamp of expire date + * pieces[5] Name of the cookie + * pieces[6] Value of the cookie + */ + CookieControlAction action; + char *piece; + char *line_marker = line; + + 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 */ + 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); + cookie->name = dStrdup(dStrsep(&line_marker, "\t")); + cookie->value = 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.txt file!\n"); + Cookies_free_cookie(cookie); + continue; + } + + 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; + } + + /* Save cookie in memory */ + Cookies_add_cookie(cookie); + } + } + + filename = dStrconcat(dGethomedir(), "/.dillo/cookies", NULL); + if ((old_cookies_file_stream = fopen(filename, "r")) != NULL) { + dFree(filename); + 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; + } + + 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; + } + + /* Save cookie in memory */ + Cookies_add_cookie(cookie); + } + } + fclose(old_cookies_file_stream); + } else { + dFree(filename); + } + +} + +/* + * Flush cookies to disk and free all the memory allocated. + */ +void Cookies_save_and_free() +{ + int i, fd; + CookieNode *node; + CookieData_t *cookie; + +#ifndef HAVE_LOCKF + struct flock lck; +#endif + + if (disabled) + return; + + rewind(file_stream); + fd = fileno(file_stream); + ftruncate(fd, 0); + fprintf(file_stream, 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", + cookie->domain, + cookie->path, + cookie->secure ? "TRUE" : "FALSE", + (long)cookie->expires_at, + cookie->name, + cookie->value); + /* dFree(ports_str); */ + } + + Cookies_free_cookie(cookie); + } + dList_remove(cookies, node); + dFree(node->domain); + dList_free(node->dlist); + dFree(node); + } + +#ifdef HAVE_LOCKF + lockf(fd, F_ULOCK, 0); +#else /* POSIX file lock */ + lck.l_start = 0; /* start at beginning of file */ + lck.l_len = 0; /* lock entire file */ + lck.l_type = F_UNLCK; + lck.l_whence = SEEK_SET; /* absolute offset */ + + 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" +}; + +/* + * Take a months name and return a number between 1-12. + * E.g. 'April' -> 4 + */ +static int Cookies_get_month(const char *month_name) +{ + int i; + + for (i = 1; i <= 12; i++) { + if (!dStrncasecmp(months[i], month_name, 3)) + return i; + } + return 0; +} + +/* + * Return a local timestamp from a GMT date string + * Accept: RFC-1123 | RFC-850 | ANSI asctime | Old Netscape format. + * + * Wdy, DD-Mon-YY HH:MM:SS GMT + * Wdy, DD-Mon-YYYY HH:MM:SS GMT + * Weekday, DD-Mon-YY HH:MM:SS GMT + * Weekday, DD-Mon-YYYY HH:MM:SS GMT + * Tue May 21 13:46:22 1991\n + * Tue May 21 13:46:22 1991 + * + * (return 0 on malformed date string syntax) + */ +static time_t Cookies_create_timestamp(const char *expires) +{ + 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)) { + /* 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) && + (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); + + } else { + MSG("%s%s\n", E_msg, expires); + return (time_t) 0; + } + + /* 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; + } + + /* Calculate local timestamp. + * [stolen from Lynx... (http://lynx.browser.org)] */ + month -= 3; + if (month < 0) { + month += 12; + year--; + } + + day += (year - 1968) * 1461 / 4; + day += ((((month * 153) + 2) / 5) - 672); + ret = (time_t)((day * 60 * 60 * 24) + + (hour * 60 * 60) + + (minutes * 60) + + seconds); + + MSG("Expires in %ld seconds, at %s", + (long)ret - time(NULL), ctime(&ret)); + + return ret; +} + +/* + * Parse a string containing a list of port numbers. + */ +static void Cookies_parse_ports(int url_port, CookieData_t *cookie, + const char *port_str) +{ + 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)); + } + } + dFree(str); + } +} + +/* + * Build a string of the ports in 'cookie'. + */ +static char *Cookies_build_ports_str(CookieData_t *cookie) +{ + 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; +} + +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; + } + + node = dList_find_sorted(cookies, cookie->domain,Cookie_node_by_domain_cmp); + domain_cookies = (node) ? node->dlist : 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); + } + } + + /* 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); + } else { + dList_append(domain_cookies, cookie); + } +} + +/* + * Remove the cookie from the domain list. + * If the domain list is empty, remove the node too. + * Free the cookie. + */ +static void Cookies_remove_cookie(CookieData_t *cookie) +{ + CookieNode *node; + + 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"); + } + + Cookies_free_cookie(cookie); +} + +/* + * Return the attribute that is present at *cookie_str. This function + * will also attempt to advance cookie_str past any equal-sign. + */ +static char *Cookies_parse_attr(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; + } + } + + return NULL; +} + +/* + * Get the value starting at *cookie_str. + * broken_syntax: watch out for stupid syntax (comma in unquoted string...) + */ +static char *Cookies_parse_value(char **cookie_str, + bool_t broken_syntax, + bool_t keep_quotes) +{ + 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; + + while (*tmp != '\0' && *tmp != ';' && *tmp != ',') + tmp++; + + *cookie_str = (*tmp == ';') ? tmp + 1 : tmp; + + 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; + } + } + /* keep i as an index to the last char */ + --i; + + if ((str[0] == '\'' || str[0] == '"') && !keep_quotes) { + return i > 1 ? dStrndup(str + 1, i - 1) : NULL; + } else { + return dStrndup(str, i); + } +} + +/* + * Parse one cookie... + */ +static CookieData_t *Cookies_parse_one(int url_port, char **cookie_str) +{ + CookieData_t *cookie; + char *str = *cookie_str; + char *attr; + char *value; + int num_attr = 0; + bool_t max_age = FALSE; + bool_t discard = FALSE; + + cookie = dNew0(CookieData_t, 1); + cookie->session_only = TRUE; + + /* 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 != ',') { + /* Skip whitespace */ + while (isspace(*str)) + str++; + + /* Get attribute */ + attr = Cookies_parse_attr(&str); + if (!attr) { + MSG("Failed to parse cookie attribute!\n"); + Cookies_free_cookie(cookie); + return NULL; + } + + /* 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); + } else if (dStrcasecmp(attr, "Path") == 0) { + value = Cookies_parse_value(&str, FALSE, FALSE); + cookie->path = value; + } else if (dStrcasecmp(attr, "Domain") == 0) { + value = Cookies_parse_value(&str, FALSE, FALSE); + 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("Failed to parse cookie value!\n"); + Cookies_free_cookie(cookie); + return NULL; + } + } + } 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); + } else { + MSG("Failed to parse cookie value!\n"); + Cookies_free_cookie(cookie); + return NULL; + } + } + } 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); + dFree(value); + } else { + MSG("Failed to parse cookie value!\n"); + Cookies_free_cookie(cookie); + return NULL; + } + } else if (dStrcasecmp(attr, "Secure") == 0) { + cookie->secure = TRUE; + } else { + /* Oops! this can't be good... */ + dFree(attr); + Cookies_free_cookie(cookie); + MSG("Cookie contains illegal attribute!\n"); + return NULL; + } + + dFree(attr); + num_attr++; + } + + *cookie_str = (*str == ',') ? str + 1 : str; + + if (cookie->name && cookie->value) { + return cookie; + } else { + MSG("Cookie missing name and/or value!\n"); + Cookies_free_cookie(cookie); + return NULL; + } +} + +/* + * Iterate the cookie string until we catch all cookies. + * Return Value: a list with all the cookies! (or NULL upon error) + */ +static Dlist *Cookies_parse_string(int url_port, char *cookie_string) +{ + CookieData_t *cookie; + Dlist *ret = NULL; + char *str = cookie_string; + + /* The string may contain several cookies separated by comma. + * We'll iterate until we've catched them all */ + while (*str) { + cookie = Cookies_parse_one(url_port, &str); + + if (cookie) { + if (!ret) + ret = dList_new(4); + dList_append(ret, cookie); + } else { + MSG("Malformed cookie field, ignoring cookie: %s\n", cookie_string); + } + } + + return ret; +} + +/* + * Compare cookies by name and path (return 0 if equal) + */ +static int Cookies_cmp(const void *a, const void *b) +{ + const CookieData_t *ca = a, *cb = b; + int ret; + + if (!(ret = strcmp(ca->name, cb->name))) + ret = strcmp(ca->path, cb->path); + return ret; +} + +/* + * Validate cookies domain against some security checks. + */ +static bool_t Cookies_validate_domain(CookieData_t *cookie, char *host, + 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 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); + 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; + } + + /* A valid domain must have at least two dots in it */ + /* NOTE: this breaks cookies on localhost... */ + if (dots < 2) { + 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; + } + } + + return TRUE; +} + +/* + * Strip of the filename from a full path + */ +static char *Cookies_strip_path(const char *path) +{ + char *ret; + uint_t len; + + if (path) { + len = strlen(path); + + while (len && path[len] != '/') + len--; + ret = dStrndup(path, len + 1); + } else { + ret = dStrdup("/"); + } + + return ret; +} + +/* + * Set the value corresponding to the cookie string + */ +void Cookies_set(char *cookie_string, char *url_host, + char *url_path, int url_port) +{ + CookieControlAction action; + CookieData_t *cookie; + Dlist *list; + int i; + + if (disabled) + return; + + action = Cookies_control_check_domain(url_host); + if (action == COOKIE_DENY) { + MSG("denied SET for %s\n", url_host); + return; + } + + 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)) { + if (action == COOKIE_ACCEPT_SESSION) + cookie->session_only = TRUE; + Cookies_add_cookie(cookie); + } else { + MSG("Rejecting cookie for %s from host %s path %s\n", + cookie->domain, url_host, url_path); + Cookies_free_cookie(cookie); + } + } + dList_free(list); + } +} + +/* + * Compare the cookie with the supplied data to see if it matches + */ +static bool_t Cookies_match(CookieData_t *cookie, int port, + const char *path, bool_t is_ssl) +{ + void *data; + int i; + + /* 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; + } + return FALSE; + } + + /* It's a match */ + return TRUE; +} + +/* + * Return a string that contains all relevant cookies as headers. + */ +char *Cookies_get(char *url_host, char *url_path, + char *url_scheme, int url_port) +{ + char *domain_str, *q, *str, *path; + CookieData_t *cookie; + Dlist *matching_cookies; + CookieNode *node; + Dlist *domain_cookies; + bool_t is_ssl; + Dstr *cookie_dstring; + int i; + + if (disabled) + return dStrdup(""); + + 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); + } + } + } + + /* 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_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); + return str; +} + +/* ------------------------------------------------------------- + * Access control routines + * ------------------------------------------------------------- */ + + +/* + * Get the cookie control rules (from cookiesrc). + * Return value: + * 0 = Parsed OK, with cookies enabled + * 1 = Parsed OK, with cookies disabled + * 2 = Can't open the control file + */ +static int Cookie_control_init(void) +{ + CookieControl cc; + FILE *stream; + char *filename; + char line[LINE_MAXLEN]; + char domain[LINE_MAXLEN]; + char rule[LINE_MAXLEN]; + int i, j; + bool_t enabled = FALSE; + + /* Get a file pointer */ + filename = dStrconcat(dGethomedir(), "/.dillo/cookiesrc", NULL); + stream = Cookies_fopen(filename, "DEFAULT DENY\n"); + dFree(filename); + + if (!stream) + return 2; + + /* Get all lines in the file */ + while (!feof(stream)) { + line[0] = '\0'; + fgets(line, LINE_MAXLEN, stream); + + /* Remove leading and trailing whitespaces */ + dStrstrip(line); + + if (line[0] != '\0' && line[0] != '#') { + i = 0; + j = 0; + + /* Get the domain */ + while (!isspace(line[i])) + domain[j++] = line[i++]; + domain[j] = '\0'; + + /* Skip past whitespaces */ + i++; + while (isspace(line[i])) + i++; + + /* Get the rule */ + j = 0; + while (line[i] != '\0' && !isspace(line[i])) + rule[j++] = line[i++]; + rule[j] = '\0'; + + if (dStrcasecmp(rule, "ACCEPT") == 0) + cc.action = COOKIE_ACCEPT; + else if (dStrcasecmp(rule, "ACCEPT_SESSION") == 0) + cc.action = COOKIE_ACCEPT_SESSION; + else if (dStrcasecmp(rule, "DENY") == 0) + cc.action = COOKIE_DENY; + else { + MSG("Cookies: rule '%s' for domain '%s' is not recognised.\n", + rule, domain); + continue; + } + + cc.domain = dStrdup(domain); + if (dStrcasecmp(cc.domain, "DEFAULT") == 0) { + /* Set the default action */ + default_action = cc.action; + dFree(cc.domain); + } else { + a_List_add(ccontrol, num_ccontrol, num_ccontrol_max); + ccontrol[num_ccontrol++] = cc; + } + + if (cc.action != COOKIE_DENY) + enabled = TRUE; + } + } + + fclose(stream); + + return (enabled ? 0 : 1); +} + +/* + * Check the rules for an appropriate action for this domain + */ +static CookieControlAction Cookies_control_check_domain(const char *domain) +{ + int i, diff; + + for (i = 0; i < num_ccontrol; i++) { + if (ccontrol[i].domain[0] == '.') { + diff = strlen(domain) - strlen(ccontrol[i].domain); + if (diff >= 0) { + if (dStrcasecmp(domain + diff, ccontrol[i].domain) != 0) + continue; + } else { + continue; + } + } else { + if (dStrcasecmp(domain, ccontrol[i].domain) != 0) + continue; + } + + /* If we got here we have a match */ + return( ccontrol[i].action ); + } + + return default_action; +} + +/* -- Dpi parser ----------------------------------------------------------- */ + +/* + * Parse a data stream (dpi protocol) + * 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) +{ + char *p, *cmd, *cookie, *host, *path, *scheme; + int port; + + if (!(p = strchr(Buf, '>'))) { + /* Haven't got a full tag */ + MSG("Haven't got a full tag!\n"); + return 1; + } + + cmd = a_Dpip_get_attr(Buf, BufSize, "cmd"); + + if (cmd && strcmp(cmd, "DpiBye") == 0) { + dFree(cmd); + MSG("Cookies dpi (pid %d): Got DpiBye.\n", (int)getpid()); + exit(0); + + } 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); + + dFree(path); + dFree(host); + dFree(cookie); + return 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); + dFree(scheme); + dFree(path); + dFree(host); + + cmd = a_Dpip_build_cmd("cmd=%s cookie=%s", "get_cookie_answer", cookie); + + if (sock_handler_write_str(sh, 1, cmd)) { + dFree(cookie); + dFree(cmd); + return 1; + } + dFree(cookie); + dFree(cmd); + + return 2; + } + + return 0; +} + +/* -- Termination handlers ----------------------------------------------- */ +/* + * (was to delete the local namespace socket), + * but this is handled by 'dpid' now. + */ +static void cleanup(void) +{ + Cookies_save_and_free(); + MSG("cleanup\n"); + /* no more cleanup required */ +} + +/* + * Perform any necessary cleanups upon abnormal termination + */ +static void termination_handler(int signum) +{ + exit(signum); +} + + +/* + * -- MAIN ------------------------------------------------------------------- + */ +int main (void) { + struct sockaddr_un spun; + int temp_sock_descriptor; + socklen_t address_size; + char *buf; + int code; + SockHandler *sh; + + /* Arrange the cleanup function for terminations via exit() */ + atexit(cleanup); + + /* Arrange the cleanup function for abnormal terminations */ + if (signal (SIGINT, termination_handler) == SIG_IGN) + signal (SIGINT, SIG_IGN); + if (signal (SIGHUP, termination_handler) == SIG_IGN) + signal (SIGHUP, SIG_IGN); + if (signal (SIGTERM, termination_handler) == SIG_IGN) + signal (SIGTERM, SIG_IGN); + + Cookies_init(); + MSG("(v.1) accepting connections...\n"); + + if (disabled) + exit(1); + + /* some OSes may need this... */ + address_size = sizeof(struct sockaddr_un); + + while (1) { + temp_sock_descriptor = + accept(STDIN_FILENO, (struct sockaddr *)&spun, &address_size); + if (temp_sock_descriptor == -1) { + perror("[accept]"); + exit(1); + } + + /* create the SockHandler structure */ + sh = sock_handler_new(temp_sock_descriptor,temp_sock_descriptor,8*1024); + + while (1) { + code = 1; + if ((buf = sock_handler_read(sh)) != NULL) { + /* Let's see what we fished... */ + code = srv_parse_buf(sh, buf, strlen(buf)); + } + if (code == 1) + exit(1); + else if (code == 2) + break; + } + + _MSG("Closing SockHandler\n"); + sock_handler_close(sh); + sock_handler_free(sh); + + }/*while*/ +} + +#endif /* !DISABLE_COOKIES */ |