/* 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/. */
// Verifies that a TRRServiceChannel preemptively sends Proxy-Authorization
// when the nsIProxyInfo returned by the registered proxy filter carries a
// bearer token (as IPPChannelFilter constructs via newProxyInfo's
// aProxyAuthorizationHeader argument).
//
// The HTTP/2 proxy is started in auth-required mode: a CONNECT without a
// Proxy-Authorization header is answered with 407 and the stream is closed.
// TRRServiceChannel has no nsHttpChannelAuthProvider, so it cannot retry
// after a 407. Therefore a successful DNS resolution through the proxy is
// only possible if the channel includes Proxy-Authorization on the first
// CONNECT.
"use strict";
/* import-globals-from head_trr.js */
/* import-globals-from head_channels.js */
const { NodeHTTP2ProxyServer } = ChromeUtils.importESModule(
"resource://testing-common/NodeServer.sys.mjs"
);
const BEARER_TOKEN =
"Bearer trr-preemptive-test-token";
// Mirrors IPPChannelFilter.applyFilter: returns an nsIProxyInfo whose
// proxyAuthorizationHeader is the bearer token. The TRRServiceChannel under
// test should then copy that onto its request head before the CONNECT goes
// out (see TRRServiceChannel::OnProxyAvailable).
class BearerProxyChannelFilter {
QueryInterface = ChromeUtils.generateQI([
"nsIProtocolProxyChannelFilter"]);
constructor(host, port, trrUriPrefix) {
this._host = host;
this._port = port;
this._trrUriPrefix = trrUriPrefix;
}
applyFilter(channel, defaultPI, cb) {
// Only proxy the TRR DoH endpoint. Anything else (e.g. cert revocation
// lookups for the proxy itself) stays direct.
if (!channel.URI.spec.startsWith(
this._trrUriPrefix)) {
cb.onProxyFilterResult(defaultPI);
return;
}
const pps = Cc[
"@mozilla.org/network/protocol-proxy-service;1"].getService(
Ci.nsIProtocolProxyService
);
cb.onProxyFilterResult(
pps.newProxyInfo(
"https",
this._host,
this._port,
BEARER_TOKEN,
// aProxyAuthorizationHeader
"",
// aConnectionIsolationKey
Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST,
10,
// failover timeout
null
)
);
}
}
let pps;
let proxy;
let trrServer;
let filter;
add_setup(async
function setup() {
trr_test_setup();
const certdb = Cc[
"@mozilla.org/security/x509certdb;1"].getService(
Ci.nsIX509CertDB
);
addCertFromFile(certdb,
"http2-ca.pem",
"CTu,u,u");
addCertFromFile(certdb,
"proxy-ca.pem",
"CTu,u,u");
trrServer =
new TRRServer();
await trrServer.start();
await trrServer.registerDoHAnswers(
"test.proxy.com",
"A", {
answers: [
{
name:
"test.proxy.com",
ttl:
55,
type:
"A",
flush:
false,
data:
"3.3.3.3",
},
],
});
// Wrap the DoH handler so we can assert that the inner DoH request the
// server sees does NOT carry Proxy-Authorization. That header is for the
// proxy hop only and must never reach the origin.
await trrServer.execute(`
global.dnsQuerySawProxyAuth =
false;
global.originalDnsQueryHandler = global.path_handlers[
"/dns-query"];
`);
await trrServer.registerPathHandler(
"/dns-query",
function (req, resp) {
if (req.headers[
"proxy-authorization"]) {
global.dnsQuerySawProxyAuth =
true;
}
return global.originalDnsQueryHandler(req, resp);
});
proxy =
new NodeHTTP2ProxyServer();
// auth=true: reject CONNECT requests without Proxy-Authorization with 407.
// Skip the proxy's built-in proxy filter; we install our own that carries
// the bearer token.
await proxy.startWithoutProxyFilter(
0,
true);
pps = Cc[
"@mozilla.org/network/protocol-proxy-service;1"].getService(
Ci.nsIProtocolProxyService
);
filter =
new BearerProxyChannelFilter(
"localhost",
proxy.port(),
`https:
//foo.example.com:${trrServer.port()}/dns-query`
);
pps.registerChannelFilter(filter,
0);
registerCleanupFunction(async () => {
if (filter) {
pps.unregisterChannelFilter(filter);
filter =
null;
}
if (proxy) {
await proxy.stop();
proxy =
null;
}
if (trrServer) {
await trrServer.stop();
trrServer =
null;
}
trr_clear_prefs();
});
});
add_task(async
function test_trr_preemptive_proxy_auth() {
Services.prefs.setIntPref(
"network.trr.mode",
3);
Services.prefs.setIntPref(
"network.trr.request_timeout_mode_trronly_ms",
5000
);
Services.prefs.setCharPref(
"network.trr.uri",
`https:
//foo.example.com:${trrServer.port()}/dns-query`
);
Services.dns.clearCache(
true);
// If the bearer is not attached to the CONNECT, the proxy answers 407 and
// TRR cannot retry — the listener would observe a failure here.
await
new TRRDNSListener(
"test.proxy.com",
"3.3.3.3");
Assert.ok(
!(await trrServer.execute(
"global.dnsQuerySawProxyAuth")),
"Proxy-Authorization must not leak through to the DoH server"
);
});