summaryrefslogtreecommitdiffstats
path: root/netwerk/test/unit/test_dooh.js
blob: bbcdc5a377a84e6fa7d1a5e25996fbd3552639f4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
/* Any copyright is dedicated to the Public Domain.
 * https://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

/* import-globals-from trr_common.js */

Cu.importGlobalProperties(["fetch"]);

const { setTimeout } = ChromeUtils.importESModule(
  "resource://gre/modules/Timer.sys.mjs"
);

let httpServer;
let ohttpServer;
let ohttpEncodedConfig = "not a valid config";

// Decapsulate the request, send it to the actual TRR, receive the response,
// encapsulate it, and send it back through `response`.
async function forwardToTRR(request, response) {
  let inputStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
    Ci.nsIScriptableInputStream
  );
  inputStream.init(request.bodyInputStream);
  let requestBody = inputStream.readBytes(inputStream.available());
  let ohttpResponse = ohttpServer.decapsulate(stringToBytes(requestBody));
  let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService(
    Ci.nsIBinaryHttp
  );
  let decodedRequest = bhttp.decodeRequest(ohttpResponse.request);
  let headers = {};
  for (
    let i = 0;
    i < decodedRequest.headerNames.length && decodedRequest.headerValues.length;
    i++
  ) {
    headers[decodedRequest.headerNames[i]] = decodedRequest.headerValues[i];
  }
  let uri = `${decodedRequest.scheme}://${decodedRequest.authority}${decodedRequest.path}`;
  let body = new Uint8Array(decodedRequest.content.length);
  for (let i = 0; i < decodedRequest.content.length; i++) {
    body[i] = decodedRequest.content[i];
  }
  try {
    // Timeout after 10 seconds.
    let fetchInProgress = true;
    let controller = new AbortController();
    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    setTimeout(() => {
      if (fetchInProgress) {
        controller.abort();
      }
    }, 10000);
    let trrResponse = await fetch(uri, {
      method: decodedRequest.method,
      headers,
      body: decodedRequest.method == "POST" ? body : undefined,
      credentials: "omit",
      signal: controller.signal,
    });
    fetchInProgress = false;
    let data = new Uint8Array(await trrResponse.arrayBuffer());
    let trrResponseContent = [];
    for (let i = 0; i < data.length; i++) {
      trrResponseContent.push(data[i]);
    }
    let trrResponseHeaderNames = [];
    let trrResponseHeaderValues = [];
    for (let header of trrResponse.headers) {
      trrResponseHeaderNames.push(header[0]);
      trrResponseHeaderValues.push(header[1]);
    }
    let binaryResponse = new BinaryHttpResponse(
      trrResponse.status,
      trrResponseHeaderNames,
      trrResponseHeaderValues,
      trrResponseContent
    );
    let responseBytes = bhttp.encodeResponse(binaryResponse);
    let encResponse = ohttpResponse.encapsulate(responseBytes);
    response.setStatusLine(request.httpVersion, 200, "OK");
    response.setHeader("Content-Type", "message/ohttp-res", false);
    response.write(bytesToString(encResponse));
  } catch (e) {
    // Some tests involve the responder either timing out or closing the
    // connection unexpectedly.
  }
}

add_setup(async function setup() {
  h2Port = trr_test_setup();
  runningOHTTPTests = true;

  if (mozinfo.socketprocess_networking) {
    Services.dns; // Needed to trigger socket process.
    await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched);
  }

  Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY);

  let ohttp = Cc["@mozilla.org/network/oblivious-http;1"].getService(
    Ci.nsIObliviousHttp
  );
  ohttpServer = ohttp.server();

  httpServer = new HttpServer();
  httpServer.registerPathHandler("/relay", function (request, response) {
    response.processAsync();
    forwardToTRR(request, response).then(() => {
      response.finish();
    });
  });
  httpServer.registerPathHandler("/config", function (request, response) {
    response.setStatusLine(request.httpVersion, 200, "OK");
    response.setHeader("Content-Type", "application/ohttp-keys", false);
    response.write(ohttpEncodedConfig);
  });
  httpServer.start(-1);

  Services.prefs.setBoolPref("network.trr.use_ohttp", true);
  // On windows the TTL fetch will race with clearing the cache
  // to refresh the cache entry.
  Services.prefs.setBoolPref("network.dns.get-ttl", false);

  registerCleanupFunction(async () => {
    trr_clear_prefs();
    Services.prefs.clearUserPref("network.trr.use_ohttp");
    Services.prefs.clearUserPref("network.trr.ohttp.config_uri");
    Services.prefs.clearUserPref("network.trr.ohttp.relay_uri");
    Services.prefs.clearUserPref("network.trr.ohttp.uri");
    Services.prefs.clearUserPref("network.dns.get-ttl");
    await new Promise((resolve, reject) => {
      httpServer.stop(resolve);
    });
  });
});

// Test that if DNS-over-OHTTP isn't configured, the implementation falls back
// to platform resolution.
add_task(async function test_ohttp_not_configured() {
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=2.2.2.2");
  await new TRRDNSListener("example.com", "127.0.0.1");
});

add_task(async function set_ohttp_invalid_prefs() {
  let configPromise = TestUtils.topicObserved("ohttp-service-config-loaded");
  Services.prefs.setCharPref(
    "network.trr.ohttp.relay_uri",
    "http://nonexistent.test"
  );
  Services.prefs.setCharPref(
    "network.trr.ohttp.config_uri",
    "http://nonexistent.test"
  );

  Cc["@mozilla.org/network/oblivious-http-service;1"].getService(
    Ci.nsIObliviousHttpService
  );
  await configPromise;
});

// Test that if DNS-over-OHTTP has an invalid configuration, the implementation
// falls back to platform resolution.
add_task(async function test_ohttp_invalid_prefs_fallback() {
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=2.2.2.2");
  await new TRRDNSListener("example.com", "127.0.0.1");
});

add_task(async function set_ohttp_prefs_500_error() {
  let configPromise = TestUtils.topicObserved("ohttp-service-config-loaded");
  Services.prefs.setCharPref(
    "network.trr.ohttp.relay_uri",
    `http://localhost:${httpServer.identity.primaryPort}/relay`
  );
  Services.prefs.setCharPref(
    "network.trr.ohttp.config_uri",
    `http://localhost:${httpServer.identity.primaryPort}/500error`
  );
  await configPromise;
});

// Test that if DNS-over-OHTTP has an invalid configuration, the implementation
// falls back to platform resolution.
add_task(async function test_ohttp_500_error_fallback() {
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=2.2.2.2");
  await new TRRDNSListener("example.com", "127.0.0.1");
});

add_task(async function retryConfigOnConnectivityChange() {
  Services.prefs.setCharPref("network.trr.confirmationNS", "skip");
  // First we make sure the config is properly loaded
  setModeAndURI(2, "doh?responseIP=2.2.2.2");
  let ohttpService = Cc[
    "@mozilla.org/network/oblivious-http-service;1"
  ].getService(Ci.nsIObliviousHttpService);
  ohttpService.clearTRRConfig();
  ohttpEncodedConfig = bytesToString(ohttpServer.encodedConfig);
  let configPromise = TestUtils.topicObserved("ohttp-service-config-loaded");
  Services.prefs.setCharPref(
    "network.trr.ohttp.relay_uri",
    `http://localhost:${httpServer.identity.primaryPort}/relay`
  );
  Services.prefs.setCharPref(
    "network.trr.ohttp.config_uri",
    `http://localhost:${httpServer.identity.primaryPort}/config`
  );
  let [, status] = await configPromise;
  equal(status, "success");
  info("retryConfigOnConnectivityChange setup complete");

  ohttpService.clearTRRConfig();

  let port = httpServer.identity.primaryPort;
  // Stop the server so getting the config fails.
  await httpServer.stop();

  configPromise = TestUtils.topicObserved("ohttp-service-config-loaded");
  Services.obs.notifyObservers(
    null,
    "network:captive-portal-connectivity-changed"
  );
  [, status] = await configPromise;
  equal(status, "failed");

  // Should fallback to native DNS since the config is empty
  Services.dns.clearCache(true);
  await new TRRDNSListener("example.com", "127.0.0.1");

  // Start the server back again.
  httpServer.start(port);
  Assert.equal(
    port,
    httpServer.identity.primaryPort,
    "server should get the same port"
  );

  // Still the config hasn't been reloaded.
  await new TRRDNSListener("example2.com", "127.0.0.1");

  // Signal a connectivity change so we reload the config
  configPromise = TestUtils.topicObserved("ohttp-service-config-loaded");
  Services.obs.notifyObservers(
    null,
    "network:captive-portal-connectivity-changed"
  );
  [, status] = await configPromise;
  equal(status, "success");

  await new TRRDNSListener("example3.com", "2.2.2.2");

  // Now check that we also reload a missing config if a TRR confirmation fails.
  ohttpService.clearTRRConfig();
  configPromise = TestUtils.topicObserved("ohttp-service-config-loaded");
  Services.obs.notifyObservers(
    null,
    "network:trr-confirmation",
    "CONFIRM_FAILED"
  );
  [, status] = await configPromise;
  equal(status, "success");
  await new TRRDNSListener("example4.com", "2.2.2.2");

  // set the config to an invalid value and check that as long as the URL
  // doesn't change, we dont reload it again on connectivity notifications.
  ohttpEncodedConfig = "not a valid config";
  configPromise = TestUtils.topicObserved("ohttp-service-config-loaded");
  Services.obs.notifyObservers(
    null,
    "network:captive-portal-connectivity-changed"
  );

  await new TRRDNSListener("example5.com", "2.2.2.2");

  // The change should not cause any config reload because we already have a config.
  [, status] = await configPromise;
  equal(status, "no-changes");

  await new TRRDNSListener("example6.com", "2.2.2.2");
  // Clear the config_uri pref so it gets set to the proper value in the next test.
  configPromise = TestUtils.topicObserved("ohttp-service-config-loaded");
  Services.prefs.setCharPref("network.trr.ohttp.config_uri", ``);
  await configPromise;
});

add_task(async function set_ohttp_prefs_valid() {
  let ohttpService = Cc[
    "@mozilla.org/network/oblivious-http-service;1"
  ].getService(Ci.nsIObliviousHttpService);
  ohttpService.clearTRRConfig();
  let configPromise = TestUtils.topicObserved("ohttp-service-config-loaded");
  ohttpEncodedConfig = bytesToString(ohttpServer.encodedConfig);
  Services.prefs.setCharPref(
    "network.trr.ohttp.config_uri",
    `http://localhost:${httpServer.identity.primaryPort}/config`
  );
  await configPromise;
});

add_task(test_A_record);

add_task(test_AAAA_records);

add_task(test_RFC1918);

add_task(test_GET_ECS);

add_task(test_timeout_mode3);

add_task(test_strict_native_fallback);

add_task(test_no_answers_fallback);

add_task(test_404_fallback);

add_task(test_mode_1_and_4);

add_task(test_CNAME);

add_task(test_name_mismatch);

add_task(test_mode_2);

add_task(test_excluded_domains);

add_task(test_captiveportal_canonicalURL);

add_task(test_parentalcontrols);

// TRR-first check that DNS result is used if domain is part of the builtin-excluded-domains pref
add_task(test_builtin_excluded_domains);

add_task(test_excluded_domains_mode3);

add_task(test25e);

add_task(test_parentalcontrols_mode3);

add_task(test_builtin_excluded_domains_mode3);

add_task(count_cookies);

// This test doesn't work with having a JS httpd server as a relay.
// add_task(test_connection_closed);

add_task(test_fetch_time);

add_task(test_fqdn);

add_task(test_ipv6_trr_fallback);

add_task(test_ipv4_trr_fallback);

add_task(test_no_retry_without_doh);