diff options
author | Gabriel Ivascu <gabrielivascu@gnome.org> | 2017-09-12 20:19:25 +0300 |
---|---|---|
committer | Gabriel Ivascu <gabrielivascu@gnome.org> | 2017-10-03 18:29:53 +0200 |
commit | 59e83d94ef0a376ade9c962e7b84142499522351 (patch) | |
tree | 1fbb132fd1b70068a7571c5a6cfb88ca4f383321 | |
parent | cfe43c63018665010d4f4366fa03473ced8d7bee (diff) | |
download | epiphany-59e83d94ef0a376ade9c962e7b84142499522351.tar.gz |
safe-browsing: Implement database update operation
The initial update is too large and it takes a few seconds to complete.
Will have to change this to run in a separate thread to not block the UI thread.
-rw-r--r-- | data/org.gnome.epiphany.gschema.xml | 10 | ||||
-rw-r--r-- | embed/ephy-embed-shell.c | 5 | ||||
-rw-r--r-- | lib/ephy-prefs.h | 2 | ||||
-rw-r--r-- | lib/meson.build | 2 | ||||
-rw-r--r-- | lib/safe-browsing/ephy-gsb-service.c | 209 | ||||
-rw-r--r-- | lib/safe-browsing/ephy-gsb-service.h | 3 | ||||
-rw-r--r-- | lib/safe-browsing/ephy-gsb-storage.c | 645 | ||||
-rw-r--r-- | lib/safe-browsing/ephy-gsb-storage.h | 25 | ||||
-rw-r--r-- | lib/safe-browsing/ephy-gsb-utils.c | 128 | ||||
-rw-r--r-- | lib/safe-browsing/ephy-gsb-utils.h | 44 |
10 files changed, 1047 insertions, 26 deletions
diff --git a/data/org.gnome.epiphany.gschema.xml b/data/org.gnome.epiphany.gschema.xml index b0319e70e..78de54ac8 100644 --- a/data/org.gnome.epiphany.gschema.xml +++ b/data/org.gnome.epiphany.gschema.xml @@ -222,6 +222,16 @@ <summary>Enable site-specific quirks</summary> <description>Enable quirks to make specific websites work better. You might want to disable this setting if debugging a specific issue.</description> </key> + <key type="b" name="enable-safe-browsing"> + <default>true</default> + <summary>Enable safe browsing</summary> + <description>Whether to enable safe browsing. Safe browsing operates via Google Safe Browsing API v4.</description> + </key> + <key type="s" name="gsb-api-key"> + <default>'AIzaSyAtuURrRblYXvwCyDC5ZFq0mEw1x4VN6KA'</default> + <summary>Google Safe Browsing API key</summary> + <description>The API key used to access the Google Safe Browsing API v4.</description> + </key> </schema> <schema id="org.gnome.Epiphany.state"> <key type="s" name="download-dir"> diff --git a/embed/ephy-embed-shell.c b/embed/ephy-embed-shell.c index 47541ded2..9003701d2 100644 --- a/embed/ephy-embed-shell.c +++ b/embed/ephy-embed-shell.c @@ -591,11 +591,14 @@ ephy_embed_shell_get_global_gsb_service (EphyEmbedShell *shell) g_return_val_if_fail (EPHY_IS_EMBED_SHELL (shell), NULL); if (priv->global_gsb_service == NULL) { + char *api_key; char *filename; + api_key = g_settings_get_string (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_GSB_API_KEY); filename = g_build_filename (ephy_dot_dir (), EPHY_GSB_FILE, NULL); - priv->global_gsb_service = ephy_gsb_service_new (filename); + priv->global_gsb_service = ephy_gsb_service_new (api_key, filename); + g_free (api_key); g_free (filename); } diff --git a/lib/ephy-prefs.h b/lib/ephy-prefs.h index 4d565e64c..3f359209f 100644 --- a/lib/ephy-prefs.h +++ b/lib/ephy-prefs.h @@ -99,6 +99,8 @@ static const char * const ephy_prefs_state_schema[] = { #define EPHY_PREFS_WEB_ENABLE_ADBLOCK "enable-adblock" #define EPHY_PREFS_WEB_REMEMBER_PASSWORDS "remember-passwords" #define EPHY_PREFS_WEB_ENABLE_SITE_SPECIFIC_QUIRKS "enable-site-specific-quirks" +#define EPHY_PREFS_WEB_ENABLE_SAFE_BROWSING "enable-safe-browsing" +#define EPHY_PREFS_WEB_GSB_API_KEY "gsb-api-key" static const char * const ephy_prefs_web_schema[] = { EPHY_PREFS_WEB_FONT_MIN_SIZE, diff --git a/lib/meson.build b/lib/meson.build index fdb5d75a2..6244e7033 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -50,6 +50,7 @@ libephymisc_sources = [ 'history/ephy-history-types.c', 'safe-browsing/ephy-gsb-service.c', 'safe-browsing/ephy-gsb-storage.c', + 'safe-browsing/ephy-gsb-utils.c', enums ] @@ -62,6 +63,7 @@ libephymisc_deps = [ gnome_desktop_dep, gtk_dep, icu_uc_dep, + json_glib_dep, libdazzle_dep, libsecret_dep, libsoup_dep, diff --git a/lib/safe-browsing/ephy-gsb-service.c b/lib/safe-browsing/ephy-gsb-service.c index dda2add02..2a3ce428e 100644 --- a/lib/safe-browsing/ephy-gsb-service.c +++ b/lib/safe-browsing/ephy-gsb-service.c @@ -23,23 +23,186 @@ #include "ephy-debug.h" #include "ephy-gsb-storage.h" +#include "ephy-user-agent.h" + +#include <libsoup/soup.h> +#include <math.h> +#include <stdio.h> +#include <string.h> + +#define API_PREFIX "https://safebrowsing.googleapis.com/v4/" +#define CURRENT_TIME (g_get_real_time () / 1000000) /* seconds */ +#define DEFAULT_WAIT_TIME (30 * 60) /* seconds */ struct _EphyGSBService { GObject parent_instance; + char *api_key; EphyGSBStorage *storage; + SoupSession *session; }; G_DEFINE_TYPE (EphyGSBService, ephy_gsb_service, G_TYPE_OBJECT); enum { PROP_0, + PROP_API_KEY, PROP_GSB_STORAGE, LAST_PROP }; static GParamSpec *obj_properties[LAST_PROP]; +static inline gboolean +json_object_has_non_null_string_member (JsonObject *object, + const char *member) +{ + JsonNode *node; + + node = json_object_get_member (object, member); + if (!node || !JSON_NODE_HOLDS_VALUE (node)) + return FALSE; + + return json_node_get_string (node) != NULL; +} + +static inline gboolean +json_object_has_non_null_array_member (JsonObject *object, + const char *member) +{ + JsonNode *node; + + node = json_object_get_member (object, member); + if (!node) + return FALSE; + + return JSON_NODE_HOLDS_ARRAY (node); +} + +static void +update_threat_lists_cb (SoupSession *session, + SoupMessage *msg, + gpointer user_data) +{ + EphyGSBService *self = EPHY_GSB_SERVICE (user_data); + JsonNode *node; + JsonObject *object; + JsonArray *responses; + gint64 next_update_time; + + if (msg->status_code != 200) { + LOG ("Cannot update GSB threat lists. Server responded: %u, %s", + msg->status_code, msg->response_body->data); + return; + } + + node = json_from_string (msg->response_body->data, NULL); + object = json_node_get_object (node); + responses = json_object_get_array_member (object, "listUpdateResponses"); + + for (guint i = 0; i < json_array_get_length (responses); i++) { + EphyGSBThreatList *list; + JsonObject *lur = json_array_get_object_element (responses, i); + const char *type = json_object_get_string_member (lur, "responseType"); + JsonObject *checksum = json_object_get_object_member (lur, "checksum"); + const char *remote_checksum = json_object_get_string_member (checksum, "sha256"); + char *local_checksum; + + list = ephy_gsb_threat_list_new (json_object_get_string_member (lur, "threatType"), + json_object_get_string_member (lur, "platformType"), + json_object_get_string_member (lur, "threatEntryType"), + json_object_get_string_member (lur, "newClientState"), + CURRENT_TIME); + LOG ("Updating list %s/%s/%s", list->threat_type, list->platform_type, list->threat_entry_type); + + /* If full update, clear all previous hash prefixes for the given list. */ + if (!g_strcmp0 (type, "FULL_UPDATE")) { + LOG ("FULL UPDATE, clearing all previous hash prefixes..."); + ephy_gsb_storage_clear_hash_prefixes (self->storage, list); + } + + /* Removals need to be handled before additions. */ + if (json_object_has_non_null_array_member (lur, "removals")) { + JsonArray *removals = json_object_get_array_member (lur, "removals"); + for (guint k = 0; k < json_array_get_length (removals); k++) { + JsonObject *tes = json_array_get_object_element (removals, k); + JsonObject *raw_indices = json_object_get_object_member (tes, "rawIndices"); + JsonArray *indices = json_object_get_array_member (raw_indices, "indices"); + ephy_gsb_storage_delete_hash_prefixes (self->storage, list, indices); + } + } + + /* Handle additions. */ + if (json_object_has_non_null_array_member (lur, "additions")) { + JsonArray *additions = json_object_get_array_member (lur, "additions"); + for (guint k = 0; k < json_array_get_length (additions); k++) { + JsonObject *tes = json_array_get_object_element (additions, k); + JsonObject *raw_hashes = json_object_get_object_member (tes, "rawHashes"); + gint64 prefix_size = json_object_get_int_member (raw_hashes, "prefixSize"); + const char *hashes = json_object_get_string_member (raw_hashes, "rawHashes"); + ephy_gsb_storage_insert_hash_prefixes (self->storage, list, prefix_size, hashes); + } + } + + /* Verify checksum. */ + local_checksum = ephy_gsb_storage_compute_checksum (self->storage, list); + if (!g_strcmp0 (local_checksum, remote_checksum)) { + LOG ("Local checksum matches the remote checksum, updating client state..."); + ephy_gsb_storage_update_client_state (self->storage, list, FALSE); + } else { + LOG ("Local checksum does NOT match the remote checksum, clearing list..."); + ephy_gsb_storage_clear_hash_prefixes (self->storage, list); + ephy_gsb_storage_update_client_state (self->storage, list, TRUE); + } + + g_free (local_checksum); + ephy_gsb_threat_list_free (list); + } + + /* Update next update time. */ + if (json_object_has_non_null_string_member (object, "minimumWaitDuration")) { + const char *duration_str; + double duration; + + duration_str = json_object_get_string_member (object, "minimumWaitDuration"); + /* Handle the trailing 's' character. */ + sscanf (duration_str, "%lfs", &duration); + next_update_time = CURRENT_TIME + (gint64)ceil (duration); + } else { + next_update_time = CURRENT_TIME + DEFAULT_WAIT_TIME; + } + + ephy_gsb_storage_set_next_update_time (self->storage, next_update_time); + /* TODO: Schedule a next update in (next_update_time - CURRENT_TIME) seconds. */ + + json_node_unref (node); +} + +static void +ephy_gsb_service_update_threat_lists (EphyGSBService *self) +{ + SoupMessage *msg; + GList *threat_lists; + char *url; + char *body; + + g_assert (EPHY_IS_GSB_SERVICE (self)); + g_assert (ephy_gsb_storage_is_operable (self->storage)); + + threat_lists = ephy_gsb_storage_get_threat_lists (self->storage); + if (!threat_lists) + return; + + body = ephy_gsb_utils_make_list_updates_request (threat_lists); + url = g_strdup_printf ("%sthreatListUpdates:fetch?key=%s", API_PREFIX, self->api_key); + msg = soup_message_new (SOUP_METHOD_POST, url); + soup_message_set_request (msg, "application/json", SOUP_MEMORY_TAKE, body, strlen (body)); + soup_session_queue_message (self->session, msg, update_threat_lists_cb, self); + + g_free (url); + g_list_free_full (threat_lists, (GDestroyNotify)ephy_gsb_threat_list_free); +} + static void ephy_gsb_service_set_property (GObject *object, guint prop_id, @@ -49,6 +212,10 @@ ephy_gsb_service_set_property (GObject *object, EphyGSBService *self = EPHY_GSB_SERVICE (object); switch (prop_id) { + case PROP_API_KEY: + g_free (self->api_key); + self->api_key = g_strdup (g_value_get_string (value)); + break; case PROP_GSB_STORAGE: if (self->storage) g_object_unref (self->storage); @@ -68,6 +235,9 @@ ephy_gsb_service_get_property (GObject *object, EphyGSBService *self = EPHY_GSB_SERVICE (object); switch (prop_id) { + case PROP_API_KEY: + g_value_set_string (value, self->api_key); + break; case PROP_GSB_STORAGE: g_value_set_object (value, self->storage); break; @@ -77,11 +247,22 @@ ephy_gsb_service_get_property (GObject *object, } static void +ephy_gsb_service_finalize (GObject *object) +{ + EphyGSBService *self = EPHY_GSB_SERVICE (object); + + g_free (self->api_key); + + G_OBJECT_CLASS (ephy_gsb_service_parent_class)->finalize (object); +} + +static void ephy_gsb_service_dispose (GObject *object) { EphyGSBService *self = EPHY_GSB_SERVICE (object); g_clear_object (&self->storage); + g_clear_object (&self->session); G_OBJECT_CLASS (ephy_gsb_service_parent_class)->dispose (object); } @@ -90,15 +271,25 @@ static void ephy_gsb_service_constructed (GObject *object) { EphyGSBService *self = EPHY_GSB_SERVICE (object); + gint64 next_update_time; G_OBJECT_CLASS (ephy_gsb_service_parent_class)->constructed (object); - /* TODO: Perform an initial database update if necessary. */ + if (!ephy_gsb_storage_is_operable (self->storage)) + return; + + next_update_time = ephy_gsb_storage_get_next_update_time (self->storage); + if (CURRENT_TIME >= next_update_time) { + /* TODO: This takes too long, needs to run in a separate thread. */ + ephy_gsb_service_update_threat_lists (self); + } } static void ephy_gsb_service_init (EphyGSBService *self) { + self->session = soup_session_new (); + g_object_set (self->session, "user-agent", ephy_user_agent_get_internal (), NULL); } static void @@ -110,6 +301,14 @@ ephy_gsb_service_class_init (EphyGSBServiceClass *klass) object_class->get_property = ephy_gsb_service_get_property; object_class->constructed = ephy_gsb_service_constructed; object_class->dispose = ephy_gsb_service_dispose; + object_class->finalize = ephy_gsb_service_finalize; + + obj_properties[PROP_API_KEY] = + g_param_spec_string ("api-key", + "API key", + "The API key to access the Google Safe Browsing API", + NULL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); obj_properties[PROP_GSB_STORAGE] = g_param_spec_object ("gsb-storage", @@ -122,13 +321,17 @@ ephy_gsb_service_class_init (EphyGSBServiceClass *klass) } EphyGSBService * -ephy_gsb_service_new (const char *db_path) +ephy_gsb_service_new (const char *api_key, + const char *db_path) { EphyGSBService *service; EphyGSBStorage *storage; storage = ephy_gsb_storage_new (db_path); - service = g_object_new (EPHY_TYPE_GSB_SERVICE, "gsb-storage", storage, NULL); + service = g_object_new (EPHY_TYPE_GSB_SERVICE, + "api-key", api_key, + "gsb-storage", storage, + NULL); g_object_unref (storage); return service; diff --git a/lib/safe-browsing/ephy-gsb-service.h b/lib/safe-browsing/ephy-gsb-service.h index bcb407292..83713949d 100644 --- a/lib/safe-browsing/ephy-gsb-service.h +++ b/lib/safe-browsing/ephy-gsb-service.h @@ -28,6 +28,7 @@ G_BEGIN_DECLS G_DECLARE_FINAL_TYPE (EphyGSBService, ephy_gsb_service, EPHY, GSB_SERVICE, GObject) -EphyGSBService *ephy_gsb_service_new (const char *db_path); +EphyGSBService *ephy_gsb_service_new (const char *api_key, + const char *db_path); G_END_DECLS diff --git a/lib/safe-browsing/ephy-gsb-storage.c b/lib/safe-browsing/ephy-gsb-storage.c index 6b7c2de07..27edf0a03 100644 --- a/lib/safe-browsing/ephy-gsb-storage.c +++ b/lib/safe-browsing/ephy-gsb-storage.c @@ -26,10 +26,32 @@ #include <errno.h> #include <glib/gstdio.h> +#include <string.h> -/* Update this if you modify the database table structure. */ +#define CUE_LEN 4 + +/* Keep this lower than 200 or else you'll get "too many SQL variables" error + * in ephy_gsb_storage_insert_batch(). SQLITE_MAX_VARIABLE_NUMBER is hardcoded + * in sqlite3 as 999. + */ +#define BATCH_SIZE 199 + +/* Update schema version if you: + * 1) Modify the database table structure. + * 2) Add new threat lists below. + */ #define SCHEMA_VERSION "1.0" +/* The available Linux threat lists of Google Safe Browsing API v4. + * The format is {THREAT_TYPE, PLATFORM_TYPE, THREAT_ENTRY_TYPE}. + */ +static const char * const gsb_linux_threat_lists[][3] = { + {"MALWARE", "LINUX", "URL"}, + {"SOCIAL_ENGINEERING", "LINUX", "URL"}, + {"UNWANTED_SOFTWARE", "LINUX", "URL"}, + {"MALWARE", "LINUX", "IP_RANGE"}, +}; + struct _EphyGSBStorage { GObject parent_instance; @@ -50,6 +72,55 @@ enum { static GParamSpec *obj_properties[LAST_PROP]; static gboolean +bind_threat_list_params (EphySQLiteStatement *statement, + EphyGSBThreatList *list, + int threat_type_col, + int platform_type_col, + int threat_entry_type_col, + int client_state_col) +{ + GError *error = NULL; + + g_assert (statement); + g_assert (list); + + if (list->threat_type && threat_type_col >= 0) { + ephy_sqlite_statement_bind_string (statement, threat_type_col, list->threat_type, &error); + if (error) { + g_warning ("Failed to bind threat type: %s", error->message); + g_error_free (error); + return FALSE; + } + } + if (list->platform_type && platform_type_col >= 0) { + ephy_sqlite_statement_bind_string (statement, platform_type_col, list->platform_type, &error); + if (error) { + g_warning ("Failed to bind platform type: %s", error->message); + g_error_free (error); + return FALSE; + } + } + if (list->threat_entry_type && threat_entry_type_col >= 0) { + ephy_sqlite_statement_bind_string (statement, threat_entry_type_col, list->threat_entry_type, &error); + if (error) { + g_warning ("Failed to bind threat entry type: %s", error->message); + g_error_free (error); + return FALSE; + } + } + if (list->client_state && client_state_col >= 0) { + ephy_sqlite_statement_bind_string (statement, client_state_col, list->client_state, &error); + if (error) { + g_warning ("Failed to bind client state: %s", error->message); + g_error_free (error); + return FALSE; + } + } + + return TRUE; +} + +static gboolean ephy_gsb_storage_init_metadata_table (EphyGSBStorage *self) { GError *error = NULL; @@ -62,8 +133,8 @@ ephy_gsb_storage_init_metadata_table (EphyGSBStorage *self) return TRUE; sql = "CREATE TABLE metadata (" - "name LONGVARCHAR NOT NULL PRIMARY KEY," - "value LONGVARCHAR NOT NULL" + "name VARCHAR NOT NULL PRIMARY KEY," + "value VARCHAR NOT NULL" ")"; ephy_sqlite_connection_execute (self->db, sql, &error); if (error) { @@ -88,8 +159,11 @@ ephy_gsb_storage_init_metadata_table (EphyGSBStorage *self) static gboolean ephy_gsb_storage_init_threats_table (EphyGSBStorage *self) { + EphySQLiteStatement *statement = NULL; GError *error = NULL; + GString *string = NULL; const char *sql; + gboolean retval = FALSE; g_assert (EPHY_IS_GSB_STORAGE (self)); g_assert (EPHY_IS_SQLITE_CONNECTION (self->db)); @@ -98,21 +172,58 @@ ephy_gsb_storage_init_threats_table (EphyGSBStorage *self) return TRUE; sql = "CREATE TABLE threats (" - "threat_type LONGVARCHAR NOT NULL," - "platform_type LONGVARCHAR NOT NULL," - "threat_entry_type LONGVARCHAR NOT NULL," - "client_state LONGVARCHAR," + "threat_type VARCHAR NOT NULL," + "platform_type VARCHAR NOT NULL," + "threat_entry_type VARCHAR NOT NULL," + "client_state VARCHAR," "timestamp INTEGER NOT NULL DEFAULT (CAST(strftime('%s', 'now') AS INT))," "PRIMARY KEY (threat_type, platform_type, threat_entry_type)" ")"; ephy_sqlite_connection_execute (self->db, sql, &error); if (error) { g_warning ("Failed to create threats table: %s", error->message); - g_error_free (error); - return FALSE; + goto out; } - return TRUE; + sql = "INSERT INTO threats (threat_type, platform_type, threat_entry_type) VALUES "; + string = g_string_new (sql); + for (guint i = 0; i < G_N_ELEMENTS (gsb_linux_threat_lists); i++) + g_string_append (string, "(?, ?, ?),"); + /* Remove trailing comma character. */ + g_string_erase (string, string->len - 1, -1); + + statement = ephy_sqlite_connection_create_statement (self->db, string->str, &error); + if (error) { + g_warning ("Failed to create threats table insert statement: %s", error->message); + goto out; + } + + for (guint i = 0; i < G_N_ELEMENTS (gsb_linux_threat_lists); i++) { + EphyGSBThreatList *list = ephy_gsb_threat_list_new (gsb_linux_threat_lists[i][0], + gsb_linux_threat_lists[i][1], + gsb_linux_threat_lists[i][2], + NULL, 0); + bind_threat_list_params (statement, list, i * 3, i * 3 + 1, i * 3 + 2, -1); + ephy_gsb_threat_list_free (list); + } + + ephy_sqlite_statement_step (statement, &error); + if (error) { + g_warning ("Failed to insert initial data into threats table: %s", error->message); + goto out; + } + + retval = TRUE; + +out: + if (string) + g_string_free (string, TRUE); + if (statement) + g_object_unref (statement); + if (error) + g_error_free (error); + + return retval; } static gboolean @@ -129,10 +240,10 @@ ephy_gsb_storage_init_hash_prefix_table (EphyGSBStorage *self) sql = "CREATE TABLE hash_prefix (" "cue BLOB NOT NULL," /* The first 4 bytes. */ - "value BLOB NOT NULL," /* The prefix itself, can vary from 4 bytes to 32 bytes. */ - "threat_type LONGVARCHAR NOT NULL," - "platform_type LONGVARCHAR NOT NULL," - "threat_entry_type LONGVARCHAR NOT NULL," + "value BLOB NOT NULL," /* The prefix itself, can vary from 4 to 32 bytes. */ + "threat_type VARCHAR NOT NULL," + "platform_type VARCHAR NOT NULL," + "threat_entry_type VARCHAR NOT NULL," "timestamp INTEGER NOT NULL DEFAULT (CAST(strftime('%s', 'now') AS INT))," "negative_expires_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', 'now') AS INT))," "PRIMARY KEY (value, threat_type, platform_type, threat_entry_type)," @@ -164,9 +275,9 @@ ephy_gsb_storage_init_hash_full_table (EphyGSBStorage *self) sql = "CREATE TABLE hash_full (" "value BLOB NOT NULL," /* The 32 bytes full hash. */ - "threat_type LONGVARCHAR NOT NULL," - "platform_type LONGVARCHAR NOT NULL," - "threat_entry_type LONGVARCHAR NOT NULL," + "threat_type VARCHAR NOT NULL," + "platform_type VARCHAR NOT NULL," + "threat_entry_type VARCHAR NOT NULL," "timestamp INTEGER NOT NULL DEFAULT (CAST(strftime('%s', 'now') AS INT))," "expires_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', 'now') AS INT))," "PRIMARY KEY (value, threat_type, platform_type, threat_entry_type)" @@ -207,12 +318,18 @@ ephy_gsb_storage_open_db (EphyGSBStorage *self) } /* Enable foreign keys. */ - ephy_sqlite_connection_execute (self->db, "PRAGMA foreign_keys = ON", &error); + ephy_sqlite_connection_execute (self->db, "PRAGMA foreign_keys=ON", &error); if (error) { g_warning ("Failed to enable foreign keys pragma: %s", error->message); goto out_err; } + ephy_sqlite_connection_execute (self->db, "PRAGMA synchronous=OFF", &error); + if (error) { + g_warning ("Failed to disable synchronous pragma: %s", error->message); + goto out_err; + } + return TRUE; out_err: @@ -277,7 +394,7 @@ ephy_gsb_storage_check_schema_version (EphyGSBStorage *self) sql = "SELECT value FROM metadata WHERE name='schema_version'"; statement = ephy_sqlite_connection_create_statement (self->db, sql, &error); if (error) { - g_warning ("Failed to build select schema version statement: %s", error->message); + g_warning ("Failed to create select schema version statement: %s", error->message); g_error_free (error); return FALSE; } @@ -408,3 +525,493 @@ ephy_gsb_storage_is_operable (EphyGSBStorage *self) return self->is_operable; } + +gint64 +ephy_gsb_storage_get_next_update_time (EphyGSBStorage *self) +{ + EphySQLiteStatement *statement = NULL; + GError *error = NULL; + const char *next_update_at; + const char *sql; + gint64 next_update_time; + + g_assert (EPHY_IS_GSB_STORAGE (self)); + g_assert (self->is_operable); + + sql = "SELECT value FROM metadata WHERE name='next_update_at'"; + statement = ephy_sqlite_connection_create_statement (self->db, sql, &error); + if (error) { + g_warning ("Failed to create select next update statement: %s", error->message); + g_error_free (error); + return G_MAXINT64; + } + + ephy_sqlite_statement_step (statement, &error); + if (error) { + g_warning ("Failed to retrieve next update time: %s", error->message); + g_error_free (error); + g_object_unref (statement); + return G_MAXINT64; + } + + next_update_at = ephy_sqlite_statement_get_column_as_string (statement, 0); + sscanf (next_update_at, "%ld", &next_update_time); + + g_object_unref (statement); + + return next_update_time; +} + +void +ephy_gsb_storage_set_next_update_time (EphyGSBStorage *self, + gint64 next_update_time) +{ + EphySQLiteStatement *statement = NULL; + GError *error = NULL; + char *value = NULL; + const char *sql; + + g_assert (EPHY_IS_GSB_STORAGE (self)); + g_assert (self->is_operable); + + sql = "UPDATE metadata SET value=? WHERE name='next_update_at'"; + statement = ephy_sqlite_connection_create_statement (self->db, sql, &error); + if (error) { + g_warning ("Failed to create update next update time statement: %s", error->message); + goto out; + } + + value = g_strdup_printf ("%ld", next_update_time);; + ephy_sqlite_statement_bind_string (statement, 0, value, &error); + if (error) { + g_warning ("Failed to bind string in next update time statement: %s", error->message); + goto out; + } + + ephy_sqlite_statement_step (statement, &error); + if (error) + g_warning ("Failed to update next update time: %s", error->message); + +out: + g_free (value); + if (statement) + g_object_unref (statement); + if (error) + g_error_free (error); +} + +GList * +ephy_gsb_storage_get_threat_lists (EphyGSBStorage *self) +{ + EphySQLiteStatement *statement = NULL; + GError *error = NULL; + GList *threat_lists = NULL; + const char *sql; + + g_assert (EPHY_IS_GSB_STORAGE (self)); + g_assert (self->is_operable); + + sql = "SELECT threat_type, platform_type, threat_entry_type, client_state, timestamp FROM threats"; + statement = ephy_sqlite_connection_create_statement (self->db, sql, &error); + if (error) { + g_warning ("Failed to create select threat lists statement: %s", error->message); + g_error_free (error); + return NULL; + } + + while (ephy_sqlite_statement_step (statement, &error)) { + const char *threat_type = ephy_sqlite_statement_get_column_as_string (statement, 0); + const char *platform_type = ephy_sqlite_statement_get_column_as_string (statement, 1); + const char *threat_entry_type = ephy_sqlite_statement_get_column_as_string (statement, 2); + const char *client_state = ephy_sqlite_statement_get_column_as_string (statement, 3); + gint64 timestamp = ephy_sqlite_statement_get_column_as_int64 (statement, 4); + EphyGSBThreatList *list = ephy_gsb_threat_list_new (threat_type, platform_type, + threat_entry_type, client_state, + timestamp); + threat_lists = g_list_prepend (threat_lists, list); + } + + if (error) { + g_warning ("Failed to execute select threat lists statement: %s", error->message); + g_error_free (error); + } + + g_object_unref (statement); + + return g_list_reverse (threat_lists); +} + +char * +ephy_gsb_storage_compute_checksum (EphyGSBStorage *self, + EphyGSBThreatList *list) +{ + EphySQLiteStatement *statement = NULL; + GError *error = NULL; + const char *sql; + char *retval = NULL; + GChecksum *checksum = NULL; + guint8 *digest = NULL; + gsize digest_len = g_checksum_type_get_length (G_CHECKSUM_SHA256); + + g_assert (EPHY_IS_GSB_STORAGE (self)); + g_assert (self->is_operable); + g_assert (list); + + sql = "SELECT value FROM hash_prefix WHERE " + "threat_type=? AND platform_type=? AND threat_entry_type=? " + "ORDER BY value"; + statement = ephy_sqlite_connection_create_statement (self->db, sql, &error); + if (error) { + g_warning ("Failed to create select hash prefix statement: %s", error->message); + goto out; + } + + if (!bind_threat_list_params (statement, list, 0, 1, 2, -1)) + goto out; + + checksum = g_checksum_new (G_CHECKSUM_SHA256); + while (ephy_sqlite_statement_step (statement, &error)) { + g_checksum_update (checksum, + ephy_sqlite_statement_get_column_as_blob (statement, 0), + ephy_sqlite_statement_get_column_size (statement, 0)); + } + + if (error) { + g_warning ("Failed to execute select hash prefix statement: %s", error->message); + goto out; + } + + digest = g_malloc (digest_len); + g_checksum_get_digest (checksum, digest, &digest_len); + retval = g_base64_encode (digest, digest_len); + +out: + g_free (digest); + if (statement) + g_object_unref (statement); + if (checksum) + g_checksum_free (checksum); + if (error) + g_error_free (error); + + return retval; +} + +void +ephy_gsb_storage_update_client_state (EphyGSBStorage *self, + EphyGSBThreatList *list, + gboolean clear) +{ + EphySQLiteStatement *statement = NULL; + GError *error = NULL; + const char *sql; + + g_assert (EPHY_IS_GSB_STORAGE (self)); + g_assert (self->is_operable); + g_assert (list); + + if (clear) { + sql = "UPDATE threats SET " + "timestamp=(CAST(strftime('%s', 'now') AS INT)), client_state=NULL " + "WHERE threat_type=? AND platform_type=? AND threat_entry_type=?"; + } else { + sql = "UPDATE threats SET " + "timestamp=(CAST(strftime('%s', 'now') AS INT)), client_state=? " + "WHERE threat_type=? AND platform_type=? AND threat_entry_type=?"; + } + + statement = ephy_sqlite_connection_create_statement (self->db, sql, &error); + if (error) { + g_warning ("Failed to create update threats statement: %s", error->message); + goto out; + } + + if (!bind_threat_list_params (statement, list, 1, 2, 3, clear ? -1 : 0)) + goto out; + + ephy_sqlite_statement_step (statement, &error); + if (error) + g_warning ("Failed to execute update threat statement: %s", error->message); + +out: + if (statement) + g_object_unref (statement); + if (error) + g_error_free (error); +} + +void +ephy_gsb_storage_clear_hash_prefixes (EphyGSBStorage *self, + EphyGSBThreatList *list) +{ + EphySQLiteStatement *statement = NULL; + GError *error = NULL; + const char *sql; + + g_assert (EPHY_IS_GSB_STORAGE (self)); + g_assert (self->is_operable); + g_assert (list); + + sql = "DELETE FROM hash_prefix WHERE " + "threat_type=? AND platform_type=? AND threat_entry_type=?"; + statement = ephy_sqlite_connection_create_statement (self->db, sql, &error); + if (error) { + g_warning ("Failed to create delete hash prefix statement: %s", error->message); + goto out; + } + + if (!bind_threat_list_params (statement, list, 0, 1, 2, -1)) + goto out; + + ephy_sqlite_statement_step (statement, &error); + if (error) + g_warning ("Failed to execute clear hash prefix statement: %s", error->message); + +out: + if (statement) + g_object_unref (statement); + if (error) + g_error_free (error); +} + +static GList * +ephy_gsb_storage_get_hash_prefixes_to_delete (EphyGSBStorage *self, + EphyGSBThreatList *list, + GHashTable *indices, + gsize *num_prefixes) +{ + EphySQLiteStatement *statement = NULL; + GError *error = NULL; + GList *prefixes = NULL; + const char *sql; + guint index = 0; + + g_assert (EPHY_IS_GSB_STORAGE (self)); + g_assert (self->is_operable); + g_assert (list); + g_assert (indices); + + *num_prefixes = 0; + + sql = "SELECT value FROM hash_prefix WHERE " + "threat_type=? AND platform_type=? AND threat_entry_type=? " + "ORDER BY value"; + statement = ephy_sqlite_connection_create_statement (self->db, sql, &error); + if (error) { + g_warning ("Failed to create select prefix value statement: %s", error->message); + goto out; + } + + if (!bind_threat_list_params (statement, list, 0, 1, 2, -1)) + goto out; + + while (ephy_sqlite_statement_step (statement, &error)) { + if (g_hash_table_contains (indices, GINT_TO_POINTER (index))) { + const guint8 *blob = ephy_sqlite_statement_get_column_as_blob (statement, 0); + gsize size = ephy_sqlite_statement_get_column_size (statement, 0); + prefixes = g_list_prepend (prefixes, g_bytes_new (blob, size)); + *num_prefixes += 1; + } + index++; + } + + if (error) + g_warning ("Failed to execute select prefix value statement: %s", error->message); + +out: + if (statement) + g_object_unref (statement); + if (error) + g_error_free (error); + + return prefixes; +} + +static GList * +ephy_gsb_storage_delete_batch (EphyGSBStorage *self, + EphyGSBThreatList *list, + GList *prefixes, + gsize num_prefixes) +{ + EphySQLiteStatement *statement = NULL; + GError *error = NULL; + GString *sql; + + g_assert (EPHY_IS_GSB_STORAGE (self)); + g_assert (self->is_operable); + g_assert (list); + g_assert (prefixes); + + sql = g_string_new ("DELETE FROM hash_prefix WHERE " + "threat_type=? AND platform_type=? and threat_entry_type=? " + "AND value IN ("); + for (gsize i = 0; i < num_prefixes; i++) + g_string_append (sql, "?,"); + /* Replace trailing comma character with close parenthesis character. */ + g_string_overwrite (sql, sql->len - 1, ")"); + + statement = ephy_sqlite_connection_create_statement (self->db, sql->str, &error); + if (error) { + g_warning ("Failed to create delete hash prefix statement: %s", error->message); + goto out; + } + + if (!bind_threat_list_params (statement, list, 0, 1, 2, -1)) + goto out; + + for (gsize i = 0; i < num_prefixes; i++) { + GBytes *prefix = (GBytes *)prefixes->data; + ephy_sqlite_statement_bind_blob (statement, i + 3, + g_bytes_get_data (prefix, NULL), + g_bytes_get_size (prefix), + &error); + if (error) { + g_warning ("Failed to bind blob in delete hash prefix statement: %s", error->message); + goto out; + } + prefixes = prefixes->next; + } + + ephy_sqlite_statement_step (statement, &error); + if (error) + g_warning ("Failed to execute delete hash prefix statement: %s", error->message); + +out: + g_string_free (sql, TRUE); + if (statement) + g_object_unref (statement); + if (error) + g_error_free (error); + + /* Return where we left off. */ + return prefixes; +} + +void +ephy_gsb_storage_delete_hash_prefixes (EphyGSBStorage *self, + EphyGSBThreatList *list, + JsonArray *indices) +{ + GList *prefixes = NULL; + GList *head = NULL; + GHashTable *set; + gsize num_prefixes; + + g_assert (EPHY_IS_GSB_STORAGE (self)); + g_assert (self->is_operable); + g_assert (list); + g_assert (indices); + + LOG ("Deleting %u hash prefixes...", json_array_get_length (indices)); + + /* Move indices from the JSON array to a hash table set. */ + set = g_hash_table_new (g_direct_hash, g_direct_equal); + for (guint i = 0; i < json_array_get_length (indices); i++) + g_hash_table_add (set, GINT_TO_POINTER (json_array_get_int_element (indices, i))); + + prefixes = ephy_gsb_storage_get_hash_prefixes_to_delete (self, list, set, &num_prefixes); + head = prefixes; + + for (gsize i = 0; i < num_prefixes / BATCH_SIZE; i++) + head = ephy_gsb_storage_delete_batch (self, list, head, BATCH_SIZE); + + if (num_prefixes % BATCH_SIZE != 0) + ephy_gsb_storage_delete_batch (self, list, head, num_prefixes % BATCH_SIZE); + + g_hash_table_unref (set); + g_list_free_full (prefixes, (GDestroyNotify)g_bytes_unref); +} + +static void +ephy_gsb_storage_insert_batch (EphyGSBStorage *self, + EphyGSBThreatList *list, + const guint8 *prefixes, + gsize start, + gsize end, + gsize len) +{ + EphySQLiteStatement *statement = NULL; + GError *error = NULL; + GString *sql; + gsize id = 0; + + g_assert (EPHY_IS_GSB_STORAGE (self)); + g_assert (self->is_operable); + g_assert (list); + g_assert (prefixes); + + sql = g_string_new ("INSERT INTO hash_prefix " + "(cue, value, threat_type, platform_type, threat_entry_type) VALUES "); + for (gsize k = start; k < end; k += len) + g_string_append (sql, "(?, ?, ?, ?, ?),"); + /* Remove trailing comma character. */ + g_string_erase (sql, sql->len - 1, -1); + + statement = ephy_sqlite_connection_create_statement (self->db, sql->str, &error); + g_string_free (sql, TRUE); + + if (error) { + g_warning ("Failed to create insert hash prefix statement: %s", error->message); + goto out; + } + + for (gsize k = start; k < end; k += len) { + if (!ephy_sqlite_statement_bind_blob (statement, id++, prefixes + k, CUE_LEN, NULL) || + !ephy_sqlite_statement_bind_blob (statement, id++, prefixes + k, len, NULL) || + !bind_threat_list_params (statement, list, id, id + 1, id + 2, -1)) { + g_warning ("Failed to bind values in hash prefix statement"); + goto out; + } + id += 3; + } + + ephy_sqlite_statement_step (statement, &error); + if (error) + g_warning ("Failed to execute insert hash prefix statement: %s", error->message); + +out: + if (statement) + g_object_unref (statement); + if (error) + g_error_free (error); +} + +void +ephy_gsb_storage_insert_hash_prefixes (EphyGSBStorage *self, + EphyGSBThreatList *list, + gsize prefix_len, + const char *prefixes_b64) +{ + guint8 *prefixes; + gsize prefixes_len; + gsize num_batches; + gboolean leftovers; + + g_assert (EPHY_IS_GSB_STORAGE (self)); + g_assert (self->is_operable); + g_assert (list); + g_assert (prefix_len > 0); + g_assert (prefixes_b64); + + prefixes = g_base64_decode (prefixes_b64, &prefixes_len); + num_batches = (prefixes_len / prefix_len) / BATCH_SIZE; + leftovers = (prefixes_len / prefix_len) % BATCH_SIZE != 0; + + LOG ("Inserting %lu hash prefixes of size %ld...", prefixes_len / prefix_len, prefix_len); + + for (gsize i = 0; i < num_batches; i++) { + ephy_gsb_storage_insert_batch (self, list, prefixes, + i * prefix_len * BATCH_SIZE, + (i + 1) * prefix_len * BATCH_SIZE, + prefix_len); + } + + if (leftovers) { + ephy_gsb_storage_insert_batch (self, list, prefixes, + num_batches * prefix_len * BATCH_SIZE, + prefixes_len - 1, + prefix_len); + } + + g_free (prefixes); +} diff --git a/lib/safe-browsing/ephy-gsb-storage.h b/lib/safe-browsing/ephy-gsb-storage.h index 8731ecd81..31a9f3120 100644 --- a/lib/safe-browsing/ephy-gsb-storage.h +++ b/lib/safe-browsing/ephy-gsb-storage.h @@ -20,7 +20,10 @@ #pragma once +#include "ephy-gsb-utils.h" + #include <glib-object.h> +#include <json-glib/json-glib.h> G_BEGIN_DECLS @@ -28,7 +31,25 @@ G_BEGIN_DECLS G_DECLARE_FINAL_TYPE (EphyGSBStorage, ephy_gsb_storage, EPHY, GSB_STORAGE, GObject) -EphyGSBStorage *ephy_gsb_storage_new (const char *db_path); -gboolean ephy_gsb_storage_is_operable (EphyGSBStorage *self); +EphyGSBStorage *ephy_gsb_storage_new (const char *db_path); +gboolean ephy_gsb_storage_is_operable (EphyGSBStorage *self); +gint64 ephy_gsb_storage_get_next_update_time (EphyGSBStorage *self); +void ephy_gsb_storage_set_next_update_time (EphyGSBStorage *self, + gint64 next_update_time); +GList *ephy_gsb_storage_get_threat_lists (EphyGSBStorage *self); +char *ephy_gsb_storage_compute_checksum (EphyGSBStorage *self, + EphyGSBThreatList *list); +void ephy_gsb_storage_update_client_state (EphyGSBStorage *self, + EphyGSBThreatList *list, + gboolean clear); +void ephy_gsb_storage_clear_hash_prefixes (EphyGSBStorage *self, + EphyGSBThreatList *list); +void ephy_gsb_storage_delete_hash_prefixes (EphyGSBStorage *self, + EphyGSBThreatList *list, + JsonArray *indices); +void ephy_gsb_storage_insert_hash_prefixes (EphyGSBStorage *self, + EphyGSBThreatList *list, + gsize prefix_len, + const char *prefixes_b64); G_END_DECLS diff --git a/lib/safe-browsing/ephy-gsb-utils.c b/lib/safe-browsing/ephy-gsb-utils.c new file mode 100644 index 000000000..8cef2e8a9 --- /dev/null +++ b/lib/safe-browsing/ephy-gsb-utils.c @@ -0,0 +1,128 @@ +/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Copyright © 2017 Gabriel Ivascu <gabrielivascu@gnome.org> + * + * This file is part of Epiphany. + * + * Epiphany 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. + * + * Epiphany is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Epiphany. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" +#include "ephy-gsb-utils.h" + +#include <json-glib/json-glib.h> + +EphyGSBThreatList * +ephy_gsb_threat_list_new (const char *threat_type, + const char *platform_type, + const char *threat_entry_type, + const char *client_state, + gint64 timestamp) +{ + EphyGSBThreatList *list; + + g_assert (threat_type); + g_assert (platform_type); + g_assert (threat_entry_type); + + list = g_slice_new (EphyGSBThreatList); + list->threat_type = g_strdup (threat_type); + list->platform_type = g_strdup (platform_type); + list->threat_entry_type = g_strdup (threat_entry_type); + list->client_state = g_strdup (client_state); + list->timestamp = timestamp; + + return list; +} +void +ephy_gsb_threat_list_free (EphyGSBThreatList *list) +{ + g_assert (list); + + g_free (list->threat_type); + g_free (list->platform_type); + g_free (list->threat_entry_type); + g_free (list->client_state); + g_slice_free (EphyGSBThreatList, list); +} + +static JsonObject * +ephy_gsb_utils_make_client_info (void) +{ + JsonObject *client_info; + + client_info = json_object_new (); + json_object_set_string_member (client_info, "clientId", "Epiphany"); + json_object_set_string_member (client_info, "clientVersion", VERSION); + + return client_info; +} + +static JsonObject * +ephy_gsb_utils_make_contraints (void) +{ + JsonObject *constraints; + JsonArray *compressions; + + compressions = json_array_new (); + json_array_add_string_element (compressions, "RAW"); + + constraints = json_object_new (); + /* No restriction for the number of update entries. */ + json_object_set_int_member (constraints, "maxUpdateEntries", 0); + /* No restriction for the number of database entries. */ + json_object_set_int_member (constraints, "maxDatabaseEntries", 0); + /* Let the server pick the geographic region automatically. */ + json_object_set_null_member (constraints, "region"); + json_object_set_array_member (constraints, "supportedCompressions", compressions); + + return constraints; +} + +char * +ephy_gsb_utils_make_list_updates_request (GList *threat_lists) +{ + JsonArray *requests; + JsonObject *body_obj; + JsonNode *body_node; + char *retval; + + g_assert (threat_lists); + + requests = json_array_new (); + for (GList *l = threat_lists; l && l->data; l = l->next) { + EphyGSBThreatList *list = (EphyGSBThreatList *)l->data; + JsonObject *request = json_object_new (); + + json_object_set_string_member (request, "threatType", list->threat_type); + json_object_set_string_member (request, "platformType", list->platform_type); + json_object_set_string_member (request, "threatEntryType", list->threat_entry_type); + json_object_set_string_member (request, "state", list->client_state); + json_object_set_object_member (request, "constraints", ephy_gsb_utils_make_contraints ()); + json_array_add_object_element (requests, request); + } + + body_obj = json_object_new (); + json_object_set_object_member (body_obj, "client", ephy_gsb_utils_make_client_info ()); + json_object_set_array_member (body_obj, "listUpdateRequests", requests); + + body_node = json_node_new (JSON_NODE_OBJECT); + json_node_set_object (body_node, body_obj); + retval = json_to_string (body_node, FALSE); + + json_object_unref (body_obj); + json_node_unref (body_node); + + return retval; +} diff --git a/lib/safe-browsing/ephy-gsb-utils.h b/lib/safe-browsing/ephy-gsb-utils.h new file mode 100644 index 000000000..e7906fb2e --- /dev/null +++ b/lib/safe-browsing/ephy-gsb-utils.h @@ -0,0 +1,44 @@ +/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Copyright © 2017 Gabriel Ivascu <gabrielivascu@gnome.org> + * + * This file is part of Epiphany. + * + * Epiphany 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. + * + * Epiphany is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Epiphany. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <glib.h> + +G_BEGIN_DECLS + +typedef struct { + char *threat_type; + char *platform_type; + char *threat_entry_type; + char *client_state; + gint64 timestamp; +} EphyGSBThreatList; + +EphyGSBThreatList *ephy_gsb_threat_list_new (const char *threat_type, + const char *platform_type, + const char *threat_entry_type, + const char *client_state, + gint64 timestamp); +void ephy_gsb_threat_list_free (EphyGSBThreatList *list); + +char *ephy_gsb_utils_make_list_updates_request (GList *threat_lists); + +G_END_DECLS |