diff --git a/streaming/index.js b/streaming/index.js index e00da1bb83..aba3db7908 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -2,6 +2,7 @@ import fs from 'node:fs'; import http from 'node:http'; +import net from 'node:net'; import path from 'node:path'; import url from 'node:url'; @@ -9,6 +10,7 @@ import cors from 'cors'; import dotenv from 'dotenv'; import express from 'express'; import { JSDOM } from 'jsdom'; +import proxyaddr from 'proxy-addr'; import { WebSocketServer } from 'ws'; import * as Database from './database.js'; @@ -146,8 +148,13 @@ const startServer = async () => { }); const app = express(); + const trustProxy = proxyaddr.compile( + process.env.TRUSTED_PROXY_IP ? + process.env.TRUSTED_PROXY_IP.split(/(?:\s*,\s*|\s+)/) : + ['loopback', 'uniquelocal'] + ); - app.set('trust proxy', process.env.TRUSTED_PROXY_IP ? process.env.TRUSTED_PROXY_IP.split(/(?:\s*,\s*|\s+)/) : 'loopback,uniquelocal'); + app.set('trust proxy', trustProxy); app.use(httpLogger); app.use(cors()); @@ -161,6 +168,15 @@ const startServer = async () => { // logger. This decorates the `request` object. attachWebsocketHttpLogger(request); + // Define the `request.ip` property + Object.defineProperty(request, 'ip', { + configurable: true, + enumerable: true, + get() { + return proxyaddr(this, trustProxy); + } + }); + request.log.info("HTTP Upgrade Requested"); /** @param {Error} err */ @@ -349,28 +365,53 @@ const startServer = async () => { const isInScope = (req, necessaryScopes) => req.scopes.some(scope => necessaryScopes.includes(scope)); + const ACCESS_TOKEN_UPDATE_FREQUENCY = 24 * 60 * 60 * 1000; + /** + * Fetches the account from the access token, updating access token usage if necessary. + * `req.ip` comes from `proxyaddr` * @param {string} token - * @param {any} req + * @param {http.IncomingMessage & { ip: string } & ResolvedAccount} req * @returns {Promise} */ const accountFromToken = async (token, req) => { - const result = await pgPool.query('SELECT oauth_access_tokens.id, oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1', [token]); + const result = await pgPool.query('SELECT oauth_access_tokens.id, oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes, oauth_access_tokens.last_used_at FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1', [token]); - if (result.rows.length === 0) { + if (result.rows.length === 0 || result.rows.length > 1) { throw new AuthenticationError('Invalid access token'); } - req.accessTokenId = result.rows[0].id; - req.scopes = result.rows[0].scopes.split(' '); - req.accountId = result.rows[0].account_id; - req.chosenLanguages = result.rows[0].chosen_languages; + const accessToken = result.rows[0]; + + // Track the usage of the access token if necessary: + // This is the same code as: app/controllers/concerns/api/access_token_tracking_concern.rb + if (accessToken.last_used_at === null || accessToken.last_used_at < Date.now() - ACCESS_TOKEN_UPDATE_FREQUENCY) { + let query, variables = []; + if (req.ip && net.isIP(req.ip)) { + query = 'UPDATE "oauth_access_tokens" SET "last_used_at" = $2, "last_used_ip" = $3 WHERE "oauth_access_tokens"."id" = $1'; + variables = [ accessToken.id, new Date(), req.ip ]; + } else { + query = 'UPDATE "oauth_access_tokens" SET "last_used_at" = $2 WHERE "oauth_access_tokens"."id" = $1'; + variables = [ accessToken.id, new Date() ]; + } + + try { + await pgPool.query(query, variables); + } catch (err) { + req.log.error(err, 'Error updating Access Token usage tracking'); + } + } + + req.accessTokenId = accessToken.id; + req.scopes = accessToken.scopes.split(' '); + req.accountId = accessToken.account_id; + req.chosenLanguages = accessToken.chosen_languages; return { - accessTokenId: result.rows[0].id, - scopes: result.rows[0].scopes.split(' '), - accountId: result.rows[0].account_id, - chosenLanguages: result.rows[0].chosen_languages, + accessTokenId: accessToken.id, + scopes: accessToken.scopes.split(' '), + accountId: accessToken.account_id, + chosenLanguages: accessToken.chosen_languages, }; }; diff --git a/streaming/package.json b/streaming/package.json index 2419ffd273..18b97149be 100644 --- a/streaming/package.json +++ b/streaming/package.json @@ -27,6 +27,7 @@ "pino": "^9.0.0", "pino-http": "^10.0.0", "prom-client": "^15.0.0", + "proxy-addr": "~2.0.7", "uuid": "^11.0.0", "ws": "^8.12.1" }, @@ -34,11 +35,13 @@ "@types/cors": "^2.8.16", "@types/express": "^4.17.17", "@types/pg": "^8.6.6", + "@types/proxy-addr": "^2.0.3", "@types/uuid": "^10.0.0", "@types/ws": "^8.5.9", "eslint-define-config": "^2.0.0", "pino-pretty": "^11.0.0", - "typescript": "^5.0.4" + "typescript": "^5.0.4", + "wscat": "^6.0.1" }, "optionalDependencies": { "bufferutil": "^4.0.7", diff --git a/yarn.lock b/yarn.lock index a51d49ca56..2779b86727 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3053,6 +3053,7 @@ __metadata: "@types/cors": "npm:^2.8.16" "@types/express": "npm:^4.17.17" "@types/pg": "npm:^8.6.6" + "@types/proxy-addr": "npm:^2.0.3" "@types/uuid": "npm:^10.0.0" "@types/ws": "npm:^8.5.9" bufferutil: "npm:^4.0.7" @@ -3068,10 +3069,12 @@ __metadata: pino-http: "npm:^10.0.0" pino-pretty: "npm:^11.0.0" prom-client: "npm:^15.0.0" + proxy-addr: "npm:~2.0.7" typescript: "npm:^5.0.4" utf-8-validate: "npm:^6.0.3" uuid: "npm:^11.0.0" ws: "npm:^8.12.1" + wscat: "npm:^6.0.1" dependenciesMeta: bufferutil: optional: true @@ -4073,6 +4076,15 @@ __metadata: languageName: node linkType: hard +"@types/proxy-addr@npm:^2.0.3": + version: 2.0.3 + resolution: "@types/proxy-addr@npm:2.0.3" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/680f5eeaf434461daf856d694fd6b228c9850f736f377b8fc1d373ba282feca25bc642eeed93f3a9511d9406a9b93014b94c4d7cb8488646482ca0610d931e30 + languageName: node + linkType: hard + "@types/punycode@npm:^2.1.0": version: 2.1.4 resolution: "@types/punycode@npm:2.1.4" @@ -6402,6 +6414,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^12.1.0, commander@npm:~12.1.0": + version: 12.1.0 + resolution: "commander@npm:12.1.0" + checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9 + languageName: node + linkType: hard + "commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -6416,13 +6435,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:~12.1.0": - version: 12.1.0 - resolution: "commander@npm:12.1.0" - checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9 - languageName: node - linkType: hard - "comment-parser@npm:1.4.1": version: 1.4.1 resolution: "comment-parser@npm:1.4.1" @@ -12390,6 +12402,13 @@ __metadata: languageName: node linkType: hard +"mute-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "mute-stream@npm:2.0.0" + checksum: 10c0/2cf48a2087175c60c8dcdbc619908b49c07f7adcfc37d29236b0c5c612d6204f789104c98cc44d38acab7b3c96f4a3ec2cfdc4934d0738d876dbefa2a12c69f4 + languageName: node + linkType: hard + "nan@npm:^2.12.1": version: 2.17.0 resolution: "nan@npm:2.17.0" @@ -15060,6 +15079,15 @@ __metadata: languageName: node linkType: hard +"read@npm:^4.0.0": + version: 4.0.0 + resolution: "read@npm:4.0.0" + dependencies: + mute-stream: "npm:^2.0.0" + checksum: 10c0/448dd2cb8163fa7004dbe9e7fc9b0814cedd55028e2d45fbebd774f6b05e3ac046b092f3910a4eff942471187afa0b56b5db6caf2cd230d264d8d8fe22f9af6f + languageName: node + linkType: hard + "readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.2, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.6": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" @@ -18777,7 +18805,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.11.0, ws@npm:^8.12.1, ws@npm:^8.18.0": +"ws@npm:^8.0.0, ws@npm:^8.11.0, ws@npm:^8.12.1, ws@npm:^8.18.0": version: 8.18.0 resolution: "ws@npm:8.18.0" peerDependencies: @@ -18792,6 +18820,20 @@ __metadata: languageName: node linkType: hard +"wscat@npm:^6.0.1": + version: 6.0.1 + resolution: "wscat@npm:6.0.1" + dependencies: + commander: "npm:^12.1.0" + https-proxy-agent: "npm:^7.0.5" + read: "npm:^4.0.0" + ws: "npm:^8.0.0" + bin: + wscat: bin/wscat + checksum: 10c0/8245b6cdebb6bde8bc6bf647991d289107bdbda24f51cf820683f40a06b72226e606fbc8d263e4812018928315200377e615035918399bdcaca8b0b0182ecbd1 + languageName: node + linkType: hard + "xml-name-validator@npm:^4.0.0": version: 4.0.0 resolution: "xml-name-validator@npm:4.0.0"