Skip to content

Commit

Permalink
Drop support for Node 18, add support for 20, 21. (#487)
Browse files Browse the repository at this point in the history
* Update trivial dependencies

* Drop uuid dep

* Node 20 updates

* Update eslint packages

* Update document on versioning

* Use node 20 everywhere

* changelog

* Fixup test

* Fixup test fails

* Cleanup linting
  • Loading branch information
Half-Shot authored Jan 5, 2024
1 parent 2334b0b commit 7ea2f5b
Show file tree
Hide file tree
Showing 19 changed files with 440 additions and 369 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ on:

jobs:
docs:
runs-on: ubuntu-20.04
container: node:18
runs-on: ubuntu-22.04
container: node:20
steps:
- uses: actions/checkout@v3

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ on:

jobs:
publish-to-npm:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 18
node-version-file: .node-version
- run: yarn --strict-semver --frozen-lockfile
- run: yarn build
- uses: JS-DevTools/npm-publish@v1
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ on:

jobs:
lint:
runs-on: ubuntu-20.04
container: node:18
runs-on: ubuntu-22.04
container: node:20
steps:
- uses: actions/checkout@v2
- run: yarn --strict-semver --frozen-lockfile
Expand All @@ -19,8 +19,8 @@ jobs:
test:
strategy:
matrix:
node-version: [18, 20]
runs-on: ubuntu-20.04
node-version: [20, 21]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Use Node.js
Expand All @@ -30,8 +30,8 @@ jobs:
- run: yarn --frozen-lockfile
- run: yarn build && yarn test
test-postgres:
runs-on: ubuntu-20.04
container: node:18
runs-on: ubuntu-22.04
container: node:20
services:
postgres:
image: postgres:latest
Expand Down
1 change: 1 addition & 0 deletions .nodeversion
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20
1 change: 1 addition & 0 deletions changelog.d/487.removal
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Drop support for Node 18, add support for Node 20, 21.
6 changes: 3 additions & 3 deletions docs/platforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ Node.JS version falls from Active to Maintenance, we will migrate projects to th
For users who are on distributions packaging only Maintenance versions of Node.JS, we'd suggest either using Docker
or an alternative Node.JS source.

At time of writing (April 2023) we support 18.X and 20.X as they are the Active and Current releases of Node.JS.
At time of writing (January 2024) we support 20.X and 21.X as they are the Active and Current releases of Node.JS.

Bridge projects do not support odd-versioned Node.JS releases, as these are short lived non-LTS versions and are
difficult to support since they have a 6 month shelf life.
Bridge projects do not provide long term support to odd-versioned Node.JS releases, as these are short lived non-LTS
versions and are difficult to support since they have a 6 month shelf life.

See https://nodejs.org/en/about/releases/ for more information about Node.JS releases.

Expand Down
47 changes: 22 additions & 25 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"matrix-org"
],
"engines": {
"node": ">=18"
"node": ">=20"
},
"author": "Matrix.org",
"license": "Apache-2.0",
Expand All @@ -31,12 +31,11 @@
"homepage": "https://github.com/matrix-org/matrix-appservice-bridge#readme",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"axios": "^0.27.2",
"chalk": "^4.1.0",
"express": "^4.18.1",
"express-rate-limit": "^6.7.0",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"extend": "^3.0.2",
"ip-cidr": "^3.0.4",
"ip-cidr": "^3.0.0",
"is-my-json-valid": "^2.20.5",
"js-yaml": "^4.0.0",
"matrix-appservice": "^2.0.0",
Expand All @@ -45,33 +44,31 @@
"nopt": "^5.0.0",
"p-queue": "^6.6.2",
"pkginfo": "^0.4.1",
"postgres": "^3.3.1",
"prom-client": "^14.1.0",
"uuid": "^8.3.2",
"winston": "^3.3.3",
"postgres": "^3.4.3",
"prom-client": "^15.1.0",
"winston": "^3.11.0",
"winston-daily-rotate-file": "^4.5.1"
},
"devDependencies": {
"@tsconfig/node18": "^2.0.0",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.14",
"@types/extend": "^3.0.1",
"@tsconfig/node20": "^20.1.2",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/extend": "^3.0.4",
"@types/jasmine": "^4.0.3",
"@types/js-yaml": "^4.0.0",
"@types/nedb": "^1.8.11",
"@types/node": "^18",
"@types/nopt": "^3.0.29",
"@types/pkginfo": "^0.4.0",
"@types/uuid": "^8.3.1",
"@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1",
"eslint": "^8.39.0",
"@types/js-yaml": "^4.0.9",
"@types/nedb": "^1.8.16",
"@types/node": "^20",
"@types/nopt": "^3.0.32",
"@types/pkginfo": "^0.4.3",
"@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.17.0",
"eslint": "^8.56.0",
"jasmine": "^4.2.1",
"jasmine-spec-reporter": "^7.0.0",
"nyc": "^15.1.0",
"ts-node": "^10.2.1",
"ts-node": "^10.9.2",
"typedoc": "^0.23.28",
"typescript": "^5.0.4",
"winston-transport": "^4.4.0"
"typescript": "^5.3.3",
"winston-transport": "^4.6.0"
}
}
19 changes: 7 additions & 12 deletions spec/unit/utils/matrix-host-resolver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,12 @@ import "jasmine";
import { MatrixHostResolver, DefaultCacheForMs, MaxCacheForMs, MinCacheForMs } from "../../../src/index";

