3 |
4 |
5 |
17 |
18 |
19 | HTTPS is enabled by default for all navigations from the location bar and bookmarks.
20 | If a site does not support https, you can opt in to using http by default for that site by adding the domain to the following list.
21 | When https by default is turned off for a domain, its subdomains will also not use https by default.
22 |
23 |
24 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Rob Wu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/firefox/options.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var domains_nohttps_input = document.getElementById('domains_nohttps_input');
4 | var enable_logging_input = document.getElementById('enable_logging_input');
5 | var save_button = document.getElementById('save_button');
6 |
7 | browser.storage.local.get({
8 | domains_nohttps: '',
9 | enable_logging: false,
10 | }).then(({domains_nohttps, enable_logging}) => {
11 | if (domains_nohttps) {
12 | domains_nohttps_input.value = domains_nohttps;
13 | }
14 | enable_logging_input.checked = enable_logging;
15 | });
16 |
17 |
18 | let throttle;
19 |
20 | function commitChange() {
21 | clearTimeout(throttle);
22 |
23 | let domains_nohttps = domains_nohttps_input.value;
24 | // TODO: Validate?
25 | browser.storage.local.set({domains_nohttps}).then(() => {
26 | if (domains_nohttps_input.value === domains_nohttps) {
27 | save_button.value = 'Saved!';
28 | }
29 | });
30 | }
31 |
32 | save_button.onclick = commitChange;
33 | domains_nohttps_input.onchange = commitChange;
34 | domains_nohttps_input.oninput = () => {
35 | save_button.value = 'Save';
36 | clearTimeout(throttle);
37 | throttle = setTimeout(commitChange, 1000);
38 | };
39 |
40 | enable_logging_input.onchange = () => {
41 | browser.storage.local.set({
42 | enable_logging: enable_logging_input.checked,
43 | });
44 | };
45 |
--------------------------------------------------------------------------------
/chrome/https-by-default.patch:
--------------------------------------------------------------------------------
1 | diff --git a/components/omnibox/browser/location_bar_model_impl.cc b/components/omnibox/browser/location_bar_model_impl.cc
2 | index 689f02345fd8..9539bd697e0c 100644
3 | --- a/components/omnibox/browser/location_bar_model_impl.cc
4 | +++ b/components/omnibox/browser/location_bar_model_impl.cc
5 | @@ -41,12 +41,15 @@ LocationBarModelImpl::~LocationBarModelImpl() {
6 |
7 | // LocationBarModelImpl Implementation.
8 | std::u16string LocationBarModelImpl::GetFormattedFullURL() const {
9 | - return GetFormattedURL(url_formatter::kFormatUrlOmitDefaults);
10 | + return GetFormattedURL(
11 | + url_formatter::kFormatUrlOmitDefaults &
12 | + ~url_formatter::kFormatUrlOmitHTTP);
13 | }
14 |
15 | std::u16string LocationBarModelImpl::GetURLForDisplay() const {
16 | url_formatter::FormatUrlTypes format_types =
17 | - url_formatter::kFormatUrlOmitDefaults;
18 | + url_formatter::kFormatUrlOmitDefaults &
19 | + ~url_formatter::kFormatUrlOmitHTTP;
20 | if (delegate_->ShouldTrimDisplayUrlAfterHostName()) {
21 | format_types |= url_formatter::kFormatUrlTrimAfterHost;
22 | }
23 | diff --git a/components/omnibox/browser/autocomplete_input.cc b/components/omnibox/browser/autocomplete_input.cc
24 | index 43fc0dc783d8..1f7e5ce9dee1 100644
25 | --- a/components/omnibox/browser/autocomplete_input.cc
26 | +++ b/components/omnibox/browser/autocomplete_input.cc
27 | @@ -285,6 +285,17 @@ metrics::OmniboxInputType AutocompleteInput::Parse(
28 | // between an HTTP URL and a query, or the scheme is HTTP or HTTPS, in which
29 | // case we should reject invalid formulations.
30 |
31 | + if (!parts->scheme.is_nonempty() &&
32 | + base::EqualsCaseInsensitiveASCII(parsed_scheme_utf8, url::kHttpScheme)) {
33 | + // Scheme was not specified. url_fixer::FixupURL automatically adds http:,
34 | + // but we want to default to https instead.
35 | + if (scheme)
36 | + *scheme = base::ASCIIToUTF16(url::kHttpsScheme);
37 | + GURL::Replacements replacements;
38 | + replacements.SetSchemeStr(url::kHttpsScheme);
39 | + *canonicalized_url = canonicalized_url->ReplaceComponents(replacements);
40 | + }
41 | +
42 | // Determine the host family. We get this information by (re-)canonicalizing
43 | // the already-canonicalized host rather than using the user's original input,
44 | // in case fixup affected the result here (e.g. an input that looks like an
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HTTPS by default
2 |
3 | Upon requesting "example.com" from the location bar, your browser will attempt
4 | to load `http://example.com` over an insecure connection by default.
5 | Those who want to load the site over a secure connection have to manually put
6 | "https://" in front of the URL. Because it is easier to not type "https://",
7 | most websites are accessed over an insecure connection.
8 |
9 | This project is an endeavour to get HTTPS to become the default scheme in web
10 | browsers. With the following instructions, requesting `example.com` from the
11 | location bar will result in a navigation to `https://example.com` instead of
12 | `http://example.com` (which is the current ubiquitous but insecure default).
13 |
14 | Some websites are incorrectly configured and cannot be accessed over a secure
15 | connection. If you come across such a site, edit the URL in the location bar
16 | and insert "http://" in front of it to access the site anyway.
17 |
18 | Many of these days' web browsers hide the "http://" prefix in the location bar.
19 | When "http" is the default scheme, focusing the location bar and pressing Enter
20 | will trigger a navigation to the same URL, i.e. reload the current page. With
21 | https enabled by default, the page will not be reloaded, but you will be
22 | navigated to the https-version of the site instead (unless you put "http://" in
23 | front of the URL).
24 |
25 |
26 | # Firefox
27 |
28 | Install the [HTTPS by default add-on](https://addons.mozilla.org/en-US/firefox/addon/https-by-default)
29 | to enable https by default in Firefox.
30 |
31 | Visit `about:config` and set `browser.urlbar.trimURLs` to `false` to not hide
32 | the "http://" prefix by default.
33 |
34 |
35 | # Chrome / Chromium
36 |
37 | Chrome's extension APIs is not powerful enough to support this feature.
38 | See https://stackoverflow.com/a/26462483/938089 for instructions on getting and
39 | compiling a stable version of Chrome. Before compiling, apply the patch from
40 | this repository to get https by default:
41 |
42 | ```sh
43 | cd chromium/src # this is the location of Chromium's git repository
44 | git apply https-by-default.patch # the .patch file from this repo at chrome/.
45 | ```
46 |
47 | After applying this patch, https will be used by default for navigations that
48 | are triggered from the omnibox, and the "http://" prefix will not be removed
49 | from the omnibox.
50 |
51 |
52 | # Roadmap
53 |
54 | At the initial stage, the project's focus is to offer the option to enable https
55 | by default in web browsers. The ultimate goal is to get browser vendors to
56 | enable https by default. This is a significant change, and as such it will need
57 | compelling data to demonstrate that the change does not hinder usability. If the
58 | extensions from this project take off, I could update them to measure the impact
59 | of https by default on usability. This feature will only be added if it can be
60 | done without compromising the users' privacy.
61 |
62 |
63 | # Alternatives
64 |
65 | - HTTPS Everywhere (https://www.eff.org/https-everywhere)
66 |
67 | This is a Firefox addon and a Chrome extension that contains a huge database
68 | of rules which redirects http requests to https. This characteristic is its
69 | forte and also its weakness. The rules allows the add-on to force https for
70 | *known* sites. Unlisted sites will still be accessed over an insecure
71 | connection by default.
72 |
73 | - HTTP Strict Transport Security (https://www.owasp.org/index.php/HTTP_Strict_Transport_Security)
74 |
75 | Website authors can include the `Strict-Transport-Security` in their secure
76 | response to tell the browser to force https for subsequent visits to the site.
77 | This *only* works if the website author adds this STS header *and* if the user
78 | visits the site at least once over https, or if the site was registered in a
79 | pre-loaded HSTS list. Unfortunately, the combination of both is not quite
80 | common.
81 |
--------------------------------------------------------------------------------
/firefox/background.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017 Rob Wu (https://robwu.nl)
3 | */
4 | 'use strict';
5 |
6 | const DOMAIN_WILDCARD_LEAF_SYMBOL = Symbol('Domain wildcard prefix');
7 |
8 | var prefsParsed = {
9 | domains_nohttps: new Map(),
10 | enable_logging: false,
11 | };
12 | var prefsReady = false;
13 | var prefsReadyPromise = browser.storage.local.get({
14 | enable_logging: false,
15 | domains_nohttps: '',
16 | })
17 | .then(({domains_nohttps, enable_logging}) => {
18 | doParsePrefs(domains_nohttps);
19 | prefsParsed.enable_logging = enable_logging;
20 | }, (() => {}))
21 | .then(() => { prefsReady = true; });
22 |
23 | browser.storage.onChanged.addListener((changes) => {
24 | if (changes.domains_nohttps) {
25 | doParsePrefs(changes.domains_nohttps.newValue);
26 | }
27 | if (changes.enable_logging) {
28 | prefsParsed.enable_logging = changes.enable_logging.newValue;
29 | }
30 | });
31 |
32 |
33 | var tabCreationTimes = new Map();
34 | var tabActivatedTimes = new Map();
35 | var tabPendingRedirectInfos = new Map();
36 |
37 | browser.tabs.onCreated.addListener(tab => {
38 | if (tab.id) {
39 | tabCreationTimes.set(tab.id, Date.now());
40 | if (tab.active) {
41 | tabActivatedTimes.set(tab.id, Date.now());
42 | }
43 | }
44 | });
45 | browser.tabs.onActivated.addListener(({tabId}) => {
46 | tabActivatedTimes.set(tabId, Date.now());
47 | });
48 | browser.tabs.onRemoved.addListener(tabId => {
49 | tabCreationTimes.delete(tabId);
50 | tabActivatedTimes.delete(tabId);
51 | unregisterRedirectInfo(tabId);
52 | });
53 | browser.tabs.query({}).then(tabs => {
54 | for (let tab of tabs) {
55 | // If the extension is loading around Firefox's start-up, then we
56 | // should not rewrite URLs.
57 | // If the extension was loaded long after Firefox's start-up, then
58 | // these timestamps are probably in the past (or the tab is not
59 | // an about:blank page), and we will not inadvertently stop the
60 | // redirect from happening.
61 | tabCreationTimes.set(tab.id, tab.lastAccessed || Date.now());
62 | tabActivatedTimes.set(tab.id, tab.active ? Date.now() : 0);
63 | }
64 | });
65 |
66 | browser.webRequest.onBeforeRequest.addListener(async (details) => {
67 | if (details.originUrl) {
68 | // Likely a web-triggered navigation, or a reload of such a page.
69 | return;
70 | }
71 | if (details.tabId === -1) {
72 | // Invisible navigation. Unlikely to be requested by the user.
73 | return;
74 | }
75 |
76 | // Possibly a navigation from the awesomebar, bookmark, etc.
77 | // ... or a reload of a (discarded) tab.
78 |
79 | // I would like to only rewrite typed URLs without explicit scheme,
80 | // but unfortunately the extension API does not offer the typed text,
81 | // so we will rewrite any non-web-initiated navigation,
82 | // including bookmarks, auto-completed URLs and full URLs with "http:" prefix.
83 |
84 | let {tabId, url: requestedUrl, requestedId} = details;
85 |
86 | if (!prefsReady) {
87 | await prefsReadyPromise;
88 | }
89 |
90 | if (!shouldRedirectToHttps(requestedUrl)) {
91 | return;
92 | }
93 |
94 | let currentTab;
95 | for (let start = Date.now(); Date.now() - start < 200; ) {
96 | try {
97 | currentTab = await browser.tabs.get(tabId);
98 | } catch (e) {
99 | // Tab does not exist. E.g. when a URL is loaded in a new tab page
100 | // and the request happens before the tab exists.
101 | await new Promise(resolve => { setTimeout(resolve, 20); });
102 | }
103 | }
104 |
105 | // Heuristic: On Firefox for Android, tabs can be discarded (and its URL
106 | // becomes "about:blank"). When a tab is re-activated, the original URL is
107 | // loaded again. These URLs should not be modified by us.
108 | // On Firefox for Desktop, this can also be a new tab of unknown origin.
109 | if (currentTab && currentTab.url === 'about:blank') {
110 | let tabCreationTime = tabCreationTimes.get(tabId);
111 | if (tabCreationTime === undefined) {
112 | // The request was generated before the tab was created,
113 | // or the tab has been removed.
114 | return;
115 | }
116 | let tabActivatedTime = tabActivatedTimes.get(tabId);
117 | if (tabId === undefined) {
118 | // If the time of when the tab was first activated is unknown,
119 | // fall back to the time of when the time was last activated.
120 | tabActivatedTime = currentTab.lastAccessed;
121 | }
122 | // Typing a site takes time, so it is reasonable to choose a relatively
123 | // long time threshold. One second is a very realistic underbound for
124 | // typing some domain name. It is also large enough to allow the browser
125 | // to process the request, even if the device is very slow (CPU-wise).
126 | if (details.timeStamp - tabActivatedTime < 1000) {
127 | // Likely resuming from a discarded tab on Android.
128 | return;
129 | }
130 | // If the tab is created around the same time as the request, then this
131 | // is possibly an Alt-Enter navigation on Firefox Desktop.
132 | // But it can also be a bookmark opened in a new tab, an
133 | // extension-created tab (#15) or a URL opened via the command line (#14).
134 | // The latter cases are probably more common, so we don't redirect for
135 | // these.
136 | if (details.timeStamp - tabCreationTimes.get(tabId) < 300) {
137 | return;
138 | }
139 | }
140 |
141 | if (currentTab && isDerivedURL(currentTab.url, requestedUrl)) {
142 | // User had likely edited the current URL and pressed Enter.
143 | // Do not rewrite the request to HTTPS.
144 | return;
145 | }
146 |
147 | if (tabCreationTimes.has(tabId)) {
148 | var pendingRedirectInfo = tabPendingRedirectInfos.get(tabId);
149 | if (pendingRedirectInfo && pendingRedirectInfo.redirectedRequestId === requestedId) {
150 | // Don't rewrite redirects. Redirects are triggered by a server response, and
151 | // are certainly not the result of a manually typed URL.
152 | return;
153 | }
154 | if (pendingRedirectInfo && pendingRedirectInfo.url === requestedUrl &&
155 | currentTab && currentTab.status === 'loading') {
156 | // The previous HTTP->HTTPS navigation hasn't started, and the HTTP navigation is
157 | // attempted again. This site does probably not support HTTPS, and the user is trying
158 | // to force navigation to HTTP.
159 | return;
160 | }
161 | registerRedirectInfo(tabId, requestedUrl);
162 | }
163 |
164 | // Replace "http:" with "https:".
165 | let httpsUrl = requestedUrl.replace(':', 's:');
166 |
167 | if (prefsParsed.enable_logging) {
168 | console.log('[HTTPS by default] Redirecting ' + requestedUrl);
169 | }
170 |
171 | return {
172 | redirectUrl: httpsUrl,
173 | };
174 | }, {
175 | urls: ['http://*/*'],
176 | types: ['main_frame']
177 | }, ['blocking']);
178 |
179 | /**
180 | * Determines whether the given http:-URL should be redirected to https:.
181 | *
182 | * @param {string} requestedUrl A valid http:-URL.
183 | * @returns {boolean} Whether to redirect to https.
184 | */
185 | function shouldRedirectToHttps(requestedUrl) {
186 | let {hostname} = new URL(requestedUrl);
187 |
188 | if (!hostname.includes('.')) {
189 | // Any globally resolvable address should have a TLD.
190 | // Otherwise it is not likely to obtain a SSL certificate for it.
191 | // E.g. localhost.
192 | return false;
193 | }
194 |
195 | if (hostname.endsWith('.test') ||
196 | hostname.endsWith('.example') ||
197 | hostname.endsWith('.invalid') ||
198 | hostname.endsWith('.localhost')) {
199 | // Reserved root level DNS names - RFC 2606.
200 | return false;
201 | }
202 |
203 | if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname) ||
204 | hostname.startsWith('[') && hostname.endsWith(']')) {
205 | // Don't redirect IPv4 or IPv6 addresses.
206 | return false;
207 | }
208 |
209 | let map = prefsParsed.domains_nohttps;
210 | for (let part of hostname.split('.').reverse()) {
211 | map = map.get(part);
212 | if (!map) {
213 | break;
214 | }
215 | if (map.has(DOMAIN_WILDCARD_LEAF_SYMBOL)) {
216 | return false;
217 | }
218 | }
219 |
220 | // By default, redirect to https:.
221 | return true;
222 | }
223 |
224 | /**
225 | * Determines whether the requested URL is based on the current URL.
226 | *
227 | * @param {string} currentUrl - The current URL of the tab.
228 | * @param {string} requestedUrl - The requested http:-URL.
229 | * @returns {boolean} Whether to avoid rewriting the request to https.
230 | */
231 | function isDerivedURL(currentUrl, requestedUrl) {
232 | if (currentUrl === requestedUrl) {
233 | // In Firefox, tab.url shows the URL of the currently loaded resource in
234 | // a tab, so if the URLs are equal, it is a page reload.
235 | return true;
236 | }
237 | if (!currentUrl.startsWith('http')) {
238 | // Not a http(s) URL, e.g. about:.
239 | return false;
240 | }
241 | let cur;
242 | try {
243 | cur = new URL(currentUrl);
244 | } catch (e) {
245 | return false;
246 | }
247 | let req = new URL(requestedUrl);
248 | if (req.hostname === cur.hostname) {
249 | // The user had already accessed the domain over HTTP, so there is no
250 | // much gain in forcing a redirect to HTTPS.
251 | //
252 | // This supports the use case of editing the current (HTTP) URL and
253 | // then navigating to it.
254 | //
255 | // This also covers the following scenario:
256 | // - User opens http://xxx
257 | // - The extension redirects to https://xxx
258 | // - ...but https://xxx is not serving the content that the user expects
259 | // - User opens http://xxx again
260 | // - Extension should not redirect to https.
261 | return true;
262 | }
263 |
264 | if (cur.protocol === 'https:') {
265 | // If the current tab's URL is https, do not downgrade to http.
266 | return false;
267 | }
268 |
269 | if ((req.pathname.length > 1 ||
270 | req.search.length > 2 ||
271 | req.hash.length > 2) &&
272 | req.pathname === cur.pathname &&
273 | req.search === cur.search &&
274 | req.hash === cur.hash) {
275 | // Everything after the domain name is non-empty and equal.
276 | // The user might be trying to correct a misspelled domain name.
277 | // Do not rewrite to HTTPS.
278 | return true;
279 | }
280 |
281 | // Proceed to redirect to https.
282 | return false;
283 | }
284 |
285 | // Records the intercepted request that is going to be redirected to HTTPS.
286 | // The redirection URL will be discarded when a response is received for the main frame in the
287 | // given tab, when the tab is removed, or when the request fails.
288 | //
289 | // The caller should make sure that tabId refers to a valid tab.
290 | function registerRedirectInfo(tabId, requestedUrl) {
291 | let redirectInfo = {
292 | url: requestedUrl,
293 | redirectedRequestId: null,
294 | unregister,
295 | };
296 | function unregister() {
297 | browser.webRequest.onHeadersReceived.removeListener(onHeadersReceived);
298 | browser.webRequest.onResponseStarted.removeListener(unregister);
299 | browser.webRequest.onErrorOccurred.removeListener(unregister);
300 | tabPendingRedirectInfos.delete(tabId);
301 | }
302 |
303 | function onHeadersReceived({requestedId, statusCode}) {
304 | if (statusCode !== 301 && statusCode !== 302 && statusCode !== 303 &&
305 | statusCode !== 307 && statusCode !== 308) {
306 | unregister();
307 | return;
308 | }
309 | if (tabPendingRedirectInfos.get(tabId) !== redirectInfo) {
310 | // unregister() has been invoked between the queued webRequest event and the
311 | // actual dispatch. This is not expected to happen, but can happen in theory.
312 | // Exit now to avoid registering and leaking a webRequest event handler.
313 | return;
314 | }
315 |
316 | // When a request is restarted, the requestedId is preserved.
317 | redirectInfo.redirectedRequestId = requestedId;
318 |
319 | // If the response did not have a valid Location header, the request won't be restarted.
320 | // Detect the successful response body via webRequest.onResponseStarted.
321 | // The onHeadersReceived event can fire multiple times throughout a request, but it is safe
322 | // to call addListener because the event handler won't be registered twice.
323 | browser.webRequest.onResponseStarted.addListener(unregister, {
324 | urls: ['*://*/*'],
325 | types: ['main_frame'],
326 | tabId,
327 | });
328 | }
329 |
330 | unregisterRedirectInfo(tabId);
331 |
332 | // A request was successfully received for the main frame, so clear the registered redirection
333 | // URL. This is not necessarily a response for |requestedUrl|, any response will do.
334 | browser.webRequest.onHeadersReceived.addListener(onHeadersReceived, {
335 | urls: ['*://*/*'],
336 | types: ['main_frame'],
337 | tabId,
338 | }, ['blocking']);
339 |
340 | // The server is not reachable via HTTPs, and the URL can be unregistered because
341 | // tab.url will show the URL of the attempted navigation:
342 | browser.webRequest.onErrorOccurred.addListener(unregister, {
343 | urls: [requestedUrl],
344 | types: ['main_frame'],
345 | tabId,
346 | });
347 |
348 | // Expose the unregister function so that if somehow neither of the above events happen,
349 | // that the listener is removed when the tab is removed (via tabs.onRemoved):
350 | tabPendingRedirectInfos.set(tabId, redirectInfo);
351 | }
352 |
353 | function unregisterRedirectInfo(tabId) {
354 | let pendingRedirectInfo = tabPendingRedirectInfos.get(tabId);
355 | if (pendingRedirectInfo) {
356 | pendingRedirectInfo.unregister();
357 | tabPendingRedirectInfos.delete(tabId);
358 | }
359 | }
360 |
361 | function doParsePrefs(domains_nohttps) {
362 | prefsParsed.domains_nohttps = new Map();
363 | if (domains_nohttps) {
364 | console.assert(typeof domains_nohttps === 'string');
365 | for (let domain of domains_nohttps.split(/\s+/)) {
366 | if (!domain) {
367 | continue;
368 | }
369 | let map = prefsParsed.domains_nohttps;
370 | for (let part of domain.split('.').reverse()) {
371 | if (!map.has(part)) {
372 | map.set(part, new Map());
373 | }
374 | map = map.get(part);
375 | }
376 | map.set(DOMAIN_WILDCARD_LEAF_SYMBOL);
377 | }
378 | }
379 | }
380 |
--------------------------------------------------------------------------------