Skip to content

Commit

Permalink
Update dependencies and adjust things
Browse files Browse the repository at this point in the history
  • Loading branch information
nanaya committed Aug 22, 2023
1 parent 3280952 commit 58d8197
Show file tree
Hide file tree
Showing 9 changed files with 1,141 additions and 834 deletions.
4 changes: 2 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,13 @@ module.exports = {
'import/order': ['error', { alphabetize: { order: 'asc' } }],
'jsdoc/check-alignment': 'error',
'jsdoc/check-indentation': 'error',
'jsdoc/newline-after-description': 'error',
'jsdoc/tag-lines': 'error',
'max-classes-per-file': 'error',
'max-len': 'off',
'new-parens': 'error',
'no-bitwise': 'error',
'no-caller': 'error',
'no-console': 'warn',
'no-console': 'error',
'no-empty-function': 'error',
'no-eval': 'error',
'no-invalid-this': 'error',
Expand Down
41 changes: 20 additions & 21 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,28 @@
"name": "osu-notification-server",
"license": "AGPL-3.0",
"dependencies": {
"@types/cookie": "^0.3.2",
"@types/jsonwebtoken": "^8.3.0",
"@types/cookie": "^0.5.1",
"@types/jsonwebtoken": "^9.0.2",
"@types/mysql2": "types/mysql2",
"@types/node": "^10.12.19",
"@types/redis": "^2.8.10",
"@types/ws": "^6.0.1",
"cookie": "^0.3.1",
"dotenv": "^16.0.1",
"hot-shots": "^6.2.0",
"jsonwebtoken": "^8.4.0",
"mysql2": "^1.6.4",
"php-serialize": "^4.0.2",
"redis": "^2.8.0",
"source-map-support": "^0.5.10",
"typescript": "^4.4.2",
"ws": "^6.1.3"
"@types/node": "^18.0.0",
"@types/ws": "^8.5.5",
"cookie": "^0.5.0",
"dotenv": "^16.3.1",
"hot-shots": "^10.0.0",
"jsonwebtoken": "^9.0.1",
"mysql2": "^3.6.0",
"php-serialize": "^4.1.1",
"redis": "^4.6.7",
"source-map-support": "^0.5.21",
"typescript": "^5.1.6",
"ws": "^8.13.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.31.0",
"@typescript-eslint/parser": "^4.31.0",
"eslint": "^7.15.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsdoc": "^30.7.8",
"eslint-plugin-typescript-sort-keys": "^1.7.0"
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
"eslint": "^8.47.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsdoc": "^46.5.0",
"eslint-plugin-typescript-sort-keys": "^2.3.0"
}
}
25 changes: 5 additions & 20 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as fs from 'fs';
import * as path from 'path';
import * as dotenv from 'dotenv';
import { PoolOptions as DbConfig } from 'mysql2';
import { ClientOpts as RedisConfig, RetryStrategyOptions as RedisRetryStrategyOptions } from 'redis';
import { RedisClientOptions } from 'redis';
import { ServerOptions as ServerConfig } from 'ws';

interface Config {
Expand All @@ -25,8 +25,8 @@ interface DbNames {
}

interface RedisConfigs {
app: RedisConfig;
notification: RedisConfig;
app: RedisClientOptions;
notification: RedisClientOptions;
}

let baseDir = process.env.WEBSOCKET_BASEDIR;
Expand All @@ -44,17 +44,6 @@ if (typeof process.env.APP_KEY !== 'string') {
throw new Error('APP_KEY environment variable is not set.');
}

const redisRetry = (type: string) => (options: RedisRetryStrategyOptions) => {
const wait = 1000; // in milliseconds
const maxAttempts = 60;

if (options.attempt > maxAttempts) {
throw new Error(`Failed connecting to redis (${type})`);
}

return wait;
};

const config: Config = {
appKey: process.env.APP_KEY,
baseDir,
Expand All @@ -75,14 +64,10 @@ const config: Config = {
: Buffer.from(process.env.PASSPORT_PUBLIC_KEY),
redis: {
app: {
host: process.env.REDIS_HOST,
port: +(process.env.REDIS_PORT || 6379),
retry_strategy: redisRetry('app'),
url: `redis://${process.env.REDIS_HOST}:${process.env.REDIS_PORT || 6379}/0`,
},
notification: {
host: process.env.NOTIFICATION_REDIS_HOST,
port: +(process.env.NOTIFICATION_REDIS_PORT || 6379),
retry_strategy: redisRetry('notification'),
url: `redis://${process.env.NOTIFICATION_REDIS_HOST}:${process.env.NOTIFICATION_REDIS_PORT || 6379}/0`,
},
},
server: {
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ const getUserSession = async (req: http.IncomingMessage) => {
if (userSession == null) {
userSession = await laravelSession.verifyRequest(req);
}
} catch (err) {
failReason = err.message;
} catch (err: unknown) {
failReason = (err as Error).message;
}

if (userSession == null) {
Expand Down
25 changes: 15 additions & 10 deletions src/laravel-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
import * as crypto from 'crypto';
import * as http from 'http';
import * as url from 'url';
import { promisify } from 'util';
import * as cookie from 'cookie';
import { unserialize } from 'php-serialize';
import * as redis from 'redis';
import { RedisClientOptions, RedisClientType, RedisScripts, RedisFunctions, RedisModules, createClient as redisCreateClient } from 'redis';

interface Params {
appKey: string;
redisConfig: redis.ClientOpts;
redisConfig: RedisClientOptions;
}

interface EncryptedSession {
Expand All @@ -20,6 +19,14 @@ interface EncryptedSession {
value: string;
}

interface LaravelSessionItem {
_token: string;
// login_<authName>_<hashedAuthClass>
login_web_59ba36addc2b2f9401580f014c7f58ea4e30989d: number;
requires_verification: boolean;
verified: boolean;
}

interface Session {
csrf: string;
key: string;
Expand Down Expand Up @@ -49,13 +56,12 @@ const getCookie = (req: http.IncomingMessage, key: string) => {

export default class LaravelSession {
private key: Buffer;
private redis: redis.RedisClient;
private redisGet: any;
private redis: RedisClientType<RedisModules, RedisFunctions, RedisScripts>;
private sessionCookieNameHmac: Buffer;

constructor(params: Params) {
this.redis = redis.createClient(params.redisConfig);
this.redisGet = promisify(this.redis.get).bind(this.redis);
this.redis = redisCreateClient(params.redisConfig);
void this.redis.connect();
this.key = Buffer.from(params.appKey.slice('base64:'.length), 'base64');
// https://github.com/laravel/framework/blob/208c3976f186dcdfa0a434f4092bae7d32928465/src/Illuminate/Cookie/CookieValuePrefix.php
this.sessionCookieNameHmac = crypto.createHmac('sha1', this.key).update(`${sessionCookieName}v2`).digest();
Expand All @@ -68,11 +74,10 @@ export default class LaravelSession {
return null;
}

const serializedData = await this.redisGet(key);
const serializedData = await this.redis.get(key) as string;

const rawData = unserialize(unserialize(serializedData), {}, {strict: false});
const rawData = unserialize(unserialize(serializedData ?? '') as string, {}, { strict: false }) as LaravelSessionItem;

// login_<authName>_<hashedAuthClass>
const userId = rawData.login_web_59ba36addc2b2f9401580f014c7f58ea4e30989d;

return {
Expand Down
7 changes: 4 additions & 3 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@
import config from './config';
import noop from './noop';

function log(level: string, ...args: any) {
function log(level: string, ...args: unknown[]) {
// eslint-disable-next-line no-console
return console.log(`[${(new Date()).toJSON()}][${level}]`, ...args);
}

let debug;
if (config.debug) {
debug = (...args: any) => log('debug', ...args);
debug = (...args: unknown[]) => log('debug', ...args);
} else {
debug = noop;
}

const info = (...args: any) => log('info', ...args);
const info = (...args: unknown[]) => log('info', ...args);

const logger = {debug, info};

Expand Down
6 changes: 5 additions & 1 deletion src/oauth-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ export default class OAuthVerifier {
return null;
}

const [rows] = await this.db.execute<mysql.RowDataPacket[]>(`
interface AccessTokenRow extends mysql.RowDataPacket {
scopes: string;
user_id: number;
}
const [rows] = await this.db.execute<AccessTokenRow[]>(`
SELECT user_id, scopes
FROM oauth_access_tokens
WHERE revoked = false AND expires_at > now() AND id = ?
Expand Down
73 changes: 34 additions & 39 deletions src/redis-subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,27 @@
// See the LICENCE file in the repository root for full licence text.

import { StatsD } from 'hot-shots';
import * as redis from 'redis';
import { RedisClientOptions, RedisClientType, RedisScripts, RedisFunctions, RedisModules, createClient } from 'redis';
import logger from './logger';
import Message from './types/message';
import UserConnection from './user-connection';

interface Params {
dogstatsd: StatsD;
redisConfig: redis.ClientOpts;
}

interface UserConnections {
[key: string]: Set<UserConnection>;
redisConfig: RedisClientOptions;
}

export default class RedisSubscriber {
private dogstatsd: StatsD;
private redis: redis.RedisClient;
private userConnections: UserConnections = {};
private redis: RedisClientType<RedisModules, RedisFunctions, RedisScripts>;
private userConnections: Partial<Record<string, Set<UserConnection>>> = {};

constructor(params: Params) {
this.dogstatsd = params.dogstatsd;
this.redis = redis.createClient(params.redisConfig);
this.redis.on('message', this.onMessage);
this.redis = createClient(params.redisConfig);
void this.redis.connect();
}

onMessage = (channel: string, messageString: string) => {
logger.debug(`received message from channel ${channel}`);

const connections = this.userConnections[channel];

if (connections == null || connections.size === 0) {
return;
}

try {
// assume typing is correct if it parses, for now.
const message = JSON.parse(messageString) as Message;

connections.forEach((connection) => connection.event(channel, messageString, message));
this.dogstatsd.increment('sent', connections.size, { event: message.event });
} catch {
// do nothing
// TODO: log error?
}
};

subscribe(channels: string | string[], connection: UserConnection) {
if (!connection.isActive) {
return;
Expand All @@ -60,19 +35,18 @@ export default class RedisSubscriber {
const toSubscribe = [];

for (const channel of channels) {
if (this.userConnections[channel] == null) {
this.userConnections[channel] = new Set();
}

if (this.userConnections[channel].size === 0) {
const set = this.userConnections[channel] ??= new Set();
if (set.size === 0) {
toSubscribe.push(channel);
}

this.userConnections[channel].add(connection);
set.add(connection);
}

if (toSubscribe.length > 0) {
this.redis.subscribe(...toSubscribe);
for (const channel of toSubscribe) {
void this.redis.subscribe(channel, this.onMessage);
}
}
}

Expand Down Expand Up @@ -101,7 +75,28 @@ export default class RedisSubscriber {
}

if (toUnsubscribe.length > 0) {
this.redis.unsubscribe(...toUnsubscribe);
void this.redis.unsubscribe(toUnsubscribe);
}
}

private readonly onMessage = (messageString: string, channel: string) => {
logger.debug(`received message from channel ${channel}`);

const connections = this.userConnections[channel];

if (connections == null || connections.size === 0) {
return;
}

try {
// assume typing is correct if it parses, for now.
const message = JSON.parse(messageString) as Message;

connections.forEach((connection) => connection.event(channel, messageString, message));
this.dogstatsd.increment('sent', connections.size, { event: message.event });
} catch {
// do nothing
// TODO: log error?
}
};
}
Loading

0 comments on commit 58d8197

Please sign in to comment.