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

Subscribers

People subscribed via source and target branches

to all changes: