1 /* Internal cookies implementation */
10 #include <sys/types.h>
11 #include <sys/stat.h> /* OS/2 needs this after sys/types.h */
22 #include "bfu/dialog.h"
23 #include "cookies/cookies.h"
24 #include "cookies/dialogs.h"
25 #include "cookies/parser.h"
26 #include "config/home.h"
27 #include "config/kbdbind.h"
28 #include "config/options.h"
29 #include "intl/gettext/libintl.h"
30 #include "main/module.h"
31 #include "main/object.h"
32 #include "main/select.h"
33 #include "protocol/date.h"
34 #include "protocol/header.h"
35 #include "protocol/protocol.h"
36 #include "protocol/uri.h"
37 #include "session/session.h"
38 #include "terminal/terminal.h"
39 #include "util/conv.h"
41 #include "util/error.h"
43 #include "util/file.h"
44 #include "util/memory.h"
45 #include "util/secsave.h"
46 #include "util/string.h"
47 #include "util/time.h"
49 #define COOKIES_FILENAME "cookies"
52 static int cookies_nosave
= 0;
54 static INIT_LIST_HEAD(cookies
);
57 LIST_HEAD(struct c_domain
);
59 unsigned char domain
[1]; /* Must be at end of struct. */
62 static INIT_LIST_HEAD(c_domains
);
64 static INIT_LIST_HEAD(cookie_servers
);
66 /* Only @set_cookies_dirty may make this nonzero. */
67 static int cookies_dirty
= 0;
72 COOKIES_ACCEPT_POLICY
,
74 COOKIES_PARANOID_SECURITY
,
81 static struct option_info cookies_options
[] = {
82 INIT_OPT_TREE("", N_("Cookies"),
84 N_("Cookies options.")),
86 INIT_OPT_INT("cookies", N_("Accept policy"),
88 COOKIES_ACCEPT_NONE
, COOKIES_ACCEPT_ALL
, COOKIES_ACCEPT_ALL
,
89 N_("Cookies accepting policy:\n"
90 "0 is accept no cookies\n"
91 "1 is ask for confirmation before accepting cookie\n"
92 "2 is accept all cookies")),
94 INIT_OPT_INT("cookies", N_("Maximum age"),
95 "max_age", 0, -1, 10000, -1,
96 N_("Cookie maximum age (in days):\n"
97 "-1 is use cookie's expiration date if any\n"
98 "0 is force expiration at the end of session, ignoring cookie's\n"
100 "1+ is use cookie's expiration date, but limit age to the given\n"
103 INIT_OPT_BOOL("cookies", N_("Paranoid security"),
104 "paranoid_security", 0, 0,
105 N_("When enabled, we'll require three dots in cookies domain for all\n"
106 "non-international domains (instead of just two dots). Some countries\n"
107 "have generic second level domains (eg. .com.pl, .co.uk) and allowing\n"
108 "sites to set cookies for these generic domains could potentially be\n"
109 "very bad. Note, it is off by default as it breaks a lot of sites.")),
111 INIT_OPT_BOOL("cookies", N_("Saving"),
113 N_("Whether cookies should be loaded from and save to disk.")),
115 INIT_OPT_BOOL("cookies", N_("Resaving"),
117 N_("Save cookies after each change in cookies list? No effect when\n"
118 "cookie saving (cookies.save) is off.")),
123 #define get_opt_cookies(which) cookies_options[(which)].option.value
124 #define get_cookies_accept_policy() get_opt_cookies(COOKIES_ACCEPT_POLICY).number
125 #define get_cookies_max_age() get_opt_cookies(COOKIES_MAX_AGE).number
126 #define get_cookies_paranoid_security() get_opt_cookies(COOKIES_PARANOID_SECURITY).number
127 #define get_cookies_save() get_opt_cookies(COOKIES_SAVE).number
128 #define get_cookies_resave() get_opt_cookies(COOKIES_RESAVE).number
130 static struct cookie_server
*
131 get_cookie_server(unsigned char *host
, int hostlen
)
133 struct cookie_server
*sort_spot
= NULL
;
134 struct cookie_server
*cs
;
136 foreach (cs
, cookie_servers
) {
137 /* XXX: We must count with cases like "x.co" vs "x.co.uk"
139 int cslen
= strlen(cs
->host
);
140 int cmp
= strncasecmp(cs
->host
, host
, hostlen
);
142 if (!sort_spot
&& (cmp
> 0 || (cmp
== 0 && cslen
> hostlen
))) {
143 /* This is the first @cs with name greater than @host,
144 * our dream sort spot! */
145 sort_spot
= cs
->prev
;
148 if (cmp
|| cslen
!= hostlen
)
155 cs
= mem_calloc(1, sizeof(*cs
) + hostlen
);
156 if (!cs
) return NULL
;
158 memcpy(cs
->host
, host
, hostlen
);
159 object_nolock(cs
, "cookie_server");
161 cs
->box_item
= add_listbox_folder(&cookie_browser
, NULL
, cs
);
166 /* No sort spot found, therefore this sorts at the end. */
167 add_to_list_end(cookie_servers
, cs
);
168 del_from_list(cs
->box_item
);
169 add_to_list_end(cookie_browser
.root
.child
, cs
->box_item
);
171 /* Sort spot found, sort after it. */
172 add_at_pos(sort_spot
, cs
);
173 if (sort_spot
!= (struct cookie_server
*) &cookie_servers
) {
174 del_from_list(cs
->box_item
);
175 add_at_pos(sort_spot
->box_item
, cs
->box_item
);
176 } /* else we are already at the top anyway. */
183 done_cookie_server(struct cookie_server
*cs
)
186 if (is_object_used(cs
)) return;
188 if (cs
->box_item
) done_listbox_item(&cookie_browser
, cs
->box_item
);
194 done_cookie(struct cookie
*c
)
196 if (c
->box_item
) done_listbox_item(&cookie_browser
, c
->box_item
);
197 if (c
->server
) done_cookie_server(c
->server
);
198 mem_free_if(c
->name
);
199 mem_free_if(c
->value
);
200 mem_free_if(c
->path
);
201 mem_free_if(c
->domain
);
205 /* The cookie @c can be either in @cookies or in @cookie_queries.
206 * Because changes in @cookie_queries should not affect the cookie
207 * file, this function does not set @cookies_dirty. Instead, the
208 * caller must do that if appropriate. */
210 delete_cookie(struct cookie
*c
)
217 /* Check whether cookie's domain matches server.
218 * It returns 1 if ok, 0 else. */
220 is_domain_security_ok(unsigned char *domain
, unsigned char *server
, int server_len
)
226 if (domain
[0] == '.') domain
++;
227 domain_len
= strlen(domain
);
229 /* Match domain and server.. */
231 /* XXX: Hmm, can't we use strlcasecmp() here? --pasky */
233 if (domain_len
> server_len
) return 0;
235 /* Ensure that the domain is atleast a substring of the server before
237 if (strncasecmp(domain
, server
+ server_len
- domain_len
, domain_len
))
240 /* Allow domains which are same as servers. --<rono@sentuny.com.au> */
241 /* Mozilla does it as well ;))) and I can't figure out any security
243 if (server_len
== domain_len
)
246 /* Check whether the server is an IP address, and require an exact host
247 * match for the cookie, so any chance of IP address funkiness is
248 * eliminated (e.g. the alias 127.1 domain-matching 99.54.127.1). Idea
249 * from mozilla. (bug 562) */
250 if (is_ip_address(server
, server_len
))
253 /* Also test if domain is secure en ugh.. */
257 if (get_cookies_paranoid_security()) {
258 /* This is somehow controversial attempt (by the way violating
259 * RFC) to increase cookies security in national domains, done
260 * by Mikulas. As it breaks a lot of sites, I decided to make
261 * this optional and off by default. I also don't think this
262 * improves security considerably, as it's SITE'S fault and
263 * also no other browser probably does it. --pasky */
264 /* Mikulas' comment: Some countries have generic 2-nd level
265 * domains (like .com.pl, .co.uk ...) and it would be very bad
266 * if someone set cookies for these generic domains. Imagine
267 * for example that server http://brutalporn.com.pl sets cookie
268 * Set-Cookie: user_is=perverse_pig; domain=.com.pl -- then
269 * this cookie would be sent to all commercial servers in
273 if (domain_len
> 0) {
274 int pos
= end_with_known_tld(domain
, domain_len
);
276 if (pos
>= 1 && domain
[pos
- 1] == '.')
281 for (i
= 0; domain
[i
]; i
++)
282 if (domain
[i
] == '.' && !--need_dots
)
285 if (need_dots
> 0) return 0;
290 set_cookie(struct uri
*uri
, unsigned char *str
)
292 unsigned char *secure
, *path
;
293 struct cookie
*cookie
;
294 struct cookie_str cstr
;
297 if (get_cookies_accept_policy() == COOKIES_ACCEPT_NONE
)
301 DBG("set_cookie -> (%s) %s", struri(uri
), str
);
304 if (!parse_cookie_str(&cstr
, str
)) return;
306 cookie
= mem_calloc(1, sizeof(*cookie
));
309 object_nolock(cookie
, "cookie"); /* Debugging purpose. */
311 /* Fill main fields */
313 cookie
->name
= memacpy(str
, cstr
.nam_end
- str
);
314 cookie
->value
= memacpy(cstr
.val_start
, cstr
.val_end
- cstr
.val_start
);
315 cookie
->server
= get_cookie_server(uri
->host
, uri
->hostlen
);
316 cookie
->domain
= parse_header_param(str
, "domain");
317 if (!cookie
->domain
) cookie
->domain
= memacpy(uri
->host
, uri
->hostlen
);
319 /* Now check that all is well */
323 || !cookie
->server
) {
329 /* We don't actually set ->accept at the moment. But I have kept it
330 * since it will maybe help to fix bug 77 - Support for more
331 * finegrained control upon accepting of cookies. */
332 if (!cookie
->server
->accept
) {
341 /* Set cookie expiration if needed.
342 * Cookie expires at end of session by default,
343 * set to 0 by calloc().
346 * -1 is use cookie's expiration date if any
347 * 0 is force expiration at the end of session,
348 * ignoring cookie's expiration date
349 * 1+ is use cookie's expiration date,
350 * but limit age to the given number of days.
353 max_age
= get_cookies_max_age();
355 unsigned char *date
= parse_header_param(str
, "expires");
358 time_t expires
= parse_date(&date
, NULL
, 0, 1); /* Convert date to seconds. */
364 int seconds
= max_age
*24*3600;
365 time_t deadline
= time(NULL
) + seconds
;
367 if (expires
> deadline
) /* Over-aged cookie ? */
371 cookie
->expires
= expires
;
376 path
= parse_header_param(str
, "path");
378 unsigned char *path_end
;
380 path
= get_uri_string(uri
, URI_PATH
);
386 for (path_end
= path
+ strlen(path
) - 1;
387 path_end
>= path
; path_end
--) {
388 if (*path_end
== '/') {
396 || path
[strlen(path
) - 1] != '/')
397 add_to_strn(&path
, "/");
399 if (path
[0] != '/') {
400 add_to_strn(&path
, "x");
401 memmove(path
+ 1, path
, strlen(path
) - 1);
407 if (cookie
->domain
[0] == '.')
408 memmove(cookie
->domain
, cookie
->domain
+ 1,
409 strlen(cookie
->domain
));
411 /* cookie->secure is set to 0 by default by calloc(). */
412 secure
= parse_header_param(str
, "secure");
420 DBG("Got cookie %s = %s from %s, domain %s, "
421 "expires at %d, secure %d", cookie
->name
,
422 cookie
->value
, cookie
->server
->host
, cookie
->domain
,
423 cookie
->expires
, cookie
->secure
);
427 if (!is_domain_security_ok(cookie
->domain
, uri
->host
, uri
->hostlen
)) {
429 DBG("Domain security violated: %s vs %.*s", cookie
->domain
,
430 uri
->hostlen
, uri
->host
);
432 mem_free(cookie
->domain
);
433 cookie
->domain
= memacpy(uri
->host
, uri
->hostlen
);
436 /* We have already check COOKIES_ACCEPT_NONE */
437 if (get_cookies_accept_policy() == COOKIES_ACCEPT_ASK
) {
438 add_to_list(cookie_queries
, cookie
);
439 add_questions_entry(accept_cookie_dialog
, cookie
);
443 accept_cookie(cookie
);
447 accept_cookie(struct cookie
*cookie
)
450 struct listbox_item
*root
= cookie
->server
->box_item
;
454 cookie
->box_item
= add_listbox_leaf(&cookie_browser
, root
, cookie
);
456 /* Do not weed out duplicates when loading the cookie file. It doesn't
457 * scale at all, being O(N^2) and taking about 2s with my 500 cookies
458 * (so if you don't notice that 100ms with your 100 cookies, that's
459 * not an argument). --pasky */
460 if (!cookies_nosave
) {
461 struct cookie
*c
, *next
;
463 foreachsafe (c
, next
, cookies
) {
464 if (strcasecmp(c
->name
, cookie
->name
)
465 || strcasecmp(c
->domain
, cookie
->domain
))
469 /* @set_cookies_dirty will be called below. */
473 add_to_list(cookies
, cookie
);
476 /* XXX: This crunches CPU too. --pasky */
477 foreach (cd
, c_domains
)
478 if (!strcasecmp(cd
->domain
, cookie
->domain
))
481 domain_len
= strlen(cookie
->domain
);
482 /* One byte is reserved for domain in struct c_domain. */
483 cd
= mem_alloc(sizeof(*cd
) + domain_len
);
486 memcpy(cd
->domain
, cookie
->domain
, domain_len
+ 1);
487 add_to_list(c_domains
, cd
);
491 static unsigned int cookie_id
= 0;
494 delete_cookie(struct cookie
*c
)
500 if (!strcasecmp(d
->domain
, c
->domain
))
503 foreach (cd
, c_domains
) {
504 if (!strcasecmp(cd
->domain
, c
->domain
)) {
518 cookie
*find_cookie_id(void *idp
)
532 reject_cookie(void *idp
)
534 struct cookie
*c
= find_cookie_id(idp
);
539 set_cookies_dirty(); /* @find_cookie_id doesn't use @cookie_queries */
544 cookie_default(void *idp
, int a
)
546 struct cookie
*c
= find_cookie_id(idp
);
548 if (c
) c
->server
->accept
= a
;
553 accept_cookie_always(void *idp
)
555 cookie_default(idp
, 1);
560 accept_cookie_never(void *idp
)
562 cookie_default(idp
, 0);
567 /* Check whether domain is matching server
569 * example.com matches www.example.com/
570 * example.com doesn't match www.example.com.org/
571 * example.com doesn't match www.example.comm/
572 * example.com doesn't match example.co
575 is_in_domain(unsigned char *domain
, unsigned char *server
, int server_len
)
577 int domain_len
= strlen(domain
);
580 if (domain_len
> server_len
)
583 if (domain_len
== server_len
)
584 return !strncasecmp(domain
, server
, server_len
);
586 len
= server_len
- domain_len
;
587 if (server
[len
- 1] != '.')
590 return !strncasecmp(domain
, server
+ len
, domain_len
);
595 is_path_prefix(unsigned char *d
, unsigned char *s
)
599 /* TODO: strlcmp()? --pasky */
601 if (dl
> strlen(s
)) return 0;
603 return !memcmp(d
, s
, dl
);
608 send_cookies(struct uri
*uri
)
611 struct cookie
*c
, *next
;
612 unsigned char *path
= NULL
;
613 static struct string header
;
616 if (!uri
->host
|| !uri
->data
)
619 foreach (cd
, c_domains
)
620 if (is_in_domain(cd
->domain
, uri
->host
, uri
->hostlen
)) {
621 path
= get_uri_string(uri
, URI_PATH
);
625 if (!path
) return NULL
;
627 init_string(&header
);
630 foreachsafe (c
, next
, cookies
) {
631 if (!is_in_domain(c
->domain
, uri
->host
, uri
->hostlen
)
632 || !is_path_prefix(c
->path
, path
))
635 if (c
->expires
&& c
->expires
<= now
) {
637 DBG("Cookie %s=%s (exp %d) expired.",
638 c
->name
, c
->value
, c
->expires
);
646 /* Not sure if this is 100% right..? --pasky */
647 if (c
->secure
&& uri
->protocol
!= PROTOCOL_HTTPS
)
651 add_to_string(&header
, "; ");
653 add_to_string(&header
, c
->name
);
654 add_char_to_string(&header
, '=');
655 add_to_string(&header
, c
->value
);
657 DBG("Cookie: %s=%s", c
->name
, c
->value
);
663 if (!header
.length
) {
664 done_string(&header
);
671 static void done_cookies(struct module
*module
);
676 /* Buffer size is set to be enough to read long lines that
677 * save_cookies may write. 6 is choosen after the fprintf(..) call
678 * in save_cookies(). --Zas */
679 unsigned char in_buffer
[6 * MAX_STR_LEN
];
680 unsigned char *cookfile
= COOKIES_FILENAME
;
685 cookfile
= straconcat(elinks_home
, cookfile
, NULL
);
686 if (!cookfile
) return;
689 /* Do it here, as we will delete whole cookies list if the file was
692 done_cookies(&cookies_module
);
695 fp
= fopen(cookfile
, "rb");
696 if (elinks_home
) mem_free(cookfile
);
699 /* XXX: We don't want to overwrite the cookies file
700 * periodically to our death. */
704 while (fgets(in_buffer
, 6 * MAX_STR_LEN
, fp
)) {
705 struct cookie
*cookie
;
706 unsigned char *p
, *q
= in_buffer
;
707 enum { NAME
= 0, VALUE
, SERVER
, PATH
, DOMAIN
, EXPIRES
, SECURE
, MEMBERS
} member
;
714 /* First find all members. */
715 for (member
= NAME
; member
< MEMBERS
; member
++, q
= ++p
) {
718 if (member
+ 1 != MEMBERS
) break; /* last field ? */
723 members
[member
].pos
= q
;
724 members
[member
].len
= p
- q
;
727 if (member
!= MEMBERS
) continue; /* Invalid line. */
729 /* Skip expired cookies if any. */
730 expires
= str_to_time_t(members
[EXPIRES
].pos
);
731 if (!expires
|| expires
<= now
) {
736 /* Prepare cookie if all members and fields was read. */
737 cookie
= mem_calloc(1, sizeof(*cookie
));
738 if (!cookie
) continue;
740 cookie
->server
= get_cookie_server(members
[SERVER
].pos
, members
[SERVER
].len
);
741 cookie
->name
= memacpy(members
[NAME
].pos
, members
[NAME
].len
);
742 cookie
->value
= memacpy(members
[VALUE
].pos
, members
[VALUE
].len
);
743 cookie
->path
= memacpy(members
[PATH
].pos
, members
[PATH
].len
);
744 cookie
->domain
= memacpy(members
[DOMAIN
].pos
, members
[DOMAIN
].len
);
746 /* Check whether all fields were correctly allocated. */
747 if (!cookie
->server
|| !cookie
->name
|| !cookie
->value
748 || !cookie
->path
|| !cookie
->domain
) {
753 cookie
->expires
= expires
;
754 cookie
->secure
= !!atoi(members
[SECURE
].pos
);
756 accept_cookie(cookie
);
764 resave_cookies_bottom_half(void *always_null
)
766 if (get_cookies_save() && get_cookies_resave())
767 save_cookies(); /* checks cookies_dirty */
770 /* Note that the cookies have been modified, and register a bottom
771 * half for saving them if appropriate. We use a bottom half so that
772 * if something makes multiple changes and calls this for each change,
773 * the cookies get saved only once at the end. */
775 set_cookies_dirty(void)
777 /* Do not check @cookies_dirty here. If the previous attempt
778 * to save cookies failed, @cookies_dirty can still be nonzero
779 * even though @resave_cookies_bottom_half is no longer in the
782 /* If @resave_cookies_bottom_half is already in the queue,
783 * @register_bottom_half does nothing. */
784 register_bottom_half(resave_cookies_bottom_half
, NULL
);
790 unsigned char *cookfile
;
791 struct secure_save_info
*ssi
;
794 if (cookies_nosave
|| !elinks_home
|| !cookies_dirty
795 || get_cmd_opt_bool("anonymous"))
798 cookfile
= straconcat(elinks_home
, COOKIES_FILENAME
, NULL
);
799 if (!cookfile
) return;
801 ssi
= secure_open(cookfile
);
806 foreach (c
, cookies
) {
807 if (!c
->expires
|| c
->expires
<= now
) continue;
808 if (secure_fprintf(ssi
, "%s\t%s\t%s\t%s\t%s\t%ld\t%d\n",
811 empty_string_or_(c
->path
),
812 empty_string_or_(c
->domain
),
813 c
->expires
, c
->secure
) < 0)
817 if (!secure_close(ssi
)) cookies_dirty
= 0;
821 init_cookies(struct module
*module
)
823 if (get_cookies_save())
827 /* Like @delete_cookie, this function does not set @cookies_dirty.
828 * The caller must do that if appropriate. */
830 free_cookies_list(struct list_head
*list
)
832 while (!list_empty(*list
)) {
833 struct cookie
*cookie
= list
->next
;
835 delete_cookie(cookie
);
840 done_cookies(struct module
*module
)
842 free_list(c_domains
);
844 if (!cookies_nosave
&& get_cookies_save())
847 free_cookies_list(&cookies
);
848 free_cookies_list(&cookie_queries
);
849 /* If @save_cookies failed above, @cookies_dirty can still be
850 * nonzero. Now if @resave_cookies_bottom_half were in the
851 * queue, it could save the empty @cookies list to the file.
856 struct module cookies_module
= struct_module(
857 /* name: */ N_("Cookies"),
858 /* options: */ cookies_options
,
860 /* submodules: */ NULL
,
862 /* init: */ init_cookies
,
863 /* done: */ done_cookies