Bug 1149250 add support for https upgrades from webextensions, r?mayhemer,bz,rpl
MozReview-Commit-ID: ChIs2Q6bgEn
--- a/dom/webidl/ChannelWrapper.webidl
+++ b/dom/webidl/ChannelWrapper.webidl
@@ -66,21 +66,32 @@ interface ChannelWrapper : EventTarget {
* Cancels the request with the given nsresult status code.
*/
[Throws]
void cancel(unsigned long result);
/**
* Redirects the wrapped HTTP channel to the given URI. For other channel
* types, this method will throw. The redirect is an internal redirect, and
- * the behavior is the same as nsIHttpChannel.redirectTo.
+ * the behavior is the same as nsIHttpChannel.redirectTo.
*/
[Throws]
void redirectTo(URI url);
+ /**
+ * Requests an upgrade of the HTTP channel to a secure request. For other channel
+ * types, this method will throw. The redirect is an internal redirect, and
+ * the behavior is the same as nsIHttpChannel.upgradeToSecure. Setting this
+ * flag is only effective during the WebRequest.onBeforeRequest in
+ * Web Extensions, calling this at any other point during the request will
+ * have no effect. Setting this flag in addition to calling redirectTo
+ * results in the redirect happening rather than the upgrade request.
+ */
+ [Throws]
+ void upgradeToSecure();
/**
* The content type of the request, usually as read from the Content-Type
* header. This should be used in preference to the header to determine the
* content type of the channel.
*/
[Pure]
attribute ByteString contentType;
--- a/netwerk/protocol/http/HttpBaseChannel.cpp
+++ b/netwerk/protocol/http/HttpBaseChannel.cpp
@@ -156,16 +156,17 @@ HttpBaseChannel::HttpBaseChannel()
: mCanceled(false)
, mStartPos(UINT64_MAX)
, mStatus(NS_OK)
, mLoadFlags(LOAD_NORMAL)
, mCaps(0)
, mClassOfService(0)
, mPriority(PRIORITY_NORMAL)
, mRedirectionLimit(gHttpHandler->RedirectionLimit())
+ , mUpgradeToSecure(false)
, mApplyConversion(true)
, mIsPending(false)
, mWasOpened(false)
, mRequestObserversCalled(false)
, mResponseHeadersModified(false)
, mAllowSTS(true)
, mThirdPartyFlags(0)
, mUploadStreamHasHeaders(false)
@@ -196,16 +197,17 @@ HttpBaseChannel::HttpBaseChannel()
, mInternalRedirectCount(0)
, mForcePending(false)
, mCorsIncludeCredentials(false)
, mCorsMode(nsIHttpChannelInternal::CORS_MODE_NO_CORS)
, mRedirectMode(nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW)
, mFetchCacheMode(nsIHttpChannelInternal::FETCH_CACHE_MODE_DEFAULT)
, mOnStartRequestCalled(false)
, mOnStopRequestCalled(false)
+ , mUpgradableToSecure(true)
, mAfterOnStartRequestBegun(false)
, mTransferSize(0)
, mDecodedBodySize(0)
, mEncodedBodySize(0)
, mRequestContextID(0)
, mContentWindowId(0)
, mTopLevelOuterContentWindowId(0)
, mRequireCORSPreflight(false)
@@ -2192,16 +2194,30 @@ HttpBaseChannel::RedirectTo(nsIURI *targ
// This would break the nsIStreamListener contract.
NS_ENSURE_FALSE(mOnStartRequestCalled, NS_ERROR_NOT_AVAILABLE);
mAPIRedirectToURI = targetURI;
return NS_OK;
}
NS_IMETHODIMP
+HttpBaseChannel::UpgradeToSecure()
+{
+ // Upgrades are handled internally between http-on-modify-request and
+ // http-on-before-connect, which means upgrades are only possible during
+ // on-modify, or WebRequest.onBeforeRequest in Web Extensions. Once we are
+ // past the code path where upgrades are handled, attempting an upgrade
+ // will throw an error.
+ NS_ENSURE_TRUE(mUpgradableToSecure, NS_ERROR_NOT_AVAILABLE);
+
+ mUpgradeToSecure = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
HttpBaseChannel::GetRequestContextID(uint64_t *aRCID)
{
NS_ENSURE_ARG_POINTER(aRCID);
*aRCID = mRequestContextID;
return NS_OK;
}
NS_IMETHODIMP
--- a/netwerk/protocol/http/HttpBaseChannel.h
+++ b/netwerk/protocol/http/HttpBaseChannel.h
@@ -190,16 +190,17 @@ public:
NS_IMETHOD SetRedirectionLimit(uint32_t value) override;
NS_IMETHOD IsNoStoreResponse(bool *value) override;
NS_IMETHOD IsNoCacheResponse(bool *value) override;
NS_IMETHOD IsPrivateResponse(bool *value) override;
NS_IMETHOD GetResponseStatus(uint32_t *aValue) override;
NS_IMETHOD GetResponseStatusText(nsACString& aValue) override;
NS_IMETHOD GetRequestSucceeded(bool *aValue) override;
NS_IMETHOD RedirectTo(nsIURI *newURI) override;
+ NS_IMETHOD UpgradeToSecure() override;
NS_IMETHOD GetRequestContextID(uint64_t *aRCID) override;
NS_IMETHOD GetTransferSize(uint64_t *aTransferSize) override;
NS_IMETHOD GetDecodedBodySize(uint64_t *aDecodedBodySize) override;
NS_IMETHOD GetEncodedBodySize(uint64_t *aEncodedBodySize) override;
NS_IMETHOD SetRequestContextID(uint64_t aRCID) override;
NS_IMETHOD GetIsMainDocumentChannel(bool* aValue) override;
NS_IMETHOD SetIsMainDocumentChannel(bool aValue) override;
NS_IMETHOD GetProtocolVersion(nsACString & aProtocolVersion) override;
@@ -539,16 +540,17 @@ protected:
Atomic<nsresult, ReleaseAcquire> mStatus;
uint32_t mLoadFlags;
uint32_t mCaps;
uint32_t mClassOfService;
int16_t mPriority;
uint8_t mRedirectionLimit;
+ uint32_t mUpgradeToSecure : 1;
uint32_t mApplyConversion : 1;
uint32_t mIsPending : 1;
uint32_t mWasOpened : 1;
// if 1 all "http-on-{opening|modify|etc}-request" observers have been called
uint32_t mRequestObserversCalled : 1;
uint32_t mResponseHeadersModified : 1;
uint32_t mAllowSTS : 1;
uint32_t mThirdPartyFlags : 3;
@@ -643,16 +645,20 @@ protected:
uint32_t mRedirectMode;
uint32_t mFetchCacheMode;
// These parameters are used to ensure that we do not call OnStartRequest and
// OnStopRequest more than once.
bool mOnStartRequestCalled;
bool mOnStopRequestCalled;
+ // Defaults to true. This is set to false when it is no longer possible
+ // to upgrade the request to a secure channel.
+ uint32_t mUpgradableToSecure : 1;
+
// Defaults to false. Is set to true at the begining of OnStartRequest.
// Used to ensure methods can't be called before OnStartRequest.
bool mAfterOnStartRequestBegun;
uint64_t mTransferSize;
uint64_t mDecodedBodySize;
uint64_t mEncodedBodySize;
--- a/netwerk/protocol/http/HttpChannelChild.cpp
+++ b/netwerk/protocol/http/HttpChannelChild.cpp
@@ -2829,16 +2829,23 @@ HttpChannelChild::SetEmptyRequestHeader(
NS_IMETHODIMP
HttpChannelChild::RedirectTo(nsIURI *newURI)
{
// disabled until/unless addons run in child or something else needs this
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
+HttpChannelChild::UpgradeToSecure()
+{
+ // disabled until/unless addons run in child or something else needs this
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
HttpChannelChild::GetProtocolVersion(nsACString& aProtocolVersion)
{
aProtocolVersion = mProtocolVersion;
return NS_OK;
}
//-----------------------------------------------------------------------------
// HttpChannelChild::nsIHttpChannelInternal
--- a/netwerk/protocol/http/HttpChannelChild.h
+++ b/netwerk/protocol/http/HttpChannelChild.h
@@ -91,16 +91,17 @@ public:
// HttpBaseChannel::nsIHttpChannel
NS_IMETHOD SetReferrerWithPolicy(nsIURI *referrer, uint32_t referrerPolicy) override;
NS_IMETHOD SetRequestHeader(const nsACString& aHeader,
const nsACString& aValue,
bool aMerge) override;
NS_IMETHOD SetEmptyRequestHeader(const nsACString& aHeader) override;
NS_IMETHOD RedirectTo(nsIURI *newURI) override;
+ NS_IMETHOD UpgradeToSecure() override;
NS_IMETHOD GetProtocolVersion(nsACString& aProtocolVersion) override;
// nsIHttpChannelInternal
NS_IMETHOD SetupFallbackChannel(const char *aFallbackKey) override;
// nsISupportsPriority
NS_IMETHOD SetPriority(int32_t value) override;
// nsIClassOfService
NS_IMETHOD SetClassFlags(uint32_t inFlags) override;
NS_IMETHOD AddClassFlags(uint32_t inFlags) override;
--- a/netwerk/protocol/http/NullHttpChannel.cpp
+++ b/netwerk/protocol/http/NullHttpChannel.cpp
@@ -284,16 +284,22 @@ NullHttpChannel::IsPrivateResponse(bool
NS_IMETHODIMP
NullHttpChannel::RedirectTo(nsIURI *aNewURI)
{
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
+NullHttpChannel::UpgradeToSecure()
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
NullHttpChannel::GetRequestContextID(uint64_t *_retval)
{
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
NullHttpChannel::SetRequestContextID(uint64_t rcID)
{
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -455,26 +455,30 @@ nsHttpChannel::OnBeforeConnect()
OriginAttributes originAttributes;
if (!NS_GetOriginAttributes(this, originAttributes)) {
return NS_ERROR_FAILURE;
}
bool isHttp = false;
rv = mURI->SchemeIs("http", &isHttp);
NS_ENSURE_SUCCESS(rv,rv);
+ // At this point it is no longer possible to call HttpBaseChannel::UpgradeToSecure.
+ mUpgradableToSecure = false;
if (isHttp) {
- bool shouldUpgrade = false;
- rv = NS_ShouldSecureUpgrade(mURI,
- mLoadInfo,
- resultPrincipal,
- mPrivateBrowsing,
- mAllowSTS,
- originAttributes,
- shouldUpgrade);
- NS_ENSURE_SUCCESS(rv, rv);
+ bool shouldUpgrade = mUpgradeToSecure;
+ if (!shouldUpgrade) {
+ rv = NS_ShouldSecureUpgrade(mURI,
+ mLoadInfo,
+ resultPrincipal,
+ mPrivateBrowsing,
+ mAllowSTS,
+ originAttributes,
+ shouldUpgrade);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
if (shouldUpgrade) {
return AsyncCall(&nsHttpChannel::HandleAsyncRedirectChannelToHttps);
}
}
// ensure that we are using a valid hostname
if (!net_IsValidHostName(nsDependentCString(mConnectionInfo->Origin())))
return NS_ERROR_UNKNOWN_HOST;
--- a/netwerk/protocol/http/nsIHttpChannel.idl
+++ b/netwerk/protocol/http/nsIHttpChannel.idl
@@ -440,16 +440,28 @@ interface nsIHttpChannel : nsIChannel
* caller to call it wins.
*
* @throws NS_ERROR_NOT_AVAILABLE if called after the channel has already
* started to deliver the content to its listener.
*/
[must_use] void redirectTo(in nsIURI aTargetURI);
/**
+ * Flags a channel to be upgraded to HTTPS.
+ *
+ * Upgrading to a secure channel must happen before or during
+ * "http-on-modify-request". If redirectTo is called early as well, it
+ * will win and upgradeToSecure will be a no-op.
+ *
+ * @throws NS_ERROR_NOT_AVAILABLE if called after the channel has already
+ * started to deliver the content to its listener.
+ */
+ [must_use] void upgradeToSecure();
+
+ /**
* Identifies the request context for this load.
*/
[noscript, must_use] attribute uint64_t requestContextID;
/**
* Unique ID of the channel, shared between parent and child. Needed if
* the channel activity needs to be monitored across process boundaries,
* like in devtools net monitor. See bug 1274556.
--- a/netwerk/protocol/viewsource/nsViewSourceChannel.cpp
+++ b/netwerk/protocol/viewsource/nsViewSourceChannel.cpp
@@ -1073,16 +1073,23 @@ nsViewSourceChannel::IsPrivateResponse(b
NS_IMETHODIMP
nsViewSourceChannel::RedirectTo(nsIURI *uri)
{
return !mHttpChannel ? NS_ERROR_NULL_POINTER :
mHttpChannel->RedirectTo(uri);
}
NS_IMETHODIMP
+nsViewSourceChannel::UpgradeToSecure()
+{
+ return !mHttpChannel ? NS_ERROR_NULL_POINTER :
+ mHttpChannel->UpgradeToSecure();
+}
+
+NS_IMETHODIMP
nsViewSourceChannel::GetRequestContextID(uint64_t *_retval)
{
return !mHttpChannel ? NS_ERROR_NULL_POINTER :
mHttpChannel->GetRequestContextID(_retval);
}
NS_IMETHODIMP
nsViewSourceChannel::SetRequestContextID(uint64_t rcid)
--- a/toolkit/components/extensions/schemas/web_request.json
+++ b/toolkit/components/extensions/schemas/web_request.json
@@ -145,16 +145,21 @@
"optional": true,
"description": "If true, the request is cancelled. Used in onBeforeRequest, this prevents the request from being sent."
},
"redirectUrl": {
"type": "string",
"optional": true,
"description": "Only used as a response to the onBeforeRequest and onHeadersReceived events. If set, the original request is prevented from being sent/completed and is instead redirected to the given URL. Redirections to non-HTTP schemes such as data: are allowed. Redirects initiated by a redirect action use the original request method for the redirect, with one exception: If the redirect is initiated at the onHeadersReceived stage, then the redirect will be issued using the GET method."
},
+ "upgradeToSecure": {
+ "type": "boolean",
+ "optional": true,
+ "description": "Only used as a response to the onBeforeRequest event. If set, the original request is prevented from being sent/completed and is instead upgraded to a secure request. If any extension returns <code>redirectUrl</code> during onBeforeRequest, <code>upgradeToSecure</code> will have no affect."
+ },
"requestHeaders": {
"$ref": "HttpHeaders",
"optional": true,
"description": "Only used as a response to the onBeforeSendHeaders event. If set, the request is made with these request headers instead."
},
"responseHeaders": {
"$ref": "HttpHeaders",
"optional": true,
--- a/toolkit/components/extensions/test/mochitest/mochitest-common.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini
@@ -135,16 +135,17 @@ skip-if = os == 'android'
[test_ext_webrequest_hsts.html]
[test_ext_webrequest_basic.html]
skip-if = os == 'android' && debug # bug 1397615
[test_ext_webrequest_filter.html]
[test_ext_webrequest_frameId.html]
[test_ext_webrequest_responseBody.html]
skip-if = os == 'android' || os == 'linux' # linux, bug 1398120
[test_ext_webrequest_suspend.html]
+[test_ext_webrequest_upgrade.html]
[test_ext_webrequest_upload.html]
skip-if = os == 'android' # Currently fails in emulator tests
[test_ext_webrequest_permission.html]
[test_ext_webrequest_websocket.html]
[test_ext_webnavigation.html]
skip-if = os == 'android' && debug # bug 1397615
[test_ext_webnavigation_filters.html]
skip-if = os == 'android' && debug # bug 1397615
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upgrade.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_webRequest_upgrade() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "*://mochi.test/tests/*",
+ ],
+ },
+ background() {
+ browser.webRequest.onSendHeaders.addListener((details) => {
+ // At this point, the request should have been upgraded.
+ browser.test.assertTrue(details.url.startsWith("https:"), "request is upgraded");
+ browser.test.assertTrue(details.url.includes("file_sample"), "redirect after upgrade worked");
+ browser.test.sendMessage("finished");
+ }, {urls: ["*://mochi.test/tests/*"]});
+
+ browser.webRequest.onBeforeRequest.addListener((details) => {
+ browser.test.log(`onBeforeRequest ${details.requestId} ${details.url}`);
+ let url = new URL(details.url);
+ if (url.protocol == "http:") {
+ return {upgradeToSecure: true};
+ }
+ // After the channel is initially upgraded, we get another onBeforeRequest
+ // call. Here we can redirect again to a new url.
+ if (details.url.includes("file_mixed.html")) {
+ let redirectUrl = new URL("file_sample.html", details.url).href;
+ return {redirectUrl};
+ }
+ }, {urls: ["*://mochi.test/tests/*"]}, ["blocking"]);
+ },
+ });
+
+ yield extension.startup();
+ let win = window.open("http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_mixed.html");
+ yield extension.awaitMessage("finished");
+ win.close();
+ yield extension.unload();
+});
+
+add_task(function* test_webRequest_redirect_wins() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "*://mochi.test/tests/*",
+ ],
+ },
+ background() {
+ browser.webRequest.onSendHeaders.addListener((details) => {
+ // At this point, the request should have been redirected instead of upgraded.
+ browser.test.assertTrue(details.url.includes("file_sample"), "request was redirected");
+ browser.test.sendMessage("finished");
+ }, {urls: ["*://mochi.test/tests/*"]});
+
+ browser.webRequest.onBeforeRequest.addListener((details) => {
+ if (details.url.includes("file_mixed.html")) {
+ let redirectUrl = new URL("file_sample.html", details.url).href;
+ return {upgradeToSecure: true, redirectUrl};
+ }
+ }, {urls: ["*://mochi.test/tests/*"]}, ["blocking"]);
+ },
+ });
+
+ yield extension.startup();
+ let win = window.open("http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_mixed.html");
+ yield extension.awaitMessage("finished");
+ win.close();
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
--- a/toolkit/components/extensions/webrequest/ChannelWrapper.cpp
+++ b/toolkit/components/extensions/webrequest/ChannelWrapper.cpp
@@ -125,16 +125,28 @@ ChannelWrapper::RedirectTo(nsIURI* aURI,
rv = chan->RedirectTo(aURI);
}
if (NS_FAILED(rv)) {
aRv.Throw(rv);
}
}
void
+ChannelWrapper::UpgradeToSecure(ErrorResult& aRv)
+{
+ nsresult rv = NS_ERROR_UNEXPECTED;
+ if (nsCOMPtr<nsIHttpChannel> chan = MaybeHttpChannel()) {
+ rv = chan->UpgradeToSecure();
+ }
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ }
+}
+
+void
ChannelWrapper::SetSuspended(bool aSuspended, ErrorResult& aRv)
{
if (aSuspended != mSuspended) {
nsresult rv = NS_ERROR_UNEXPECTED;
if (nsCOMPtr<nsIChannel> chan = MaybeChannel()) {
if (aSuspended) {
rv = chan->Suspend();
} else {
--- a/toolkit/components/extensions/webrequest/ChannelWrapper.h
+++ b/toolkit/components/extensions/webrequest/ChannelWrapper.h
@@ -128,16 +128,17 @@ public:
already_AddRefed<nsIChannel> GetChannel() const { return MaybeChannel(); }
void SetChannel(nsIChannel* aChannel);
void Cancel(uint32_t result, ErrorResult& aRv);
void RedirectTo(nsIURI* uri, ErrorResult& aRv);
+ void UpgradeToSecure(ErrorResult& aRv);
bool Suspended() const { return mSuspended; }
void SetSuspended(bool aSuspended, ErrorResult& aRv);
void GetContentType(nsCString& aContentType) const;
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -816,16 +816,24 @@ HttpObserverManager = {
channel.suspended = false;
channel.redirectTo(Services.io.newURI(result.redirectUrl));
return;
} catch (e) {
Cu.reportError(e);
}
}
+ if (result.upgradeToSecure && kind === "opening") {
+ try {
+ channel.upgradeToSecure();
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+
if (opts.requestHeaders && result.requestHeaders && requestHeaders) {
requestHeaders.applyChanges(result.requestHeaders);
}
if (opts.responseHeaders && result.responseHeaders && responseHeaders) {
responseHeaders.applyChanges(result.responseHeaders);
}
}