/* -*- 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 * - a stream reset for a connect stream to the proxy does not cause session to * be closed and the request through the proxy will failed. * - a "soft" stream error on a connection to the origin server will close the * stream, but it will not close niether the HTTP/2 session to the proxy nor * to the origin server. * - a "hard" stream error on a connection to the origin server will close the * HTTP/2 session to the origin server, but it will not close the HTTP/2 * session to the proxy.
*/
function createPrincipal(url) { var ssm = Services.scriptSecurityManager; try { return ssm.createContentPrincipal(Services.io.newURI(url), {});
} catch (e) { returnnull;
}
}
function make_channel(url) { return NetUtil.newChannel({
uri: url,
loadingPrincipal: createPrincipal(url),
securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT, // 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, delay = 0) { returnnew Promise(resolve => { var listener = new ChannelListener(
(request, data) => {
request.QueryInterface(Ci.nsIHttpChannel); const status = request.status; const http_code = status ? undefined : request.responseStatus;
request.QueryInterface(Ci.nsIProxiedChannel); const proxy_connect_response_code =
request.httpProxyConnectResponseCode;
resolve({ status, http_code, data, proxy_connect_response_code });
}, null,
flags
); if (delay > 0) {
do_timeout(delay, function () {
channel.asyncOpen(listener);
});
} else {
channel.asyncOpen(listener);
}
});
}
let initial_session_count = 0;
class http2ProxyCode { static listen(server, envport) { if (!server) { return Promise.resolve(0);
}
// We need to track active connections so we can forcefully close keep-alive // connections when shutting down the proxy.
proxy.socketIndex = 0;
proxy.socketMap = {};
proxy.on("connection", function (socket) {
let index = proxy.socketIndex++;
proxy.socketMap[index] = socket;
socket.on("close", function () { delete proxy.socketMap[index];
});
});
proxy.closeSockets = function () { for (let i in proxy.socketMap) {
proxy.socketMap[i].destroy();
}
};
const authorization_token = headers["proxy-authorization"]; if (target == "407.example.com:443") {
stream.respond({ ":status": 407 }); // Deliberately send no Proxy-Authenticate header
stream.end(); return;
} if (target == "407.basic.example.com:443") { // we want to return a different response than 407 to not re-request // credentials (and thus loop) but also not 200 to not let the channel // attempt to waste time connecting a non-existing https server - hence // 418 I'm a teapot :) if ("Basic dXNlcjpwYXNz" == authorization_token) {
stream.respond({ ":status": 418 });
stream.end(); return;
}
stream.respond({ ":status": 407, "proxy-authenticate": "Basic realm='foo'",
});
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 == "429.example.com:443") { // 429 Too Many Requests, a response code that a proxy should return when receiving too many requests
stream.respond({ ":status": 429 });
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;
} if (target == "reset.example.com:443") { // always reset the stream.
stream.close(0x0); return;
}
++proxy.sessionToOriginServersCount; const net = require("net"); const socket = net.connect(serverPort, "127.0.0.1", () => { try {
stream.respond({ ":status": 200 });
socket.pipe(stream);
stream.pipe(socket);
} catch (exception) {
console.log(exception);
stream.close();
}
});
socket.on("error", error => { thrownew Error(
`Unexpected error when conneting the HTTP/2 server from the HTTP/2 proxy during CONNECT handling: '${error}'`
);
});
});
}
}
async function proxy_session_counter() {
let data = await NodeServer.execute(
processId,
`http2ProxyCode.proxySessionCount()`
); return parseInt(data) - initial_session_count;
}
async function proxy_session_to_origin_server_counter() {
let data = await NodeServer.execute(
processId,
`http2ProxyCode.proxySessionToOriginServersCount()`
); return parseInt(data) - initial_session_count;
}
let processId;
add_task(async function setup() { // Set to allow the cert presented by our H2 server
do_get_profile();
// 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");
// Even with network state isolation active, we don't end up using the // partitioned principal.
Services.prefs.setBoolPref("privacy.partition.network_state", true);
// make all native resolve calls "secretly" resolve localhost instead
Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
filter = new ProxyFilter("https", "localhost", proxy_port, 0);
pps.registerFilter(filter, 10);
// 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";
Assert.equal(foo.status, Cr.NS_OK); Assert.equal(foo.proxy_connect_response_code, 200); 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.proxy_connect_response_code, 200); 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, proxy_connect_response_code } = await get_response(
chan,
CL_EXPECT_FAILURE
);
Assert.equal(status, Cr.NS_ERROR_PROXY_AUTHENTICATION_FAILED); Assert.equal(proxy_connect_response_code, 407); 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"
);
});
// The proxy responses with 407 with Proxy-Authenticate header presence. Make // sure that we prompt the auth prompt to ask for credentials.
add_task(async function proxy_auth_basic() { const chan = make_channel(`https://407.basic.example.com/`); const auth_prompt = { triggered: false };
chan.notificationCallbacks = new AuthRequestor(
() => new SimpleAuthPrompt2(auth_prompt)
); const { status, http_code, proxy_connect_response_code } = await get_response(
chan,
CL_EXPECT_FAILURE
);
// 418 indicates we pass the basic authentication. Assert.equal(status, Cr.NS_ERROR_PROXY_CONNECTION_REFUSED); Assert.equal(proxy_connect_response_code, 418); Assert.equal(http_code, undefined); Assert.equal(auth_prompt.triggered, true, "Auth prompt should 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, proxy_connect_response_code } = await get_response(
make_channel(`https://502.example.com/`),
CL_EXPECT_FAILURE
);
Assert.equal(status, Cr.NS_ERROR_PROXY_BAD_GATEWAY); Assert.equal(proxy_connect_response_code, 502); 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, proxy_connect_response_code } = await get_response(
make_channel(`https://502.example.com/`),
CL_EXPECT_FAILURE
);
Assert.equal(status, Cr.NS_ERROR_PROXY_BAD_GATEWAY); Assert.equal(proxy_connect_response_code, 502); 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, proxy_connect_response_code } = await get_response(
make_channel(`https://504.example.com/`),
CL_EXPECT_FAILURE
);
Assert.equal(status, Cr.NS_ERROR_PROXY_GATEWAY_TIMEOUT); Assert.equal(proxy_connect_response_code, 504); 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, proxy_connect_response_code } = await get_response(
make_channel(`https://404.example.com/`),
CL_EXPECT_FAILURE
);
Assert.equal(status, Cr.NS_ERROR_UNKNOWN_HOST); Assert.equal(proxy_connect_response_code, 404); Assert.equal(http_code, undefined); Assert.equal(
await proxy_session_counter(),
1, "No new session created by 404 after 504"
);
});
Assert.equal(status, Cr.NS_ERROR_PROXY_TOO_MANY_REQUESTS); Assert.equal(proxy_connect_response_code, 429); Assert.equal(http_code, undefined); Assert.equal(
await proxy_session_counter(),
1, "No new session created by 429 after 504"
);
});
Assert.equal(status, Cr.NS_ERROR_NET_INTERRUPT); Assert.equal(proxy_connect_response_code, 0); Assert.equal(http_code, undefined); Assert.equal(
await proxy_session_counter(),
1, "No new session created by 429 after 504"
);
});
// The soft errors are not closing the session.
add_task(async function origin_server_stream_soft_failure() { var current_num_sessions_to_origin_server =
await proxy_session_to_origin_server_counter();
Assert.equal(status, Cr.NS_ERROR_ILLEGAL_VALUE); Assert.equal(proxy_connect_response_code, 200); Assert.equal(http_code, undefined); Assert.equal(
await proxy_session_counter(),
1, "No session to the proxy closed by soft stream errors"
); Assert.equal(
await proxy_session_to_origin_server_counter(),
current_num_sessions_to_origin_server, "No session to the origin server closed by soft stream errors"
);
});
// The soft errors are not closing the session.
add_task(
async function origin_server_stream_soft_failure_multiple_streams_not_affected() { var current_num_sessions_to_origin_server =
await proxy_session_to_origin_server_counter();
let should_succeed = get_response(
make_channel(`https://foo.example.com/750ms`)
);
Assert.equal(failed.status, Cr.NS_ERROR_ILLEGAL_VALUE); Assert.equal(failed.proxy_connect_response_code, 200); Assert.equal(failed.http_code, undefined); Assert.equal(succeeded.status, Cr.NS_OK); Assert.equal(succeeded.proxy_connect_response_code, 200); Assert.equal(succeeded.http_code, 200); Assert.equal(
await proxy_session_counter(),
1, "No session to the proxy closed by soft stream errors"
); Assert.equal(
await proxy_session_to_origin_server_counter(),
current_num_sessions_to_origin_server, "No session to the origin server closed by soft stream errors"
);
}
);
// Make sure that the above error codes don't kill the session to the proxy.
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.equal(foo.proxy_connect_response_code, 200); Assert.ok(foo.data.match("random-request-1")); Assert.equal(alt1.status, Cr.NS_OK); Assert.equal(alt1.proxy_connect_response_code, 200); Assert.equal(alt1.http_code, 200); Assert.ok(alt1.data.match("random-request-2")); Assert.equal(
await proxy_session_counter(),
1, "No new session to the proxy created after stream 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";
Assert.equal(foo.status, Cr.NS_OK); Assert.equal(foo.proxy_connect_response_code, 200); 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.proxy_connect_response_code, 200); 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.proxy_connect_response_code, 200); 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.proxy_connect_response_code, 502); Assert.equal(failure1.http_code, undefined); Assert.equal(failure2.status, Cr.NS_ERROR_PROXY_BAD_GATEWAY); Assert.equal(failure2.proxy_connect_response_code, 502); Assert.equal(failure2.http_code, undefined); Assert.equal(
await proxy_session_counter(),
2, "No new session created by 502"
);
});
Assert.equal(foo.status, Cr.NS_OK); Assert.equal(foo.proxy_connect_response_code, 200); 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.proxy_connect_response_code, 200); 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.proxy_connect_response_code, 200); 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, "The number of sessions has not changed"
);
});
// The hard errors are closing the session.
add_task(async function origin_server_stream_hard_failure() { var current_num_sessions_to_origin_server =
await proxy_session_to_origin_server_counter(); const { status, http_code, proxy_connect_response_code } = await get_response(
make_channel(`https://foo.example.com/illegalhpackhard`),
CL_EXPECT_FAILURE
);
Assert.equal(status, 0x804b0053); Assert.equal(proxy_connect_response_code, 200); Assert.equal(http_code, undefined); Assert.equal(
await proxy_session_counter(),
2, "No new session to the proxy."
); Assert.equal(
await proxy_session_to_origin_server_counter(),
current_num_sessions_to_origin_server, "No new session to the origin server yet."
);
// Check the a new session ill be opened. const foo = await get_response(
make_channel(`https://foo.example.com/random-request-1`)
);
Assert.equal(
await proxy_session_counter(),
2, "No new session to the proxy is created after a hard stream failure on the session to the origin server."
); Assert.equal(
await proxy_session_to_origin_server_counter(),
current_num_sessions_to_origin_server + 1, "A new session to the origin server after a hard stream error"
);
});
// The hard errors are closing the session.
add_task(
async function origin_server_stream_hard_failure_multiple_streams_affected() { var current_num_sessions_to_origin_server =
await proxy_session_to_origin_server_counter();
let should_fail = get_response(
make_channel(`https://foo.example.com/750msNoData`),
CL_EXPECT_FAILURE
); const failed1 = await get_response(
make_channel(`https://foo.example.com/illegalhpackhard`),
CL_EXPECT_FAILURE,
10
);
const failed2 = await should_fail;
Assert.equal(failed1.status, 0x804b0053); Assert.equal(failed1.proxy_connect_response_code, 200); Assert.equal(failed1.http_code, undefined); Assert.equal(failed2.status, 0x804b0053); Assert.equal(failed2.proxy_connect_response_code, 200); Assert.equal(failed2.http_code, undefined); Assert.equal(
await proxy_session_counter(),
2, "No new session to the proxy"
); Assert.equal(
await proxy_session_to_origin_server_counter(),
current_num_sessions_to_origin_server, "No session to the origin server yet."
); // Check the a new session ill be opened. const foo = await get_response(
make_channel(`https://foo.example.com/random-request-1`)
);
Assert.equal(
await proxy_session_counter(),
2, "No new session to the proxy is created after a hard stream failure on the session to the origin server."
);
Assert.equal(
await proxy_session_to_origin_server_counter(),
current_num_sessions_to_origin_server + 1, "A new session to the origin server after a hard stream error"
);
}
);
Messung V0.5
¤ Dauer der Verarbeitung: 0.45 Sekunden
(vorverarbeitet)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.