Move HTTP server functions to a separate class

This commit is contained in:
Samuel Elliott 2022-10-30 04:27:12 +00:00
parent 9cb4ced59f
commit 516349e5df
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
3 changed files with 132 additions and 138 deletions

View File

@ -17,6 +17,7 @@ import { AuthPolicy, AuthToken, ZncPresenceEventStreamEvent } from '../../api/zn
import { addCliFeatureUserAgent } from '../../util/useragent.js'; import { addCliFeatureUserAgent } from '../../util/useragent.js';
import { ErrorResponse } from '../../api/util.js'; import { ErrorResponse } from '../../api/util.js';
import Users, { CoralUser } from '../../common/users.js'; import Users, { CoralUser } from '../../common/users.js';
import { EventStreamResponse, HttpServer, ResponseError } from '../util/http-server.js';
declare global { declare global {
namespace Express { namespace Express {
@ -90,7 +91,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
const FRIEND_CODE = /^\d{4}-\d{4}-\d{4}$/; const FRIEND_CODE = /^\d{4}-\d{4}-\d{4}$/;
class Server { class Server extends HttpServer {
require_token = true; require_token = true;
update_interval = 30 * 1000; update_interval = 30 * 1000;
@ -104,6 +105,8 @@ class Server {
readonly storage: persist.LocalStorage, readonly storage: persist.LocalStorage,
readonly users: Users<CoralUser>, readonly users: Users<CoralUser>,
) { ) {
super();
const app = this.app = express(); const app = this.app = express();
app.use('/api/znc', (req, res, next) => { app.use('/api/znc', (req, res, next) => {
@ -118,64 +121,64 @@ class Server {
next(); next();
}); });
app.get('/api/znc/auth', this.createApiRequestHandler(r => this.handleAuthRequest(r), true)); app.get('/api/znc/auth', this.createProxyRequestHandler(r => this.handleAuthRequest(r), true));
app.get('/api/znc/token', this.authTokenMiddleware, app.get('/api/znc/token', this.authTokenMiddleware,
this.createApiRequestHandler(r => this.handleTokenRequest(r))); this.createProxyRequestHandler(r => this.handleTokenRequest(r)));
app.delete('/api/znc/token', this.authTokenMiddleware, app.delete('/api/znc/token', this.authTokenMiddleware,
this.createApiRequestHandler(r => this.handleDeleteTokenRequest(r))); this.createProxyRequestHandler(r => this.handleDeleteTokenRequest(r)));
app.get('/api/znc/tokens', this.createApiRequestHandler(r => this.handleTokensRequest(r), true)); app.get('/api/znc/tokens', this.createProxyRequestHandler(r => this.handleTokensRequest(r), true));
app.post('/api/znc/tokens', bodyParser.json(), app.post('/api/znc/tokens', bodyParser.json(),
this.createApiRequestHandler(r => this.handleCreateTokenRequest(r), true)); this.createProxyRequestHandler(r => this.handleCreateTokenRequest(r), true));
app.get('/api/znc/announcements', this.authTokenMiddleware, this.localAuthMiddleware, app.get('/api/znc/announcements', this.authTokenMiddleware, this.localAuthMiddleware,
this.createApiRequestHandler(r => this.handleCreateTokenRequest(r), true)); this.createProxyRequestHandler(r => this.handleCreateTokenRequest(r), true));
app.get('/api/znc/user', this.authTokenMiddleware, this.localAuthMiddleware, app.get('/api/znc/user', this.authTokenMiddleware, this.localAuthMiddleware,
this.createApiRequestHandler(r => this.handleCurrentUserRequest(r))); this.createProxyRequestHandler(r => this.handleCurrentUserRequest(r)));
app.get('/api/znc/user/presence', this.authTokenMiddleware, this.localAuthMiddleware, app.get('/api/znc/user/presence', this.authTokenMiddleware, this.localAuthMiddleware,
this.createApiRequestHandler(r => this.handleUserPresenceRequest(r))); this.createProxyRequestHandler(r => this.handleUserPresenceRequest(r)));
app.get('/api/znc/friends', this.authTokenMiddleware, this.localAuthMiddleware, app.get('/api/znc/friends', this.authTokenMiddleware, this.localAuthMiddleware,
this.createApiRequestHandler(r => this.handleFriendsRequest(r))); this.createProxyRequestHandler(r => this.handleFriendsRequest(r)));
app.get('/api/znc/friends/favourites', this.authTokenMiddleware, this.localAuthMiddleware, app.get('/api/znc/friends/favourites', this.authTokenMiddleware, this.localAuthMiddleware,
this.createApiRequestHandler(r => this.handleFavouriteFriendsRequest(r))); this.createProxyRequestHandler(r => this.handleFavouriteFriendsRequest(r)));
app.get('/api/znc/friends/presence', this.authTokenMiddleware, this.localAuthMiddleware, app.get('/api/znc/friends/presence', this.authTokenMiddleware, this.localAuthMiddleware,
this.createApiRequestHandler(r => this.handleFriendsPresenceRequest(r))); this.createProxyRequestHandler(r => this.handleFriendsPresenceRequest(r)));
app.get('/api/znc/friends/favourites/presence', this.authTokenMiddleware, this.localAuthMiddleware, app.get('/api/znc/friends/favourites/presence', this.authTokenMiddleware, this.localAuthMiddleware,
this.createApiRequestHandler(r => this.handleFavouriteFriendsPresenceRequest(r))); this.createProxyRequestHandler(r => this.handleFavouriteFriendsPresenceRequest(r)));
app.get('/api/znc/friend/:nsaid', this.authTokenMiddleware, this.localAuthMiddleware, app.get('/api/znc/friend/:nsaid', this.authTokenMiddleware, this.localAuthMiddleware,
this.createApiRequestHandler(r => this.handleFriendRequest(r, r.req.params.nsaid))); this.createProxyRequestHandler(r => this.handleFriendRequest(r, r.req.params.nsaid)));
app.post('/api/znc/friend/:nsaid', bodyParser.json(), app.post('/api/znc/friend/:nsaid', bodyParser.json(),
this.createApiRequestHandler(r => this.handleUpdateFriendRequest(r, r.req.params.nsaid), true)); this.createProxyRequestHandler(r => this.handleUpdateFriendRequest(r, r.req.params.nsaid), true));
app.get('/api/znc/friend/:nsaid/presence', this.authTokenMiddleware, this.localAuthMiddleware, app.get('/api/znc/friend/:nsaid/presence', this.authTokenMiddleware, this.localAuthMiddleware,
this.createApiRequestHandler(r => this.handleFriendPresenceRequest(r, r.req.params.nsaid))); this.createProxyRequestHandler(r => this.handleFriendPresenceRequest(r, r.req.params.nsaid)));
app.get('/api/znc/webservices', this.authTokenMiddleware, this.localAuthMiddleware, app.get('/api/znc/webservices', this.authTokenMiddleware, this.localAuthMiddleware,
this.createApiRequestHandler(r => this.handleWebServicesRequest(r))); this.createProxyRequestHandler(r => this.handleWebServicesRequest(r)));
app.get('/api/znc/webservice/:id/token', app.get('/api/znc/webservice/:id/token',
this.createApiRequestHandler(r => this.handleWebServiceTokenRequest(r, r.req.params.id), true)); this.createProxyRequestHandler(r => this.handleWebServiceTokenRequest(r, r.req.params.id), true));
app.get('/api/znc/activeevent', this.authTokenMiddleware, this.localAuthMiddleware, app.get('/api/znc/activeevent', this.authTokenMiddleware, this.localAuthMiddleware,
this.createApiRequestHandler(r => this.handleActiveEventRequest(r))); this.createProxyRequestHandler(r => this.handleActiveEventRequest(r)));
app.get('/api/znc/event/:id', app.get('/api/znc/event/:id',
this.createApiRequestHandler(r => this.handleEventRequest(r, r.req.params.id), true)); this.createProxyRequestHandler(r => this.handleEventRequest(r, r.req.params.id), true));
app.get('/api/znc/user/:id', app.get('/api/znc/user/:id',
this.createApiRequestHandler(r => this.handleUserRequest(r, r.req.params.id), true)); this.createProxyRequestHandler(r => this.handleUserRequest(r, r.req.params.id), true));
app.get('/api/znc/friendcode/:friendcode', this.localAuthMiddleware, app.get('/api/znc/friendcode/:friendcode', this.localAuthMiddleware,
this.createApiRequestHandler(r => this.handleFriendCodeRequest(r, r.req.params.friendcode), true)); this.createProxyRequestHandler(r => this.handleFriendCodeRequest(r, r.req.params.friendcode), true));
app.get('/api/znc/friendcode', this.localAuthMiddleware, app.get('/api/znc/friendcode', this.localAuthMiddleware,
this.createApiRequestHandler(r => this.handleFriendCodeUrlRequest(r), true)); this.createProxyRequestHandler(r => this.handleFriendCodeUrlRequest(r), true));
app.get('/api/znc/presence/events', this.localAuthMiddleware, app.get('/api/znc/presence/events', this.localAuthMiddleware,
this.createApiRequestHandler(r => this.handlePresenceEventStreamRequest(r), true)); this.createProxyRequestHandler(r => this.handlePresenceEventStreamRequest(r), true));
} }
protected createApiRequestHandler(callback: (data: RequestDataWithUser) => Promise<{} | void>, auth: true): RequestHandler protected createProxyRequestHandler(callback: (data: RequestDataWithUser) => Promise<{} | void>, auth: true): RequestHandler
protected createApiRequestHandler(callback: (data: RequestData) => Promise<{} | void>, auth?: boolean): RequestHandler protected createProxyRequestHandler(callback: (data: RequestData) => Promise<{} | void>, auth?: boolean): RequestHandler
protected createApiRequestHandler(callback: (data: RequestDataWithUser) => Promise<{} | void>, auth = false) { protected createProxyRequestHandler(callback: (data: RequestDataWithUser) => Promise<{} | void>, auth = false) {
return async (req: Request, res: Response) => { return async (req: Request, res: Response) => {
try { try {
const user = req.coralUser ?? auth ? await this.getCoralUser(req) : undefined; const user = req.coralUser ?? auth ? await this.getCoralUser(req) : undefined;
@ -202,34 +205,6 @@ class Server {
}; };
} }
protected createApiMiddleware(
callback: (req: Request, res: Response) => Promise<void>
): RequestHandler {
return async (req: Request, res: Response, next: NextFunction) => {
try {
await callback.call(null, req, res);
next();
} catch (err) {
if (err instanceof ResponseError) {
err.sendResponse(req, res);
} else {
this.sendJsonResponse(res, {
error: err,
error_message: (err as Error).message,
}, 500);
}
}
};
}
protected sendJsonResponse(res: Response, data: {}, status?: number) {
if (status) res.statusCode = status;
res.setHeader('Content-Type', 'application/json');
res.end(res.req.headers['accept']?.match(/\/html\b/i) ?
JSON.stringify(data, null, 4) : JSON.stringify(data));
}
protected async _cache<T>( protected async _cache<T>(
id: string, callback: () => Promise<T>, id: string, callback: () => Promise<T>,
promises: Map<string, Promise<[number, T]>>, promises: Map<string, Promise<[number, T]>>,
@ -765,9 +740,6 @@ class Server {
// //
async handlePresenceEventStreamRequest({req, res, user}: RequestDataWithUser) { async handlePresenceEventStreamRequest({req, res, user}: RequestDataWithUser) {
res.setHeader('Cache-Control', 'no-store');
res.setHeader('Content-Type', 'text/event-stream');
const na_session_token = req.headers['authorization']!.substr(3); const na_session_token = req.headers['authorization']!.substr(3);
const i = new ZncNotifications(this.storage, na_session_token, user.nso, user.data, user); const i = new ZncNotifications(this.storage, na_session_token, user.nso, user.data, user);
@ -775,7 +747,8 @@ class Server {
i.friend_notifications = true; i.friend_notifications = true;
i.update_interval = this.update_interval / 1000; i.update_interval = this.update_interval / 1000;
const es = i.notifications = new EventStreamNotificationManager(req, res); const stream = new EventStreamResponse(req, res);
i.notifications = new EventStreamNotificationManager(stream);
try { try {
await i.loop(true); await i.loop(true);
@ -786,7 +759,7 @@ class Server {
this.resetAuthTimeout(na_session_token, () => user.data.user.id); this.resetAuthTimeout(na_session_token, () => user.data.user.id);
} }
} catch (err) { } catch (err) {
es.sendEvent('error', { stream.sendEvent('error', {
error: (err as Error).name, error: (err as Error).name,
error_message: (err as Error).message, error_message: (err as Error).message,
}); });
@ -794,71 +767,44 @@ class Server {
} }
} }
class ResponseError extends Error {
constructor(readonly status: number, readonly code: string, message?: string) {
super(message);
}
sendResponse(req: Request, res: Response) {
const data = {
error: this.code,
error_message: this.message,
};
res.statusCode = this.status;
res.setHeader('Content-Type', 'application/json');
res.end(req.headers['accept']?.match(/\/html\b/i) ?
JSON.stringify(data, null, 4) : JSON.stringify(data));
}
}
function cacheMaxAge(updated_timestamp_ms: number, update_interval_ms: number) { function cacheMaxAge(updated_timestamp_ms: number, update_interval_ms: number) {
return Math.floor(((updated_timestamp_ms + update_interval_ms) - Date.now()) / 1000); return Math.floor(((updated_timestamp_ms + update_interval_ms) - Date.now()) / 1000);
} }
class EventStreamNotificationManager extends NotificationManager { class EventStreamNotificationManager extends NotificationManager {
constructor( constructor(readonly stream: EventStreamResponse) {
public req: Request,
public res: Response
) {
super(); super();
} }
sendEvent(event: string | null, ...data: unknown[]) {
if (event) this.res.write('event: ' + event + '\n');
for (const d of data) this.res.write('data: ' + JSON.stringify(d) + '\n');
this.res.write('\n');
}
onPresenceUpdated( onPresenceUpdated(
friend: CurrentUser | Friend, prev?: CurrentUser | Friend, type?: PresenceEvent, friend: CurrentUser | Friend, prev?: CurrentUser | Friend, type?: PresenceEvent,
naid?: string, ir?: boolean naid?: string, ir?: boolean
) { ) {
this.sendEvent(ZncPresenceEventStreamEvent.PRESENCE_UPDATED, { this.stream.sendEvent(ZncPresenceEventStreamEvent.PRESENCE_UPDATED, {
id: friend.nsaId, presence: friend.presence, prev: prev?.presence, id: friend.nsaId, presence: friend.presence, prev: prev?.presence,
}); });
} }
onFriendOnline(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) { onFriendOnline(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) {
this.sendEvent(ZncPresenceEventStreamEvent.FRIEND_ONLINE, { this.stream.sendEvent(ZncPresenceEventStreamEvent.FRIEND_ONLINE, {
id: friend.nsaId, presence: friend.presence, prev: prev?.presence, id: friend.nsaId, presence: friend.presence, prev: prev?.presence,
}); });
} }
onFriendOffline(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) { onFriendOffline(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) {
this.sendEvent(ZncPresenceEventStreamEvent.FRIEND_OFFLINE, { this.stream.sendEvent(ZncPresenceEventStreamEvent.FRIEND_OFFLINE, {
id: friend.nsaId, presence: friend.presence, prev: prev?.presence, id: friend.nsaId, presence: friend.presence, prev: prev?.presence,
}); });
} }
onFriendPlayingChangeTitle(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) { onFriendPlayingChangeTitle(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) {
this.sendEvent(ZncPresenceEventStreamEvent.FRIEND_TITLE_CHANGE, { this.stream.sendEvent(ZncPresenceEventStreamEvent.FRIEND_TITLE_CHANGE, {
id: friend.nsaId, presence: friend.presence, prev: prev?.presence, id: friend.nsaId, presence: friend.presence, prev: prev?.presence,
}); });
} }
onFriendTitleStateChange(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) { onFriendTitleStateChange(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) {
this.sendEvent(ZncPresenceEventStreamEvent.FRIEND_TITLE_STATECHANGE, { this.stream.sendEvent(ZncPresenceEventStreamEvent.FRIEND_TITLE_STATECHANGE, {
id: friend.nsaId, presence: friend.presence, prev: prev?.presence, id: friend.nsaId, presence: friend.presence, prev: prev?.presence,
}); });
} }

View File

@ -13,6 +13,7 @@ import Users, { CoralUser } from '../common/users.js';
import { Friend } from '../api/coral-types.js'; import { Friend } from '../api/coral-types.js';
import { getBulletToken, SavedBulletToken } from '../common/auth/splatnet3.js'; import { getBulletToken, SavedBulletToken } from '../common/auth/splatnet3.js';
import SplatNet3Api from '../api/splatnet3.js'; import SplatNet3Api from '../api/splatnet3.js';
import { HttpServer, ResponseError } from './util/http-server.js';
const debug = createDebug('cli:presence-server'); const debug = createDebug('cli:presence-server');
@ -172,7 +173,7 @@ export class SplatNet3User {
} }
} }
class Server { class Server extends HttpServer {
allow_all_users = false; allow_all_users = false;
update_interval = 30 * 1000; update_interval = 30 * 1000;
@ -184,6 +185,8 @@ class Server {
readonly splatnet3_users: Users<SplatNet3User> | null, readonly splatnet3_users: Users<SplatNet3User> | null,
readonly user_ids: string[], readonly user_ids: string[],
) { ) {
super();
const app = this.app = express(); const app = this.app = express();
app.use('/api/presence', (req, res, next) => { app.use('/api/presence', (req, res, next) => {
@ -204,32 +207,6 @@ class Server {
this.handlePresenceRequest(req, res, req.params.user))); this.handlePresenceRequest(req, res, req.params.user)));
} }
sendJsonResponse(res: Response, data: {}, status?: number) {
if (status) res.statusCode = status;
res.setHeader('Content-Type', 'application/json');
res.end(res.req.headers['accept']?.match(/\/html\b/i) ?
JSON.stringify(data, replacer, 4) : JSON.stringify(data, replacer));
}
createApiRequestHandler(callback: (req: Request, res: Response) => Promise<{} | void>) {
return async (req: Request, res: Response) => {
try {
const result = await callback.call(null, req, res);
if (result) this.sendJsonResponse(res, result);
else res.end();
} catch (err) {
if (err instanceof ResponseError) {
err.sendResponse(req, res);
} else {
this.sendJsonResponse(res, {
error: err,
error_message: (err as Error).message,
}, 500);
}
}
};
}
async handleAllUsersRequest(req: Request, res: Response) { async handleAllUsersRequest(req: Request, res: Response) {
if (!this.allow_all_users) { if (!this.allow_all_users) {
@ -467,24 +444,6 @@ class Server {
} }
} }
class ResponseError extends Error {
constructor(readonly status: number, readonly code: string, message?: string) {
super(message);
}
sendResponse(req: Request, res: Response) {
const data = {
error: this.code,
error_message: this.message,
};
res.statusCode = this.status;
res.setHeader('Content-Type', 'application/json');
res.end(req.headers['accept']?.match(/\/html\b/i) ?
JSON.stringify(data, null, 4) : JSON.stringify(data));
}
}
function createScheduleFest( function createScheduleFest(
fest: Fest_schedule, vote_team?: string, state?: FestVoteState | null fest: Fest_schedule, vote_team?: string, state?: FestVoteState | null
): Fest_schedule { ): Fest_schedule {

View File

@ -0,0 +1,89 @@
import createDebug from 'debug';
import { NextFunction, Request, RequestHandler, Response } from 'express';
import { ErrorResponse } from '../../api/util.js';
const debug = createDebug('cli:util:http-server');
export class HttpServer {
protected createApiRequestHandler(callback: (req: Request, res: Response) => Promise<{} | void>, auth = false) {
return async (req: Request, res: Response) => {
try {
const result = await callback.call(null, req, res);
if (result) this.sendJsonResponse(res, result);
else res.end();
} catch (err) {
if (err instanceof ResponseError) {
err.sendResponse(req, res);
} else {
this.sendJsonResponse(res, {
error: err,
error_message: (err as Error).message,
}, 500);
}
}
};
}
protected createApiMiddleware(
callback: (req: Request, res: Response) => Promise<void>
): RequestHandler {
return async (req: Request, res: Response, next: NextFunction) => {
try {
await callback.call(null, req, res);
next();
} catch (err) {
if (err instanceof ResponseError) {
err.sendResponse(req, res);
} else {
this.sendJsonResponse(res, {
error: err,
error_message: (err as Error).message,
}, 500);
}
}
};
}
protected sendJsonResponse(res: Response, data: {}, status?: number) {
if (status) res.statusCode = status;
res.setHeader('Content-Type', 'application/json');
res.end(res.req.headers['accept']?.match(/\/html\b/i) ?
JSON.stringify(data, null, 4) : JSON.stringify(data));
}
}
export class ResponseError extends Error {
constructor(readonly status: number, readonly code: string, message?: string) {
super(message);
}
sendResponse(req: Request, res: Response) {
const data = {
error: this.code,
error_message: this.message,
};
res.statusCode = this.status;
res.setHeader('Content-Type', 'application/json');
res.end(req.headers['accept']?.match(/\/html\b/i) ?
JSON.stringify(data, null, 4) : JSON.stringify(data));
}
}
export class EventStreamResponse {
constructor(
readonly req: Request,
readonly res: Response,
) {
res.setHeader('Cache-Control', 'no-store');
res.setHeader('Content-Type', 'text/event-stream');
}
sendEvent(event: string | null, ...data: unknown[]) {
if (event) this.res.write('event: ' + event + '\n');
for (const d of data) this.res.write('data: ' + JSON.stringify(d) + '\n');
this.res.write('\n');
}
}