function createMHR(wellKnownServerName?: string, wellKnownHeaders: {[key: string]: string} = {}, srvRecords?: SrvRecord[], currentTimeMs?: number) {
const axios: any = {
get: () => {
const response: any = {};
if (wellKnownServerName) {
response.data = {'m.server': wellKnownServerName};
}
response.headers = wellKnownHeaders || {};
response.status = wellKnownServerName ? 200 : 404;
return response;
}
};
const fetchFn = async () => ({
ok: !!wellKnownServerName,
headers: new Headers(wellKnownHeaders),
status: wellKnownServerName ? 200 : 404,
json: async () => ( wellKnownServerName ? {'m.server': wellKnownServerName} : {"error": "Test failure"})
} satisfies Partial<Response>) as unknown ;
const dns = {
resolveSrv: async () => {
if (srvRecords) {
Expand All @@ -23,7 +18,7 @@ function createMHR(wellKnownServerName?: string, wellKnownHeaders: {[key: string
}
}
}
return new MatrixHostResolver({axios, dns, currentTimeMs});
return new MatrixHostResolver({fetch: fetchFn as (typeof fetch), dns, currentTimeMs});
}

describe("MatrixHostResolver", () => {
Expand Down
1 change: 1 addition & 0 deletions src/components/bridge-store.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Expand Down
2 changes: 1 addition & 1 deletion src/components/client-request-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class ClientRequestCache<T, P extends Array<unknown>> {
this.requestContent.delete(key);
return new Promise<T>((resolve) => {
// TypeScript doesn't understand that `args :P` will satisfy this.requestFunc
resolve((this.requestFunc as any).apply(null, [key, ...args]))
resolve((this.requestFunc).apply(null, [key, ...args]))
}).then((result) => {
if (result !== undefined) {
this.requestContent.set(key, {
Expand Down
1 change: 1 addition & 0 deletions src/components/config-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import yaml from "js-yaml";
import validator from "is-my-json-valid";
import extend from "extend";

/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
type Schema = any;

interface ValidationError extends Error {
Expand Down
1 change: 1 addition & 0 deletions src/components/event-bridge-store.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Expand Down
2 changes: 1 addition & 1 deletion src/components/request-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class RequestFactory {
private _resolves: HandlerFunction[] = [];
private _rejects: HandlerFunction[] = [];
private _timeouts: {fn: TimeoutFunction, timeout: number}[] = [];
private timeoutHandles = new Set<NodeJS.Timer>();
private timeoutHandles = new Set<NodeJS.Timeout>();


/**
Expand Down
1 change: 1 addition & 0 deletions src/components/user-activity-store.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Expand Down
34 changes: 21 additions & 13 deletions src/provisioning/api.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { Application, default as express, NextFunction, Request, Response, Router, Router as router } from "express";
import { ProvisioningStore } from "./store";
import { Server } from "http";
import { v4 as uuid } from "uuid";
import axios from "axios";
import { ErrCode, IApiError, ProvisioningRequest, ApiError } from ".";
import { URL } from "url";
import { MatrixHostResolver } from "../utils/matrix-host-resolver";
import IPCIDR from "ip-cidr";
import { isIP } from "net";
import { promises as dns } from "dns";
import ratelimiter, { Options as RatelimitOptions } from "express-rate-limit";
import { Methods } from "./request";
import { Logger } from "..";
import { randomUUID } from "crypto";
import IPCIDR from "ip-cidr";

// Borrowed from
// https://github.com/matrix-org/synapse/blob/91221b696156e9f1f9deecd425ae58af03ebb5d3/docs/sample_config.yaml#L215
Expand Down Expand Up @@ -375,30 +374,34 @@ export class ProvisioningApi {
// Now do the token exchange
try {
const requestUrl = new URL("/_matrix/federation/v1/openid/userinfo", url);
const response = await axios.get<{sub: string}>(requestUrl.toString(), {
params: {
access_token: openIdToken,
},
requestUrl.searchParams.set('access_token', openIdToken);
const response = await fetch(requestUrl, {
headers: {
'Host': hostHeader,
}
});
if (!response.data.sub) {
log.warn(`Server responded with invalid sub information for ${server}`, response.data);
if (!response.ok) {
log.warn(`Server responded with a status of ${response.status}`);
log.debug(`Server response:`, await response.text());
throw new ApiError("Server did not respond positively to request", ErrCode.BadOpenID);
}
const data = await response.json() as { sub: string };
if (!data.sub) {
log.warn(`Server responded with invalid sub information for ${server}`, data);
throw new ApiError("Server did not respond with the correct sub information", ErrCode.BadOpenID);
}
const userId = response.data.sub;
const userId = data.sub;

const mxidMatch = userId.match(/([^:]+):(.+)/);
if (!mxidMatch) {
throw new ApiError("Server did not respond with a valid MXID", ErrCode.BadOpenID);
}
const [, _localpart, serverName] = mxidMatch;
const [,, serverName] = mxidMatch;
if (serverName !== server) {
throw new ApiError("Server returned a MXID belonging to another homeserver", ErrCode.BadOpenID);
}

const token = this.widgetTokenPrefix + uuid().replace(/-/g, "");
const token = this.widgetTokenPrefix + randomUUID().replace(/-/g, "");
const expiresTs = Date.now() + this.widgetTokenLifetimeMs;
await this.store.createSession({
userId,
Expand All @@ -409,7 +412,12 @@ export class ProvisioningApi {
}
catch (ex) {
log.warn(`Failed to exchange the token for ${server}`, ex);
throw new ApiError("Failed to exchange token", ErrCode.BadOpenID);
if (ex instanceof ApiError) {
throw ex;
}
else {
throw new ApiError("Failed to exchange token", ErrCode.BadOpenID);
}
}
}

Expand Down
36 changes: 17 additions & 19 deletions src/utils/matrix-host-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Axios } from "axios";
import { URL } from "url";
import { isIP } from "net";
import { promises as dns, SrvRecord } from "dns"
Expand Down Expand Up @@ -40,13 +39,13 @@ interface DnsInterface {
* [server discovery section of the spec](https://spec.matrix.org/v1.1/server-server-api/#server-discovery).
*/
export class MatrixHostResolver {
private axios: Axios;
private fetch: typeof fetch;
private dns: DnsInterface;
private resultCache = new Map<string, CachedResult>();

constructor(private readonly opts: {axios?: Axios, dns?: DnsInterface, currentTimeMs?: number} = {}) {
constructor(private readonly opts: {fetch?: typeof fetch, dns?: DnsInterface, currentTimeMs?: number} = {}) {
// To allow for easier mocking.
this.axios = opts.axios || new Axios({ timeout: WellKnownTimeout });
this.fetch = opts.fetch ?? fetch;
this.dns = opts.dns || dns;
}

Expand Down Expand Up @@ -98,24 +97,22 @@ export class MatrixHostResolver {

private async getWellKnown(serverName: string): Promise<{mServer: string, cacheFor: number}> {
const url = `https://${serverName}/.well-known/matrix/server`;
const wellKnown = await this.axios.get<MatrixServerWellKnown>(
url, {
validateStatus: null,
});
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), WellKnownTimeout);
// Will throw on timeout.
const wellKnown = await this.fetch(url, {signal: controller.signal });
clearTimeout(timeout);
if (wellKnown.status !== 200) {
throw Error('Well known request returned non-200');
}
let data: MatrixServerWellKnown;
if (typeof wellKnown.data === "object") {
data = wellKnown.data;
}
else if (typeof wellKnown.data === "string") {
data = JSON.parse(wellKnown.data);
let wellKnownData: MatrixServerWellKnown;
try {
wellKnownData = await wellKnown.json() as MatrixServerWellKnown;
}
else {
catch (ex) {
throw Error('Invalid datatype for well-known response');
}
const mServer = data["m.server"];
const mServer = wellKnownData["m.server"];
if (typeof mServer !== "string") {
throw Error("Missing 'm.server' in well-known response");
}
Expand All @@ -127,17 +124,18 @@ export class MatrixHostResolver {
}

let cacheFor = DefaultCacheForMs;
if (wellKnown.headers['Expires']) {
const expiresHeader = wellKnown.headers.get('Expires');
if (expiresHeader) {
try {
cacheFor = new Date(wellKnown.headers['Expires']).getTime() - this.currentTime;
cacheFor = new Date(expiresHeader).getTime() - this.currentTime;
}
catch (ex) {
log.warn(`Expires header provided by ${url} could not be parsed`, ex);
}
}

// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
const cacheControlHeader = wellKnown.headers['Cache-Control']?.toLowerCase()
const cacheControlHeader: string[] = wellKnown.headers.get('Cache-Control')?.toLowerCase()
.split(',')
.map(s => s.trim()) || [];

Expand Down
3 changes: 0 additions & 3 deletions src/utils/package-info.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { join, dirname } from "path";
import { statSync } from "fs";
import pkginfo from "pkginfo";
import { Logger } from "../components/logging";

const log = new Logger("PackageInfo");

// This may be defined if the script is run via NPM: https://docs.npmjs.com/cli/v8/using-npm/scripts#packagejson-vars
let BridgeVersion: string|undefined = process.env.npm_package_version;
Expand Down
Loading

0 comments on commit 7ea2f5b

Please sign in to comment.