Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,30 @@ Attributes set on server spans:
| `url.path` | Request URL path (without query) | Always |
| `url.query` | Query string (without `?`) | When query string is present |
| `url.scheme` | `http` or `https` | Always |
| `server.address` | Requested host, without the port | When `Host` header is present |
| `server.address` | Requested host, without the port | When a host source is present |
| `server.port` | Requested port, or its default | When the port is known |
| `client.address` | Client address, behind proxies when known, else the immediate peer | When the client or peer is known |
| `network.peer.address` | Immediate peer address | When the socket exposes it |
| `network.peer.port` | Immediate peer port | When the peer address is set |
| `network.protocol.version` | HTTP version (e.g. `1.1`) | Always |
| `user_agent.original` | Client `User-Agent` header | When the header is present |
| `http.response.status_code` | Response status code | When response finishes |
| `error.type` | HTTP status code as string | On 5xx responses |

The address attributes follow the proxy headers the way OpenTelemetry's
own HTTP instrumentation does, although a client can spoof them.
`server.address` and `server.port` come from the first of the `Forwarded`
header's `host` parameter, the first `X-Forwarded-Host` entry, and the
`Host` header, each parsed for both the host and the port, including
bracketed IPv6. When the chosen value omits the port, the default comes
from the `Forwarded` `proto` parameter, the first `X-Forwarded-Proto`
entry, or, for the `Host` header, the request scheme, in any letter case.
A port that is not a decimal number from 0 to 65535 is omitted. `client.address` comes from the first `Forwarded` `for`
parameter or the first `X-Forwarded-For` entry, and otherwise from the
immediate peer. `network.peer.address` and `network.peer.port` come
from the socket, so behind a proxy they name the proxy. A malformed
`Forwarded` header is treated as absent.

### HTTP/2 server

A span with kind `SERVER` is created for each incoming HTTP/2 request
Expand All @@ -115,11 +134,20 @@ Attributes set on HTTP/2 server spans:
| `url.path` | Request URL path (without query) | Always |
| `url.query` | Query string (without `?`) | When query string is present |
| `url.scheme` | `http` or `https` | Always |
| `server.address` | Requested host, without the port | When `:authority` is present |
| `server.address` | Requested host, without the port | When a host source is present |
| `server.port` | Requested port, or its default | When the port is known |
| `client.address` | Client address, behind proxies when known, else the immediate peer | When the client or peer is known |
| `network.peer.address` | Immediate peer address | When the socket exposes it |
| `network.peer.port` | Immediate peer port | When the peer address is set |
| `network.protocol.version` | `2.0` | Always |
| `user_agent.original` | Client `User-Agent` header | When the header is present |
| `http.response.status_code` | Response status code | When the response is sent |
| `error.type` | HTTP status code as string | On 5xx responses |

The address attributes follow the same rules as for HTTP/1 server
spans. The `:authority` pseudo-header stands in for the `Host` header,
and the peer attributes come from the session's socket.

### HTTP client

A span with kind `CLIENT` is created for each outgoing HTTP request made via
Expand All @@ -143,8 +171,8 @@ Attributes set on client spans:
| `error.type` | Status code or error name | On 4xx/5xx or errors |

The `server.port` value is the decimal port from the Host header, or the scheme
default (`80` for `http:`, `443` for `https:`) when the header omits it; a
port that is not a decimal number is omitted.
default (`80` for `http:`, `443` for `https:`) when the header omits it. A
port that is not a decimal number from 0 to 65535 is omitted.

### Undici/Fetch client

Expand Down
168 changes: 168 additions & 0 deletions lib/forwarded.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
'use strict';

// This parses the Forwarded header (RFC 7239). The header is a
// comma-separated list of elements, each a semicolon-separated list of
// name=value parameters, where a value is a token or a quoted string with
// backslash escapes. Parameter names are case-insensitive, so they are
// lowercased, and values keep their case. A malformed header yields null,
// so a caller treats garbage as absent rather than guessing at it.

// RFC 7230 section 3.2.6 token characters, the alphabet parameter names
// and unquoted values are built from.
function isTokenChar(code) {
return (
code === 0x21 ||
(code >= 0x23 && code <= 0x27) ||
code === 0x2a ||
code === 0x2b ||
code === 0x2d ||
code === 0x2e ||
(code >= 0x30 && code <= 0x39) ||
(code >= 0x41 && code <= 0x5a) ||
(code >= 0x5e && code <= 0x7a) ||
code === 0x7c ||
code === 0x7e
);
}

// These are the delimiters of the same section. An unquoted value ends at
// them, and a quoted one may contain them.
function isDelimiter(code) {
return (
code === 0x22 ||
code === 0x28 ||
code === 0x29 ||
code === 0x2c ||
code === 0x2f ||
(code >= 0x3a && code <= 0x40) ||
(code >= 0x5b && code <= 0x5d) ||
code === 0x7b ||
code === 0x7d
);
}

function isPrint(code) {
return code >= 0x20 && code <= 0x7e;
}

function isExtended(code) {
return code >= 0x80 && code <= 0xff;
}

// Only a quoted value can carry escapes, and only printable ones, so the
// backslashes are dropped once the value is complete.
function unescapeValue(value) {
return value.replace(/\\(.)/g, '$1');
}

function parseForwarded(header) {
const elements = [];
let element = {};
let parameter;
let start = -1;
let end = -1;
let inQuotes = false;
let escaping = false;
let unescape = false;
let code;

for (let i = 0; i < header.length; i++) {
code = header.charCodeAt(i);

if (parameter === undefined) {
// Whitespace between elements is not part of any parameter.
if (i !== 0 && start === -1 && (code === 0x20 || code === 0x09)) continue;
if (isTokenChar(code)) {
if (start === -1) start = i;
} else if (code === 0x3d && start !== -1) {
parameter = header.slice(start, i).toLowerCase();
start = -1;
} else {
return null;
}
continue;
}

if (escaping && (code === 0x09 || isPrint(code) || isExtended(code))) {
escaping = false;
} else if (isTokenChar(code)) {
// A token cannot resume after whitespace ended the value.
if (end !== -1) return null;
if (start === -1) start = i;
} else if (isDelimiter(code) || isExtended(code)) {
if (inQuotes) {
if (code === 0x22) {
inQuotes = false;
end = i;
} else if (code === 0x5c) {
if (start === -1) start = i;
escaping = unescape = true;
} else if (start === -1) {
start = i;
}
} else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) {
// A quote opens a value only directly after the equals sign.
inQuotes = true;
} else if (
(code === 0x2c || code === 0x3b) &&
(start !== -1 || end !== -1)
) {
if (start !== -1) {
if (end === -1) end = i;
element[parameter] = unescape
? unescapeValue(header.slice(start, end))
: header.slice(start, end);
} else {
// A closed quoted pair can be empty, but an unquoted value cannot.
element[parameter] = '';
}
if (code === 0x2c) {
elements.push(element);
element = {};
}
parameter = undefined;
start = end = -1;
} else {
return null;
}
} else if (code === 0x20 || code === 0x09) {
if (end !== -1) continue;
if (inQuotes) {
if (start === -1) start = i;
} else if (start !== -1) {
// Trailing whitespace is not part of an unquoted value.
end = i;
} else {
return null;
}
} else {
return null;
}
}

// The header must end inside a complete pair, not right after a
// separator, inside quotes, or on trailing whitespace.
if (
parameter === undefined ||
inQuotes ||
(start === -1 && end === -1) ||
code === 0x20 ||
code === 0x09
)
return null;

if (start !== -1) {
if (end === -1) end = header.length;
element[parameter] = unescape
? unescapeValue(header.slice(start, end))
: header.slice(start, end);
} else {
element[parameter] = '';
}
elements.push(element);
return elements;
}

module.exports = {
parseForwarded,
};
Loading
Loading