Merge lp:~lemonboy/midori/midori-hsts into lp:midori

Proposed by The Lemon Man
Status: Merged
Approved by: Paweł Forysiuk
Approved revision: 7044
Merged at revision: 7074
Proposed branch: lp:~lemonboy/midori/midori-hsts
Merge into: lp:midori
Diff against target: 230 lines (+117/-49)
1 file modified
katze/midori-hsts.vala (+117/-49)
To merge this branch: bzr merge lp:~lemonboy/midori/midori-hsts
Reviewer Review Type Date Requested Status
Paweł Forysiuk Approve
Review via email: mp+274049@code.launchpad.net

Commit message

Make HSTS handle subdomains

Description of the change

At the moment the plugin serializes the whole table every time it adds a new rule since there's no stable destructor at the moment.

To post a comment you must log in.
lp:~lemonboy/midori/midori-hsts updated
7044. By The Lemon Man

Improvements for the HSTS plugin.

Revision history for this message
The Lemon Man (lemonboy) wrote :

After a quick look at how the Cookie jar is implemented in libsoup it seems to me that writing out the whole list every time isn't a bad idea.
On the other end we might just detach this SessionFeature before terminating the process, leaving the serialization to happen in the 'detach' callback.

Revision history for this message
Paweł Forysiuk (tuxator) wrote :

