• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * soup-cookie-jar-text.c: cookies.txt-based cookie storage
4  *
5  * Copyright (C) 2007, 2008 Red Hat, Inc.
6  */
7 
8 #ifdef HAVE_CONFIG_H
9 #include <config.h>
10 #endif
11 
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <string.h>
15 
16 #include "soup-cookie-jar-text.h"
17 #include "soup.h"
18 
19 /**
20  * SECTION:soup-cookie-jar-text
21  * @short_description: Text-file-based ("cookies.txt") Cookie Jar
22  *
23  * #SoupCookieJarText is a #SoupCookieJar that reads cookies from and
24  * writes them to a text file in the Mozilla "cookies.txt" format.
25  **/
26 
27 enum {
28 	PROP_0,
29 
30 	PROP_FILENAME,
31 
32 	LAST_PROP
33 };
34 
35 typedef struct {
36 	char *filename;
37 
38 } SoupCookieJarTextPrivate;
39 
40 G_DEFINE_TYPE_WITH_PRIVATE (SoupCookieJarText, soup_cookie_jar_text, SOUP_TYPE_COOKIE_JAR)
41 
42 static void load (SoupCookieJar *jar);
43 
44 static void
soup_cookie_jar_text_init(SoupCookieJarText * text)45 soup_cookie_jar_text_init (SoupCookieJarText *text)
46 {
47 }
48 
49 static void
soup_cookie_jar_text_finalize(GObject * object)50 soup_cookie_jar_text_finalize (GObject *object)
51 {
52 	SoupCookieJarTextPrivate *priv =
53 		soup_cookie_jar_text_get_instance_private (SOUP_COOKIE_JAR_TEXT (object));
54 
55 	g_free (priv->filename);
56 
57 	G_OBJECT_CLASS (soup_cookie_jar_text_parent_class)->finalize (object);
58 }
59 
60 static void
soup_cookie_jar_text_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)61 soup_cookie_jar_text_set_property (GObject *object, guint prop_id,
62 				   const GValue *value, GParamSpec *pspec)
63 {
64 	SoupCookieJarTextPrivate *priv =
65 		soup_cookie_jar_text_get_instance_private (SOUP_COOKIE_JAR_TEXT (object));
66 
67 	switch (prop_id) {
68 	case PROP_FILENAME:
69 		priv->filename = g_value_dup_string (value);
70 		load (SOUP_COOKIE_JAR (object));
71 		break;
72 	default:
73 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
74 		break;
75 	}
76 }
77 
78 static void
soup_cookie_jar_text_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)79 soup_cookie_jar_text_get_property (GObject *object, guint prop_id,
80 				   GValue *value, GParamSpec *pspec)
81 {
82 	SoupCookieJarTextPrivate *priv =
83 		soup_cookie_jar_text_get_instance_private (SOUP_COOKIE_JAR_TEXT (object));
84 
85 	switch (prop_id) {
86 	case PROP_FILENAME:
87 		g_value_set_string (value, priv->filename);
88 		break;
89 	default:
90 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
91 		break;
92 	}
93 }
94 
95 /**
96  * soup_cookie_jar_text_new:
97  * @filename: the filename to read to/write from
98  * @read_only: %TRUE if @filename is read-only
99  *
100  * Creates a #SoupCookieJarText.
101  *
102  * @filename will be read in at startup to create an initial set of
103  * cookies. If @read_only is %FALSE, then the non-session cookies will
104  * be written to @filename when the 'changed' signal is emitted from
105  * the jar. (If @read_only is %TRUE, then the cookie jar will only be
106  * used for this session, and changes made to it will be lost when the
107  * jar is destroyed.)
108  *
109  * Return value: the new #SoupCookieJar
110  *
111  * Since: 2.26
112  **/
113 SoupCookieJar *
soup_cookie_jar_text_new(const char * filename,gboolean read_only)114 soup_cookie_jar_text_new (const char *filename, gboolean read_only)
115 {
116 	g_return_val_if_fail (filename != NULL, NULL);
117 
118 	return g_object_new (SOUP_TYPE_COOKIE_JAR_TEXT,
119 			     SOUP_COOKIE_JAR_TEXT_FILENAME, filename,
120 			     SOUP_COOKIE_JAR_READ_ONLY, read_only,
121 			     NULL);
122 }
123 
124 static SoupSameSitePolicy
string_to_same_site_policy(const char * string)125 string_to_same_site_policy (const char *string)
126 {
127 	if (strcmp (string, "Lax") == 0)
128 		return SOUP_SAME_SITE_POLICY_LAX;
129 	else if (strcmp (string, "Strict") == 0)
130 		return SOUP_SAME_SITE_POLICY_STRICT;
131 	else if (strcmp (string, "None") == 0)
132 		return SOUP_SAME_SITE_POLICY_NONE;
133 	else
134 		g_return_val_if_reached (SOUP_SAME_SITE_POLICY_NONE);
135 }
136 
137 static const char *
same_site_policy_to_string(SoupSameSitePolicy policy)138 same_site_policy_to_string (SoupSameSitePolicy policy)
139 {
140 	switch (policy) {
141 	case SOUP_SAME_SITE_POLICY_STRICT:
142 		return "Strict";
143 	case SOUP_SAME_SITE_POLICY_LAX:
144 		return "Lax";
145 	case SOUP_SAME_SITE_POLICY_NONE:
146 		return "None";
147 	}
148 
149 	g_return_val_if_reached ("None");
150 }
151 
152 static SoupCookie*
parse_cookie(char * line,time_t now)153 parse_cookie (char *line, time_t now)
154 {
155 	char **result;
156 	SoupCookie *cookie = NULL;
157 	gboolean http_only;
158 	gulong expire_time;
159 	int max_age;
160 	char *host, *path, *secure, *expires, *name, *value, *samesite = NULL;
161 	gsize result_length;
162 
163 	if (g_str_has_prefix (line, "#HttpOnly_")) {
164 		http_only = TRUE;
165 		line += strlen ("#HttpOnly_");
166 	} else if (*line == '#' || g_ascii_isspace (*line))
167 		return cookie;
168 	else
169 		http_only = FALSE;
170 
171 	result = g_strsplit (line, "\t", -1);
172 	result_length = g_strv_length (result);
173 	if (result_length < 7)
174 		goto out;
175 
176 	/* Check this first */
177 	expires = result[4];
178 	expire_time = strtoul (expires, NULL, 10);
179 	if (now >= expire_time)
180 		goto out;
181 	max_age = (expire_time - now <= G_MAXINT ? expire_time - now : G_MAXINT);
182 
183 	host = result[0];
184 
185 	/* result[1] is not used because it's redundat; it's a boolean
186 	 * value regarding whether the cookie should be used for
187 	 * sub-domains of the domain that is set for the cookie. It is
188 	 * TRUE if host starts with '.', and FALSE otherwise.
189 	 */
190 
191 	path = result[2];
192 	secure = result[3];
193 
194 	name = result[5];
195 	value = result[6];
196 
197 	if (result_length == 8)
198 		samesite = result[7];
199 
200 	cookie = soup_cookie_new (name, value, host, path, max_age);
201 
202 	if (samesite != NULL)
203 		soup_cookie_set_same_site_policy (cookie, string_to_same_site_policy (samesite));
204 
205 	if (strcmp (secure, "FALSE") != 0)
206 		soup_cookie_set_secure (cookie, TRUE);
207 	if (http_only)
208 		soup_cookie_set_http_only (cookie, TRUE);
209 
210  out:
211 	g_strfreev (result);
212 
213 	return cookie;
214 }
215 
216 static void
parse_line(SoupCookieJar * jar,char * line,time_t now)217 parse_line (SoupCookieJar *jar, char *line, time_t now)
218 {
219 	SoupCookie *cookie;
220 
221 	cookie = parse_cookie (line, now);
222 	if (cookie)
223 		soup_cookie_jar_add_cookie (jar, cookie);
224 }
225 
226 static void
load(SoupCookieJar * jar)227 load (SoupCookieJar *jar)
228 {
229 	SoupCookieJarTextPrivate *priv =
230 		soup_cookie_jar_text_get_instance_private (SOUP_COOKIE_JAR_TEXT (jar));
231 	char *contents = NULL, *line, *p;
232 	gsize length = 0;
233 	time_t now = time (NULL);
234 
235 	/* FIXME: error? */
236 	if (!g_file_get_contents (priv->filename, &contents, &length, NULL))
237 		return;
238 
239 	line = contents;
240 	for (p = contents; *p; p++) {
241 		/* \r\n comes out as an extra empty line and gets ignored */
242 		if (*p == '\r' || *p == '\n') {
243 			*p = '\0';
244 			parse_line (jar, line, now);
245 			line = p + 1;
246 		}
247 	}
248 	parse_line (jar, line, now);
249 
250 	g_free (contents);
251 }
252 
253 static void
write_cookie(FILE * out,SoupCookie * cookie)254 write_cookie (FILE *out, SoupCookie *cookie)
255 {
256 	fseek (out, 0, SEEK_END);
257 
258 	fprintf (out, "%s%s\t%s\t%s\t%s\t%lu\t%s\t%s\t%s\n",
259 		 cookie->http_only ? "#HttpOnly_" : "",
260 		 cookie->domain,
261 		 *cookie->domain == '.' ? "TRUE" : "FALSE",
262 		 cookie->path,
263 		 cookie->secure ? "TRUE" : "FALSE",
264 		 (gulong)soup_date_to_time_t (cookie->expires),
265 		 cookie->name,
266 		 cookie->value,
267 		 same_site_policy_to_string (soup_cookie_get_same_site_policy (cookie)));
268 }
269 
270 static void
delete_cookie(const char * filename,SoupCookie * cookie)271 delete_cookie (const char *filename, SoupCookie *cookie)
272 {
273 	char *contents = NULL, *line, *p;
274 	gsize length = 0;
275 	FILE *f;
276 	SoupCookie *c;
277 	time_t now = time (NULL);
278 
279 	if (!g_file_get_contents (filename, &contents, &length, NULL))
280 		return;
281 
282 	f = fopen (filename, "w");
283 	if (!f) {
284 		g_free (contents);
285 		return;
286 	}
287 
288 	line = contents;
289 	for (p = contents; *p; p++) {
290 		/* \r\n comes out as an extra empty line and gets ignored */
291 		if (*p == '\r' || *p == '\n') {
292 			*p = '\0';
293 			c = parse_cookie (line, now);
294 			line = p + 1;
295 			if (!c)
296 				continue;
297 			if (!soup_cookie_equal (cookie, c))
298 				write_cookie (f, c);
299 			soup_cookie_free (c);
300 		}
301 	}
302 	c = parse_cookie (line, now);
303 	if (c) {
304 		if (!soup_cookie_equal (cookie, c))
305 			write_cookie (f, c);
306 		soup_cookie_free (c);
307 	}
308 
309 	g_free (contents);
310 	fclose (f);
311 }
312 
313 static void
soup_cookie_jar_text_changed(SoupCookieJar * jar,SoupCookie * old_cookie,SoupCookie * new_cookie)314 soup_cookie_jar_text_changed (SoupCookieJar *jar,
315 			      SoupCookie    *old_cookie,
316 			      SoupCookie    *new_cookie)
317 {
318 	FILE *out;
319 	SoupCookieJarTextPrivate *priv =
320 		soup_cookie_jar_text_get_instance_private (SOUP_COOKIE_JAR_TEXT (jar));
321 
322 	/* We can sort of ignore the semantics of the 'changed'
323 	 * signal here and simply delete the old cookie if present
324 	 * and write the new cookie if present. That will do the
325 	 * right thing for all 'added', 'deleted' and 'modified'
326 	 * meanings.
327 	 */
328 	/* Also, delete_cookie takes the filename and write_cookie
329 	 * a FILE pointer. Seems more convenient that way considering
330 	 * the implementations of the functions
331 	 */
332 	if (old_cookie)
333 		delete_cookie (priv->filename, old_cookie);
334 
335 	if (new_cookie) {
336 		gboolean write_header = FALSE;
337 
338 		if (!g_file_test (priv->filename, G_FILE_TEST_EXISTS))
339 			write_header = TRUE;
340 
341 		out = fopen (priv->filename, "a");
342 		if (!out) {
343 			/* FIXME: error? */
344 			return;
345 		}
346 
347 		if (write_header) {
348 			fprintf (out, "# HTTP Cookie File\n");
349 			fprintf (out, "# http://www.netscape.com/newsref/std/cookie_spec.html\n");
350 			fprintf (out, "# This is a generated file!  Do not edit.\n");
351 			fprintf (out, "# To delete cookies, use the Cookie Manager.\n\n");
352 		}
353 
354 		if (new_cookie->expires)
355 			write_cookie (out, new_cookie);
356 
357 		if (fclose (out) != 0) {
358 			/* FIXME: error? */
359 			return;
360 		}
361 	}
362 }
363 
364 static gboolean
soup_cookie_jar_text_is_persistent(SoupCookieJar * jar)365 soup_cookie_jar_text_is_persistent (SoupCookieJar *jar)
366 {
367 	return TRUE;
368 }
369 
370 static void
soup_cookie_jar_text_class_init(SoupCookieJarTextClass * text_class)371 soup_cookie_jar_text_class_init (SoupCookieJarTextClass *text_class)
372 {
373 	SoupCookieJarClass *cookie_jar_class =
374 		SOUP_COOKIE_JAR_CLASS (text_class);
375 	GObjectClass *object_class = G_OBJECT_CLASS (text_class);
376 
377 	cookie_jar_class->is_persistent = soup_cookie_jar_text_is_persistent;
378 	cookie_jar_class->changed       = soup_cookie_jar_text_changed;
379 
380 	object_class->finalize     = soup_cookie_jar_text_finalize;
381 	object_class->set_property = soup_cookie_jar_text_set_property;
382 	object_class->get_property = soup_cookie_jar_text_get_property;
383 
384 	/**
385 	 * SOUP_COOKIE_JAR_TEXT_FILENAME:
386 	 *
387 	 * Alias for the #SoupCookieJarText:filename property. (The
388 	 * cookie-storage filename.)
389 	 **/
390 	g_object_class_install_property (
391 		object_class, PROP_FILENAME,
392 		g_param_spec_string (SOUP_COOKIE_JAR_TEXT_FILENAME,
393 				     "Filename",
394 				     "Cookie-storage filename",
395 				     NULL,
396 				     G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
397 				     G_PARAM_STATIC_STRINGS));
398 }
399