Skip to content

chore(deps): update #5444

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 26, 2025
Merged
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
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@
"commitlint",
"eslintcache",
"hono",
"privkey"
"privkey",
"geomanist"
],
"ignorePaths": [
"CHANGELOG.md",
Expand Down
8 changes: 7 additions & 1 deletion client-src/socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ let maxRetries = 10;
// eslint-disable-next-line import/no-mutable-exports
export let client = null;

let timeout;

/**
* @param {string} url
* @param {{ [handler: string]: (data?: any, params?: any) => any }} handlers
Expand All @@ -33,6 +35,10 @@ const socket = function initSocket(url, handlers, reconnect) {
client.onOpen(() => {
retries = 0;

if (timeout) {
clearTimeout(timeout);
}

if (typeof reconnect !== "undefined") {
maxRetries = reconnect;
}
Expand All @@ -57,7 +63,7 @@ const socket = function initSocket(url, handlers, reconnect) {

log.info("Trying to reconnect...");

setTimeout(() => {
timeout = setTimeout(() => {
socket(url, handlers, reconnect);
}, retryInMs);
}
Expand Down
4 changes: 2 additions & 2 deletions examples/.assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ table {
code {
background-color: rgba(70, 94, 105, 0.06);
border-radius: 3px;
font-family: "Source Code Pro", Consolas, "Liberation Mono", Menlo, Courier,
monospace;
font-family:
"Source Code Pro", Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 1.44rem;
margin: 0;
max-width: 100%;
Expand Down
204 changes: 142 additions & 62 deletions lib/Server.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ function useFn(route, fn) {
return /** @type {BasicApplication} */ ({});
}

const DEFAULT_ALLOWED_PROTOCOLS = /^(file|.+-extension):/i;

