aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/IO/tls.c25
-rw-r--r--src/IO/tls.h2
-rw-r--r--src/Makefile.am2
-rw-r--r--src/cache.c10
-rw-r--r--src/dillo.cc3
-rw-r--r--src/hsts.c360
-rw-r--r--src/hsts.h19
-rw-r--r--src/nav.c1
-rw-r--r--src/paths.hh1
-rw-r--r--src/url.c38
10 files changed, 451 insertions, 10 deletions
diff --git a/src/IO/tls.c b/src/IO/tls.c
index 89ad7989..dfe76744 100644
--- a/src/IO/tls.c
+++ b/src/IO/tls.c
@@ -64,7 +64,7 @@ void a_Tls_init()
#define CERT_STATUS_NONE 0
#define CERT_STATUS_RECEIVING 1
-#define CERT_STATUS_GOOD 2
+#define CERT_STATUS_CLEAN 2
#define CERT_STATUS_BAD 3
#define CERT_STATUS_USER_ACCEPTED 4
@@ -402,18 +402,29 @@ int a_Tls_connect_ready(const DilloUrl *url)
return ret;
}
+static int Tls_cert_status(const DilloUrl *url)
+{
+ Server_t *s = dList_find_sorted(servers, url, Tls_servers_by_url_cmp);
+
+ return s ? s->cert_status : CERT_STATUS_NONE;
+}
+
/*
* Did we find problems with the certificate, and did the user proceed to
* reject the connection?
*/
static int Tls_user_said_no(const DilloUrl *url)
{
- Server_t *s = dList_find_sorted(servers, url, Tls_servers_by_url_cmp);
-
- if (!s)
- return FALSE;
+ return Tls_cert_status(url) == CERT_STATUS_BAD;
+}
- return s->cert_status == CERT_STATUS_BAD;
+/*
+ * Did everything seem proper with the certificate -- no warnings to
+ * click through?
+ */
+int a_Tls_certificate_is_clean(const DilloUrl *url)
+{
+ return Tls_cert_status(url) == CERT_STATUS_CLEAN;
}
/******************** BEGINNING OF STUFF DERIVED FROM wget-1.16.3 */
@@ -894,7 +905,7 @@ static int Tls_examine_certificate(SSL *ssl, Server_t *srv,const char *host)
if (choice == 2)
srv->cert_status = CERT_STATUS_BAD;
else if (choice == -1)
- srv->cert_status = CERT_STATUS_GOOD;
+ srv->cert_status = CERT_STATUS_CLEAN;
else
srv->cert_status = CERT_STATUS_USER_ACCEPTED;
diff --git a/src/IO/tls.h b/src/IO/tls.h
index e3892cb2..9bc89de5 100644
--- a/src/IO/tls.h
+++ b/src/IO/tls.h
@@ -15,6 +15,7 @@ void a_Tls_init();
#ifdef ENABLE_SSL
+int a_Tls_certificate_is_clean(const DilloUrl *url);
int a_Tls_connect_ready(const DilloUrl *url);
void a_Tls_reset_server_state(const DilloUrl *url);
@@ -30,6 +31,7 @@ int a_Tls_read(void *conn, void *buf, size_t len);
int a_Tls_write(void *conn, void *buf, size_t len);
#else
+#define a_Tls_certificate_is_clean(host) 0
#define a_Tls_connect_ready(url) TLS_CONNECT_NEVER
#define a_Tls_reset_server_state(url) ;
#define a_Tls_handshake(fd, url) ;
diff --git a/src/Makefile.am b/src/Makefile.am
index 57a68148..fe557bb4 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -37,6 +37,8 @@ dillo_SOURCES = \
bw.c \
cookies.c \
cookies.h \
+ hsts.c \
+ hsts.h \
auth.c \
auth.h \
md5.c \
diff --git a/src/cache.c b/src/cache.c
index d8f1a123..cc33db9c 100644
--- a/src/cache.c
+++ b/src/cache.c
@@ -26,6 +26,7 @@
#include "dicache.h"
#include "nav.h"
#include "cookies.h"
+#include "hsts.h"
#include "misc.h"
#include "capi.h"
#include "decode.h"
@@ -653,7 +654,7 @@ static void Cache_parse_header(CacheEntry_t *entry)
{
char *header = entry->Header->str;
bool_t server1point0 = !strncmp(entry->Header->str, "HTTP/1.0", 8);
- char *Length, *Type, *location_str, *encoding, *connection;
+ char *Length, *Type, *location_str, *encoding, *connection, *hsts;
#ifndef DISABLE_COOKIES
Dlist *Cookies;
#endif
@@ -721,6 +722,13 @@ static void Cache_parse_header(CacheEntry_t *entry)
dFree(connection);
}
+ if (!dStrAsciiCasecmp(URL_SCHEME(entry->Url), "https") &&
+ !a_Url_host_is_ip(URL_HOST(entry->Url)) &&
+ (hsts = Cache_parse_field(header, "Strict-Transport-Security"))) {
+ a_Hsts_set(hsts, entry->Url);
+ dFree(hsts);
+ }
+
/*
* Get Transfer-Encoding and initialize decoder
*/
diff --git a/src/dillo.cc b/src/dillo.cc
index 6e28f155..24271103 100644
--- a/src/dillo.cc
+++ b/src/dillo.cc
@@ -51,6 +51,7 @@
#include "capi.h"
#include "dicache.h"
#include "cookies.h"
+#include "hsts.h"
#include "domain.h"
#include "auth.h"
#include "styleengine.hh"
@@ -483,6 +484,7 @@ int main(int argc, char **argv)
a_Dicache_init();
a_Bw_init();
a_Cookies_init();
+ a_Hsts_init(Paths::getPrefsFP(PATHS_HSTS_PRELOAD));
a_Auth_init();
a_UIcmd_init();
StyleEngine::init();
@@ -596,6 +598,7 @@ int main(int argc, char **argv)
*/
a_Domain_freeall();
a_Cookies_freeall();
+ a_Hsts_freeall();
a_Cache_freeall();
a_Dicache_freeall();
a_Http_freeall();
diff --git a/src/hsts.c b/src/hsts.c
new file mode 100644
index 00000000..5874e44f
--- /dev/null
+++ b/src/hsts.c
@@ -0,0 +1,360 @@
+/*
+ * File: hsts.c
+ * HTTP Strict Transport Security
+ *
+ * Copyright 2015 corvid
+ *
+ * This program 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.
+ *
+ */
+
+/* To preload hosts, as of 2015, chromium is the list keeper:
+ * https://src.chromium.org/viewvc/chrome/trunk/src/net/http/transport_security_state_static.json
+ * although mozilla's is easier to work from (and they trim it based on
+ * criteria such as max-age must be at least some number of months)
+ * https://mxr.mozilla.org/mozilla-central/source/security/manager/ssl/nsSTSPreloadList.inc?raw=1
+ */
+
+#include <time.h>
+#include <errno.h>
+#include <limits.h> /* for INT_MAX */
+#include <ctype.h> /* for isspace */
+#include <stdlib.h> /* for strtol */
+
+#include "hsts.h"
+#include "msg.h"
+#include "../dlib/dlib.h"
+#include "IO/tls.h"
+
+typedef struct {
+ char *host;
+ time_t expires_at;
+ bool_t subdomains;
+} HstsData_t;
+
+/* When there is difficulty in representing future dates, use the (by far)
+ * most likely latest representable time of January 19, 2038.
+ */
+static time_t hsts_latest_representable_time;
+static Dlist *domains;
+
+static void Hsts_free_policy(HstsData_t *p)
+{
+ dFree(p->host);
+ dFree(p);
+}
+
+void a_Hsts_freeall()
+{
+ HstsData_t *policy;
+ int i, n = dList_length(domains);
+
+ for (i = 0; i < n; i++) {
+ policy = dList_nth_data(domains, i);
+ Hsts_free_policy(policy);
+ }
+ dList_free(domains);
+}
+
+/*
+ * Compare function for searching a domain node by domain string
+ */
+static int Domain_node_domain_str_cmp(const void *v1, const void *v2)
+{
+ const HstsData_t *node = v1;
+ const char *host = v2;
+
+ return dStrAsciiCasecmp(node->host, host);
+}
+
+static HstsData_t *Hsts_get_policy(const char *host)
+{
+ return dList_find_sorted(domains, host, Domain_node_domain_str_cmp);
+}
+
+static void Hsts_remove_policy(HstsData_t *policy)
+{
+ if (policy) {
+ _MSG("HSTS: removed policy for %s\n", policy->host);
+ Hsts_free_policy(policy);
+ dList_remove(domains, policy);
+ }
+}
+
+/*
+ * Return the time_t for a future time.
+ */
+static time_t Hsts_future_time(long seconds_from_now)
+{
+ time_t ret, now = time(NULL);
+ struct tm *tm = gmtime(&now);
+
+ if (seconds_from_now > INT_MAX - tm->tm_sec)
+ tm->tm_sec = INT_MAX;
+ else
+ tm->tm_sec += seconds_from_now;
+
+ ret = mktime(tm);
+ if (ret == (time_t) -1)
+ ret = hsts_latest_representable_time;
+
+ return ret;
+}
+
+/*
+ * Compare function for searching domains.
+ */
+static int Domain_node_cmp(const void *v1, const void *v2)
+{
+ const HstsData_t *node1 = v1, *node2 = v2;
+
+ return dStrAsciiCasecmp(node1->host, node2->host);
+}
+
+static void Hsts_set_policy(const char *host, long max_age, bool_t subdomains)
+{
+ time_t exp = Hsts_future_time(max_age);
+ HstsData_t *policy = Hsts_get_policy(host);
+
+ _MSG("HSTS: %s %s%s: until %s", (policy ? "modify" : "add"), host,
+ (subdomains ? " and subdomains" : ""), ctime(&exp));
+
+ if (policy == NULL) {
+ policy = dNew0(HstsData_t, 1);
+ policy->host = dStrdup(host);
+ dList_insert_sorted(domains, policy, Domain_node_cmp);
+ }
+ policy->subdomains = subdomains;
+ policy->expires_at = exp;
+}
+
+/*
+ * Read the next attribute.
+ */
+static char *Hsts_parse_attr(const char **header_str)
+{
+ const char *str;
+ uint_t len;
+
+ while (dIsspace(**header_str))
+ (*header_str)++;
+
+ str = *header_str;
+ /* find '=' at end of attr, ';' after attr/val pair, '\0' end of string */
+ len = strcspn(str, "=;");
+ *header_str += len;
+
+ while (len && (str[len - 1] == ' ' || str[len - 1] == '\t'))
+ len--;
+ return dStrndup(str, len);
+}
+
+/*
+ * Get the value in *header_str.
+ */
+static char *Hsts_parse_value(const char **header_str)
+{
+ uint_t len;
+ const char *str;
+
+ if (**header_str == '=') {
+ (*header_str)++;
+ while (dIsspace(**header_str))
+ (*header_str)++;
+
+ str = *header_str;
+ /* finds ';' after attr/val pair or '\0' at end of string */
+ len = strcspn(str, ";");
+ *header_str += len;
+
+ while (len && (str[len - 1] == ' ' || str[len - 1] == '\t'))
+ len--;
+ } else {
+ str = *header_str;
+ len = 0;
+ }
+ return dStrndup(str, len);
+}
+
+/*
+ * Advance past any value.
+ */
+static void Hsts_eat_value(const char **str)
+{
+ if (**str == '=')
+ *str += strcspn(*str, ";");
+}
+
+/*
+ * The reponse for this url had an HSTS header, so let's take action.
+ */
+void a_Hsts_set(const char *header, const DilloUrl *url)
+{
+ long max_age;
+ const char *host = URL_HOST(url);
+ bool_t max_age_valid = FALSE, subdomains = FALSE;
+
+ _MSG("HSTS header for %s: %s\n", host, header);
+
+ if (!a_Tls_certificate_is_clean(url)) {
+ /* RFC 6797 gives rationale in section 14.3. */
+ _MSG("But there were certificate warnings, so ignore it (!)\n");
+ return;
+ }
+
+ /* Iterate until there is nothing left of the string */
+ while (*header) {
+ char *attr;
+ char *value;
+
+ /* Get attribute */
+ attr = Hsts_parse_attr(&header);
+
+ /* Get the value for the attribute and store it */
+ if (dStrAsciiCasecmp(attr, "max-age") == 0) {
+ value = Hsts_parse_value(&header);
+ if (isdigit(*value)) {
+ errno = 0;
+ max_age = strtol(value, NULL, 10);
+ if (errno == ERANGE)
+ max_age = INT_MAX;
+ max_age_valid = TRUE;
+ }
+ dFree(value);
+ } else if (dStrAsciiCasecmp(attr, "includeSubDomains") == 0) {
+ subdomains = TRUE;
+ Hsts_eat_value(&header);
+ } else if (dStrAsciiCasecmp(attr, "preload") == 0) {
+ /* 'preload' is not part of the RFC, but what does google care for
+ * standards? They require that 'preload' be specified by a domain
+ * in order to be added to their preload list.
+ */
+ } else {
+ MSG("HSTS: header contains unknown attribute: '%s'\n", attr);
+ Hsts_eat_value(&header);
+ }
+
+ dFree(attr);
+
+ if (*header == ';')
+ header++;
+ }
+ if (max_age_valid) {
+ if (max_age > 0)
+ Hsts_set_policy(host, max_age, subdomains);
+ else
+ Hsts_remove_policy(Hsts_get_policy(host));
+ }
+}
+
+static bool_t Hsts_expired(HstsData_t *policy)
+{
+ time_t now = time(NULL);
+ bool_t ret = (now > policy->expires_at) ? TRUE : FALSE;
+
+ if (ret) {
+ _MSG("HSTS: expired\n");
+ }
+ return ret;
+}
+
+bool_t a_Hsts_require_https(const char *host)
+{
+ bool_t ret = FALSE;
+
+ if (host) {
+ HstsData_t *policy = Hsts_get_policy(host);
+
+ if (policy) {
+ _MSG("HSTS: matched host %s\n", host);
+ if (Hsts_expired(policy))
+ Hsts_remove_policy(policy);
+ else
+ ret = TRUE;
+ }
+ if (!ret) {
+ const char *domain_str;
+
+ for (domain_str = strchr(host+1, '.');
+ domain_str != NULL && *domain_str;
+ domain_str = strchr(domain_str+1, '.')) {
+ policy = Hsts_get_policy(domain_str+1);
+
+ if (policy && policy->subdomains) {
+ _MSG("HSTS: matched %s under %s subdomain rule\n", host,
+ policy->host);
+ if (Hsts_expired(policy)) {
+ Hsts_remove_policy(policy);
+ } else {
+ ret = TRUE;
+ break;
+ }
+ }
+ }
+ }
+ }
+ return ret;
+}
+
+static void Hsts_preload(FILE *stream)
+{
+ const int LINE_MAXLEN = 4096;
+ const long ONE_YEAR = 60 * 60 * 24 * 365;
+
+ char *rc, *subdomains;
+ char line[LINE_MAXLEN];
+ char domain[LINE_MAXLEN];
+
+ /* Get all lines in the file */
+ while (!feof(stream)) {
+ line[0] = '\0';
+ rc = fgets(line, LINE_MAXLEN, stream);
+ if (!rc && ferror(stream)) {
+ MSG_WARN("HSTS: Error while reading preload entries: %s\n",
+ dStrerror(errno));
+ return; /* bail out */
+ }
+
+ /* Remove leading and trailing whitespace */
+ dStrstrip(line);
+
+ if (line[0] != '\0' && line[0] != '#') {
+ int i = 0, j = 0;
+
+ /* Get the domain */
+ while (line[i] != '\0' && !dIsspace(line[i]))
+ domain[j++] = line[i++];
+ domain[j] = '\0';
+
+ /* Skip past whitespace */
+ while (dIsspace(line[i]))
+ i++;
+
+ subdomains = line + i;
+
+ if (dStrAsciiCasecmp(subdomains, "true") == 0)
+ Hsts_set_policy(domain, ONE_YEAR, TRUE);
+ else if (dStrAsciiCasecmp(subdomains, "false") == 0)
+ Hsts_set_policy(domain, ONE_YEAR, FALSE);
+ else {
+ MSG_WARN("HSTS: format of line not recognized. Ignoring '%s'.\n",
+ line);
+ }
+ }
+ }
+}
+
+void a_Hsts_init(FILE *preload_file)
+{
+ struct tm future_tm = {7, 14, 3, 19, 0, 138, 0, 0, 0, 0, 0};
+
+ hsts_latest_representable_time = mktime(&future_tm);
+ domains = dList_new(32);
+
+ if (preload_file)
+ Hsts_preload(preload_file);
+}
+
diff --git a/src/hsts.h b/src/hsts.h
new file mode 100644
index 00000000..693aec10
--- /dev/null
+++ b/src/hsts.h
@@ -0,0 +1,19 @@
+#ifndef __HSTS_H__
+#define __HSTS_H__
+
+#include "d_size.h"
+#include "url.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+void a_Hsts_init(FILE *fp);
+void a_Hsts_set(const char *header, const DilloUrl *url);
+bool_t a_Hsts_require_https(const char *host);
+void a_Hsts_freeall( void );
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+#endif /* !__HSTS_H__ */
diff --git a/src/nav.c b/src/nav.c
index 4ccb28be..3aac475a 100644
--- a/src/nav.c
+++ b/src/nav.c
@@ -353,6 +353,7 @@ void a_Nav_push(BrowserWindow *bw, const DilloUrl *url,
a_Nav_cancel_expect(bw);
a_Bw_expect(bw, url);
Nav_open_url(bw, url, requester, 0);
+ a_UIcmd_set_location_text(bw, URL_STR(url));
}
/*
diff --git a/src/paths.hh b/src/paths.hh
index 8f52cd86..ecc02f8b 100644
--- a/src/paths.hh
+++ b/src/paths.hh
@@ -15,6 +15,7 @@
#define PATHS_RC_PREFS "dillorc"
#define PATHS_RC_KEYS "keysrc"
#define PATHS_RC_DOMAIN "domainrc"
+#define PATHS_HSTS_PRELOAD "hsts_preload"
class Paths {
public:
diff --git a/src/url.c b/src/url.c
index e2eac48a..124b9dcc 100644
--- a/src/url.c
+++ b/src/url.c
@@ -46,6 +46,7 @@
#include <ctype.h>
#include "url.h"
+#include "hsts.h"
#include "msg.h"
static const char *HEX = "0123456789ABCDEF";
@@ -140,10 +141,17 @@ static DilloUrl *Url_object_new(const char *uri_str)
url = dNew0(DilloUrl, 1);
+ /* url->buffer is given a little extra room in case HSTS needs to transform
+ * a URL string ending in ":80" to ":443".
+ */
+ int len = strlen(uri_str)+2;
+ s = dNew(char, len);
+ memcpy(s, uri_str, len-1);
+ s = dStrstrip(s);
+
/* remove leading & trailing space from buffer */
- url->buffer = dStrstrip(dStrdup(uri_str));
+ url->buffer = s;
- s = (char *) url->buffer;
p = strpbrk(s, ":/?#");
if (p && p[0] == ':' && p > s) { /* scheme */
*p = 0;
@@ -412,6 +420,32 @@ DilloUrl* a_Url_new(const char *url_str, const char *base_url)
dFree(str1);
dFree(str2);
+
+ /*
+ * A site's HTTP Strict Transport Security policy may direct us to transform
+ * URLs like "http://en.wikipedia.org:80" to "https://en.wikipedia.org:443".
+ */
+ if (url->scheme && !dStrAsciiCasecmp(url->scheme, "http") &&
+ a_Hsts_require_https(a_Url_hostname(url))) {
+ const char *const scheme = "https";
+
+ MSG("url: HSTS transformation for %s.\n", url->url_string->str);
+ url->scheme = scheme;
+ if (url->port == URL_HTTP_PORT)
+ url->port = URL_HTTPS_PORT;
+
+ if (url->authority) {
+ int len = strlen(url->authority);
+
+ if (len >= 3 && !strcmp(url->authority + len-3, ":80")) {
+ strcpy((char *)url->authority + len-2, "443");
+ }
+ }
+
+ dStr_free(url->url_string, TRUE);
+ url->url_string = NULL;
+ }
+
return url;
}