Looks sensible. Lets get it in so it can use some real life testing.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'katze/midori-hsts.vala'
--- katze/midori-hsts.vala 2015-03-23 11:33:24 +0000
+++ katze/midori-hsts.vala 2015-10-10 15:48:12 +0000
@@ -10,13 +10,16 @@
10*/10*/
1111
12namespace Midori {12namespace Midori {
13 [CCode (cname = "g_hostname_is_ip_address")]
14 extern bool hostname_is_ip_address (string hostname);
15
13 public class HSTS : GLib.Object, Soup.SessionFeature {16 public class HSTS : GLib.Object, Soup.SessionFeature {
14 public class Directive {17 public class Directive {
15 public Soup.Date? expires = null;18 public Soup.Date? expires = null;
16 public bool sub_domains = false;19 public bool sub_domains = false;
1720
18 public Directive (bool include_sub_domains) {21 public Directive (string max_age, bool include_sub_domains) {
19 expires = new Soup.Date.from_now (int.MAX);22 expires = new Soup.Date.from_string (max_age);
20 sub_domains = include_sub_domains;23 sub_domains = include_sub_domains;
21 }24 }
2225
@@ -26,16 +29,28 @@
26 return;29 return;
2730
28 string? max_age = param_list.lookup ("max-age");31 string? max_age = param_list.lookup ("max-age");
29 if (max_age != null)32 if (max_age == null)
30 expires = new Soup.Date.from_now (max_age.to_int ());33 return;
31 // if (param_list.lookup_extended ("includeSubDomains", null, null))34
35 if (max_age != null) {
36 int val = max_age.to_int ();
37 if (val != 0)
38 expires = new Soup.Date.from_now (val);
39 }
40
32 if ("includeSubDomains" in header)41 if ("includeSubDomains" in header)
33 sub_domains = true;42 sub_domains = true;
43
34 Soup.header_free_param_list (param_list);44 Soup.header_free_param_list (param_list);
35 }45 }
3646
47 public bool is_expired () {
48 return expires.is_past ();
49 }
50
37 public bool is_valid () {51 public bool is_valid () {
38 return expires != null && !expires.is_past ();52 // The max-age parameter is *required*
53 return expires != null;
39 }54 }
40 }55 }
4156
@@ -57,63 +72,99 @@
57 string? line = yield stream.read_line_async ();72 string? line = yield stream.read_line_async ();
58 if (line == null)73 if (line == null)
59 break;74 break;
60 string[] parts = line.split (" ", 2);75
61 if (parts[0] == null || parts[1] == null)76 // hostname ' ' expiration-date ' ' allow-subdomains <y|n>
62 break;77 string[] parts = line.split (" ", 3);
63 var directive = new Directive.from_header (parts[1]);78 if (parts[0] == null || parts[1] == null || parts[2] == null)
64 if (directive.is_valid ())79 continue;
65 append_to_whitelist (parts[0], directive);80
81 var host = parts[0]._strip ();
82 var expire = parts[1]._strip ();
83 var allow_subdomains = bool.parse (parts[2]._strip ());
84
85 var directive = new Directive (expire, allow_subdomains);
86 if (directive.is_valid () && !directive.is_expired ()) {
87 if (debug)
88 stdout.printf ("HSTS: loading rule for %s\n", host);
89 whitelist_append (host, directive);
90 }
66 } while (true);91 } while (true);
67 }92 }
68 catch (Error error) { }93 catch (Error error) { }
69 }94 }
7095
71 /* No sub-features */
72 public bool add_feature (Type type) { return false; }
73 public bool remove_feature (Type type) { return false; }
74 public bool has_feature (Type type) { return false; }
75
76 public void attach (Soup.Session session) { session.request_queued.connect (queued); }
77 public void detach (Soup.Session session) { /* FIXME disconnect */ }
78
79 /* Never called but required by the interface */
80 public void request_started (Soup.Session session, Soup.Message msg, Soup.Socket socket) { }
81 public void request_queued (Soup.Session session, Soup.Message message) { }
82 public void request_unqueued (Soup.Session session, Soup.Message msg) { }
83
84 bool should_secure_host (string host) {
85 Directive? directive = whitelist.lookup (host);
86 if (directive == null)
87 directive = whitelist.lookup ("*." + host);
88 return directive != null && directive.is_valid ();
89 }
90
91 void queued (Soup.Session session, Soup.Message message) {96 void queued (Soup.Session session, Soup.Message message) {
92 if (should_secure_host (message.uri.host)) {97 /* Only trust the HSTS headers sent over a secure connection */
98 if (message.uri.scheme == "https") {
99 message.finished.connect (strict_transport_security_handled);
100 }
101 else if (whitelist_lookup (message.uri.host)) {
93 message.uri.set_scheme ("https");102 message.uri.set_scheme ("https");
94 session.requeue_message (message);103 session.requeue_message (message);
95 if (debug)104 if (debug)
96 stdout.printf ("HSTS: Enforce %s\n", message.uri.host);105 stdout.printf ("HSTS: Enforce %s\n", message.uri.host);
97 }106 }
98 else if (message.uri.scheme == "http")107 }
99 message.finished.connect (strict_transport_security_handled);108
100 }109 bool whitelist_lookup (string host) {
101110 Directive? directive = null;
102 void append_to_whitelist (string host, Directive directive) {111 bool is_subdomain = false;
112
113 if (hostname_is_ip_address (host))
114 return false;
115
116 // try an exact match first
117 directive = whitelist.lookup (host);
118
119 // no luck, try walking the domain tree
120 if (directive == null) {
121 int offset = 0;
122 for (offset = host.index_of_char ('.', offset) + 1;
123 offset > 0;
124 offset = host.index_of_char ('.', offset) + 1) {
125 string component = host.substring(offset);
126
127 directive = whitelist.lookup (component);
128 if (directive != null) {
129 is_subdomain = true;
130 break;
131 }
132 }
133 }
134
135 return directive != null &&
136 !directive.is_expired () &&
137 (is_subdomain? directive.sub_domains: true);
138 }
139
140 void whitelist_append (string host, Directive directive) {
103 whitelist.insert (host, directive);141 whitelist.insert (host, directive);
104 if (directive.sub_domains)142 }
105 whitelist.insert ("*." + host, directive);143
106 }144 void whitelist_remove (string host) {
107145 whitelist.remove(host);
108 async void append_to_cache (string host, string header) {146 }
147
148 async void whitelist_serialize () {
109 if (Midori.Paths.is_readonly ())149 if (Midori.Paths.is_readonly ())
110 return;150 return;
111151
112 string filename = Paths.get_config_filename_for_writing ("hsts");152 string filename = Paths.get_config_filename_for_writing ("hsts");
113 try {153 try {
114 var file = File.new_for_path (filename);154 var file = File.new_for_path (filename);
115 var stream = file.append_to/* FIXME _async*/ (FileCreateFlags.NONE);155 var stream = file.replace (null, false, FileCreateFlags.NONE);
116 yield stream.write_async ((host + " " + header + "\n").data);156
157 foreach (string host in whitelist.get_keys ()) {
158 var directive = whitelist.lookup (host);
159
160 // Don't serialize the expired directives
161 if (directive.is_expired ())
162 continue;
163
164 yield stream.write_async (("%s %s %s\n".printf (host,
165 directive.expires.to_string (Soup.DateFormat.ISO8601_COMPACT),
166 directive.sub_domains.to_string ())).data);
167 }
117 yield stream.flush_async ();168 yield stream.flush_async ();
118 }169 }
119 catch (Error error) {170 catch (Error error) {
@@ -130,14 +181,31 @@
130 return;181 return;
131182
132 var directive = new Directive.from_header (hsts);183 var directive = new Directive.from_header (hsts);
133 if (directive.is_valid ()) {184
134 append_to_whitelist (message.uri.host, directive);
135 append_to_cache.begin (message.uri.host, hsts);
136 }
137 if (debug)185 if (debug)
138 stdout.printf ("HSTS: '%s' sets '%s' valid? %s\n",186 stdout.printf ("HSTS: '%s' sets '%s' valid? %s\n",
139 message.uri.host, hsts, directive.is_valid ().to_string ());187 message.uri.host, hsts, directive.is_valid ().to_string ());
188
189 if (directive.is_valid ())
190 whitelist_append (message.uri.host, directive);
191 else
192 whitelist_remove (message.uri.host);
193
194
195 whitelist_serialize.begin ();
140 }196 }
141197
198 /* No sub-features */
199 public bool add_feature (Type type) { return false; }
200 public bool remove_feature (Type type) { return false; }
201 public bool has_feature (Type type) { return false; }
202
203 public void attach (Soup.Session session) { session.request_queued.connect (queued); }
204 public void detach (Soup.Session session) { /* FIXME disconnect */ }
205
206 /* Never called but required by the interface */
207 public void request_started (Soup.Session session, Soup.Message msg, Soup.Socket socket) { }
208 public void request_queued (Soup.Session session, Soup.Message message) { }
209 public void request_unqueued (Soup.Session session, Soup.Message msg) { }
142 }210 }
143}211}

Subscribers

People subscribed via source and target branches

to all changes: