Skip to content

Instantly share code, notes, and snippets.

@shuhei

shuhei/README.md

Last active Feb 12, 2020
Embed
What would you like to do?
Firefox http2 beacon bug

Create a key pair.

openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' \
  -keyout localhost-privkey.pem -out localhost-cert.pem

Run the server.

node index.js

Open https://localhost:8443/ with Firefox.

const http2 = require("http2");
const { HTTP2_HEADER_PATH } = http2.constants;
const fs = require("fs");
const SCRIPT = `
function fetchAndSendBeacon(button, beaconPath) {
fetch("/slow").then(() => {
button.textContent += ": done";
}, () => {
button.textContent += ": error";
});
setTimeout(() => {
navigator.sendBeacon(beaconPath, "{}");
}, 0);
}
document.getElementById("without-body").addEventListener("click", (e) => {
fetchAndSendBeacon(e.currentTarget, "/beacon-without-body");
});
document.getElementById("with-body").addEventListener("click", (e) => {
fetchAndSendBeacon(e.currentTarget, "/beacon-with-body");
});
document.getElementById("with-body-and-delay").addEventListener("click", (e) => {
fetchAndSendBeacon(e.currentTarget, "/beacon-with-body-and-delay");
});
document.getElementById("with-double-body").addEventListener("click", (e) => {
fetchAndSendBeacon(e.currentTarget, "/beacon-with-double-body");
});
`;
const PAGE = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
</head>
<body>
<button id="without-body">Without Body</button>
<button id="with-body">With Body</button>
<button id="with-body-and-delay">With Body and Delay</button>
<button id="with-double-body">With Double Body</button>
<script>${SCRIPT}</script>
</body>
</html>
`;
function log(...args) {
console.log(`\u001b[32m[${new Date().toISOString()}]\u001b[0m`, ...args);
}
const server = http2.createSecureServer({
key: fs.readFileSync("localhost-privkey.pem"),
cert: fs.readFileSync("localhost-cert.pem")
});
server.on("error", err => {
log("server error", err);
});
server.on("session", session => {
log("session created");
session.on("error", err => {
log("session error:", err);
});
session.on("stream", (stream, headers) => {
stream.on("error", err => {
log("stream error:", err);
});
switch (headers[HTTP2_HEADER_PATH]) {
case "/":
stream.respond({
"content-type": "text/html",
":status": 200
});
stream.end(PAGE);
break;
case "/beacon-without-body":
stream.respond({
"content-type": "text/plain",
":status": 200
});
stream.end();
break;
case "/beacon-with-body": {
const body = "Hello World";
stream.respond({
"content-type": "text/plain",
"content-length": body.length,
":status": 200
});
stream.end(body);
break;
}
case "/beacon-with-body-and-delay": {
const body = "Hello World";
stream.respond({
"content-type": "text/plain",
"content-length": body.length,
":status": 200
});
stream.write(body);
// Firefox sends GOAWAY if this delay exists.
setTimeout(() => {
stream.end();
}, 10);
break;
}
case "/beacon-with-double-body": {
const body = "Hello World";
stream.respond({
"content-type": "text/plain",
"content-length": body.length * 2,
":status": 200
});
stream.write(body);
setTimeout(() => {
if (!stream.closed) {
stream.end(body);
}
}, 10);
break;
}
case "/slow":
setTimeout(() => {
if (!stream.closed) {
stream.respond({
"content-type": "text/plain",
":status": 200
});
stream.end("Hello World");
}
}, 500);
break;
default:
stream.respond({
"content-type": "text/html",
":status": 401
});
stream.end("<h1>Not found</h1>");
}
});
});
server.listen(8443);
@shuhei

This comment has been minimized.

Copy link
Owner Author

@shuhei shuhei commented Feb 11, 2020

Also, if it doesn't send goaway if the content-length is not set in the response body.

@shuhei

This comment has been minimized.

Copy link
Owner Author

@shuhei shuhei commented Feb 12, 2020

Code that can set NS_ERROR_NET_INTERRUPT (from grep):

dom/base/Navigator.cpp:1050:  aRequest->Cancel(NS_ERROR_NET_INTERRUPT);`
netwerk/base/nsSocketTransport2.cpp:155:      rv = NS_ERROR_NET_INTERRUPT;
netwerk/protocol/http/Http2Session.cpp:3390:                              : NS_ERROR_NET_INTERRUPT;
netwerk/protocol/http/Http3Session.cpp:671:      CloseStream(stream, NS_ERROR_NET_INTERRUPT);
netwerk/protocol/http/nsHttpChannel.cpp:8372:          aStatus = NS_ERROR_NET_INTERRUPT;
netwerk/protocol/http/nsHttpTransaction.cpp:1631:      return NS_ERROR_NET_INTERRUPT;

Not relevant:

  • netwerk/protocol/http/Http2Session.cpp:3390: This should be called after RST_STREAM, but RST_STREAM is not captured in Wireshark when GOAWAY happens.
  • netwerk/protocol/http/Http3Session.cpp:671: This issue is about HTTP2 instead of HTTP3.
  • netwerk/protocol/http/nsHttpChannel.cpp:8372: This line is about Range request failure, but the beacon is not using Range header.

Relevant?:

  • dom/base/Navigator.cpp:1050: The initial ticket for navigator.sendBeacon has some discussions around it. https://bugzilla.mozilla.org/show_bug.cgi?id=936340
  • netwerk/base/nsSocketTransport2.cpp:155: PR_END_OF_FILE_ERROR from NSPR (Netscape Portable Runtime) is translated to NS_ERROR_NET_INTERRUPT. It seems that PR_END_OF_FILE_ERROR can come from TLS-related low-level functions. This may not be relevant.
  • netwerk/protocol/http/nsHttpTransaction.cpp:1631: A comment suggests that it's for 100 class response, but not sure.
@shuhei

This comment has been minimized.

Copy link
Owner Author

@shuhei shuhei commented Feb 12, 2020

  • 73: I can reproduce.
  • 74.0b2 (Beta): I can reproduce.
  • 75.0a1 (Nightly): I can't reproduce.
@shuhei

This comment has been minimized.

Copy link
Owner Author

@shuhei shuhei commented Feb 12, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.