Bug 1545421 - New nsresult error codes for 407, 502 and 504 http response codes returned by proxies + test, r=dragana

Differential Revision: https://phabricator.services.mozilla.com/D32817

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Honza Bambas 2019-05-31 17:29:53 +00:00
parent ce7abe5c5c
commit c35df87597
15 changed files with 622 additions and 106 deletions

View File

@ -4104,7 +4104,8 @@ nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI,
formatStrCount = 1; formatStrCount = 1;
errorDescriptionID = "dnsNotFound2"; errorDescriptionID = "dnsNotFound2";
error = "dnsNotFound"; error = "dnsNotFound";
} else if (NS_ERROR_CONNECTION_REFUSED == aError) { } else if (NS_ERROR_CONNECTION_REFUSED == aError ||
NS_ERROR_PROXY_BAD_GATEWAY == aError) {
NS_ENSURE_ARG_POINTER(aURI); NS_ENSURE_ARG_POINTER(aURI);
addHostPort = true; addHostPort = true;
error = "connectionFailure"; error = "connectionFailure";
@ -4112,7 +4113,8 @@ nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI,
NS_ENSURE_ARG_POINTER(aURI); NS_ENSURE_ARG_POINTER(aURI);
addHostPort = true; addHostPort = true;
error = "netInterrupt"; error = "netInterrupt";
} else if (NS_ERROR_NET_TIMEOUT == aError) { } else if (NS_ERROR_NET_TIMEOUT == aError ||
NS_ERROR_PROXY_GATEWAY_TIMEOUT == aError) {
NS_ENSURE_ARG_POINTER(aURI); NS_ENSURE_ARG_POINTER(aURI);
// Get the host // Get the host
nsAutoCString host; nsAutoCString host;
@ -4341,6 +4343,7 @@ nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI,
error = "proxyResolveFailure"; error = "proxyResolveFailure";
break; break;
case NS_ERROR_PROXY_CONNECTION_REFUSED: case NS_ERROR_PROXY_CONNECTION_REFUSED:
case NS_ERROR_PROXY_AUTHENTICATION_FAILED:
// Proxy connection was refused. // Proxy connection was refused.
error = "proxyConnectFailure"; error = "proxyConnectFailure";
break; break;
@ -6935,15 +6938,18 @@ nsresult nsDocShell::EndPageLoad(nsIWebProgress* aProgress,
aStatus == NS_ERROR_CONNECTION_REFUSED || aStatus == NS_ERROR_CONNECTION_REFUSED ||
aStatus == NS_ERROR_UNKNOWN_PROXY_HOST || aStatus == NS_ERROR_UNKNOWN_PROXY_HOST ||
aStatus == NS_ERROR_PROXY_CONNECTION_REFUSED || aStatus == NS_ERROR_PROXY_CONNECTION_REFUSED ||
aStatus == NS_ERROR_PROXY_AUTHENTICATION_FAILED ||
aStatus == NS_ERROR_BLOCKED_BY_POLICY) && aStatus == NS_ERROR_BLOCKED_BY_POLICY) &&
(isTopFrame || UseErrorPages())) { (isTopFrame || UseErrorPages())) {
DisplayLoadError(aStatus, url, nullptr, aChannel); DisplayLoadError(aStatus, url, nullptr, aChannel);
} else if (aStatus == NS_ERROR_NET_TIMEOUT || } else if (aStatus == NS_ERROR_NET_TIMEOUT ||
aStatus == NS_ERROR_PROXY_GATEWAY_TIMEOUT ||
aStatus == NS_ERROR_REDIRECT_LOOP || aStatus == NS_ERROR_REDIRECT_LOOP ||
aStatus == NS_ERROR_UNKNOWN_SOCKET_TYPE || aStatus == NS_ERROR_UNKNOWN_SOCKET_TYPE ||
aStatus == NS_ERROR_NET_INTERRUPT || aStatus == NS_ERROR_NET_INTERRUPT ||
aStatus == NS_ERROR_NET_RESET || aStatus == NS_ERROR_OFFLINE || aStatus == NS_ERROR_NET_RESET ||
aStatus == NS_ERROR_MALWARE_URI || aStatus == NS_ERROR_PROXY_BAD_GATEWAY ||
aStatus == NS_ERROR_OFFLINE || aStatus == NS_ERROR_MALWARE_URI ||
aStatus == NS_ERROR_PHISHING_URI || aStatus == NS_ERROR_PHISHING_URI ||
aStatus == NS_ERROR_UNWANTED_URI || aStatus == NS_ERROR_UNWANTED_URI ||
aStatus == NS_ERROR_HARMFUL_URI || aStatus == NS_ERROR_HARMFUL_URI ||

View File

@ -12,6 +12,17 @@ function getTestServerPort() {
return port; return port;
} }
function getTestProxyPort() {
let portEnv = Cc["@mozilla.org/process/environment;1"]
.getService(Ci.nsIEnvironment).get("MOZHTTP2_PROXY_PORT");
let port = parseInt(portEnv, 10);
if (!Number.isFinite(port) || port < 1 || port > 65535) {
throw new Error(`Invalid port in MOZHTTP2_PROXY_PORT env var: ${portEnv}`);
}
info(`Using HTTP/2 proxy on port ${port}`);
return port;
}
function readFile(file) { function readFile(file) {
let fstream = Cc["@mozilla.org/network/file-input-stream;1"] let fstream = Cc["@mozilla.org/network/file-input-stream;1"]
.createInstance(Ci.nsIFileInputStream); .createInstance(Ci.nsIFileInputStream);

View File

@ -145,6 +145,9 @@ XPC_MSG_DEF(NS_ERROR_ALREADY_CONNECTED , "The connection is already
XPC_MSG_DEF(NS_ERROR_NOT_CONNECTED , "The connection does not exist") XPC_MSG_DEF(NS_ERROR_NOT_CONNECTED , "The connection does not exist")
XPC_MSG_DEF(NS_ERROR_CONNECTION_REFUSED , "The connection was refused") XPC_MSG_DEF(NS_ERROR_CONNECTION_REFUSED , "The connection was refused")
XPC_MSG_DEF(NS_ERROR_PROXY_CONNECTION_REFUSED , "The connection to the proxy server was refused") XPC_MSG_DEF(NS_ERROR_PROXY_CONNECTION_REFUSED , "The connection to the proxy server was refused")
XPC_MSG_DEF(NS_ERROR_PROXY_AUTHENTICATION_FAILED , "The proxy requires authentication")
XPC_MSG_DEF(NS_ERROR_PROXY_BAD_GATEWAY , "The request failed on the proxy")
XPC_MSG_DEF(NS_ERROR_PROXY_GATEWAY_TIMEOUT , "The request timed out on the proxy")
XPC_MSG_DEF(NS_ERROR_NET_TIMEOUT , "The connection has timed out") XPC_MSG_DEF(NS_ERROR_NET_TIMEOUT , "The connection has timed out")
XPC_MSG_DEF(NS_ERROR_OFFLINE , "The requested action could not be completed in the offline state") XPC_MSG_DEF(NS_ERROR_OFFLINE , "The requested action could not be completed in the offline state")
XPC_MSG_DEF(NS_ERROR_PORT_ACCESS_NOT_ALLOWED , "Establishing a connection to an unsafe or otherwise banned port was prohibited") XPC_MSG_DEF(NS_ERROR_PORT_ACCESS_NOT_ALLOWED , "Establishing a connection to an unsafe or otherwise banned port was prohibited")

View File

@ -1036,7 +1036,7 @@ nsresult Http2Stream::ConvertResponseHeaders(Http2Decompressor* decompressor,
if ((httpResponseCode / 100) != 2) { if ((httpResponseCode / 100) != 2) {
MapStreamToPlainText(); MapStreamToPlainText();
} }
MapStreamToHttpConnection(); MapStreamToHttpConnection(httpResponseCode);
ClearTransactionsBlockedOnTunnel(); ClearTransactionsBlockedOnTunnel();
} else if (mIsWebsocket) { } else if (mIsWebsocket) {
LOG3(("Http2Stream %p websocket response code %d", this, httpResponseCode)); LOG3(("Http2Stream %p websocket response code %d", this, httpResponseCode));
@ -1599,12 +1599,14 @@ void Http2Stream::MapStreamToPlainText() {
qiTrans->ForcePlainText(); qiTrans->ForcePlainText();
} }
void Http2Stream::MapStreamToHttpConnection() { void Http2Stream::MapStreamToHttpConnection(int32_t httpResponseCode) {
RefPtr<SpdyConnectTransaction> qiTrans( RefPtr<SpdyConnectTransaction> qiTrans(
mTransaction->QuerySpdyConnectTransaction()); mTransaction->QuerySpdyConnectTransaction());
MOZ_ASSERT(qiTrans); MOZ_ASSERT(qiTrans);
qiTrans->MapStreamToHttpConnection(mSocketTransport, qiTrans->MapStreamToHttpConnection(mSocketTransport,
mTransaction->ConnectionInfo()); mTransaction->ConnectionInfo(),
mIsTunnel ? httpResponseCode : -1);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------

View File

@ -369,7 +369,7 @@ class Http2Stream : public nsAHttpSegmentReader,
private: private:
void ClearTransactionsBlockedOnTunnel(); void ClearTransactionsBlockedOnTunnel();
void MapStreamToPlainText(); void MapStreamToPlainText();
void MapStreamToHttpConnection(); void MapStreamToHttpConnection(int32_t httpResponseCode = -1);
bool mIsTunnel; bool mIsTunnel;
bool mPlainTextTunnel; bool mPlainTextTunnel;

View File

@ -1038,7 +1038,8 @@ void SpdyConnectTransaction::ForcePlainText() {
} }
void SpdyConnectTransaction::MapStreamToHttpConnection( void SpdyConnectTransaction::MapStreamToHttpConnection(
nsISocketTransport* aTransport, nsHttpConnectionInfo* aConnInfo) { nsISocketTransport* aTransport, nsHttpConnectionInfo* aConnInfo,
int32_t httpResponseCode) {
mConnInfo = aConnInfo; mConnInfo = aConnInfo;
mTunnelTransport = new SocketTransportShim(aTransport, mIsWebsocket); mTunnelTransport = new SocketTransportShim(aTransport, mIsWebsocket);
@ -1046,10 +1047,27 @@ void SpdyConnectTransaction::MapStreamToHttpConnection(
mTunnelStreamOut = new OutputStreamShim(this, mIsWebsocket); mTunnelStreamOut = new OutputStreamShim(this, mIsWebsocket);
mTunneledConn = new nsHttpConnection(); mTunneledConn = new nsHttpConnection();
switch (httpResponseCode) {
case 404:
CreateShimError(NS_ERROR_UNKNOWN_HOST);
break;
case 407:
CreateShimError(NS_ERROR_PROXY_AUTHENTICATION_FAILED);
break;
case 502:
CreateShimError(NS_ERROR_PROXY_BAD_GATEWAY);
break;
case 504:
CreateShimError(NS_ERROR_PROXY_GATEWAY_TIMEOUT);
break;
default:
break;
}
// this new http connection has a specific hashkey (i.e. to a particular // this new http connection has a specific hashkey (i.e. to a particular
// host via the tunnel) and is associated with the tunnel streams // host via the tunnel) and is associated with the tunnel streams
LOG(("SpdyConnectTransaction new httpconnection %p %s\n", mTunneledConn.get(), LOG(("SpdyConnectTransaction %p new httpconnection %p %s\n", this,
aConnInfo->HashKey().get())); mTunneledConn.get(), aConnInfo->HashKey().get()));
nsCOMPtr<nsIInterfaceRequestor> callbacks; nsCOMPtr<nsIInterfaceRequestor> callbacks;
GetSecurityCallbacks(getter_AddRefs(callbacks)); GetSecurityCallbacks(getter_AddRefs(callbacks));
@ -1202,6 +1220,9 @@ nsresult SpdyConnectTransaction::ReadSegments(nsAHttpSegmentReader* reader,
} }
void SpdyConnectTransaction::CreateShimError(nsresult code) { void SpdyConnectTransaction::CreateShimError(nsresult code) {
LOG(("SpdyConnectTransaction::CreateShimError %p 0x%08" PRIx32, this,
static_cast<uint32_t>(code)));
MOZ_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_ASSERT(OnSocketThread(), "not on socket thread");
MOZ_ASSERT(NS_FAILED(code)); MOZ_ASSERT(NS_FAILED(code));

View File

@ -207,7 +207,8 @@ class SpdyConnectTransaction final : public NullHttpTransaction {
// error. // error.
void ForcePlainText(); void ForcePlainText();
void MapStreamToHttpConnection(nsISocketTransport* aTransport, void MapStreamToHttpConnection(nsISocketTransport* aTransport,
nsHttpConnectionInfo* aConnInfo); nsHttpConnectionInfo* aConnInfo,
int32_t httpResponseCode);
MOZ_MUST_USE nsresult ReadSegments(nsAHttpSegmentReader* reader, MOZ_MUST_USE nsresult ReadSegments(nsAHttpSegmentReader* reader,
uint32_t count, uint32_t* countRead) final; uint32_t count, uint32_t* countRead) final;

View File

@ -1954,16 +1954,18 @@ nsresult nsHttpChannel::ProcessFailedProxyConnect(uint32_t httpStatus) {
rv = NS_ERROR_CONNECTION_REFUSED; rv = NS_ERROR_CONNECTION_REFUSED;
break; break;
case 403: // HTTP/1.1: "Forbidden" case 403: // HTTP/1.1: "Forbidden"
case 407: // ProcessAuthentication() failed
case 501: // HTTP/1.1: "Not Implemented" case 501: // HTTP/1.1: "Not Implemented"
// user sees boilerplate Mozilla "Proxy Refused Connection" page. // user sees boilerplate Mozilla "Proxy Refused Connection" page.
rv = NS_ERROR_PROXY_CONNECTION_REFUSED; rv = NS_ERROR_PROXY_CONNECTION_REFUSED;
break; break;
// Squid sends 404 if DNS fails (regular 404 from target is tunneled) case 407: // ProcessAuthentication() failed (e.g. no header)
rv = NS_ERROR_PROXY_AUTHENTICATION_FAILED;
break;
// Squid sends 404 if DNS fails (regular 404 from target is tunneled)
case 404: // HTTP/1.1: "Not Found" case 404: // HTTP/1.1: "Not Found"
// RFC 2616: "some deployed proxies are known to return 400 or 500 when // RFC 2616: "some deployed proxies are known to return 400 or
// DNS lookups time out." (Squid uses 500 if it runs out of sockets: so // 500 when DNS lookups time out." (Squid uses 500 if it runs
// we have a conflict here). // out of sockets: so we have a conflict here).
case 400: // HTTP/1.1 "Bad Request" case 400: // HTTP/1.1 "Bad Request"
case 500: // HTTP/1.1: "Internal Server Error" case 500: // HTTP/1.1: "Internal Server Error"
/* User sees: "Address Not Found: Firefox can't find the server at /* User sees: "Address Not Found: Firefox can't find the server at
@ -1972,8 +1974,10 @@ nsresult nsHttpChannel::ProcessFailedProxyConnect(uint32_t httpStatus) {
rv = NS_ERROR_UNKNOWN_HOST; rv = NS_ERROR_UNKNOWN_HOST;
break; break;
case 502: // HTTP/1.1: "Bad Gateway" (invalid resp from target server) case 502: // HTTP/1.1: "Bad Gateway" (invalid resp from target server)
// Squid returns 503 if target request fails for anything but DNS. rv = NS_ERROR_PROXY_BAD_GATEWAY;
break;
case 503: // HTTP/1.1: "Service Unavailable" case 503: // HTTP/1.1: "Service Unavailable"
// Squid returns 503 if target request fails for anything but DNS.
/* User sees: "Failed to Connect: /* User sees: "Failed to Connect:
* Firefox can't establish a connection to the server at * Firefox can't establish a connection to the server at
* www.foo.com. Though the site seems valid, the browser * www.foo.com. Though the site seems valid, the browser
@ -1986,7 +1990,7 @@ nsresult nsHttpChannel::ProcessFailedProxyConnect(uint32_t httpStatus) {
case 504: // HTTP/1.1: "Gateway Timeout" case 504: // HTTP/1.1: "Gateway Timeout"
// user sees: "Network Timeout: The server at www.foo.com // user sees: "Network Timeout: The server at www.foo.com
// is taking too long to respond." // is taking too long to respond."
rv = NS_ERROR_NET_TIMEOUT; rv = NS_ERROR_PROXY_GATEWAY_TIMEOUT;
break; break;
// Confused proxy server or malicious response // Confused proxy server or malicious response
default: default:

View File

@ -0,0 +1,176 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This test checks following expectations when using HTTP/1 proxy:
*
* - check we are seeing expected nsresult error codes on channels
* (nsIChannel.status) corresponding to different proxy status code
* responses (502, 504, 407, ...)
* - check we don't try to ask for credentials or otherwise authenticate to
* the proxy when 407 is returned and there is no Proxy-Authenticate
* response header sent
*/
const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
let server_port;
let http_server;
class ProxyFilter {
constructor(type, host, port, flags) {
this._type = type;
this._host = host;
this._port = port;
this._flags = flags;
this.QueryInterface = ChromeUtils.generateQI([Ci.nsIProtocolProxyFilter]);
}
applyFilter(pps, uri, pi, cb) {
if (uri.spec.match(/(\/proxy-session-counter)/)) {
cb.onProxyFilterResult(pi);
return;
}
cb.onProxyFilterResult(pps.newProxyInfo(
this._type, this._host, this._port,
"", "", this._flags, 1000, null));
}
};
class UnxpectedAuthPrompt2 {
constructor(signal) {
this.signal = signal;
this.QueryInterface = ChromeUtils.generateQI([Ci.nsIAuthPrompt2]);
}
asyncPromptAuth() {
this.signal.triggered = true;
throw Cr.ERROR_UNEXPECTED;
}
};
class AuthRequestor {
constructor(prompt) {
this.prompt = prompt;
this.QueryInterface = ChromeUtils.generateQI([Ci.nsIInterfaceRequestor]);
}
getInterface(iid) {
if (iid.equals(Ci.nsIAuthPrompt2)) {
return this.prompt();
}
throw Cr.NS_ERROR_NO_INTERFACE;
}
};
function make_channel(url) {
return NetUtil.newChannel({
uri: url,
loadUsingSystemPrincipal: true,
// Using TYPE_DOCUMENT for the authentication dialog test, it'd be blocked for other types
contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
});
}
function get_response(channel, flags = CL_ALLOW_UNKNOWN_CL) {
return new Promise(resolve => {
channel.asyncOpen(new ChannelListener((request, data) => {
request.QueryInterface(Ci.nsIHttpChannel);
const status = request.status;
const http_code = status ? undefined : request.responseStatus;
resolve({ status, http_code, data });
}, null, flags));
});
}
function connect_handler(request, response) {
Assert.equal(request.method, "CONNECT");
switch (request.host) {
case "404.example.com":
response.setStatusLine(request.httpVersion, 404, "Not found");
break;
case "407.example.com":
response.setStatusLine(request.httpVersion, 407, "Authenticate");
// And deliberately no Proxy-Authenticate header
break;
case "502.example.com":
response.setStatusLine(request.httpVersion, 502, "Bad Gateway");
break;
case "504.example.com":
response.setStatusLine(request.httpVersion, 504, "Gateway timeout");
break;
default:
response.setStatusLine(request.httpVersion, 500, "I am dumb");
}
}
add_task(async function setup() {
http_server = new HttpServer();
http_server.identity.add("https", "404.example.com", 443);
http_server.identity.add("https", "407.example.com", 443);
http_server.identity.add("https", "502.example.com", 443);
http_server.identity.add("https", "504.example.com", 443);
http_server.registerPathHandler("CONNECT", connect_handler);
http_server.start(-1);
server_port = http_server.identity.primaryPort;
// make all native resolve calls "secretly" resolve localhost instead
Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
pps.registerFilter(new ProxyFilter("http", "localhost", server_port, 0), 10);
});
registerCleanupFunction(() => {
Services.prefs.clearUserPref("network.dns.native-is-localhost");
});
/**
* Test series beginning.
*/
// The proxy responses with 407 instead of 200 Connected, make sure we get a proper error
// code from the channel and not try to ask for any credentials.
add_task(async function proxy_auth_failure() {
const chan = make_channel(`https://407.example.com/`);
const auth_prompt = { triggered: false };
chan.notificationCallbacks = new AuthRequestor(() => new UnxpectedAuthPrompt2(auth_prompt));
const { status, http_code } = await get_response(chan, CL_EXPECT_FAILURE);
Assert.equal(status, Cr.NS_ERROR_PROXY_AUTHENTICATION_FAILED);
Assert.equal(http_code, undefined);
Assert.equal(auth_prompt.triggered, false, "Auth prompt didn't trigger");
});
// 502 Bad gateway code returned by the proxy.
add_task(async function proxy_bad_gateway_failure() {
const { status, http_code } = await get_response(make_channel(`https://502.example.com/`), CL_EXPECT_FAILURE);
Assert.equal(status, Cr.NS_ERROR_PROXY_BAD_GATEWAY);
Assert.equal(http_code, undefined);
});
// 504 Gateway timeout code returned by the proxy.
add_task(async function proxy_gateway_timeout_failure() {
const { status, http_code } = await get_response(make_channel(`https://504.example.com/`), CL_EXPECT_FAILURE);
Assert.equal(status, Cr.NS_ERROR_PROXY_GATEWAY_TIMEOUT);
Assert.equal(http_code, undefined);
});
// 404 Not Found means the proxy could not resolve the host.
add_task(async function proxy_host_not_found_failure() {
const { status, http_code } = await get_response(make_channel(`https://404.example.com/`), CL_EXPECT_FAILURE);
Assert.equal(status, Cr.NS_ERROR_UNKNOWN_HOST);
Assert.equal(http_code, undefined);
});
add_task(async function shutdown() {
await new Promise(resolve => {
http_server.stop(resolve);
});
});

View File

@ -0,0 +1,266 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This test checks following expectations when using HTTP/2 proxy:
*
* - when we request https access, we don't create different sessions for
* different origins, only new tunnels inside a single session
* - when the isolation key (`proxy_isolation`) is changed, new single session
* is created for new requests to same origins as before
* - error code returned from the tunnel (a proxy error - not end-server
* error!) doesn't kill the existing session
* - check we are seeing expected nsresult error codes on channels
* (nsIChannel.status) corresponding to different proxy status code
* responses (502, 504, 407, ...)
* - check we don't try to ask for credentials or otherwise authenticate to
* the proxy when 407 is returned and there is no Proxy-Authenticate
* response header sent
*/
const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
let proxy_port;
let server_port;
// See moz-http2
const proxy_auth = 'authorization-token';
let proxy_isolation;
class ProxyFilter {
constructor(type, host, port, flags) {
this._type = type;
this._host = host;
this._port = port;
this._flags = flags;
this.QueryInterface = ChromeUtils.generateQI([Ci.nsIProtocolProxyFilter]);
}
applyFilter(pps, uri, pi, cb) {
if (uri.spec.match(/(\/proxy-session-counter)/)) {
cb.onProxyFilterResult(pi);
return;
}
cb.onProxyFilterResult(pps.newProxyInfo(
this._type, this._host, this._port,
proxy_auth, proxy_isolation, this._flags, 1000, null));
}
};
class UnxpectedAuthPrompt2 {
constructor(signal) {
this.signal = signal;
this.QueryInterface = ChromeUtils.generateQI([Ci.nsIAuthPrompt2]);
}
asyncPromptAuth() {
this.signal.triggered = true;
throw Cr.ERROR_UNEXPECTED;
}
};
class AuthRequestor {
constructor(prompt) {
this.prompt = prompt;
this.QueryInterface = ChromeUtils.generateQI([Ci.nsIInterfaceRequestor]);
}
getInterface(iid) {
if (iid.equals(Ci.nsIAuthPrompt2)) {
return this.prompt();
}
throw Cr.NS_ERROR_NO_INTERFACE;
}
};
function make_channel(url) {
return NetUtil.newChannel({
uri: url,
loadUsingSystemPrincipal: true,
// Using TYPE_DOCUMENT for the authentication dialog test, it'd be blocked for other types
contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
});
}
function get_response(channel, flags = CL_ALLOW_UNKNOWN_CL) {
return new Promise(resolve => {
channel.asyncOpen(new ChannelListener((request, data) => {
request.QueryInterface(Ci.nsIHttpChannel);
const status = request.status;
const http_code = status ? undefined : request.responseStatus;
resolve({ status, http_code, data });
}, null, flags));
});
}
let initial_session_count = 0;
function proxy_session_counter() {
return new Promise(async resolve => {
const channel = make_channel(`https://localhost:${server_port}/proxy-session-counter`);
const { data } = await get_response(channel);
resolve(parseInt(data) - initial_session_count);
});
}
add_task(async function setup() {
const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
server_port = env.get("MOZHTTP2_PORT");
Assert.notEqual(server_port, null);
proxy_port = env.get("MOZHTTP2_PROXY_PORT");
Assert.notEqual(proxy_port, null);
// Set to allow the cert presented by our H2 server
do_get_profile();
Services.prefs.setBoolPref("network.http.spdy.enabled", true);
Services.prefs.setBoolPref("network.http.spdy.enabled.http2", true);
// make all native resolve calls "secretly" resolve localhost instead
Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
// The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
// so add that cert to the trust list as a signing cert.
let certdb = Cc["@mozilla.org/security/x509certdb;1"]
.getService(Ci.nsIX509CertDB);
addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
pps.registerFilter(new ProxyFilter("https", "localhost", proxy_port, 0), 10);
initial_session_count = await proxy_session_counter();
info(`Initial proxy session count = ${initial_session_count}`);
});
registerCleanupFunction(() => {
Services.prefs.clearUserPref("network.http.spdy.enabled");
Services.prefs.clearUserPref("network.http.spdy.enabled.http2");
Services.prefs.clearUserPref("network.dns.native-is-localhost");
});
/**
* Test series beginning.
*/
// Check we reach the h2 end server and keep only one session with the proxy for two different origin.
// Here we use the first isolation token.
add_task(async function proxy_success_one_session() {
proxy_isolation = "TOKEN1";
const foo = await get_response(make_channel(`https://foo.example.com/random-request-1`));
const alt1 = await get_response(make_channel(`https://alt1.example.com/random-request-2`));
Assert.equal(foo.status, Cr.NS_OK);
Assert.equal(foo.http_code, 200);
Assert.ok(foo.data.match("random-request-1"));
Assert.ok(foo.data.match("You Win!"));
Assert.equal(alt1.status, Cr.NS_OK);
Assert.equal(alt1.http_code, 200);
Assert.ok(alt1.data.match("random-request-2"));
Assert.ok(alt1.data.match("You Win!"));
Assert.equal(await proxy_session_counter(), 1, "Created just one session with the proxy");
});
// The proxy responses with 407 instead of 200 Connected, make sure we get a proper error
// code from the channel and not try to ask for any credentials.
add_task(async function proxy_auth_failure() {
const chan = make_channel(`https://407.example.com/`);
const auth_prompt = { triggered: false };
chan.notificationCallbacks = new AuthRequestor(() => new UnxpectedAuthPrompt2(auth_prompt));
const { status, http_code } = await get_response(chan, CL_EXPECT_FAILURE);
Assert.equal(status, Cr.NS_ERROR_PROXY_AUTHENTICATION_FAILED);
Assert.equal(http_code, undefined);
Assert.equal(auth_prompt.triggered, false, "Auth prompt didn't trigger");
Assert.equal(await proxy_session_counter(), 1, "No new session created by 407");
});
// 502 Bad gateway code returned by the proxy, still one session only, proper different code
// from the channel.
add_task(async function proxy_bad_gateway_failure() {
const { status, http_code } = await get_response(make_channel(`https://502.example.com/`), CL_EXPECT_FAILURE);
Assert.equal(status, Cr.NS_ERROR_PROXY_BAD_GATEWAY);
Assert.equal(http_code, undefined);
Assert.equal(await proxy_session_counter(), 1, "No new session created by 502 after 407");
});
// Second 502 Bad gateway code returned by the proxy, still one session only with the proxy.
add_task(async function proxy_bad_gateway_failure_two() {
const { status, http_code } = await get_response(make_channel(`https://502.example.com/`), CL_EXPECT_FAILURE);
Assert.equal(status, Cr.NS_ERROR_PROXY_BAD_GATEWAY);
Assert.equal(http_code, undefined);
Assert.equal(await proxy_session_counter(), 1, "No new session created by second 502");
});
// 504 Gateway timeout code returned by the proxy, still one session only, proper different code
// from the channel.
add_task(async function proxy_gateway_timeout_failure() {
const { status, http_code } = await get_response(make_channel(`https://504.example.com/`), CL_EXPECT_FAILURE);
Assert.equal(status, Cr.NS_ERROR_PROXY_GATEWAY_TIMEOUT);
Assert.equal(http_code, undefined);
Assert.equal(await proxy_session_counter(), 1, "No new session created by 504 after 502");
});
// 404 Not Found means the proxy could not resolve the host. As for other error responses
// we still expect this not to close the existing session.
add_task(async function proxy_host_not_found_failure() {
const { status, http_code } = await get_response(make_channel(`https://404.example.com/`), CL_EXPECT_FAILURE);
Assert.equal(status, Cr.NS_ERROR_UNKNOWN_HOST);
Assert.equal(http_code, undefined);
Assert.equal(await proxy_session_counter(), 1, "No new session created by 404 after 504");
});
// Make sure that the above error codes don't kill the session and we still reach the end server
add_task(async function proxy_success_still_one_session() {
const foo = await get_response(make_channel(`https://foo.example.com/random-request-1`));
const alt1 = await get_response(make_channel(`https://alt1.example.com/random-request-2`));
Assert.equal(foo.status, Cr.NS_OK);
Assert.equal(foo.http_code, 200);
Assert.ok(foo.data.match("random-request-1"));
Assert.equal(alt1.status, Cr.NS_OK);
Assert.equal(alt1.http_code, 200);
Assert.ok(alt1.data.match("random-request-2"));
Assert.equal(await proxy_session_counter(), 1, "No new session created after proxy error codes");
});
// Have a new isolation key, this means we are expected to create a new, and again one only,
// session with the proxy to reach the end server.
add_task(async function proxy_success_isolated_session() {
Assert.notEqual(proxy_isolation, "TOKEN2");
proxy_isolation = "TOKEN2";
const foo = await get_response(make_channel(`https://foo.example.com/random-request-1`));
const alt1 = await get_response(make_channel(`https://alt1.example.com/random-request-2`));
const lh = await get_response(make_channel(`https://localhost/random-request-3`));
Assert.equal(foo.status, Cr.NS_OK);
Assert.equal(foo.http_code, 200);
Assert.ok(foo.data.match("random-request-1"));
Assert.ok(foo.data.match("You Win!"));
Assert.equal(alt1.status, Cr.NS_OK);
Assert.equal(alt1.http_code, 200);
Assert.ok(alt1.data.match("random-request-2"));
Assert.ok(alt1.data.match("You Win!"));
Assert.equal(lh.status, Cr.NS_OK);
Assert.equal(lh.http_code, 200);
Assert.ok(lh.data.match("random-request-3"));
Assert.ok(lh.data.match("You Win!"));
Assert.equal(await proxy_session_counter(), 2, "Just one new session seen after changing the isolation key");
});
// Check that error codes are still handled the same way with new isolation, just in case.
add_task(async function proxy_bad_gateway_failure_isolated() {
const failure1 = await get_response(make_channel(`https://502.example.com/`), CL_EXPECT_FAILURE);
const failure2 = await get_response(make_channel(`https://502.example.com/`), CL_EXPECT_FAILURE);
Assert.equal(failure1.status, Cr.NS_ERROR_PROXY_BAD_GATEWAY);
Assert.equal(failure1.http_code, undefined);
Assert.equal(failure2.status, Cr.NS_ERROR_PROXY_BAD_GATEWAY);
Assert.equal(failure2.http_code, undefined);
Assert.equal(await proxy_session_counter(), 2, "No new session created by 502");
});

View File

@ -1,72 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// This test checks that the proxy-auth header is propagated to the CONNECT request when
// set on a proxy-info object via a proxy filter
const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
class TestFilter {
constructor(type, host, port, flags, auth) {
this._type = type;
this._host = host;
this._port = port;
this._flags = flags;
this._auth = auth;
this.QueryInterface = ChromeUtils.generateQI([Ci.nsIProtocolProxyFilter]);
}
applyFilter(pps, uri, pi, cb) {
cb.onProxyFilterResult(pps.newProxyInfo(
this._type, this._host, this._port,
this._auth, "", this._flags, 1000, null));
}
};
let httpServer = null;
let port;
let connectProcessesed = false;
const proxyAuthHeader = 'proxy-auth-header-value';
function make_channel(url) {
return NetUtil.newChannel({
uri: url,
loadUsingSystemPrincipal: true
});
}
function connect_handler(request, response)
{
Assert.equal(request.method, "CONNECT");
Assert.ok(request.hasHeader("Proxy-Authorization"));
Assert.equal(request.getHeader("Proxy-Authorization"), proxyAuthHeader);
// Just refuse to connect, we have what we need now.
response.setStatusLine(request.httpVersion, 500, "STOP");
connectProcessesed = true;
}
function finish_test()
{
Assert.ok(connectProcessesed);
httpServer.stop(do_test_finished);
}
function run_test()
{
httpServer = new HttpServer();
httpServer.identity.add("https", "mozilla.org", 443);
httpServer.registerPathHandler("CONNECT", connect_handler);
httpServer.start(-1);
port = httpServer.identity.primaryPort;
pps.registerFilter(new TestFilter("http", "localhost", port, 0, proxyAuthHeader), 10);
let chan = make_channel("https://mozilla.org/");
chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE));
do_test_pending();
}

View File

@ -450,7 +450,9 @@ skip-if = os == "android" # DNSv6 issues on android
[test_stale-while-revalidate_negative.js] [test_stale-while-revalidate_negative.js]
[test_stale-while-revalidate_positive.js] [test_stale-while-revalidate_positive.js]
[test_stale-while-revalidate_max-age-0.js] [test_stale-while-revalidate_max-age-0.js]
[test_proxy-authorization-via-proxyinfo.js] [test_http1-proxy.js]
# Bug 1549368 [test_http2-proxy.js]
run-sequentially = one http2 node proxy is used for all tests, this test is using global session counter
# so far has to be disabled until we update nodejs to at least 8.4+, then keep off on android (os == "android")
skip-if = true skip-if = true
[test_head_request_no_response_body.js] [test_head_request_no_response_body.js]

View File

@ -11,11 +11,19 @@ if (process.env.NODE_HTTP2_ROOT) {
} }
var http2 = require(node_http2_root); var http2 = require(node_http2_root);
var fs = require('fs'); var fs = require('fs');
var net = require('net');
var url = require('url'); var url = require('url');
var crypto = require('crypto'); var crypto = require('crypto');
const dnsPacket = require(`${node_http2_root}/../dns-packet`); const dnsPacket = require(`${node_http2_root}/../dns-packet`);
const ip = require(`${node_http2_root}/../node-ip`); const ip = require(`${node_http2_root}/../node-ip`);
let http2_internal = null;
try {
http2_internal = require('http2');
} catch (_) {
// silently ignored
}
// Hook into the decompression code to log the decompressed name-value pairs // Hook into the decompression code to log the decompressed name-value pairs
var compression_module = node_http2_root + "/lib/protocol/compressor"; var compression_module = node_http2_root + "/lib/protocol/compressor";
var http2_compression = require(compression_module); var http2_compression = require(compression_module);
@ -76,6 +84,7 @@ function getHttpContent(path) {
var content = '<!doctype html>' + var content = '<!doctype html>' +
'<html>' + '<html>' +
'<head><title>HOORAY!</title></head>' + '<head><title>HOORAY!</title></head>' +
// 'You Win!' used in tests to check we reached this server
'<body>You Win! (by requesting' + path + ')</body>' + '<body>You Win! (by requesting' + path + ')</body>' +
'</html>'; '</html>';
return content; return content;
@ -1111,6 +1120,12 @@ function handleRequest(req, res) {
return; return;
} }
else if (u.pathname === "/proxy-session-counter") {
// Incremented with every newly created session on the proxy
res.end(proxy_session_count.toString());
return;
}
res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Type', 'text/html');
if (req.httpVersionMajor != 2) { if (req.httpVersionMajor != 2) {
res.setHeader('Connection', 'close'); res.setHeader('Connection', 'close');
@ -1141,18 +1156,91 @@ server.on('connection', function(socket) {
}); });
}); });
var proxy = http2_internal ? http2_internal.createSecureServer(options) : null;
var proxy_session_count = 0;
if (http2_internal) {
proxy.on('session', () => {
// Can be queried directly on the h2 server under "/proxy-session-counter"
++proxy_session_count;
});
proxy.on('stream', (stream, headers) => {
if (headers[':method'] !== 'CONNECT') {
// Only accept CONNECT requests
stream.close(http2.constants.NGHTTP2_REFUSED_STREAM);
return;
}
const target = headers[':authority'];
const authorization_token = headers['proxy-authorization'];
if ('authorization-token' != authorization_token || target == '407.example.com:443') {
stream.respond({ ':status': 407 });
// Deliberately send no Proxy-Authenticate header
stream.end();
return;
}
if (target == '404.example.com:443') {
// 404 Not Found, a response code that a proxy should return when the host can't be found
stream.respond({ ':status': 404 });
stream.end();
return;
}
if (target == '502.example.com:443') {
// 502 Bad Gateway, a response code mostly resembling immediate connection error
stream.respond({ ':status': 502 });
stream.end();
return;
}
if (target == '504.example.com:443') {
// 504 Gateway Timeout, did not receive a timely response from an upstream server
stream.respond({ ':status': 504 });
stream.end();
return;
}
const socket = net.connect(serverPort, '127.0.0.1', () => {
try {
stream.respond({ ':status': 200 });
socket.pipe(stream);
stream.pipe(socket);
} catch (exception) {
stream.close(http2.constants.NGHTTP2_STREAM_CLOSED);
}
});
socket.on('error', (error) => {
throw `Unxpected error when conneting the HTTP/2 server from the HTTP/2 proxy during CONNECT handling: '${error}'`;
});
});
}
var serverPort; var serverPort;
function listenok() {
serverPort = server._server.address().port; const listen = (server, envport) => {
console.log('HTTP2 server listening on port ' + serverPort); if (!server) {
} return Promise.resolve(0);
var portSelection = 0;
var envport = process.env.MOZHTTP2_PORT;
if (envport !== undefined) {
try {
portSelection = parseInt(envport, 10);
} catch (e) {
portSelection = -1;
} }
let portSelection = 0;
if (envport !== undefined) {
try {
portSelection = parseInt(envport, 10);
} catch (e) {
portSelection = -1;
}
}
return new Promise(resolve => {
server.listen(portSelection, "0.0.0.0", 200, () => {
resolve(server.address().port);
});
});
} }
server.listen(portSelection, "0.0.0.0", 200, listenok);
Promise.all([
listen(server, process.env.MOZHTTP2_PORT).then(port => serverPort = port),
listen(proxy, process.env.MOZHTTP2_PROXY_PORT)
]).then(([serverPort, proxyPort]) => {
console.log(`HTTP2 server listening on ports ${serverPort},${proxyPort}`);
});

View File

@ -1131,9 +1131,11 @@ class XPCShellTests(object):
# tell us it's started # tell us it's started
msg = process.stdout.readline() msg = process.stdout.readline()
if 'server listening' in msg: if 'server listening' in msg:
searchObj = re.search(r'HTTP2 server listening on port (.*)', msg, 0) searchObj = re.search(r'HTTP2 server listening on ports ([0-9]+),([0-9]+)',
msg, 0)
if searchObj: if searchObj:
self.env["MOZHTTP2_PORT"] = searchObj.group(1) self.env["MOZHTTP2_PORT"] = searchObj.group(1)
self.env["MOZHTTP2_PROXY_PORT"] = searchObj.group(2)
except OSError as e: except OSError as e:
# This occurs if the subprocess couldn't be started # This occurs if the subprocess couldn't be started
self.log.error('Could not run %s server: %s' % (name, str(e))) self.log.error('Could not run %s server: %s' % (name, str(e)))

View File

@ -329,6 +329,12 @@ with modules["NETWORK"]:
errors["NS_ERROR_NET_INTERRUPT"] = FAILURE(71) errors["NS_ERROR_NET_INTERRUPT"] = FAILURE(71)
# The connection attempt to a proxy failed. # The connection attempt to a proxy failed.
errors["NS_ERROR_PROXY_CONNECTION_REFUSED"] = FAILURE(72) errors["NS_ERROR_PROXY_CONNECTION_REFUSED"] = FAILURE(72)
# The proxy requires authentication; used when we can't easily propagate 407s.
errors["NS_ERROR_PROXY_AUTHENTICATION_FAILED"] = FAILURE(407)
# The proxy failed to connect the remote server.
errors["NS_ERROR_PROXY_BAD_GATEWAY"] = FAILURE(502)
# The proxy did get any response from the remote server in time.
errors["NS_ERROR_PROXY_GATEWAY_TIMEOUT"] = FAILURE(504)
# A transfer was only partially done when it completed. # A transfer was only partially done when it completed.
errors["NS_ERROR_NET_PARTIAL_TRANSFER"] = FAILURE(76) errors["NS_ERROR_NET_PARTIAL_TRANSFER"] = FAILURE(76)
# HTTP/2 detected invalid TLS configuration # HTTP/2 detected invalid TLS configuration