Bug 997: Fix unlikely stack corruption in get_pasv_socket.
[elinks/elinks-j605.git] / src / cookies / cookies.c
blobdfb28edf3f53beb809324e074728d0ef7e015836
1 /* Internal cookies implementation */
3 #ifdef HAVE_CONFIG_H
4 #include "config.h"
5 #endif
7 #include <stdio.h>
8 #include <stdlib.h>
9 #include <string.h>
10 #include <sys/types.h>
11 #include <sys/stat.h> /* OS/2 needs this after sys/types.h */
12 #ifdef HAVE_TIME_H
13 #include <time.h>
14 #endif
16 #include "elinks.h"
18 #if 0
19 #define DEBUG_COOKIES
20 #endif
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"
40 #ifdef DEBUG_COOKIES
41 #include "util/error.h"
42 #endif
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);
56 struct c_domain {
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;
69 enum cookies_option {
70 COOKIES_TREE,
72 COOKIES_ACCEPT_POLICY,
73 COOKIES_MAX_AGE,
74 COOKIES_PARANOID_SECURITY,
75 COOKIES_SAVE,
76 COOKIES_RESAVE,
78 COOKIES_OPTIONS,
81 static struct option_info cookies_options[] = {
82 INIT_OPT_TREE("", N_("Cookies"),
83 "cookies", 0,
84 N_("Cookies options.")),
86 INIT_OPT_INT("cookies", N_("Accept policy"),
87 "accept_policy", 0,
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"
99 " expiration date\n"
100 "1+ is use cookie's expiration date, but limit age to the given\n"
101 " number of days")),
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"),
112 "save", 0, 1,
113 N_("Whether cookies should be loaded from and save to disk.")),
115 INIT_OPT_BOOL("cookies", N_("Resaving"),
116 "resave", 0, 1,
117 N_("Save cookies after each change in cookies list? No effect when\n"
118 "cookie saving (cookies.save) is off.")),
120 NULL_OPTION_INFO,
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"
138 * below! */
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)
149 continue;
151 object_lock(cs);
152 return cs;
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);
163 object_lock(cs);
165 if (!sort_spot) {
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);
170 } else {
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. */
179 return cs;
182 static void
183 done_cookie_server(struct cookie_server *cs)
185 object_unlock(cs);
186 if (is_object_used(cs)) return;
188 if (cs->box_item) done_listbox_item(&cookie_browser, cs->box_item);
189 del_from_list(cs);
190 mem_free(cs);
193 void
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);
202 mem_free(c);
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. */
209 void
210 delete_cookie(struct cookie *c)
212 del_from_list(c);
213 done_cookie(c);
217 /* Check whether cookie's domain matches server.
218 * It returns 1 if ok, 0 else. */
219 static int
220 is_domain_security_ok(unsigned char *domain, unsigned char *server, int server_len)
222 int i;
223 int domain_len;
224 int need_dots;
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
236 * continuing. */
237 if (strncasecmp(domain, server + server_len - domain_len, domain_len))
238 return 0;
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
242 * risk. --pasky */
243 if (server_len == domain_len)
244 return 1;
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))
251 return 0;
253 /* Also test if domain is secure en ugh.. */
255 need_dots = 1;
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
270 * Poland. */
271 need_dots = 2;
273 if (domain_len > 0) {
274 int pos = end_with_known_tld(domain, domain_len);
276 if (pos >= 1 && domain[pos - 1] == '.')
277 need_dots = 1;
281 for (i = 0; domain[i]; i++)
282 if (domain[i] == '.' && !--need_dots)
283 break;
285 if (need_dots > 0) return 0;
286 return 1;
289 void
290 set_cookie(struct uri *uri, unsigned char *str)
292 unsigned char *secure, *path;
293 struct cookie *cookie;
294 struct cookie_str cstr;
295 int max_age;
297 if (get_cookies_accept_policy() == COOKIES_ACCEPT_NONE)
298 return;
300 #ifdef DEBUG_COOKIES
301 DBG("set_cookie -> (%s) %s", struri(uri), str);
302 #endif
304 if (!parse_cookie_str(&cstr, str)) return;
306 cookie = mem_calloc(1, sizeof(*cookie));
307 if (!cookie) return;
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 */
320 if (!cookie->domain
321 || !cookie->name
322 || !cookie->value
323 || !cookie->server) {
324 done_cookie(cookie);
325 return;
328 #if 0
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) {
333 #ifdef DEBUG_COOKIES
334 DBG("Dropped.");
335 #endif
336 done_cookie(cookie);
337 return;
339 #endif
341 /* Set cookie expiration if needed.
342 * Cookie expires at end of session by default,
343 * set to 0 by calloc().
345 * max_age:
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();
354 if (max_age) {
355 unsigned char *date = parse_header_param(str, "expires");
357 if (date) {
358 time_t expires = parse_date(&date, NULL, 0, 1); /* Convert date to seconds. */
360 mem_free(date);
362 if (expires) {
363 if (max_age > 0) {
364 int seconds = max_age*24*3600;
365 time_t deadline = time(NULL) + seconds;
367 if (expires > deadline) /* Over-aged cookie ? */
368 expires = deadline;
371 cookie->expires = expires;
376 path = parse_header_param(str, "path");
377 if (!path) {
378 unsigned char *path_end;
380 path = get_uri_string(uri, URI_PATH);
381 if (!path) {
382 done_cookie(cookie);
383 return;
386 for (path_end = path + strlen(path) - 1;
387 path_end >= path; path_end--) {
388 if (*path_end == '/') {
389 path_end[1] = '\0';
390 break;
394 } else {
395 if (!path[0]
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);
402 path[0] = '/';
405 cookie->path = path;
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");
413 if (secure) {
414 cookie->secure = 1;
415 mem_free(secure);
418 #ifdef DEBUG_COOKIES
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);
425 #endif
427 if (!is_domain_security_ok(cookie->domain, uri->host, uri->hostlen)) {
428 #ifdef DEBUG_COOKIES
429 DBG("Domain security violated: %s vs %.*s", cookie->domain,
430 uri->hostlen, uri->host);
431 #endif
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);
440 return;
443 accept_cookie(cookie);
446 void
447 accept_cookie(struct cookie *cookie)
449 struct c_domain *cd;
450 struct listbox_item *root = cookie->server->box_item;
451 int domain_len;
453 if (root)
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))
466 continue;
468 delete_cookie(c);
469 /* @set_cookies_dirty will be called below. */
473 add_to_list(cookies, cookie);
474 set_cookies_dirty();
476 /* XXX: This crunches CPU too. --pasky */
477 foreach (cd, c_domains)
478 if (!strcasecmp(cd->domain, cookie->domain))
479 return;
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);
484 if (!cd) return;
486 memcpy(cd->domain, cookie->domain, domain_len + 1);
487 add_to_list(c_domains, cd);
490 #if 0
491 static unsigned int cookie_id = 0;
493 static void
494 delete_cookie(struct cookie *c)
496 struct c_domain *cd;
497 struct cookie *d;
499 foreach (d, cookies)
500 if (!strcasecmp(d->domain, c->domain))
501 goto end;
503 foreach (cd, c_domains) {
504 if (!strcasecmp(cd->domain, c->domain)) {
505 del_from_list(cd);
506 mem_free(cd);
507 break;
511 end:
512 del_from_list(c);
513 done_cookie(c);
517 static struct
518 cookie *find_cookie_id(void *idp)
520 int id = (int) idp;
521 struct cookie *c;
523 foreach (c, cookies)
524 if (c->id == id)
525 return c;
527 return NULL;
531 static void
532 reject_cookie(void *idp)
534 struct cookie *c = find_cookie_id(idp);
536 if (!c) return;
538 delete_cookie(c);
539 set_cookies_dirty(); /* @find_cookie_id doesn't use @cookie_queries */
543 static void
544 cookie_default(void *idp, int a)
546 struct cookie *c = find_cookie_id(idp);
548 if (c) c->server->accept = a;
552 static void
553 accept_cookie_always(void *idp)
555 cookie_default(idp, 1);
559 static void
560 accept_cookie_never(void *idp)
562 cookie_default(idp, 0);
563 reject_cookie(idp);
565 #endif
567 /* Check whether domain is matching server
568 * Ie.
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
574 static int
575 is_in_domain(unsigned char *domain, unsigned char *server, int server_len)
577 int domain_len = strlen(domain);
578 int len;
580 if (domain_len > server_len)
581 return 0;
583 if (domain_len == server_len)
584 return !strncasecmp(domain, server, server_len);
586 len = server_len - domain_len;
587 if (server[len - 1] != '.')
588 return 0;
590 return !strncasecmp(domain, server + len, domain_len);
594 static inline int
595 is_path_prefix(unsigned char *d, unsigned char *s)
597 int dl = strlen(d);
599 /* TODO: strlcmp()? --pasky */
601 if (dl > strlen(s)) return 0;
603 return !memcmp(d, s, dl);
607 struct string *
608 send_cookies(struct uri *uri)
610 struct c_domain *cd;
611 struct cookie *c, *next;
612 unsigned char *path = NULL;
613 static struct string header;
614 time_t now;
616 if (!uri->host || !uri->data)
617 return NULL;
619 foreach (cd, c_domains)
620 if (is_in_domain(cd->domain, uri->host, uri->hostlen)) {
621 path = get_uri_string(uri, URI_PATH);
622 break;
625 if (!path) return NULL;
627 init_string(&header);
629 now = time(NULL);
630 foreachsafe (c, next, cookies) {
631 if (!is_in_domain(c->domain, uri->host, uri->hostlen)
632 || !is_path_prefix(c->path, path))
633 continue;
635 if (c->expires && c->expires <= now) {
636 #ifdef DEBUG_COOKIES
637 DBG("Cookie %s=%s (exp %d) expired.",
638 c->name, c->value, c->expires);
639 #endif
640 delete_cookie(c);
642 set_cookies_dirty();
643 continue;
646 /* Not sure if this is 100% right..? --pasky */
647 if (c->secure && uri->protocol != PROTOCOL_HTTPS)
648 continue;
650 if (header.length)
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);
656 #ifdef DEBUG_COOKIES
657 DBG("Cookie: %s=%s", c->name, c->value);
658 #endif
661 mem_free(path);
663 if (!header.length) {
664 done_string(&header);
665 return NULL;
668 return &header;
671 static void done_cookies(struct module *module);
674 void
675 load_cookies(void) {
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;
681 FILE *fp;
682 time_t now;
684 if (elinks_home) {
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
690 * removed */
691 cookies_nosave = 1;
692 done_cookies(&cookies_module);
693 cookies_nosave = 0;
695 fp = fopen(cookfile, "rb");
696 if (elinks_home) mem_free(cookfile);
697 if (!fp) return;
699 /* XXX: We don't want to overwrite the cookies file
700 * periodically to our death. */
701 cookies_nosave = 1;
703 now = time(NULL);
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;
708 struct {
709 unsigned char *pos;
710 int len;
711 } members[MEMBERS];
712 time_t expires;
714 /* First find all members. */
715 for (member = NAME; member < MEMBERS; member++, q = ++p) {
716 p = strchr(q, '\t');
717 if (!p) {
718 if (member + 1 != MEMBERS) break; /* last field ? */
719 p = strchr(q, '\n');
720 if (!p) break;
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) {
732 set_cookies_dirty();
733 continue;
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) {
749 done_cookie(cookie);
750 continue;
753 cookie->expires = expires;
754 cookie->secure = !!atoi(members[SECURE].pos);
756 accept_cookie(cookie);
759 cookies_nosave = 0;
760 fclose(fp);
763 static void
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. */
774 void
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
780 * queue. */
781 cookies_dirty = 1;
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);
787 void
788 save_cookies(void) {
789 struct cookie *c;
790 unsigned char *cookfile;
791 struct secure_save_info *ssi;
792 time_t now;
794 if (cookies_nosave || !elinks_home || !cookies_dirty
795 || get_cmd_opt_bool("anonymous"))
796 return;
798 cookfile = straconcat(elinks_home, COOKIES_FILENAME, NULL);
799 if (!cookfile) return;
801 ssi = secure_open(cookfile);
802 mem_free(cookfile);
803 if (!ssi) return;
805 now = time(NULL);
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",
809 c->name, c->value,
810 c->server->host,
811 empty_string_or_(c->path),
812 empty_string_or_(c->domain),
813 c->expires, c->secure) < 0)
814 break;
817 if (!secure_close(ssi)) cookies_dirty = 0;
820 static void
821 init_cookies(struct module *module)
823 if (get_cookies_save())
824 load_cookies();
827 /* Like @delete_cookie, this function does not set @cookies_dirty.
828 * The caller must do that if appropriate. */
829 static void
830 free_cookies_list(struct list_head *list)
832 while (!list_empty(*list)) {
833 struct cookie *cookie = list->next;
835 delete_cookie(cookie);
839 static void
840 done_cookies(struct module *module)
842 free_list(c_domains);
844 if (!cookies_nosave && get_cookies_save())
845 save_cookies();
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.
852 * Prevent that. */
853 cookies_dirty = 0;
856 struct module cookies_module = struct_module(
857 /* name: */ N_("Cookies"),
858 /* options: */ cookies_options,
859 /* events: */ NULL,
860 /* submodules: */ NULL,
861 /* data: */ NULL,
862 /* init: */ init_cookies,
863 /* done: */ done_cookies