Source code

Revision control

Copy as Markdown

Other Tools

/* 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 https://mozilla.org/MPL/2.0/. */
function policyExpired(policy) {
let currentDate = new Date();
return (currentDate - policy.creation) / 1_000 > policy.nel.max_age;
}
function errorType(aChannel) {
// TODO: we have to map a lot more error codes
switch (aChannel.status) {
case Cr.NS_ERROR_UNKNOWN_HOST:
// TODO: if there is no connectivity, return "dns.unreachable"
return "dns.name_not_resolved";
case Cr.NS_ERROR_REDIRECT_LOOP:
return "http.response.redirect_loop";
case Cr.NS_BINDING_REDIRECTED:
return "ok";
case Cr.NS_ERROR_NET_TIMEOUT:
return "tcp.timed_out";
case Cr.NS_ERROR_NET_RESET:
return "tcp.reset";
case Cr.NS_ERROR_CONNECTION_REFUSED:
return "tcp.refused";
default:
break;
}
if (
aChannel.status == Cr.NS_OK &&
(aChannel.responseStatus / 100 == 2 || aChannel.responseStatus == 304)
) {
return "ok";
}
if (
aChannel.status == Cr.NS_OK &&
aChannel.responseStatus >= 400 &&
aChannel.responseStatus <= 599
) {
return "http.error";
}
return "unknown" + aChannel.status;
}
function channelPhase(aChannel) {
const NS_NET_STATUS_RESOLVING_HOST = 0x4b0003;
const NS_NET_STATUS_RESOLVED_HOST = 0x4b000b;
const NS_NET_STATUS_CONNECTING_TO = 0x4b0007;
const NS_NET_STATUS_CONNECTED_TO = 0x4b0004;
const NS_NET_STATUS_TLS_HANDSHAKE_STARTING = 0x4b000c;
const NS_NET_STATUS_TLS_HANDSHAKE_ENDED = 0x4b000d;
const NS_NET_STATUS_SENDING_TO = 0x4b0005;
const NS_NET_STATUS_WAITING_FOR = 0x4b000a;
const NS_NET_STATUS_RECEIVING_FROM = 0x4b0006;
const NS_NET_STATUS_READING = 0x4b0008;
const NS_NET_STATUS_WRITING = 0x4b0009;
let lastStatus = aChannel.QueryInterface(
Ci.nsIHttpChannelInternal
).lastTransportStatus;
switch (lastStatus) {
case NS_NET_STATUS_RESOLVING_HOST:
case NS_NET_STATUS_RESOLVED_HOST:
return "dns";
case NS_NET_STATUS_CONNECTING_TO:
case NS_NET_STATUS_CONNECTED_TO: // TODO: is this right?
return "connection";
case NS_NET_STATUS_TLS_HANDSHAKE_STARTING:
case NS_NET_STATUS_TLS_HANDSHAKE_ENDED:
return "connection";
case NS_NET_STATUS_SENDING_TO:
case NS_NET_STATUS_WAITING_FOR:
case NS_NET_STATUS_RECEIVING_FROM:
case NS_NET_STATUS_READING:
case NS_NET_STATUS_WRITING:
return "application";
default:
// XXX(valentin): we default to DNS, but we should never get here.
return "dns";
}
}
export class NetworkErrorLogging {
constructor() {}
// Policy cache
policyCache = {};
// TODO: maybe persist policies to disk?
registerPolicy(aChannel) {
// 1. Abort these steps if any of the following conditions are true:
// 1.1 The result of executing the "Is origin potentially trustworthy?" algorithm on request's origin is not Potentially Trustworthy.
if (
!Services.scriptSecurityManager.getChannelResultPrincipal(aChannel)
.isOriginPotentiallyTrustworthy
) {
return;
}
// 4. Let header be the value of the response header whose name is NEL.
// 5. Let list be the result of executing the algorithm defined in Section 4 of [HTTP-JFV] on header. If that algorithm results in an error, or if list is empty, abort these steps.
let list = [];
aChannel.getOriginalResponseHeader("NEL", {
QueryInterface: ChromeUtils.generateQI(["nsIHttpHeaderVisitor"]),
visitHeader: (aHeader, aValue) => {
list.push(aValue);
// We only care about the first one so we could exit early
// We could throw early, but that makes the errors show up in stderr.
// The performance impact of not throwing is minimal.
// throw new Error(Cr.NS_ERROR_ABORT);
},
});
// 1.2 response does not contain a response header whose name is NEL.
if (!list.length) {
return;
}
// 2. Let origin be request's origin.
let origin =
Services.scriptSecurityManager.getChannelResultPrincipal(aChannel).origin;
// 3. Let key be the result of calling determine the network partition key, given request.
let key = Services.io.originAttributesForNetworkState(aChannel);
// 6. Let item be the first element of list.
let item = JSON.parse(list[0]);
// 7. If item has no member named max_age, or that member's value is not a number, abort these steps.
if (!item.max_age || !Number.isInteger(item.max_age)) {
return;
}
// 8. If the value of item's max_age member is 0, then remove any NEL policy from the policy cache whose origin is origin, and skip the remaining steps.
if (!item.max_age) {
delete this.policyCache[String([key, origin])];
return;
}
// 9. If item has no member named report_to, or that member's value is not a string, abort these steps.
if (!item.report_to || typeof item.report_to != "string") {
return;
}
// 10. If item has a member named success_fraction, whose value is not a number in the range 0.0 to 1.0, inclusive, abort these steps.
if (
item.success_fraction &&
(typeof item.success_fraction != "number" ||
item.success_fraction < 0 ||
item.success_fraction > 1)
) {
return;
}
// 11. If item has a member named failure_fraction, whose value is not a number in the range 0.0 to 1.0, inclusive, abort these steps.
if (
item.failure_fraction &&
(typeof item.failure_fraction != "number" ||
item.failure_fraction < 0 ||
item.success_fraction > 1)
) {
return;
}
// 12. If item has a member named request_headers, whose value is not a list, or if any element of that list is not a string, abort these steps.
if (
item.request_headers &&
!Array.isArray(
item.request_headers ||
!item.request_headers.every(e => typeof e == "string")
)
) {
return;
}
// 13. If item has a member named response_headers, whose value is not a list, or if any element of that list is not a string, abort these steps.
if (
item.response_headers &&
!Array.isArray(
item.response_headers ||
!item.response_headers.every(e => typeof e == "string")
)
) {
return;
}
// 14. Let policy be a new NEL policy whose properties are set as follows:
let policy = {};
// received IP address
// XXX: What should we do when using a proxy?
try {
policy.ip_address = aChannel.QueryInterface(
Ci.nsIHttpChannelInternal
).remoteAddress;
} catch (e) {
return;
}
// origin
policy.origin = origin;
if (item.include_subdomains) {
policy.subdomains = true;
}
policy.request_headers = item.request_headers;
policy.response_headers = item.response_headers;
policy.ttl = item.max_age;
policy.creation = new Date();
policy.successful_sampling_rate = item.success_fraction || 0.0;
policy.failure_sampling_rate = item.failure_fraction || 1.0;
// TODO: Remove these when no longer needed
policy.nel = item;
let reportTo = JSON.parse(
aChannel.QueryInterface(Ci.nsIHttpChannel).getResponseHeader("Report-To")
);
policy.reportTo = reportTo;
// 15. If there is already an entry in the policy cache for (key, origin), replace it with policy; otherwise, insert policy into the policy cache for (key, origin).
this.policyCache[String([key, origin])] = policy;
}
choosePolicyForRequest(aChannel) {
// 1. Let origin be request's origin.
let principal =
Services.scriptSecurityManager.getChannelResultPrincipal(aChannel);
let origin = principal.origin;
// 2. Let key be the result of calling determine the network partition key, given request.
let key = Services.io.originAttributesForNetworkState(aChannel);
// 3. If there is an entry in the policy cache for (key, origin):
let policy = this.policyCache[String([key, origin])];
// 3.1. Let policy be that entry.
if (policy) {
// 3.2. If policy is not expired, return it.
if (!policyExpired(policy)) {
return { policy, key, origin };
}
}
// 4. For each parent origin that is a superdomain match of origin:
// 4.1. If there is an entry in the policy cache for (key, parent origin):
// 4.1.1. Let policy be that entry.
// 4.1.2. If policy is not expired, and its subdomains flag is include, return it.
while (principal.nextSubDomainPrincipal) {
principal = principal.nextSubDomainPrincipal;
origin = principal.origin;
policy = this.policyCache[String([key, origin])];
if (policy && !policyExpired(policy)) {
return { policy, key, origin };
}
}
// 5. Return no policy.
return {};
}
generateNELReport(aChannel) {
// 1. If the result of executing the "Is origin potentially trustworthy?" algorithm on request's origin is not Potentially Trustworthy, return null.
if (
!Services.scriptSecurityManager.getChannelResultPrincipal(aChannel)
.isOriginPotentiallyTrustworthy
) {
return;
}
// 2. Let origin be request's origin.
let origin =
Services.scriptSecurityManager.getChannelResultPrincipal(aChannel).origin;
// 3. Let policy be the result of executing 5.1 Choose a policy for a request on request. If policy is no policy, return null.
let {
policy,
key,
origin: policyOrigin,
} = this.choosePolicyForRequest(aChannel);
if (!policy) {
return;
}
// 4. Determine the active sampling rate for this request:
let samplingRate = 0.0;
if (
aChannel.status == Cr.NS_OK &&
aChannel.responseStatus >= 200 &&
aChannel.responseStatus <= 299
) {
// If request succeeded, let sampling rate be policy's successful sampling rate.
samplingRate = policy.successful_sampling_rate || 0.0;
} else {
// If request failed, let sampling rate be policy's failure sampling rate.
samplingRate = policy.successful_sampling_rate || 1.0;
}
// 5. Decide whether or not to report on this request. Let roll be a random number between 0.0 and 1.0, inclusive. If roll ≥ sampling rate, return null.
if (Math.random() >= samplingRate) {
return;
}
// 6. Let report body be a new ECMAScript object with the following properties:
let phase = channelPhase(aChannel);
let report_body = {
sampling_fraction: samplingRate,
elapsed_time: 1, // TODO
phase,
type: errorType(aChannel), // TODO
};
// 7. If report body's phase property is not dns, append the following properties to report body:
if (phase != "dns") {
// XXX: should we actually report server_ip?
// It could be used to detect the presence of a PiHole.
report_body.server_ip = aChannel.QueryInterface(
Ci.nsIHttpChannelInternal
).remoteAddress;
report_body.protocol = aChannel.protocolVersion;
}
// 8. If report body's phase property is not dns or connection, append the following properties to report body:
// referrer?
// method
// request_headers?
// response_headers?
// status_code
if (phase != "dns" && phase != "connection") {
report_body.method = aChannel.requestMethod;
report_body.status_code = aChannel.responseStatus;
}
// 9. If origin is not equal to policy's origin, policy's subdomains flag is include, and report body's phase property is not dns, return null.
if (
origin != policyOrigin &&
policy.subdomains &&
report_body.phase != "dns"
) {
return;
}
// 10. If report body's phase property is not dns, and report body's server_ip property is non-empty and not equal to policy's received IP address:
if (phase != "dns" && report_body.server_ip != policy.ip_address) {
// 10.1 Set report body's phase to dns.
report_body.phase = "dns";
// 10.2 Set report body's type to dns.address_changed.
report_body.type = "dns.address_changed";
// 10.3 Clear report body's request_headers, response_headers, status_code, and elapsed_time properties.
delete report_body.request_headers;
delete report_body.response_headers;
delete report_body.status_code;
delete report_body.elapsed_time;
}
if (phase == "dns") {
//TODO this is just to pass the test sends-report-on-subdomain-dns-failure.https.html
report_body.method = aChannel.requestMethod;
report_body.status_code = 0;
// TODO
}
// 11. If policy is stale, then delete policy from the policy cache.
let currentDate = new Date();
if ((currentDate - policy.creation) / 1_000 > 172800) {
// Delete the policy.
delete this.policyCache[String([key, policyOrigin])];
// XXX: should we exit here, or continue submit the report?
}
// 12. Return report body and policy.
// 1. Let url be request's URL.
// 2. Clear url's fragment.
let uriMutator = aChannel.URI.mutate().setRef("");
// 3. If report body's phase property is dns or connection:
// Clear url's path and query.
if (report_body.phase == "dns" || report_body.phase == "connection") {
uriMutator.setPathQueryRef("");
}
// 4. Generate a network report given these parameters:
let report = {
type: "network-error",
url: aChannel.URI.specIgnoringRef, // uriMutator.finalize().spec, // XXX: sends-report-on-subdomain-dns-failure.https.html expects full URL
user_agent: Cc["@mozilla.org/network/protocol;1?name=http"].getService(
Ci.nsIHttpProtocolHandler
).userAgent,
body: report_body,
};
// XXX: this would benefit from using the actual reporting API,
// but it's not clear how easy it is to:
// - use it in the parent process
// - have it use the Report-To header
if (policy && policy.reportTo.group === policy.nel.report_to) {
// TODO: defer to later.
fetch(policy.reportTo.endpoints[0].url, {
method: "POST",
mode: "cors",
credentials: "omit",
headers: {
"Content-Type": "application/reports+json",
},
body: JSON.stringify([report]),
triggeringPrincipal:
Services.scriptSecurityManager.getChannelResultPrincipal(aChannel),
});
}
}
QueryInterface = ChromeUtils.generateQI(["nsINetworkErrorLogging"]);
}