548 lines
15 KiB
JavaScript
548 lines
15 KiB
JavaScript
"use strict";
|
|
|
|
const { HttpServer } = ChromeUtils.importESModule(
|
|
"resource://testing-common/httpd.sys.mjs"
|
|
);
|
|
|
|
var h2Port;
|
|
var h3Port;
|
|
|
|
// https://foo.example.com:(h3Port)
|
|
// https://bar.example.com:(h3Port) <- invalid for bar, but ok for foo
|
|
var h1Foo; // server http://foo.example.com:(h1Foo.identity.primaryPort)
|
|
var h1Bar; // server http://bar.example.com:(h1bar.identity.primaryPort)
|
|
|
|
var otherServer; // server socket listening for other connection.
|
|
|
|
var h2FooRoute; // foo.example.com:H2PORT
|
|
|
|
var h3FooRoute; // foo.example.com:H3PORT
|
|
var h3BarRoute; // bar.example.com:H3PORT
|
|
var h3Route; // :H3PORT
|
|
var httpFooOrigin; // http://foo.exmaple.com:PORT/
|
|
var httpsFooOrigin; // https://foo.exmaple.com:PORT/
|
|
var httpBarOrigin; // http://bar.example.com:PORT/
|
|
var httpsBarOrigin; // https://bar.example.com:PORT/
|
|
|
|
function run_test() {
|
|
h2Port = Services.env.get("MOZHTTP2_PORT");
|
|
Assert.notEqual(h2Port, null);
|
|
Assert.notEqual(h2Port, "");
|
|
|
|
h3Port = Services.env.get("MOZHTTP3_PORT");
|
|
Assert.notEqual(h3Port, null);
|
|
Assert.notEqual(h3Port, "");
|
|
|
|
// Set to allow the cert presented by our H3 server
|
|
do_get_profile();
|
|
|
|
Services.prefs.setBoolPref("network.http.http2.enabled", true);
|
|
Services.prefs.setBoolPref("network.http.http3.enable", true);
|
|
Services.prefs.setBoolPref("network.http.altsvc.enabled", true);
|
|
Services.prefs.setBoolPref("network.http.altsvc.oe", true);
|
|
Services.prefs.setCharPref(
|
|
"network.dns.localDomains",
|
|
"foo.example.com, bar.example.com"
|
|
);
|
|
|
|
// 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. The same cert is used
|
|
// for both h3FooRoute and h3BarRoute though it is only valid for
|
|
// the foo.example.com domain name.
|
|
let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
|
|
Ci.nsIX509CertDB
|
|
);
|
|
addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
|
|
|
|
h1Foo = new HttpServer();
|
|
h1Foo.registerPathHandler("/altsvc-test", h1Server);
|
|
h1Foo.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK);
|
|
h1Foo.start(-1);
|
|
h1Foo.identity.setPrimary(
|
|
"http",
|
|
"foo.example.com",
|
|
h1Foo.identity.primaryPort
|
|
);
|
|
|
|
h1Bar = new HttpServer();
|
|
h1Bar.registerPathHandler("/altsvc-test", h1Server);
|
|
h1Bar.start(-1);
|
|
h1Bar.identity.setPrimary(
|
|
"http",
|
|
"bar.example.com",
|
|
h1Bar.identity.primaryPort
|
|
);
|
|
|
|
h2FooRoute = "foo.example.com:" + h2Port;
|
|
|
|
h3FooRoute = "foo.example.com:" + h3Port;
|
|
h3BarRoute = "bar.example.com:" + h3Port;
|
|
h3Route = ":" + h3Port;
|
|
|
|
httpFooOrigin = "http://foo.example.com:" + h1Foo.identity.primaryPort + "/";
|
|
httpsFooOrigin = "https://" + h3FooRoute + "/";
|
|
httpBarOrigin = "http://bar.example.com:" + h1Bar.identity.primaryPort + "/";
|
|
httpsBarOrigin = "https://" + h3BarRoute + "/";
|
|
dump(
|
|
"http foo - " +
|
|
httpFooOrigin +
|
|
"\n" +
|
|
"https foo - " +
|
|
httpsFooOrigin +
|
|
"\n" +
|
|
"http bar - " +
|
|
httpBarOrigin +
|
|
"\n" +
|
|
"https bar - " +
|
|
httpsBarOrigin +
|
|
"\n"
|
|
);
|
|
|
|
doTest1();
|
|
}
|
|
|
|
function h1Server(metadata, response) {
|
|
response.setStatusLine(metadata.httpVersion, 200, "OK");
|
|
response.setHeader("Content-Type", "text/plain", false);
|
|
response.setHeader("Connection", "close", false);
|
|
response.setHeader("Cache-Control", "no-cache", false);
|
|
response.setHeader("Access-Control-Allow-Origin", "*", false);
|
|
response.setHeader("Access-Control-Allow-Method", "GET", false);
|
|
response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false);
|
|
|
|
try {
|
|
// If needed, prefix Alt-Svc with "h3=".
|
|
if (metadata.getHeader("x-altsvc").includes("=")) {
|
|
response.setHeader("Alt-Svc", metadata.getHeader("x-altsvc"), false);
|
|
} else {
|
|
response.setHeader(
|
|
"Alt-Svc",
|
|
"h3=" + metadata.getHeader("x-altsvc"),
|
|
false
|
|
);
|
|
}
|
|
} catch (e) {}
|
|
|
|
var body = "Q: What did 0 say to 8? A: Nice Belt!\n";
|
|
response.bodyOutputStream.write(body, body.length);
|
|
}
|
|
|
|
function h1ServerWK(metadata, response) {
|
|
response.setStatusLine(metadata.httpVersion, 200, "OK");
|
|
response.setHeader("Content-Type", "application/json", false);
|
|
response.setHeader("Connection", "close", false);
|
|
response.setHeader("Cache-Control", "no-cache", false);
|
|
response.setHeader("Access-Control-Allow-Origin", "*", false);
|
|
response.setHeader("Access-Control-Allow-Method", "GET", false);
|
|
response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false);
|
|
|
|
var body = '["http://foo.example.com:' + h1Foo.identity.primaryPort + '"]';
|
|
response.bodyOutputStream.write(body, body.length);
|
|
}
|
|
|
|
function resetPrefs() {
|
|
Services.prefs.clearUserPref("network.http.http2.enabled");
|
|
Services.prefs.clearUserPref("network.http.http3.enable");
|
|
Services.prefs.clearUserPref("network.dns.localDomains");
|
|
Services.prefs.clearUserPref("network.http.altsvc.enabled");
|
|
Services.prefs.clearUserPref("network.http.altsvc.oe");
|
|
Services.prefs.clearUserPref("network.dns.localDomains");
|
|
Services.prefs.clearUserPref("network.security.ports.banned");
|
|
}
|
|
|
|
function makeChan(origin) {
|
|
return NetUtil.newChannel({
|
|
uri: origin + "altsvc-test",
|
|
loadUsingSystemPrincipal: true,
|
|
}).QueryInterface(Ci.nsIHttpChannel);
|
|
}
|
|
|
|
var origin;
|
|
var xaltsvc;
|
|
var loadWithoutClearingMappings = false;
|
|
var disallowH3 = false;
|
|
var disallowH2 = false;
|
|
var testKeepAliveNotSet = false;
|
|
var nextTest;
|
|
var expectPass = true;
|
|
var waitFor = 0;
|
|
var originAttributes = {};
|
|
|
|
var Listener = function (expectedHttpVersion, expectedRoute) {
|
|
this._expectedRoute = expectedRoute;
|
|
this._expectedHttpVersion = expectedHttpVersion;
|
|
};
|
|
|
|
Listener.prototype = {
|
|
onStartRequest: function testOnStartRequest(request) {
|
|
Assert.ok(request instanceof Ci.nsIHttpChannel);
|
|
|
|
if (expectPass) {
|
|
if (!Components.isSuccessCode(request.status)) {
|
|
do_throw(
|
|
"Channel should have a success code! (" + request.status + ")"
|
|
);
|
|
}
|
|
Assert.equal(request.responseStatus, 200);
|
|
} else {
|
|
Assert.equal(Components.isSuccessCode(request.status), false);
|
|
}
|
|
},
|
|
|
|
onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
|
|
read_stream(stream, cnt);
|
|
},
|
|
|
|
onStopRequest: function testOnStopRequest(request, status) {
|
|
var routed = "";
|
|
try {
|
|
routed = request.getRequestHeader("Alt-Used");
|
|
} catch (e) {}
|
|
dump("routed is " + routed + "\n");
|
|
Assert.equal(Components.isSuccessCode(status), expectPass);
|
|
|
|
function assertHttpVersion(request, expectedHttpVersion) {
|
|
if (expectedHttpVersion) {
|
|
const httpVersion = request?.protocolVersion || "";
|
|
Assert.equal(httpVersion, httpVersion);
|
|
}
|
|
}
|
|
|
|
if (waitFor != 0) {
|
|
Assert.equal(routed, "");
|
|
do_test_pending();
|
|
loadWithoutClearingMappings = true;
|
|
do_timeout(waitFor, () => {
|
|
doTest(this._expectedHttpVersion, this._expectedRoute);
|
|
});
|
|
waitFor = 0;
|
|
xaltsvc = "NA";
|
|
} else if (xaltsvc == "NA") {
|
|
Assert.equal(routed, "");
|
|
nextTest();
|
|
} else if (this._expectedRoute && this._expectedRoute == routed) {
|
|
assertHttpVersion(request, this._expectedHttpVersion);
|
|
nextTest();
|
|
} else if (routed == xaltsvc) {
|
|
Assert.equal(routed, xaltsvc); // always true, but a useful log
|
|
assertHttpVersion(request, this._expectedHttpVersion);
|
|
nextTest();
|
|
} else {
|
|
dump("poll later for alt svc mapping\n");
|
|
do_test_pending();
|
|
loadWithoutClearingMappings = true;
|
|
do_timeout(500, () => {
|
|
doTest(this._expectedHttpVersion, this._expectedRoute);
|
|
});
|
|
}
|
|
|
|
do_test_finished();
|
|
},
|
|
};
|
|
|
|
function testsDone() {
|
|
dump("testDone\n");
|
|
resetPrefs();
|
|
do_test_pending();
|
|
otherServer.close();
|
|
do_test_pending();
|
|
h1Foo.stop(do_test_finished);
|
|
do_test_pending();
|
|
h1Bar.stop(do_test_finished);
|
|
}
|
|
|
|
function doTest(expectedHttpVersion, expectedRoute) {
|
|
dump("execute doTest " + origin + "\n");
|
|
var chan = makeChan(origin);
|
|
var listener = new Listener(expectedHttpVersion, expectedRoute);
|
|
if (xaltsvc != "NA") {
|
|
chan.setRequestHeader("x-altsvc", xaltsvc, false);
|
|
}
|
|
if (testKeepAliveNotSet) {
|
|
chan.setRequestHeader("Connection", "close", false);
|
|
testKeepAliveNotSet = false;
|
|
}
|
|
if (loadWithoutClearingMappings) {
|
|
chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
|
|
} else {
|
|
chan.loadFlags =
|
|
Ci.nsIRequest.LOAD_FRESH_CONNECTION |
|
|
Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
|
|
}
|
|
if (disallowH3) {
|
|
let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal);
|
|
internalChannel.allowHttp3 = false;
|
|
disallowH3 = false;
|
|
}
|
|
if (disallowH2) {
|
|
let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal);
|
|
internalChannel.allowSpdy = false;
|
|
disallowH2 = false;
|
|
}
|
|
loadWithoutClearingMappings = false;
|
|
chan.loadInfo.originAttributes = originAttributes;
|
|
chan.asyncOpen(listener);
|
|
}
|
|
|
|
// xaltsvc is overloaded to do two things..
|
|
// 1] it is sent in the x-altsvc request header, and the response uses the value in the Alt-Svc response header
|
|
// 2] the test polls until necko sets Alt-Used to that value (i.e. it uses that route)
|
|
//
|
|
// When xaltsvc is set to h3Route (i.e. :port with the implied hostname) it doesn't match the alt-used,
|
|
// which is always explicit, so it needs to be changed after the channel is created but before the
|
|
// listener is invoked
|
|
|
|
// http://foo served from h3=:port
|
|
function doTest1() {
|
|
dump("doTest1()\n");
|
|
origin = httpFooOrigin;
|
|
xaltsvc = h3Route;
|
|
nextTest = doTest2;
|
|
do_test_pending();
|
|
doTest("h3");
|
|
xaltsvc = h3FooRoute;
|
|
}
|
|
|
|
// http://foo served from h3=foo:port
|
|
function doTest2() {
|
|
dump("doTest2()\n");
|
|
origin = httpFooOrigin;
|
|
xaltsvc = h3FooRoute;
|
|
nextTest = doTest3;
|
|
do_test_pending();
|
|
doTest("h3");
|
|
}
|
|
|
|
// http://foo served from h3=bar:port
|
|
// requires cert for foo
|
|
function doTest3() {
|
|
dump("doTest3()\n");
|
|
origin = httpFooOrigin;
|
|
xaltsvc = h3BarRoute;
|
|
nextTest = doTest4;
|
|
do_test_pending();
|
|
doTest("h3");
|
|
}
|
|
|
|
// https://bar should fail because host bar has cert for foo
|
|
function doTest4() {
|
|
dump("doTest4()\n");
|
|
origin = httpsBarOrigin;
|
|
xaltsvc = "";
|
|
expectPass = false;
|
|
nextTest = doTest5;
|
|
do_test_pending();
|
|
doTest();
|
|
}
|
|
|
|
// http://bar via h3 on bar
|
|
// should not use TLS/h3 because h3BarRoute is not auth'd for bar
|
|
// however the test ought to PASS (i.e. get a 200) because fallback
|
|
// to plaintext happens.. thus the timeout
|
|
function doTest5() {
|
|
dump("doTest5()\n");
|
|
origin = httpBarOrigin;
|
|
xaltsvc = h3BarRoute;
|
|
expectPass = true;
|
|
waitFor = 500;
|
|
nextTest = doTest6;
|
|
do_test_pending();
|
|
doTest("h3");
|
|
}
|
|
|
|
// http://bar served from h3=:port, which is like the bar route in 8
|
|
function doTest6() {
|
|
dump("doTest6()\n");
|
|
origin = httpBarOrigin;
|
|
xaltsvc = h3Route;
|
|
expectPass = true;
|
|
waitFor = 500;
|
|
nextTest = doTest7;
|
|
do_test_pending();
|
|
doTest();
|
|
xaltsvc = h3BarRoute;
|
|
}
|
|
|
|
// check again https://bar should fail because host bar has cert for foo
|
|
function doTest7() {
|
|
dump("doTest7()\n");
|
|
origin = httpsBarOrigin;
|
|
xaltsvc = "";
|
|
expectPass = false;
|
|
nextTest = doTest8;
|
|
do_test_pending();
|
|
doTest();
|
|
}
|
|
|
|
// http://bar served from h3=foo, should fail because host foo only has
|
|
// cert for foo. Fail in this case means alt-svc is not used, but content
|
|
// is served
|
|
function doTest8() {
|
|
dump("doTest8()\n");
|
|
origin = httpBarOrigin;
|
|
xaltsvc = h3FooRoute;
|
|
expectPass = true;
|
|
waitFor = 500;
|
|
nextTest = doTest9;
|
|
do_test_pending();
|
|
doTest("h3");
|
|
}
|
|
|
|
// Test 9-12:
|
|
// Insert a cache of http://foo served from h3=:port with origin attributes.
|
|
function doTest9() {
|
|
dump("doTest9()\n");
|
|
origin = httpFooOrigin;
|
|
xaltsvc = h3Route;
|
|
originAttributes = {
|
|
userContextId: 1,
|
|
firstPartyDomain: "a.com",
|
|
};
|
|
nextTest = doTest10;
|
|
do_test_pending();
|
|
doTest("h3");
|
|
xaltsvc = h3FooRoute;
|
|
}
|
|
|
|
// Make sure we get a cache miss with a different userContextId.
|
|
function doTest10() {
|
|
dump("doTest10()\n");
|
|
origin = httpFooOrigin;
|
|
xaltsvc = "NA";
|
|
originAttributes = {
|
|
userContextId: 2,
|
|
firstPartyDomain: "a.com",
|
|
};
|
|
loadWithoutClearingMappings = true;
|
|
nextTest = doTest11;
|
|
do_test_pending();
|
|
doTest("h3");
|
|
}
|
|
|
|
// Make sure we get a cache miss with a different firstPartyDomain.
|
|
function doTest11() {
|
|
dump("doTest11()\n");
|
|
origin = httpFooOrigin;
|
|
xaltsvc = "NA";
|
|
originAttributes = {
|
|
userContextId: 1,
|
|
firstPartyDomain: "b.com",
|
|
};
|
|
loadWithoutClearingMappings = true;
|
|
nextTest = doTest12;
|
|
do_test_pending();
|
|
doTest("h3");
|
|
}
|
|
//
|
|
// Make sure we get a cache hit with the same origin attributes.
|
|
function doTest12() {
|
|
dump("doTest12()\n");
|
|
origin = httpFooOrigin;
|
|
xaltsvc = "NA";
|
|
originAttributes = {
|
|
userContextId: 1,
|
|
firstPartyDomain: "a.com",
|
|
};
|
|
loadWithoutClearingMappings = true;
|
|
nextTest = doTest13;
|
|
do_test_pending();
|
|
doTest("h3");
|
|
// This ensures a cache hit.
|
|
xaltsvc = h3FooRoute;
|
|
}
|
|
|
|
// Make sure we do not use H3 if it is disabled on a channel.
|
|
function doTest13() {
|
|
dump("doTest13()\n");
|
|
origin = httpFooOrigin;
|
|
xaltsvc = "NA";
|
|
disallowH3 = true;
|
|
originAttributes = {
|
|
userContextId: 1,
|
|
firstPartyDomain: "a.com",
|
|
};
|
|
loadWithoutClearingMappings = true;
|
|
nextTest = doTest14;
|
|
do_test_pending();
|
|
doTest("h3");
|
|
}
|
|
|
|
// Make sure we use H3 if only Http2 is disabled on a channel.
|
|
function doTest14() {
|
|
dump("doTest14()\n");
|
|
origin = httpFooOrigin;
|
|
xaltsvc = "NA";
|
|
disallowH2 = true;
|
|
originAttributes = {
|
|
userContextId: 1,
|
|
firstPartyDomain: "a.com",
|
|
};
|
|
loadWithoutClearingMappings = true;
|
|
nextTest = doTest15;
|
|
do_test_pending();
|
|
doTest("h3");
|
|
// This should ensures a cache hit.
|
|
xaltsvc = h3FooRoute;
|
|
}
|
|
|
|
// Make sure we do not use H3 if NS_HTTP_ALLOW_KEEPALIVE is not set.
|
|
function doTest15() {
|
|
dump("doTest15()\n");
|
|
origin = httpFooOrigin;
|
|
xaltsvc = "NA";
|
|
testKeepAliveNotSet = true;
|
|
originAttributes = {
|
|
userContextId: 1,
|
|
firstPartyDomain: "a.com",
|
|
};
|
|
loadWithoutClearingMappings = true;
|
|
nextTest = doTest16;
|
|
do_test_pending();
|
|
doTest("h3");
|
|
}
|
|
|
|
// Check we don't connect to blocked ports
|
|
function doTest16() {
|
|
dump("doTest16()\n");
|
|
origin = httpFooOrigin;
|
|
otherServer = Cc["@mozilla.org/network/server-socket;1"].createInstance(
|
|
Ci.nsIServerSocket
|
|
);
|
|
otherServer.init(-1, true, -1);
|
|
xaltsvc = "localhost:" + otherServer.port;
|
|
Services.prefs.setCharPref(
|
|
"network.security.ports.banned",
|
|
"" + otherServer.port
|
|
);
|
|
dump("Blocked port: " + otherServer.port);
|
|
waitFor = 500;
|
|
otherServer.asyncListen({
|
|
onSocketAccepted() {
|
|
Assert.ok(false, "Got connection to socket when we didn't expect it!");
|
|
},
|
|
onStopListening() {
|
|
// We get closed when the entire file is done, which guarantees we get the socket accept
|
|
// if we do connect to the alt-svc header
|
|
do_test_finished();
|
|
},
|
|
});
|
|
nextTest = doTest17;
|
|
do_test_pending();
|
|
doTest("h3");
|
|
}
|
|
|
|
// Make sure we do not use a draft QUIC version.
|
|
function doTest17() {
|
|
dump("doTest17()\n");
|
|
origin = httpFooOrigin;
|
|
nextTest = testsDone;
|
|
xaltsvc = "h3-29=" + h3FooRoute + ", h2=" + h2FooRoute;
|
|
disallowH2 = false;
|
|
originAttributes = {
|
|
userContextId: 1,
|
|
firstPartyDomain: "a.com",
|
|
};
|
|
loadWithoutClearingMappings = true;
|
|
do_test_pending();
|
|
doTest("h2", h2FooRoute);
|
|
}
|