mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-26 14:22:01 +00:00
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:
parent
ce7abe5c5c
commit
c35df87597
@ -4104,7 +4104,8 @@ nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI,
|
||||
formatStrCount = 1;
|
||||
errorDescriptionID = "dnsNotFound2";
|
||||
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);
|
||||
addHostPort = true;
|
||||
error = "connectionFailure";
|
||||
@ -4112,7 +4113,8 @@ nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI,
|
||||
NS_ENSURE_ARG_POINTER(aURI);
|
||||
addHostPort = true;
|
||||
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);
|
||||
// Get the host
|
||||
nsAutoCString host;
|
||||
@ -4341,6 +4343,7 @@ nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI,
|
||||
error = "proxyResolveFailure";
|
||||
break;
|
||||
case NS_ERROR_PROXY_CONNECTION_REFUSED:
|
||||
case NS_ERROR_PROXY_AUTHENTICATION_FAILED:
|
||||
// Proxy connection was refused.
|
||||
error = "proxyConnectFailure";
|
||||
break;
|
||||
@ -6935,15 +6938,18 @@ nsresult nsDocShell::EndPageLoad(nsIWebProgress* aProgress,
|
||||
aStatus == NS_ERROR_CONNECTION_REFUSED ||
|
||||
aStatus == NS_ERROR_UNKNOWN_PROXY_HOST ||
|
||||
aStatus == NS_ERROR_PROXY_CONNECTION_REFUSED ||
|
||||
aStatus == NS_ERROR_PROXY_AUTHENTICATION_FAILED ||
|
||||
aStatus == NS_ERROR_BLOCKED_BY_POLICY) &&
|
||||
(isTopFrame || UseErrorPages())) {
|
||||
DisplayLoadError(aStatus, url, nullptr, aChannel);
|
||||
} else if (aStatus == NS_ERROR_NET_TIMEOUT ||
|
||||
aStatus == NS_ERROR_PROXY_GATEWAY_TIMEOUT ||
|
||||
aStatus == NS_ERROR_REDIRECT_LOOP ||
|
||||
aStatus == NS_ERROR_UNKNOWN_SOCKET_TYPE ||
|
||||
aStatus == NS_ERROR_NET_INTERRUPT ||
|
||||
aStatus == NS_ERROR_NET_RESET || aStatus == NS_ERROR_OFFLINE ||
|
||||
aStatus == NS_ERROR_MALWARE_URI ||
|
||||
aStatus == NS_ERROR_NET_RESET ||
|
||||
aStatus == NS_ERROR_PROXY_BAD_GATEWAY ||
|
||||
aStatus == NS_ERROR_OFFLINE || aStatus == NS_ERROR_MALWARE_URI ||
|
||||
aStatus == NS_ERROR_PHISHING_URI ||
|
||||
aStatus == NS_ERROR_UNWANTED_URI ||
|
||||
aStatus == NS_ERROR_HARMFUL_URI ||
|
||||
|
@ -12,6 +12,17 @@ function getTestServerPort() {
|
||||
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) {
|
||||
let fstream = Cc["@mozilla.org/network/file-input-stream;1"]
|
||||
.createInstance(Ci.nsIFileInputStream);
|
||||
|
@ -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_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_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_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")
|
||||
|
@ -1036,7 +1036,7 @@ nsresult Http2Stream::ConvertResponseHeaders(Http2Decompressor* decompressor,
|
||||
if ((httpResponseCode / 100) != 2) {
|
||||
MapStreamToPlainText();
|
||||
}
|
||||
MapStreamToHttpConnection();
|
||||
MapStreamToHttpConnection(httpResponseCode);
|
||||
ClearTransactionsBlockedOnTunnel();
|
||||
} else if (mIsWebsocket) {
|
||||
LOG3(("Http2Stream %p websocket response code %d", this, httpResponseCode));
|
||||
@ -1599,12 +1599,14 @@ void Http2Stream::MapStreamToPlainText() {
|
||||
qiTrans->ForcePlainText();
|
||||
}
|
||||
|
||||
void Http2Stream::MapStreamToHttpConnection() {
|
||||
void Http2Stream::MapStreamToHttpConnection(int32_t httpResponseCode) {
|
||||
RefPtr<SpdyConnectTransaction> qiTrans(
|
||||
mTransaction->QuerySpdyConnectTransaction());
|
||||
MOZ_ASSERT(qiTrans);
|
||||
|
||||
qiTrans->MapStreamToHttpConnection(mSocketTransport,
|
||||
mTransaction->ConnectionInfo());
|
||||
mTransaction->ConnectionInfo(),
|
||||
mIsTunnel ? httpResponseCode : -1);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
@ -369,7 +369,7 @@ class Http2Stream : public nsAHttpSegmentReader,
|
||||
private:
|
||||
void ClearTransactionsBlockedOnTunnel();
|
||||
void MapStreamToPlainText();
|
||||
void MapStreamToHttpConnection();
|
||||
void MapStreamToHttpConnection(int32_t httpResponseCode = -1);
|
||||
|
||||
bool mIsTunnel;
|
||||
bool mPlainTextTunnel;
|
||||
|
@ -1038,7 +1038,8 @@ void SpdyConnectTransaction::ForcePlainText() {
|
||||
}
|
||||
|
||||
void SpdyConnectTransaction::MapStreamToHttpConnection(
|
||||
nsISocketTransport* aTransport, nsHttpConnectionInfo* aConnInfo) {
|
||||
nsISocketTransport* aTransport, nsHttpConnectionInfo* aConnInfo,
|
||||
int32_t httpResponseCode) {
|
||||
mConnInfo = aConnInfo;
|
||||
|
||||
mTunnelTransport = new SocketTransportShim(aTransport, mIsWebsocket);
|
||||
@ -1046,10 +1047,27 @@ void SpdyConnectTransaction::MapStreamToHttpConnection(
|
||||
mTunnelStreamOut = new OutputStreamShim(this, mIsWebsocket);
|
||||
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
|
||||
// host via the tunnel) and is associated with the tunnel streams
|
||||
LOG(("SpdyConnectTransaction new httpconnection %p %s\n", mTunneledConn.get(),
|
||||
aConnInfo->HashKey().get()));
|
||||
LOG(("SpdyConnectTransaction %p new httpconnection %p %s\n", this,
|
||||
mTunneledConn.get(), aConnInfo->HashKey().get()));
|
||||
|
||||
nsCOMPtr<nsIInterfaceRequestor> callbacks;
|
||||
GetSecurityCallbacks(getter_AddRefs(callbacks));
|
||||
@ -1202,6 +1220,9 @@ nsresult SpdyConnectTransaction::ReadSegments(nsAHttpSegmentReader* reader,
|
||||
}
|
||||
|
||||
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(NS_FAILED(code));
|
||||
|
||||
|
@ -207,7 +207,8 @@ class SpdyConnectTransaction final : public NullHttpTransaction {
|
||||
// error.
|
||||
void ForcePlainText();
|
||||
void MapStreamToHttpConnection(nsISocketTransport* aTransport,
|
||||
nsHttpConnectionInfo* aConnInfo);
|
||||
nsHttpConnectionInfo* aConnInfo,
|
||||
int32_t httpResponseCode);
|
||||
|
||||
MOZ_MUST_USE nsresult ReadSegments(nsAHttpSegmentReader* reader,
|
||||
uint32_t count, uint32_t* countRead) final;
|
||||
|
@ -1954,16 +1954,18 @@ nsresult nsHttpChannel::ProcessFailedProxyConnect(uint32_t httpStatus) {
|
||||
rv = NS_ERROR_CONNECTION_REFUSED;
|
||||
break;
|
||||
case 403: // HTTP/1.1: "Forbidden"
|
||||
case 407: // ProcessAuthentication() failed
|
||||
case 501: // HTTP/1.1: "Not Implemented"
|
||||
// user sees boilerplate Mozilla "Proxy Refused Connection" page.
|
||||
rv = NS_ERROR_PROXY_CONNECTION_REFUSED;
|
||||
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"
|
||||
// RFC 2616: "some deployed proxies are known to return 400 or 500 when
|
||||
// DNS lookups time out." (Squid uses 500 if it runs out of sockets: so
|
||||
// we have a conflict here).
|
||||
// RFC 2616: "some deployed proxies are known to return 400 or
|
||||
// 500 when DNS lookups time out." (Squid uses 500 if it runs
|
||||
// out of sockets: so we have a conflict here).
|
||||
case 400: // HTTP/1.1 "Bad Request"
|
||||
case 500: // HTTP/1.1: "Internal Server Error"
|
||||
/* 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;
|
||||
break;
|
||||
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"
|
||||
// Squid returns 503 if target request fails for anything but DNS.
|
||||
/* User sees: "Failed to Connect:
|
||||
* Firefox can't establish a connection to the server at
|
||||
* 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"
|
||||
// user sees: "Network Timeout: The server at www.foo.com
|
||||
// is taking too long to respond."
|
||||
rv = NS_ERROR_NET_TIMEOUT;
|
||||
rv = NS_ERROR_PROXY_GATEWAY_TIMEOUT;
|
||||
break;
|
||||
// Confused proxy server or malicious response
|
||||
default:
|
||||
|
176
netwerk/test/unit/test_http1-proxy.js
Normal file
176
netwerk/test/unit/test_http1-proxy.js
Normal 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);
|
||||
});
|
||||
});
|
266
netwerk/test/unit/test_http2-proxy.js
Normal file
266
netwerk/test/unit/test_http2-proxy.js
Normal 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");
|
||||
});
|
@ -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();
|
||||
}
|
@ -450,7 +450,9 @@ skip-if = os == "android" # DNSv6 issues on android
|
||||
[test_stale-while-revalidate_negative.js]
|
||||
[test_stale-while-revalidate_positive.js]
|
||||
[test_stale-while-revalidate_max-age-0.js]
|
||||
[test_proxy-authorization-via-proxyinfo.js]
|
||||
# Bug 1549368
|
||||
[test_http1-proxy.js]
|
||||
[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
|
||||
[test_head_request_no_response_body.js]
|
||||
|
@ -11,11 +11,19 @@ if (process.env.NODE_HTTP2_ROOT) {
|
||||
}
|
||||
var http2 = require(node_http2_root);
|
||||
var fs = require('fs');
|
||||
var net = require('net');
|
||||
var url = require('url');
|
||||
var crypto = require('crypto');
|
||||
const dnsPacket = require(`${node_http2_root}/../dns-packet`);
|
||||
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
|
||||
var compression_module = node_http2_root + "/lib/protocol/compressor";
|
||||
var http2_compression = require(compression_module);
|
||||
@ -76,6 +84,7 @@ function getHttpContent(path) {
|
||||
var content = '<!doctype html>' +
|
||||
'<html>' +
|
||||
'<head><title>HOORAY!</title></head>' +
|
||||
// 'You Win!' used in tests to check we reached this server
|
||||
'<body>You Win! (by requesting' + path + ')</body>' +
|
||||
'</html>';
|
||||
return content;
|
||||
@ -1111,6 +1120,12 @@ function handleRequest(req, res) {
|
||||
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');
|
||||
if (req.httpVersionMajor != 2) {
|
||||
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;
|
||||
function listenok() {
|
||||
serverPort = server._server.address().port;
|
||||
console.log('HTTP2 server listening on port ' + serverPort);
|
||||
}
|
||||
var portSelection = 0;
|
||||
var envport = process.env.MOZHTTP2_PORT;
|
||||
if (envport !== undefined) {
|
||||
try {
|
||||
portSelection = parseInt(envport, 10);
|
||||
} catch (e) {
|
||||
portSelection = -1;
|
||||
|
||||
const listen = (server, envport) => {
|
||||
if (!server) {
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
|
||||
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}`);
|
||||
});
|
||||
|
@ -1131,9 +1131,11 @@ class XPCShellTests(object):
|
||||
# tell us it's started
|
||||
msg = process.stdout.readline()
|
||||
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:
|
||||
self.env["MOZHTTP2_PORT"] = searchObj.group(1)
|
||||
self.env["MOZHTTP2_PROXY_PORT"] = searchObj.group(2)
|
||||
except OSError as e:
|
||||
# This occurs if the subprocess couldn't be started
|
||||
self.log.error('Could not run %s server: %s' % (name, str(e)))
|
||||
|
@ -329,6 +329,12 @@ with modules["NETWORK"]:
|
||||
errors["NS_ERROR_NET_INTERRUPT"] = FAILURE(71)
|
||||
# The connection attempt to a proxy failed.
|
||||
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.
|
||||
errors["NS_ERROR_NET_PARTIAL_TRANSFER"] = FAILURE(76)
|
||||
# HTTP/2 detected invalid TLS configuration
|
||||
|
Loading…
Reference in New Issue
Block a user