test_httpssvc_https_upgrade.js (11890B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const certOverrideService = Cc[ 8 "@mozilla.org/security/certoverride;1" 9 ].getService(Ci.nsICertOverrideService); 10 const { HttpServer } = ChromeUtils.importESModule( 11 "resource://testing-common/httpd.sys.mjs" 12 ); 13 const { TestUtils } = ChromeUtils.importESModule( 14 "resource://testing-common/TestUtils.sys.mjs" 15 ); 16 17 const ReferrerInfo = Components.Constructor( 18 "@mozilla.org/referrer-info;1", 19 "nsIReferrerInfo", 20 "init" 21 ); 22 23 let h2Port; 24 let trrServer; 25 26 add_setup(async function setup() { 27 trr_test_setup(); 28 29 Services.prefs.setBoolPref( 30 "dom.security.https_first_for_custom_ports", 31 false 32 ); 33 34 trrServer = new TRRServer(); 35 await trrServer.start(); 36 h2Port = trrServer.port(); 37 38 Services.prefs.setCharPref( 39 "network.trr.uri", 40 "https://foo.example.com:" + h2Port + "/doh?httpssvc_as_altsvc=1" 41 ); 42 43 Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); 44 Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); 45 Services.prefs.setBoolPref( 46 "network.dns.https_rr.check_record_with_cname", 47 false 48 ); 49 50 Services.prefs.setBoolPref( 51 "network.dns.use_https_rr_for_speculative_connection", 52 true 53 ); 54 55 registerCleanupFunction(async () => { 56 trr_clear_prefs(); 57 Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr"); 58 Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc"); 59 Services.prefs.clearUserPref( 60 "network.dns.use_https_rr_for_speculative_connection" 61 ); 62 Services.prefs.clearUserPref("network.dns.notifyResolution"); 63 Services.prefs.clearUserPref("network.dns.disablePrefetch"); 64 Services.prefs.clearUserPref("dom.security.https_first_for_custom_ports"); 65 Services.prefs.clearUserPref( 66 "network.dns.https_rr.check_record_with_cname" 67 ); 68 }); 69 70 if (mozinfo.socketprocess_networking) { 71 Services.dns; // Needed to trigger socket process. 72 await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched); 73 } 74 75 Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY); 76 }); 77 78 function makeChan(url) { 79 let chan = NetUtil.newChannel({ 80 uri: url, 81 loadUsingSystemPrincipal: true, 82 contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, 83 }).QueryInterface(Ci.nsIHttpChannel); 84 return chan; 85 } 86 87 // When observer is specified, the channel will be suspended when receiving 88 // "http-on-modify-request". 89 function channelOpenPromise(chan, flags, observer) { 90 return new Promise(resolve => { 91 function finish(req, buffer) { 92 certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( 93 false 94 ); 95 resolve([req, buffer]); 96 } 97 certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( 98 true 99 ); 100 101 if (observer) { 102 let topic = "http-on-modify-request"; 103 Services.obs.addObserver(observer, topic); 104 } 105 chan.asyncOpen(new ChannelListener(finish, null, flags)); 106 }); 107 } 108 109 class EventSinkListener { 110 getInterface(iid) { 111 if (iid.equals(Ci.nsIChannelEventSink)) { 112 return this; 113 } 114 throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); 115 } 116 asyncOnChannelRedirect(oldChan, newChan, flags, callback) { 117 Assert.equal(oldChan.URI.hostPort, newChan.URI.hostPort); 118 Assert.equal(oldChan.URI.scheme, "http"); 119 Assert.equal(newChan.URI.scheme, "https"); 120 callback.onRedirectVerifyCallback(Cr.NS_OK); 121 } 122 } 123 124 EventSinkListener.prototype.QueryInterface = ChromeUtils.generateQI([ 125 "nsIInterfaceRequestor", 126 "nsIChannelEventSink", 127 ]); 128 129 // Test if the request is upgraded to https with a HTTPSSVC record. 130 add_task(async function testUseHTTPSSVCAsHSTS() { 131 Services.dns.clearCache(true); 132 // Do DNS resolution before creating the channel, so the HTTPSSVC record will 133 // be resolved from the cache. 134 await new TRRDNSListener("test.httpssvc.com", { 135 type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, 136 }); 137 138 // Since the HTTPS RR should be served from cache, the DNS record is available 139 // before nsHttpChannel::MaybeUseHTTPSRRForUpgrade() is called. 140 let chan = makeChan(`http://test.httpssvc.com:80/server-timing`); 141 let listener = new EventSinkListener(); 142 chan.notificationCallbacks = listener; 143 144 let [req] = await channelOpenPromise(chan); 145 146 req.QueryInterface(Ci.nsIHttpChannel); 147 Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); 148 149 chan = makeChan(`http://test.httpssvc.com:80/server-timing`); 150 listener = new EventSinkListener(); 151 chan.notificationCallbacks = listener; 152 153 [req] = await channelOpenPromise(chan); 154 155 req.QueryInterface(Ci.nsIHttpChannel); 156 Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); 157 }); 158 159 // Test the case that we got an invalid DNS response. In this case, 160 // nsHttpChannel::OnHTTPSRRAvailable is called after 161 // nsHttpChannel::MaybeUseHTTPSRRForUpgrade. 162 add_task(async function testInvalidDNSResult() { 163 Services.dns.clearCache(true); 164 165 let httpserv = new HttpServer(); 166 let content = "ok"; 167 httpserv.registerPathHandler("/", function handler(metadata, response) { 168 response.setHeader("Content-Length", `${content.length}`); 169 response.bodyOutputStream.write(content, content.length); 170 }); 171 httpserv.start(-1); 172 httpserv.identity.setPrimary( 173 "http", 174 "foo.notexisted.com", 175 httpserv.identity.primaryPort 176 ); 177 178 let chan = makeChan( 179 `http://foo.notexisted.com:${httpserv.identity.primaryPort}/` 180 ); 181 let [, response] = await channelOpenPromise(chan); 182 Assert.equal(response, content); 183 await new Promise(resolve => httpserv.stop(resolve)); 184 }); 185 186 // The same test as above, but nsHttpChannel::MaybeUseHTTPSRRForUpgrade is 187 // called after nsHttpChannel::OnHTTPSRRAvailable. 188 add_task(async function testInvalidDNSResult1() { 189 Services.dns.clearCache(true); 190 191 let httpserv = new HttpServer(); 192 let content = "ok"; 193 httpserv.registerPathHandler("/", function handler(metadata, response) { 194 response.setHeader("Content-Length", `${content.length}`); 195 response.bodyOutputStream.write(content, content.length); 196 }); 197 httpserv.start(-1); 198 httpserv.identity.setPrimary( 199 "http", 200 "foo.notexisted.com", 201 httpserv.identity.primaryPort 202 ); 203 204 let chan = makeChan( 205 `http://foo.notexisted.com:${httpserv.identity.primaryPort}/` 206 ); 207 208 let topic = "http-on-modify-request"; 209 let observer = { 210 QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), 211 observe(aSubject, aTopic) { 212 if (aTopic == topic) { 213 Services.obs.removeObserver(observer, topic); 214 let channel = aSubject.QueryInterface(Ci.nsIChannel); 215 channel.suspend(); 216 217 new TRRDNSListener("foo.notexisted.com", { 218 type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, 219 expectedSuccess: false, 220 }).then(() => channel.resume()); 221 } 222 }, 223 }; 224 225 let [, response] = await channelOpenPromise(chan, 0, observer); 226 Assert.equal(response, content); 227 await new Promise(resolve => httpserv.stop(resolve)); 228 }); 229 230 add_task(async function testLiteralIP() { 231 let httpserv = new HttpServer(); 232 let content = "ok"; 233 httpserv.registerPathHandler("/", function handler(metadata, response) { 234 response.setHeader("Content-Length", `${content.length}`); 235 response.bodyOutputStream.write(content, content.length); 236 }); 237 httpserv.start(-1); 238 239 let chan = makeChan(`http://127.0.0.1:${httpserv.identity.primaryPort}/`); 240 let [, response] = await channelOpenPromise(chan); 241 Assert.equal(response, content); 242 await new Promise(resolve => httpserv.stop(resolve)); 243 }); 244 245 // Test the case that an HTTPS RR is available and the server returns a 307 246 // for redirecting back to http. 247 add_task(async function testEndlessUpgradeDowngrade() { 248 Services.dns.clearCache(true); 249 250 let httpserv = new HttpServer(); 251 let content = "okok"; 252 httpserv.start(-1); 253 let port = httpserv.identity.primaryPort; 254 httpserv.registerPathHandler( 255 `/redirect_to_http`, 256 function handler(metadata, response) { 257 response.setHeader("Content-Length", `${content.length}`); 258 response.bodyOutputStream.write(content, content.length); 259 } 260 ); 261 httpserv.identity.setPrimary("http", "test.httpsrr.redirect.com", port); 262 263 let chan = makeChan( 264 `http://test.httpsrr.redirect.com:${port}/redirect_to_http?port=${port}` 265 ); 266 267 let [, response] = await channelOpenPromise(chan); 268 Assert.equal(response, content); 269 await new Promise(resolve => httpserv.stop(resolve)); 270 }); 271 272 add_task(async function testHttpRequestBlocked() { 273 Services.dns.clearCache(true); 274 275 let dnsRequestObserver = { 276 register() { 277 this.obs = Services.obs; 278 this.obs.addObserver(this, "dns-resolution-request"); 279 }, 280 unregister() { 281 if (this.obs) { 282 this.obs.removeObserver(this, "dns-resolution-request"); 283 } 284 }, 285 observe(subject, topic) { 286 if (topic == "dns-resolution-request") { 287 Assert.ok(false, "unreachable"); 288 } 289 }, 290 }; 291 292 dnsRequestObserver.register(); 293 Services.prefs.setBoolPref("network.dns.notifyResolution", true); 294 Services.prefs.setBoolPref("network.dns.disablePrefetch", true); 295 296 let httpserv = new HttpServer(); 297 httpserv.registerPathHandler("/", function handler() { 298 Assert.ok(false, "unreachable"); 299 }); 300 httpserv.start(-1); 301 httpserv.identity.setPrimary( 302 "http", 303 "foo.blocked.com", 304 httpserv.identity.primaryPort 305 ); 306 307 let chan = makeChan( 308 `http://foo.blocked.com:${httpserv.identity.primaryPort}/` 309 ); 310 311 let topic = "http-on-modify-request"; 312 let observer = { 313 QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), 314 observe(aSubject, aTopic) { 315 if (aTopic == topic) { 316 Services.obs.removeObserver(observer, topic); 317 let channel = aSubject.QueryInterface(Ci.nsIChannel); 318 channel.cancel(Cr.NS_BINDING_ABORTED); 319 } 320 }, 321 }; 322 323 let [request] = await channelOpenPromise(chan, CL_EXPECT_FAILURE, observer); 324 request.QueryInterface(Ci.nsIHttpChannel); 325 Assert.equal(request.status, Cr.NS_BINDING_ABORTED); 326 dnsRequestObserver.unregister(); 327 await new Promise(resolve => httpserv.stop(resolve)); 328 }); 329 330 function createPrincipal(url) { 331 return Services.scriptSecurityManager.createContentPrincipal( 332 Services.io.newURI(url), 333 {} 334 ); 335 } 336 337 // Test if the Origin header stays the same after an internal HTTPS upgrade 338 // caused by HTTPS RR. 339 add_task(async function testHTTPSRRUpgradeWithOriginHeader() { 340 Services.dns.clearCache(true); 341 342 const url = "http://test.httpssvc.com:80/origin_header"; 343 const originURL = "http://example.com"; 344 let chan = Services.io 345 .newChannelFromURIWithProxyFlags( 346 Services.io.newURI(url), 347 null, 348 Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL, 349 null, 350 createPrincipal(originURL), 351 createPrincipal(url), 352 Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, 353 Ci.nsIContentPolicy.TYPE_DOCUMENT 354 ) 355 .QueryInterface(Ci.nsIHttpChannel); 356 chan.referrerInfo = new ReferrerInfo( 357 Ci.nsIReferrerInfo.EMPTY, 358 true, 359 NetUtil.newURI(url) 360 ); 361 chan.setRequestHeader("Origin", originURL, false); 362 363 let [req, buf] = await channelOpenPromise(chan); 364 365 req.QueryInterface(Ci.nsIHttpChannel); 366 Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); 367 Assert.equal(buf, originURL); 368 }); 369 370 // See bug 1899841. Test the case when network.dns.use_https_rr_as_altsvc 371 // is disabled. 372 add_task(async function testPrefDisabled() { 373 Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", false); 374 375 let chan = makeChan(`https://test.httpssvc.com:${h2Port}/server-timing`); 376 let [req] = await channelOpenPromise(chan); 377 378 req.QueryInterface(Ci.nsIHttpChannel); 379 Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); 380 });