1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * soup-cookie-jar-db.c: database-based cookie storage
4  *
5  * Using danw's soup-cookie-jar-text as template
6  * Copyright (C) 2008 Diego Escalante Urrelo
7  * Copyright (C) 2009 Collabora Ltd.
8  */
9 
10 #ifdef HAVE_CONFIG_H
11 #include <config.h>
12 #endif
13 
14 #include <stdlib.h>
15 
16 #include <sqlite3.h>
17 
18 #include "soup-cookie-jar-db.h"
19 #include "soup.h"
20 
21 /**
22  * SECTION:soup-cookie-jar-db
23  * @short_description: Database-based Cookie Jar
24  *
25  * #SoupCookieJarDB is a #SoupCookieJar that reads cookies from and
26  * writes them to a sqlite database in the new Mozilla format.
27  *
28  * (This is identical to <literal>SoupCookieJarSqlite</literal> in
29  * libsoup-gnome; it has just been moved into libsoup proper, and
30  * renamed to avoid conflicting.)
31  **/
32 
33 enum {
34 	PROP_0,
35 
36 	PROP_FILENAME,
37 
38 	LAST_PROP
39 };
40 
41 typedef struct {
42 	char *filename;
43 	sqlite3 *db;
44 } SoupCookieJarDBPrivate;
45 
46 G_DEFINE_TYPE_WITH_PRIVATE (SoupCookieJarDB, soup_cookie_jar_db, SOUP_TYPE_COOKIE_JAR)
47 
48 static void load (SoupCookieJar *jar);
49 
50 static void
soup_cookie_jar_db_init(SoupCookieJarDB * db)51 soup_cookie_jar_db_init (SoupCookieJarDB *db)
52 {
53 }
54 
55 static void
soup_cookie_jar_db_finalize(GObject * object)56 soup_cookie_jar_db_finalize (GObject *object)
57 {
58 	SoupCookieJarDBPrivate *priv =
59 		soup_cookie_jar_db_get_instance_private (SOUP_COOKIE_JAR_DB (object));
60 
61 	g_free (priv->filename);
62 	g_clear_pointer (&priv->db, sqlite3_close);
63 
64 	G_OBJECT_CLASS (soup_cookie_jar_db_parent_class)->finalize (object);
65 }
66 
67 static void
soup_cookie_jar_db_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)68 soup_cookie_jar_db_set_property (GObject *object, guint prop_id,
69 				 const GValue *value, GParamSpec *pspec)
70 {
71 	SoupCookieJarDBPrivate *priv =
72 		soup_cookie_jar_db_get_instance_private (SOUP_COOKIE_JAR_DB (object));
73 
74 	switch (prop_id) {
75 	case PROP_FILENAME:
76 		priv->filename = g_value_dup_string (value);
77 		load (SOUP_COOKIE_JAR (object));
78 		break;
79 	default:
80 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
81 		break;
82 	}
83 }
84 
85 static void
soup_cookie_jar_db_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)86 soup_cookie_jar_db_get_property (GObject *object, guint prop_id,
87 				 GValue *value, GParamSpec *pspec)
88 {
89 	SoupCookieJarDBPrivate *priv =
90 		soup_cookie_jar_db_get_instance_private (SOUP_COOKIE_JAR_DB (object));
91 
92 	switch (prop_id) {
93 	case PROP_FILENAME:
94 		g_value_set_string (value, priv->filename);
95 		break;
96 	default:
97 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
98 		break;
99 	}
100 }
101 
102 /**
103  * soup_cookie_jar_db_new:
104  * @filename: the filename to read to/write from, or %NULL
105  * @read_only: %TRUE if @filename is read-only
106  *
107  * Creates a #SoupCookieJarDB.
108  *
109  * @filename will be read in at startup to create an initial set of
110  * cookies. If @read_only is %FALSE, then the non-session cookies will
111  * be written to @filename when the 'changed' signal is emitted from
112  * the jar. (If @read_only is %TRUE, then the cookie jar will only be
113  * used for this session, and changes made to it will be lost when the
114  * jar is destroyed.)
115  *
116  * Return value: the new #SoupCookieJar
117  *
118  * Since: 2.42
119  **/
120 SoupCookieJar *
soup_cookie_jar_db_new(const char * filename,gboolean read_only)121 soup_cookie_jar_db_new (const char *filename, gboolean read_only)
122 {
123 	g_return_val_if_fail (filename != NULL, NULL);
124 
125 	return g_object_new (SOUP_TYPE_COOKIE_JAR_DB,
126 			     SOUP_COOKIE_JAR_DB_FILENAME, filename,
127 			     SOUP_COOKIE_JAR_READ_ONLY, read_only,
128 			     NULL);
129 }
130 
131 #define QUERY_ALL "SELECT id, name, value, host, path, expiry, lastAccessed, isSecure, isHttpOnly, sameSite FROM moz_cookies;"
132 #define CREATE_TABLE "CREATE TABLE moz_cookies (id INTEGER PRIMARY KEY, name TEXT, value TEXT, host TEXT, path TEXT, expiry INTEGER, lastAccessed INTEGER, isSecure INTEGER, isHttpOnly INTEGER, sameSite INTEGER)"
133 #define QUERY_INSERT "INSERT INTO moz_cookies VALUES(NULL, %Q, %Q, %Q, %Q, %d, NULL, %d, %d, %d);"
134 #define QUERY_DELETE "DELETE FROM moz_cookies WHERE name=%Q AND host=%Q;"
135 
136 enum {
137 	COL_ID,
138 	COL_NAME,
139 	COL_VALUE,
140 	COL_HOST,
141 	COL_PATH,
142 	COL_EXPIRY,
143 	COL_LAST_ACCESS,
144 	COL_SECURE,
145 	COL_HTTP_ONLY,
146 	COL_SAME_SITE_POLICY,
147 	N_COL,
148 };
149 
150 static int
callback(void * data,int argc,char ** argv,char ** colname)151 callback (void *data, int argc, char **argv, char **colname)
152 {
153 	SoupCookie *cookie = NULL;
154 	SoupCookieJar *jar = SOUP_COOKIE_JAR (data);
155 
156 	char *name, *value, *host, *path;
157 	gulong expire_time;
158 	time_t now;
159 	int max_age;
160 	gboolean http_only = FALSE, secure = FALSE;
161 	SoupSameSitePolicy same_site_policy;
162 
163 	now = time (NULL);
164 
165 	name = argv[COL_NAME];
166 	value = argv[COL_VALUE];
167 	host = argv[COL_HOST];
168 	path = argv[COL_PATH];
169 	expire_time = strtoul (argv[COL_EXPIRY], NULL, 10);
170 
171 	if (now >= expire_time)
172 		return 0;
173 	max_age = (expire_time - now <= G_MAXINT ? expire_time - now : G_MAXINT);
174 
175 	http_only = (g_strcmp0 (argv[COL_HTTP_ONLY], "1") == 0);
176 	secure = (g_strcmp0 (argv[COL_SECURE], "1") == 0);
177 	same_site_policy = g_ascii_strtoll (argv[COL_SAME_SITE_POLICY], NULL, 0);
178 
179 	cookie = soup_cookie_new (name, value, host, path, max_age);
180 
181 	if (secure)
182 		soup_cookie_set_secure (cookie, TRUE);
183 	if (http_only)
184 		soup_cookie_set_http_only (cookie, TRUE);
185 	if (same_site_policy)
186 		soup_cookie_set_same_site_policy (cookie, same_site_policy);
187 
188 	soup_cookie_jar_add_cookie (jar, cookie);
189 
190 	return 0;
191 }
192 
193 static void
try_create_table(sqlite3 * db)194 try_create_table (sqlite3 *db)
195 {
196 	char *error = NULL;
197 
198 	if (sqlite3_exec (db, CREATE_TABLE, NULL, NULL, &error)) {
199 		g_warning ("Failed to execute query: %s", error);
200 		sqlite3_free (error);
201 	}
202 }
203 
204 static void
exec_query_with_try_create_table(sqlite3 * db,const char * sql,int (* callback)(void *,int,char **,char **),void * argument)205 exec_query_with_try_create_table (sqlite3 *db,
206 				  const char *sql,
207 				  int (*callback)(void*,int,char**,char**),
208 				  void *argument)
209 {
210 	char *error = NULL;
211 	gboolean try_create = TRUE;
212 
213 try_exec:
214 	if (sqlite3_exec (db, sql, callback, argument, &error)) {
215 		if (try_create) {
216 			try_create = FALSE;
217 			try_create_table (db);
218 			sqlite3_free (error);
219 			error = NULL;
220 			goto try_exec;
221 		} else {
222 			g_warning ("Failed to execute query: %s", error);
223 			sqlite3_free (error);
224 		}
225 	}
226 }
227 
228 /* Follows sqlite3 convention; returns TRUE on error */
229 static gboolean
open_db(SoupCookieJar * jar)230 open_db (SoupCookieJar *jar)
231 {
232 	SoupCookieJarDBPrivate *priv =
233 		soup_cookie_jar_db_get_instance_private (SOUP_COOKIE_JAR_DB (jar));
234 
235 	char *error = NULL;
236 
237 	if (sqlite3_open (priv->filename, &priv->db)) {
238 		sqlite3_close (priv->db);
239 		priv->db = NULL;
240 		g_warning ("Can't open %s", priv->filename);
241 		return TRUE;
242 	}
243 
244 	if (sqlite3_exec (priv->db, "PRAGMA synchronous = OFF; PRAGMA secure_delete = 1;", NULL, NULL, &error)) {
245 		g_warning ("Failed to execute query: %s", error);
246 		sqlite3_free (error);
247 	}
248 
249 	/* Migrate old DB to include same-site info. We simply always run this as it
250 	   will safely handle a column with the same name existing */
251 	sqlite3_exec (priv->db, "ALTER TABLE moz_cookies ADD COLUMN sameSite INTEGER DEFAULT 0", NULL, NULL, NULL);
252 
253 	return FALSE;
254 }
255 
256 static void
load(SoupCookieJar * jar)257 load (SoupCookieJar *jar)
258 {
259 	SoupCookieJarDBPrivate *priv =
260 		soup_cookie_jar_db_get_instance_private (SOUP_COOKIE_JAR_DB (jar));
261 
262 	if (priv->db == NULL) {
263 		if (open_db (jar))
264 			return;
265 	}
266 
267 	exec_query_with_try_create_table (priv->db, QUERY_ALL, callback, jar);
268 }
269 
270 static void
soup_cookie_jar_db_changed(SoupCookieJar * jar,SoupCookie * old_cookie,SoupCookie * new_cookie)271 soup_cookie_jar_db_changed (SoupCookieJar *jar,
272 			    SoupCookie    *old_cookie,
273 			    SoupCookie    *new_cookie)
274 {
275 	SoupCookieJarDBPrivate *priv =
276 		soup_cookie_jar_db_get_instance_private (SOUP_COOKIE_JAR_DB (jar));
277 	char *query;
278 
279 	if (priv->db == NULL) {
280 		if (open_db (jar))
281 			return;
282 	}
283 
284 	if (old_cookie) {
285 		query = sqlite3_mprintf (QUERY_DELETE,
286 					 old_cookie->name,
287 					 old_cookie->domain);
288 		exec_query_with_try_create_table (priv->db, query, NULL, NULL);
289 		sqlite3_free (query);
290 	}
291 
292 	if (new_cookie && new_cookie->expires) {
293 		gulong expires;
294 
295 		expires = (gulong)soup_date_to_time_t (new_cookie->expires);
296 		query = sqlite3_mprintf (QUERY_INSERT,
297 					 new_cookie->name,
298 					 new_cookie->value,
299 					 new_cookie->domain,
300 					 new_cookie->path,
301 					 expires,
302 					 new_cookie->secure,
303 					 new_cookie->http_only,
304 					 soup_cookie_get_same_site_policy (new_cookie));
305 		exec_query_with_try_create_table (priv->db, query, NULL, NULL);
306 		sqlite3_free (query);
307 	}
308 }
309 
310 static gboolean
soup_cookie_jar_db_is_persistent(SoupCookieJar * jar)311 soup_cookie_jar_db_is_persistent (SoupCookieJar *jar)
312 {
313 	return TRUE;
314 }
315 
316 static void
soup_cookie_jar_db_class_init(SoupCookieJarDBClass * db_class)317 soup_cookie_jar_db_class_init (SoupCookieJarDBClass *db_class)
318 {
319 	SoupCookieJarClass *cookie_jar_class =
320 		SOUP_COOKIE_JAR_CLASS (db_class);
321 	GObjectClass *object_class = G_OBJECT_CLASS (db_class);
322 
323 	cookie_jar_class->is_persistent = soup_cookie_jar_db_is_persistent;
324 	cookie_jar_class->changed       = soup_cookie_jar_db_changed;
325 
326 	object_class->finalize     = soup_cookie_jar_db_finalize;
327 	object_class->set_property = soup_cookie_jar_db_set_property;
328 	object_class->get_property = soup_cookie_jar_db_get_property;
329 
330 	/**
331 	 * SOUP_COOKIE_JAR_DB_FILENAME:
332 	 *
333 	 * Alias for the #SoupCookieJarDB:filename property. (The
334 	 * cookie-storage filename.)
335 	 **/
336 	g_object_class_install_property (
337 		object_class, PROP_FILENAME,
338 		g_param_spec_string (SOUP_COOKIE_JAR_DB_FILENAME,
339 				     "Filename",
340 				     "Cookie-storage filename",
341 				     NULL,
342 				     G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
343 				     G_PARAM_STATIC_STRINGS));
344 }
345