/**
* @typedef {Object} BasicApplication
* @property {typeof useFn} use
Expand Down Expand Up @@ -1961,7 +1963,7 @@ class Server {
(req.headers);
const headerName = headers[":authority"] ? ":authority" : "host";

if (this.checkHeader(headers, headerName, true)) {
if (this.isValidHost(headers, headerName)) {
next();
return;
}
Expand Down Expand Up @@ -2668,8 +2670,9 @@ class Server {

if (
!headers ||
!this.checkHeader(headers, "host", true) ||
!this.checkHeader(headers, "origin", false)
!this.isValidHost(headers, "host") ||
!this.isValidHost(headers, "origin") ||
!this.isSameOrigin(headers)
) {
this.sendMessage([client], "error", "Invalid Host/Origin header");

Expand Down Expand Up @@ -2703,7 +2706,8 @@ class Server {

if (
this.options.client &&
/** @type {ClientConfiguration} */ (this.options.client).reconnect
/** @type {ClientConfiguration} */
(this.options.client).reconnect
) {
this.sendMessage(
[client],
Expand All @@ -2718,9 +2722,9 @@ class Server {
/** @type {ClientConfiguration} */
(this.options.client).overlay
) {
const overlayConfig = /** @type {ClientConfiguration} */ (
this.options.client
).overlay;
const overlayConfig =
/** @type {ClientConfiguration} */
(this.options.client).overlay;

this.sendMessage(
[client],
Expand Down Expand Up @@ -3106,106 +3110,182 @@ class Server {

/**
* @private
* @param {{ [key: string]: string | undefined }} headers
* @param {string} headerToCheck
* @param {boolean} allowIP
* @param {string} value
* @returns {boolean}
*/
checkHeader(headers, headerToCheck, allowIP) {
isHostAllowed(value) {
const { allowedHosts } = this.options;

// allow user to opt out of this security check, at their own risk
// by explicitly enabling allowedHosts
if (allowedHosts === "all") {
return true;
}

// always allow localhost host, for convenience
// allow if value is in allowedHosts
if (Array.isArray(allowedHosts) && allowedHosts.length > 0) {
for (let hostIdx = 0; hostIdx < allowedHosts.length; hostIdx++) {
/** @type {string} */
const allowedHost = allowedHosts[hostIdx];

if (allowedHost === value) {
return true;
}

// support "." as a subdomain wildcard
// e.g. ".example.com" will allow "example.com", "www.example.com", "subdomain.example.com", etc
if (allowedHost[0] === ".") {
// "example.com" (value === allowedHost.substring(1))
// "*.example.com" (value.endsWith(allowedHost))
if (
value === allowedHost.substring(1) ||
/** @type {string} */
(value).endsWith(allowedHost)
) {
return true;
}
}
}
}

// Also allow if `client.webSocketURL.hostname` provided
if (
this.options.client &&
typeof (
/** @type {ClientConfiguration} */
(this.options.client).webSocketURL
) !== "undefined"
) {
return (
/** @type {WebSocketURL} */
(/** @type {ClientConfiguration} */ (this.options.client).webSocketURL)
.hostname === value
);
}

return false;
}

/**
* @private
* @param {{ [key: string]: string | undefined }} headers
* @param {string} headerToCheck
* @returns {boolean}
*/
isValidHost(headers, headerToCheck) {
if (this.options.allowedHosts === "all") {
return true;
}

// get the Host header and extract hostname
// we don't care about port not matching
const hostHeader = headers[headerToCheck];
const header = headers[headerToCheck];

if (!hostHeader) {
if (!header) {
return false;
}

if (/^(file|.+-extension):/i.test(hostHeader)) {
if (DEFAULT_ALLOWED_PROTOCOLS.test(header)) {
return true;
}

// use the node url-parser to retrieve the hostname from the host-header.
const hostname = url.parse(
// if hostHeader doesn't have scheme, add // for parsing.
/^(.+:)?\/\//.test(hostHeader) ? hostHeader : `//${hostHeader}`,
// if header doesn't have scheme, add // for parsing.
/^(.+:)?\/\//.test(header) ? header : `//${header}`,
false,
true,
).hostname;

// allow requests with explicit IPv4 or IPv6-address if allowIP is true.
// Note that IP should not be automatically allowed for Origin headers,
// otherwise an untrusted remote IP host can send requests.
//
if (hostname === null) {
return false;
}

if (this.isHostAllowed(hostname)) {
return true;
}

// always allow requests with explicit IPv4 or IPv6-address.
// A note on IPv6 addresses:
// hostHeader will always contain the brackets denoting
// header will always contain the brackets denoting
// an IPv6-address in URLs,
// these are removed from the hostname in url.parse(),
// so we have the pure IPv6-address in hostname.
// For convenience, always allow localhost (hostname === 'localhost')
// and its subdomains (hostname.endsWith(".localhost")).
// allow hostname of listening address (hostname === this.options.host)
const isValidHostname =
(allowIP &&
hostname !== null &&
(ipaddr.IPv4.isValid(hostname) || ipaddr.IPv6.isValid(hostname))) ||
ipaddr.IPv4.isValid(hostname) ||
ipaddr.IPv6.isValid(hostname) ||
hostname === "localhost" ||
(hostname !== null && hostname.endsWith(".localhost")) ||
hostname.endsWith(".localhost") ||
hostname === this.options.host;

if (isValidHostname) {
return true;
}

const { allowedHosts } = this.options;
// disallow
return false;
}

// always allow localhost host, for convenience
// allow if hostname is in allowedHosts
if (Array.isArray(allowedHosts) && allowedHosts.length > 0) {
for (let hostIdx = 0; hostIdx < allowedHosts.length; hostIdx++) {
/** @type {string} */
const allowedHost = allowedHosts[hostIdx];
/**
* @private
* @param {{ [key: string]: string | undefined }} headers
* @returns {boolean}
*/
isSameOrigin(headers) {
if (this.options.allowedHosts === "all") {
return true;
}

if (allowedHost === hostname) {
return true;
}
const originHeader = headers.origin;

// support "." as a subdomain wildcard
// e.g. ".example.com" will allow "example.com", "www.example.com", "subdomain.example.com", etc
if (allowedHost[0] === ".") {
// "example.com" (hostname === allowedHost.substring(1))
// "*.example.com" (hostname.endsWith(allowedHost))
if (
hostname === allowedHost.substring(1) ||
/** @type {string} */ (hostname).endsWith(allowedHost)
) {
return true;
}
}
}
if (!originHeader) {
return this.options.allowedHosts === "all";
}

// Also allow if `client.webSocketURL.hostname` provided
if (
this.options.client &&
typeof (
/** @type {ClientConfiguration} */ (this.options.client).webSocketURL
) !== "undefined"
) {
return (
/** @type {WebSocketURL} */
(/** @type {ClientConfiguration} */ (this.options.client).webSocketURL)
.hostname === hostname
);
if (DEFAULT_ALLOWED_PROTOCOLS.test(originHeader)) {
return true;
}

// disallow
return false;
const origin = url.parse(originHeader, false, true).hostname;

if (origin === null) {
return false;
}

if (this.isHostAllowed(origin)) {
return true;
}

const hostHeader = headers.host;

if (!hostHeader) {
return this.options.allowedHosts === "all";
}

if (DEFAULT_ALLOWED_PROTOCOLS.test(hostHeader)) {
return true;
}

const host = url.parse(
// if hostHeader doesn't have scheme, add // for parsing.
/^(.+:)?\/\//.test(hostHeader) ? hostHeader : `//${hostHeader}`,
false,
true,
).hostname;

if (host === null) {
return false;
}

if (this.isHostAllowed(host)) {
return true;
}

return origin === host;
}

/**
Expand Down
Loading
Loading