/*
 Copyright (C) 2012 Christian Dywan <christian@twotoasts.de>

 This library is free software; you can redistribute it and/or
 modify it under the terms of the GNU Lesser General Public
 License as published by the Free Software Foundation; either
 version 2.1 of the License, or (at your option) any later version.

 See the file COPYING for the full license text.
*/

namespace Midori {
    public class HSTS : GLib.Object, Soup.SessionFeature {
        public class Directive {
            public Soup.Date? expires = null;
            public bool sub_domains = false;

            public Directive (bool include_sub_domains) {
                expires = new Soup.Date.from_now (int.MAX);
                sub_domains = include_sub_domains;
            }

            public Directive.from_header (string header) {
                var param_list = Soup.header_parse_param_list (header);
                if (param_list == null)
                    return;

                string? max_age = param_list.lookup ("max-age");
                if (max_age != null)
                    expires = new Soup.Date.from_now (max_age.to_int ());
                // if (param_list.lookup_extended ("includeSubDomains", null, null))
                if ("includeSubDomains" in header)
                    sub_domains = true;
                Soup.header_free_param_list (param_list);
            }

            public bool is_valid () {
                return expires != null && !expires.is_past ();
            }
        }

        HashTable<string, Directive> whitelist;
        bool debug = false;

        public HSTS () {
            whitelist = new HashTable<string, Directive> (str_hash, str_equal);
            read_cache.begin (File.new_for_path (Paths.get_preset_filename (null, "hsts")));
            read_cache.begin (File.new_for_path (Paths.get_config_filename_for_reading ("hsts")));
            if (strcmp (Environment.get_variable ("MIDORI_DEBUG"), "hsts") == 0)
                debug = true;
        }

        async void read_cache (File file) {
            try {
                var stream = new DataInputStream (yield file.read_async ());
                do {
                    string? line = yield stream.read_line_async ();
                    if (line == null)
                        break;
                    string[] parts = line.split (" ", 2);
                    if (parts[0] == null || parts[1] == null)
                        break;
                    var directive = new Directive.from_header (parts[1]);
                    if (directive.is_valid ())
                        append_to_whitelist (parts[0], directive);
                } while (true);
            }
            catch (Error error) { }
        }

#if HAVE_LIBSOUP_2_34_0
        /* No sub-features */
        public bool add_feature (Type type) { return false; }
        public bool remove_feature (Type type) { return false; }
        public bool has_feature (Type type) { return false; }
#endif

        public void attach (Soup.Session session) { session.request_queued.connect (queued); }
        public void detach (Soup.Session session) { /* FIXME disconnect */ }

        /* Never called but required by the interface */
        public void request_started (Soup.Session session, Soup.Message msg, Soup.Socket socket) { }
        public void request_queued (Soup.Session session, Soup.Message message) { }
        public void request_unqueued (Soup.Session session, Soup.Message msg) { }

        bool should_secure_host (string host) {
            Directive? directive = whitelist.lookup (host);
            if (directive == null)
                directive = whitelist.lookup ("*." + host);
            return directive != null && directive.is_valid ();
        }

        void queued (Soup.Session session, Soup.Message message) {
            if (should_secure_host (message.uri.host)) {
                message.uri.set_scheme ("https");
                session.requeue_message (message);
                if (debug)
                    stdout.printf ("HSTS: Enforce %s\n", message.uri.host);
            }
            else if (message.uri.scheme == "http")
                message.finished.connect (strict_transport_security_handled);
        }

        void append_to_whitelist (string host, Directive directive) {
            whitelist.insert (host, directive);
            if (directive.sub_domains)
                whitelist.insert ("*." + host, directive);
        }

        async void append_to_cache (string host, string header) {
            if (Midori.Paths.is_readonly ())
                return;

            string filename = Paths.get_config_filename_for_writing ("hsts");
            try {
                var file = File.new_for_path (filename);
                var stream = file.append_to/* FIXME _async*/ (FileCreateFlags.NONE);
                yield stream.write_async ((host + " " + header + "\n").data);
                yield stream.flush_async ();
            }
            catch (Error error) {
                critical ("Failed to update %s: %s", filename, error.message);
            }
        }

        void strict_transport_security_handled (Soup.Message message) {
            if (message == null || message.uri == null)
                return;

            unowned string? hsts = message.response_headers.get_one ("Strict-Transport-Security");
            if (hsts == null)
                return;

            var directive = new Directive.from_header (hsts);
            if (directive.is_valid ()) {
                append_to_whitelist (message.uri.host, directive);
                append_to_cache.begin (message.uri.host, hsts);
            }
            if (debug)
                stdout.printf ("HSTS: '%s' sets '%s' valid? %s\n",
                    message.uri.host, hsts, directive.is_valid ().to_string ());
        }

    }
}
