Merge lp:~midori/midori/hstsWebExtension into lp:midori

Proposed by Christian Dywan on 2015-06-13
Status: Work in progress
Proposed branch: lp:~midori/midori/hstsWebExtension
Merge into: lp:midori
Diff against target: 528 lines (+330/-89)
5 files modified
CMakeLists.txt (+1/-1)
extensions/CMakeLists.txt (+92/-82)
extensions/hsts.web.vala (+177/-0)
katze/midori-paths.vala (+10/-6)
midori/main.c (+50/-0)
To merge this branch: bzr merge lp:~midori/midori/hstsWebExtension
Reviewer Review Type Date Requested Status
Midori Devs 2015-06-13 Pending
Review via email: mp+261897@code.launchpad.net

Commit message

Implement HSTS as a web extension

Description of the change

This branch enables building of separate web extensions.

Note: At this point we probably want to go for libpeas rather than the direct WebKit2 API. That would save us an additional porting step later if we do it in one go. And give us control over extensions that we can't have otherwise.

To post a comment you must log in.

Unmerged revisions

6965. By Christian Dywan on 2015-06-13

Implement HSTS as a web extension

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'CMakeLists.txt'
2--- CMakeLists.txt 2015-03-23 11:33:24 +0000
3+++ CMakeLists.txt 2015-06-13 13:38:06 +0000
4@@ -268,9 +268,9 @@
5 install(FILES AUTHORS COPYING ChangeLog EXPAT README DESTINATION ${CMAKE_INSTALL_DOCDIR})
6
7 add_subdirectory (midori)
8-add_subdirectory (extensions)
9 enable_testing()
10 add_subdirectory (tests)
11+add_subdirectory (extensions)
12 add_subdirectory (po)
13 add_subdirectory (icons)
14 add_subdirectory (data)
15
16=== modified file 'extensions/CMakeLists.txt'
17--- extensions/CMakeLists.txt 2015-04-19 14:06:12 +0000
18+++ extensions/CMakeLists.txt 2015-06-13 13:38:06 +0000
19@@ -23,6 +23,10 @@
20 "nsplugin-manager.vala"
21 "tabs2one.c"
22 )
23+else ()
24+ list(REMOVE_ITEM EXTENSIONS
25+ "hsts.web.vala"
26+ )
27 endif ()
28
29 # FIXME: re-enable webmedia extension
30@@ -36,99 +40,105 @@
31 list(REMOVE_ITEM EXTENSIONS "tabs2one.c")
32 endif()
33
34-foreach(UNIT_SRC ${EXTENSIONS})
35- string(FIND ${UNIT_SRC} ".c" UNIT_EXTENSION)
36- if (UNIT_EXTENSION GREATER -1)
37- string(REPLACE ".c" "" UNIT ${UNIT_SRC})
38- add_library(${UNIT} MODULE ${UNIT_SRC})
39- target_link_libraries(${UNIT}
40- ${LIBMIDORI}
41- )
42- set_target_properties(${UNIT} PROPERTIES
43- COMPILE_FLAGS ${CFLAGS}
44- )
45- install(TARGETS ${UNIT}
46- LIBRARY DESTINATION ${EXTENSIONDIR}
47- )
48- endif ()
49-endforeach ()
50+include(ValaPrecompile)
51
52-foreach(UNIT_SRC ${EXTENSIONS})
53- string(FIND ${UNIT_SRC} "." UNIT_EXTENSION)
54- if (UNIT_EXTENSION EQUAL -1)
55- file(GLOB UNIT_FILES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "${UNIT_SRC}/*.c")
56- file(GLOB UNIT_FILES_VALA RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "${UNIT_SRC}/*.vala")
57- if (UNIT_FILES_VALA)
58- include(ValaPrecompile)
59- vala_precompile(UNIT_SRC_C ${UNIT_SRC}
60- ${UNIT_FILES_VALA}
61- PACKAGES
62+macro(build_extension name)
63+ set(source "${ARGN}")
64+ set(SOURCE_VALA "")
65+ foreach(FILE ${source})
66+ string(FIND ${FILE} ".web." WEB_EXTENSION)
67+ string(FIND ${FILE} ".vala" VALA_EXTENSION)
68+ if (WEB_EXTENSION GREATER -1)
69+ string(REPLACE ".vala" "" WEB_NAME "${name}_${FILE}")
70+ string(REPLACE "." "_" WEB_NAME "${WEB_NAME}")
71+ vala_precompile(WEB_EXTENSION_C "${WEB_NAME}"
72+ ${FILE}
73+ PACKAGES
74 ${PKGS}
75- OPTIONS
76- ${VALAFLAGS}
77- --use-header="${CMAKE_PROJECT_NAME}-core.h"
78- GENERATE_HEADER
79- "${UNIT_SRC}"
80- GENERATE_HEADER
81- ${UNIT}
82- CUSTOM_VAPIS
83- ${EXTRA_VAPIS}
84- "${CMAKE_SOURCE_DIR}/midori/midori.vapi"
85- "${CMAKE_BINARY_DIR}/midori/${LIBMIDORI}.vapi"
86- )
87- set(UNIT_FILES ${UNIT_FILES} ${UNIT_SRC_C})
88- endif ()
89- if (UNIT_FILES)
90- add_library(${UNIT_SRC} MODULE ${UNIT_FILES})
91- target_link_libraries(${UNIT_SRC}
92- ${LIBMIDORI}
93+ OPTIONS
94+ ${VALAFLAGS}
95+ GENERATE_HEADER
96+ ${FILE}
97+ CUSTOM_VAPIS
98+ "${CMAKE_SOURCE_DIR}/midori/webkit2gtk-web-extension-4.0.vapi"
99+ )
100+ add_library(${WEB_NAME} MODULE ${WEB_EXTENSION_C})
101+ target_link_libraries(${WEB_NAME}
102+ ${LIBMIDORI2}
103+ )
104+ install(TARGETS ${WEB_NAME}
105+ LIBRARY DESTINATION ${EXTENSIONDIR}
106+ )
107+ # Extensions with Vala code get the lenient VALA_CFLAGS
108+ set_target_properties(${WEB_NAME} PROPERTIES
109+ COMPILE_FLAGS ${VALA_CFLAGS}
110 )
111- install(TARGETS ${UNIT_SRC}
112- LIBRARY DESTINATION ${EXTENSIONDIR}
113- )
114- # extensions with vala code get the lenient VALA_CFLAGS
115- # others get the usual CFLAGS with -Wall and -Werror
116- if (UNIT_FILES_VALA)
117- set_target_properties(${UNIT_SRC} PROPERTIES
118- COMPILE_FLAGS ${VALA_CFLAGS}
119- )
120- else ()
121- set_target_properties(${UNIT_SRC} PROPERTIES
122- COMPILE_FLAGS ${CFLAGS}
123- )
124- endif ()
125- endif ()
126- endif ()
127-endforeach ()
128+ list(REMOVE_ITEM source ${FILE})
129
130-foreach(UNIT_SRC ${EXTENSIONS})
131- string(FIND ${UNIT_SRC} ".vala" UNIT_EXTENSION)
132- if (UNIT_EXTENSION GREATER -1)
133- string(REPLACE ".vala" "" UNIT ${UNIT_SRC})
134- include(ValaPrecompile)
135- vala_precompile(UNIT_SRC_C ${UNIT}
136- ${UNIT_SRC}
137+ # Mandatory unit testing
138+ add_test(NAME "test-${WEB_NAME}" COMMAND $<TARGET_FILE:midori> -t $<TARGET_FILE:${WEB_NAME}>)
139+ contain_test("test-${WEB_NAME}" $<TARGET_FILE:midori> -t $<TARGET_FILE:${WEB_NAME}>)
140+ elseif (VALA_EXTENSION GREATER -1)
141+ list(APPEND SOURCE_VALA ${FILE})
142+ endif ()
143+ endforeach()
144+ if (SOURCE_VALA)
145+ vala_precompile(SOURCE_C ${name}
146+ ${SOURCE_VALA}
147 PACKAGES
148 ${PKGS}
149 OPTIONS
150- ${VALAFLAGS}
151- --use-header="${CMAKE_PROJECT_NAME}-core.h"
152+ ${VALAFLAGS}
153 GENERATE_HEADER
154- ${UNIT}
155+ ${name}
156 CUSTOM_VAPIS
157- ${EXTRA_VAPIS}
158+ ${EXTRA_VAPIS}
159 "${CMAKE_SOURCE_DIR}/midori/midori.vapi"
160+ # "${CMAKE_SOURCE_DIR}/katze/katze.vapi"
161 "${CMAKE_BINARY_DIR}/midori/${LIBMIDORI}.vapi"
162 )
163- add_library(${UNIT} MODULE ${UNIT_SRC_C})
164- target_link_libraries(${UNIT}
165- ${LIBMIDORI}
166- )
167- set_target_properties(${UNIT} PROPERTIES
168- COMPILE_FLAGS "${VALA_CFLAGS}"
169- )
170- install(TARGETS ${UNIT}
171- LIBRARY DESTINATION ${EXTENSIONDIR}
172- )
173+ set(source ${SOURCE_C})
174+ endif ()
175+ if (source)
176+ add_library(${name} MODULE ${source})
177+ target_link_libraries(${name}
178+ ${LIBMIDORI}
179+ )
180+ install(TARGETS ${name}
181+ LIBRARY DESTINATION ${EXTENSIONDIR}
182+ )
183+ if (SOURCE_VALA)
184+ # Extensions with Vala code get the lenient VALA_CFLAGS
185+ set_target_properties(${name} PROPERTIES
186+ COMPILE_FLAGS ${VALA_CFLAGS}
187+ )
188+ else ()
189+ set_target_properties(${name} PROPERTIES
190+ COMPILE_FLAGS ${CFLAGS}
191+ )
192+ endif ()
193+ # Optional unit test
194+ add_test(NAME "test-${name}" COMMAND $<TARGET_FILE:midori> -t $<TARGET_FILE:${name}>)
195+ contain_test("test-${name}" $<TARGET_FILE:midori> -t $<TARGET_FILE:${name}>)
196+ endif ()
197+endmacro(build_extension)
198+
199+foreach(UNIT_SRC ${EXTENSIONS})
200+ string(FIND ${UNIT_SRC} ".c" UNIT_EXTENSION)
201+ if (UNIT_EXTENSION GREATER -1)
202+ string(REPLACE ".c" "" UNIT ${UNIT_SRC})
203+ build_extension(${UNIT} ${UNIT_SRC})
204+ endif ()
205+
206+ string(FIND ${UNIT_SRC} "." UNIT_EXTENSION)
207+ if (UNIT_EXTENSION GREATER -1)
208+ file(GLOB UNIT_FILES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "${UNIT_SRC}/*.c" "${UNIT_SRC}/*.vala")
209+ build_extension(${UNIT_SRC} ${UNIT_FILES})
210+ endif ()
211+
212+ string(FIND ${UNIT_SRC} ".vala" UNIT_EXTENSION)
213+ if (UNIT_EXTENSION GREATER -1)
214+ string(REPLACE ".vala" "" UNIT ${UNIT_SRC})
215+ build_extension(${UNIT} ${UNIT_SRC})
216 endif ()
217 endforeach ()
218
219=== added file 'extensions/hsts.web.vala'
220--- extensions/hsts.web.vala 1970-01-01 00:00:00 +0000
221+++ extensions/hsts.web.vala 2015-06-13 13:38:06 +0000
222@@ -0,0 +1,177 @@
223+/*
224+ Copyright (C) 2012-2014 Christian Dywan <christian@twotoasts.de>
225+
226+ This library is free software; you can redistribute it and/or
227+ modify it under the terms of the GNU Lesser General Public
228+ License as published by the Free Software Foundation; either
229+ version 2.1 of the License, or (at your option) any later version.
230+
231+ See the file COPYING for the full license text.
232+*/
233+
234+namespace HSTS {
235+ public class Directive {
236+ public Soup.Date? expires = null;
237+ public bool sub_domains = false;
238+
239+ public Directive (bool include_sub_domains) {
240+ expires = new Soup.Date.from_now (int.MAX);
241+ sub_domains = include_sub_domains;
242+ }
243+
244+ public Directive.from_header (string header) {
245+ var param_list = Soup.header_parse_param_list (header);
246+ if (param_list == null)
247+ return;
248+
249+ string? max_age = param_list.lookup ("max-age");
250+ if (max_age != null)
251+ expires = new Soup.Date.from_now (max_age.to_int ());
252+ if ("includeSubDomains" in header)
253+ sub_domains = true;
254+ Soup.header_free_param_list (param_list);
255+ }
256+
257+ public bool is_valid () {
258+ return expires != null && !expires.is_past ();
259+ }
260+ }
261+
262+ public class Whitelist : Object {
263+ HashTable<string, Directive> whitelist;
264+ string preset = "/etc/xdg/midori/hsts";
265+ File config;
266+
267+ public Whitelist () {
268+ whitelist = new HashTable<string, Directive> (str_hash, str_equal);
269+ read_cache.begin (File.new_for_path (preset));
270+ // FIXME: config = File.new_for_path (Midori.Paths.get_config_filename_for_writing ("hsts"));
271+ string config_dir = Path.build_path (Path.DIR_SEPARATOR_S,
272+ Environment.get_user_config_dir (), "midori");
273+ config = File.new_for_path (Path.build_path (Path.DIR_SEPARATOR_S, config_dir, "hsts"));
274+ read_cache.begin (config);
275+ }
276+
277+ async void read_cache (File file) {
278+ try {
279+ var stream = new DataInputStream (yield file.read_async ());
280+ do {
281+ string? line = yield stream.read_line_async ();
282+ if (line == null)
283+ break;
284+ string[] parts = line.split (" ", 2);
285+ if (parts[0] == null || parts[1] == null)
286+ break;
287+ var directive = new Directive.from_header (parts[1]);
288+ if (directive.is_valid ())
289+ append_to_whitelist (parts[0], directive);
290+ } while (true);
291+ } catch (IOError.NOT_FOUND exist_error) {
292+ /* It's no error if no cache file exists */
293+ } catch (Error error) {
294+ warning ("Failed to read cache %s: %s", file.get_path (), error.message);
295+ }
296+ }
297+
298+ public bool should_secure_host (string host) {
299+ Directive? directive = whitelist.lookup (host);
300+ if (directive == null)
301+ directive = whitelist.lookup ("*." + host);
302+ return directive != null && directive.is_valid ();
303+ }
304+
305+ void append_to_whitelist (string host, Directive directive) {
306+ whitelist.insert (host, directive);
307+ if (directive.sub_domains)
308+ whitelist.insert ("*." + host, directive);
309+ }
310+
311+ async void append_to_cache (string host, string header) {
312+ /* FIXME: Don't write in private browsing */
313+
314+ try {
315+ var stream = yield config.append_to_async (FileCreateFlags.NONE);
316+ yield stream.write_async ((host + " " + header + "\n").data);
317+ yield stream.flush_async ();
318+ }
319+ catch (Error error) {
320+ critical ("Failed to update %s: %s", config.get_path (), error.message);
321+ }
322+ }
323+
324+ public Directive? strict_transport_security_handled (string host, Soup.MessageHeaders headers) {
325+ unowned string? hsts = headers.get_one ("Strict-Transport-Security");
326+ if (hsts == null)
327+ return null;
328+
329+ var directive = new Directive.from_header (hsts);
330+ if (directive.is_valid ()) {
331+ append_to_whitelist (host, directive);
332+ append_to_cache.begin (host, hsts);
333+ }
334+ return directive;
335+ }
336+
337+ }
338+
339+ public class Extension : Object {
340+ Whitelist whitelist;
341+ bool debug = false;
342+
343+ public Extension (WebKit.WebExtension extension) {
344+ if (strcmp (Environment.get_variable ("MIDORI_DEBUG"), "hsts") == 0)
345+ debug = true;
346+ whitelist = new Whitelist ();
347+ extension.page_created.connect (page_created);
348+ }
349+
350+ void page_created (WebKit.WebPage page) {
351+ page.send_request.connect (send_request);
352+ }
353+
354+ bool send_request (WebKit.URIRequest request, WebKit.URIResponse? redirect) {
355+ Soup.MessageHeaders? headers = request.get_http_headers ();
356+ if (headers == null || !request.uri.contains ("/"))
357+ return false;
358+
359+ string host = request.uri.split ("/")[2];
360+ if (whitelist.should_secure_host (host)) {
361+ request.uri = request.uri.replace ("http://", "https://");
362+ if (debug)
363+ stdout.printf ("HSTS: Enforce %s\n", host);
364+ } else if (request.uri.has_prefix ("http://")) {
365+ var directive = whitelist.strict_transport_security_handled (host, headers);
366+ if (debug)
367+ stdout.printf ("HSTS: %s valid? %s\n",
368+ host, directive != null ? directive.is_valid ().to_string () : "/");
369+ }
370+ return false;
371+ }
372+ }
373+}
374+
375+HSTS.Extension? hsts;
376+// public void webkit_web_extension_initialize_with_user_data (WebKit.WebExtension extension, Variant user_data) {
377+public void webkit_web_extension_initialize (WebKit.WebExtension extension) {
378+ // FIXME: Midori.Paths.init_with_user_data (user_data);
379+ hsts = new HSTS.Extension (extension);
380+}
381+
382+void hsts_directive () {
383+ HSTS.Directive d;
384+ d = new HSTS.Directive.from_header ("max-age=31536000");
385+ assert (d.is_valid () && !d.sub_domains);
386+ d = new HSTS.Directive.from_header ("max-age=15768000 ; includeSubDomains");
387+ assert (d.is_valid () && d.sub_domains);
388+
389+ /* Invalid */
390+ d = new HSTS.Directive.from_header ("");
391+ assert (!d.is_valid () && !d.sub_domains);
392+ d = new HSTS.Directive.from_header ("includeSubDomains");
393+ assert (!d.is_valid () && d.sub_domains);
394+}
395+
396+public void extension_test () {
397+ Test.add_func ("/hsts/directive", hsts_directive);
398+}
399+
400
401=== added directory 'ipc'
402=== modified file 'katze/midori-paths.vala'
403--- katze/midori-paths.vala 2015-06-11 22:33:48 +0000
404+++ katze/midori-paths.vala 2015-06-13 13:38:06 +0000
405@@ -99,6 +99,7 @@
406
407 public static void init (RuntimeMode new_mode, string? config) {
408 assert (mode == RuntimeMode.UNDEFINED);
409+ assert (exec_path != null);
410 assert (new_mode != RuntimeMode.UNDEFINED);
411 mode = new_mode;
412 if (mode == RuntimeMode.PORTABLE || mode == RuntimeMode.PRIVATE)
413@@ -148,16 +149,19 @@
414 tmp_dir = get_runtime_dir ();
415 }
416 #if HAVE_WEBKIT2
417+ var context = WebKit.WebContext.get_default ();
418+ context.set_web_extensions_directory (get_lib_path (PACKAGE_NAME));
419+ context.initialize_web_extensions.connect (() => {
420+ /* TODO: Pass user data */
421+ context.set_web_extensions_initialization_user_data ("");
422+ });
423 if (cache_dir != null) {
424- /* Cache and extension dir MUST be set no later than here to work */
425- WebKit.WebContext.get_default ().set_web_extensions_directory (
426- Path.build_path (Path.DIR_SEPARATOR_S, cache_dir, "wk2ext"));
427- WebKit.WebContext.get_default ().set_disk_cache_directory (
428+ context.set_disk_cache_directory (
429 Path.build_path (Path.DIR_SEPARATOR_S, cache_dir, "web"));
430 }
431
432 if (config_dir != null) {
433- var cookie_manager = WebKit.WebContext.get_default ().get_cookie_manager ();
434+ var cookie_manager = context.get_cookie_manager ();
435 cookie_manager.set_persistent_storage (Path.build_filename (config_dir, "cookies.db"),
436 WebKit.CookiePersistentStorage.SQLITE);
437 }
438@@ -165,7 +169,7 @@
439 if (user_data_dir != null) {
440 string folder = Path.build_filename (user_data_dir, "webkit", "icondatabase");
441 #if HAVE_WEBKIT2
442- WebKit.WebContext.get_default ().set_favicon_database_directory (folder);
443+ context.set_favicon_database_directory (folder);
444 #else
445 WebKit.get_favicon_database ().set_path (folder);
446 #endif
447
448=== modified file 'midori/main.c'
449--- midori/main.c 2014-04-23 03:34:23 +0000
450+++ midori/main.c 2015-06-13 13:38:06 +0000
451@@ -62,6 +62,7 @@
452 gchar* config;
453 gboolean private;
454 gboolean portable;
455+ gchar* test;
456 gboolean plain;
457 gboolean diagnostic_dialog = FALSE;
458 gboolean debug = FALSE;
459@@ -85,6 +86,8 @@
460 { "portable", 'P', 0, G_OPTION_ARG_NONE, &portable,
461 N_("Portable mode, all runtime files are stored in one place"), NULL },
462 #endif
463+ { "test", 't', 0, G_OPTION_ARG_STRING, &test,
464+ N_("Run unit tests for the specified extension"), NULL },
465 { "plain", '\0', 0, G_OPTION_ARG_NONE, &plain,
466 N_("Plain GTK+ window with WebKit, akin to GtkLauncher"), NULL },
467 { "diagnostic-dialog", 'd', 0, G_OPTION_ARG_NONE, &diagnostic_dialog,
468@@ -118,6 +121,7 @@
469 config = NULL;
470 private = FALSE;
471 portable = FALSE;
472+ test = NULL;
473 plain = FALSE;
474 run = FALSE;
475 snapshot = NULL;
476@@ -287,6 +291,52 @@
477 return 0;
478 }
479
480+ if (test != NULL)
481+ {
482+ g_assert (g_module_supported ());
483+
484+ GModule* module = g_module_open (test, G_MODULE_BIND_LOCAL);
485+ if (module == NULL)
486+ g_error (_("Failed to load %s."), test);
487+
488+ midori_test_init (&argc, &argv);
489+
490+ typedef void (*extension_test_func)(void);
491+ extension_test_func extension_test;
492+ /* Midori.Extension */
493+ typedef GObject* (*extension_init_func)(void);
494+ extension_init_func extension_init;
495+ /* WebKit.WebExtension */
496+ typedef void (*webkit_web_extension_initialize_func)(GObject* extension);
497+ webkit_web_extension_initialize_func web_extension_init;
498+
499+ if (g_module_symbol (module, "extension_init",
500+ (gpointer) &extension_init))
501+ {
502+ /* It's fine to conditionally return no extension */
503+ if (!(extension_init ()))
504+ return 0;
505+ midori_paths_init (MIDORI_RUNTIME_MODE_NORMAL, NULL);
506+
507+ /* Not all extensions have unit tests :-( */
508+ if (!g_module_symbol (module, "extension_test", (gpointer) &extension_test))
509+ return 0;
510+ }
511+ else if (g_module_symbol (module, "webkit_web_extension_initialize",
512+ (gpointer) &web_extension_init)
513+ || g_module_symbol (module, "webkit_web_extension_initialize_with_user_data",
514+ (gpointer) &web_extension_init))
515+ {
516+ if (!g_module_symbol (module, "extension_test", (gpointer) &extension_test))
517+ g_error (_("%s doesn't provide unit tests."), test);
518+ }
519+ else
520+ g_error (_("%s doesn't look like a Midori extension."), test);
521+
522+ extension_test ();
523+ return g_test_run ();
524+ }
525+
526 if (plain)
527 {
528 GtkWidget* window = gtk_window_new (GTK_WINDOW_TOPLEVEL);

Subscribers

People subscribed via source and target branches

to all changes: