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 { ErrorResponse } from '../../api/util.js';
import Users, { CoralUser } from '../../common/users.js';
import { EventStreamResponse, HttpServer, ResponseError } from '../util/http-server.js';
declare global {
namespace Express {
@ -90,7 +91,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
const FRIEND_CODE = /^\d{4}-\d{4}-\d{4}$/;
class Server {
class Server extends HttpServer {
require_token = true;
update_interval = 30 * 1000;
@ -104,6 +105,8 @@ class Server {
readonly storage: persist.LocalStorage,
readonly users: Users<CoralUser>,
) {
super();
const app = this.app = express();
app.use('/api/znc', (req, res, next) => {
@ -118,64 +121,64 @@ class Server {
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,
this.createApiRequestHandler(r => this.handleTokenRequest(r)));
this.createProxyRequestHandler(r => this.handleTokenRequest(r)));
app.delete('/api/znc/token', this.authTokenMiddleware,
this.createApiRequestHandler(r => this.handleDeleteTokenRequest(r)));
app.get('/api/znc/tokens', this.createApiRequestHandler(r => this.handleTokensRequest(r), true));
this.createProxyRequestHandler(r => this.handleDeleteTokenRequest(r)));
app.get('/api/znc/tokens', this.createProxyRequestHandler(r => this.handleTokensRequest(r), true));
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,
this.createApiRequestHandler(r => this.handleCreateTokenRequest(r), true));
this.createProxyRequestHandler(r => this.handleCreateTokenRequest(r), true));
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,
this.createApiRequestHandler(r => this.handleUserPresenceRequest(r)));
this.createProxyRequestHandler(r => this.handleUserPresenceRequest(r)));
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,
this.createApiRequestHandler(r => this.handleFavouriteFriendsRequest(r)));
this.createProxyRequestHandler(r => this.handleFavouriteFriendsRequest(r)));
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,
this.createApiRequestHandler(r => this.handleFavouriteFriendsPresenceRequest(r)));
this.createProxyRequestHandler(r => this.handleFavouriteFriendsPresenceRequest(r)));
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(),
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,
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,
this.createApiRequestHandler(r => this.handleWebServicesRequest(r)));
this.createProxyRequestHandler(r => this.handleWebServicesRequest(r)));
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,
this.createApiRequestHandler(r => this.handleActiveEventRequest(r)));
this.createProxyRequestHandler(r => this.handleActiveEventRequest(r)));
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',
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,
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,
this.createApiRequestHandler(r => this.handleFriendCodeUrlRequest(r), true));
this.createProxyRequestHandler(r => this.handleFriendCodeUrlRequest(r), true));
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 createApiRequestHandler(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: true): RequestHandler
protected createProxyRequestHandler(callback: (data: RequestData) => Promise<{} | void>, auth?: boolean): RequestHandler
protected createProxyRequestHandler(callback: (data: RequestDataWithUser) => Promise<{} | void>, auth = false) {
return async (req: Request, res: Response) => {
try {
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>(
id: string, callback: () => Promise<T>,
promises: Map<string, Promise<[number, T]>>,
@ -765,9 +740,6 @@ class Server {
//
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 i = new ZncNotifications(this.storage, na_session_token, user.nso, user.data, user);
@ -775,7 +747,8 @@ class Server {
i.friend_notifications = true;
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 {
await i.loop(true);
@ -786,7 +759,7 @@ class Server {
this.resetAuthTimeout(na_session_token, () => user.data.user.id);
}
} catch (err) {
es.sendEvent('error', {
stream.sendEvent('error', {
error: (err as Error).name,
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) {
return Math.floor(((updated_timestamp_ms + update_interval_ms) - Date.now()) / 1000);
}
class EventStreamNotificationManager extends NotificationManager {
constructor(
public req: Request,
public res: Response
) {
constructor(readonly stream: EventStreamResponse) {
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(
friend: CurrentUser | Friend, prev?: CurrentUser | Friend, type?: PresenceEvent,
naid?: string, ir?: boolean
) {
this.sendEvent(ZncPresenceEventStreamEvent.PRESENCE_UPDATED, {
this.stream.sendEvent(ZncPresenceEventStreamEvent.PRESENCE_UPDATED, {
id: friend.nsaId, presence: friend.presence, prev: prev?.presence,
});
}
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,
});
}
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,
});
}
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,
});
}
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,
});
}

View File

@ -13,6 +13,7 @@ import Users, { CoralUser } from '../common/users.js';
import { Friend } from '../api/coral-types.js';
import { getBulletToken, SavedBulletToken } from '../common/auth/splatnet3.js';
import SplatNet3Api from '../api/splatnet3.js';
import { HttpServer, ResponseError } from './util/http-server.js';
const debug = createDebug('cli:presence-server');
@ -172,7 +173,7 @@ export class SplatNet3User {
}
}
class Server {
class Server extends HttpServer {
allow_all_users = false;
update_interval = 30 * 1000;
@ -184,6 +185,8 @@ class Server {
readonly splatnet3_users: Users<SplatNet3User> | null,
readonly user_ids: string[],
) {
super();
const app = this.app = express();
app.use('/api/presence', (req, res, next) => {
@ -204,32 +207,6 @@ class Server {
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) {
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(
fest: Fest_schedule, vote_team?: string, state?: FestVoteState | null
): 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');
}
}