mirror of
https://github.com/Terncode/pixel.horse.git
synced 2026-09-24 13:55:04 +02:00
419 lines
14 KiB
TypeScript
419 lines
14 KiB
TypeScript
import './boot';
|
|
import * as fs from 'fs';
|
|
import * as Bluebird from 'bluebird';
|
|
import * as mongoose from 'mongoose';
|
|
import * as http from 'http';
|
|
import * as morgan from 'morgan';
|
|
import * as bodyParser from 'body-parser';
|
|
import * as expressSession from 'express-session';
|
|
import * as serveFavicon from 'serve-favicon';
|
|
import * as Rollbar from 'rollbar';
|
|
import * as passport from 'passport';
|
|
import * as connectMongo from 'connect-mongo';
|
|
import * as express from 'express';
|
|
// import { WebSocketServer } from '@clusterws/cws';
|
|
import { WebSocketServer } from 'clusterws-uws';
|
|
import { compact, once } from 'lodash';
|
|
import { copySync, removeSync, ensureDirSync } from 'fs-extra';
|
|
import { createServerHost, createClientOptions, ServerOptions, ClientExtensions, Packet } from 'ag-sockets';
|
|
import { config, port, server, args, version } from './config';
|
|
import { YEAR, WEEK } from '../common/constants';
|
|
import { rollbarCheckIgnore } from '../common/rollbar';
|
|
import { isBanned } from '../common/adminUtils';
|
|
import { includes } from '../common/utils';
|
|
import { STAMP } from '../generated/hash';
|
|
import { ServerActions } from './serverActions';
|
|
import { AdminServerActions } from './adminServerActions';
|
|
import { IAccount, Account } from './db';
|
|
import { logger } from './logger';
|
|
import { settings, reloadSettings } from './settings';
|
|
import { SocketErrorHandler } from './utils/socketErrorHandler';
|
|
import { tokenService } from './serverUtils';
|
|
import {
|
|
admin as isAdmin, auth, blockMaps, wrapApi, internal, initLogRequest, notFound, inMemoryStaticFiles
|
|
} from './requestUtils';
|
|
import { StatsTracker } from './stats';
|
|
import { start } from './start';
|
|
import { createServerActionsFactory } from './serverActionsManager';
|
|
import { init, createRemovedDocument } from './internal';
|
|
import {
|
|
pollServers, pollDiskSpace, pollCertificateExpirationDate, startBansCleanup, pollMemoryUsage,
|
|
startMergesCleanup, startStrayAuthsCleanup, startClearOldIgnores, startCollectingUsersVisitedCount,
|
|
startSupporterInvitesCleanup, startPotentialDuplicatesCleanup, startAccountAlertsCleanup,
|
|
startClearTo10Origns, startClearVeryOldOrigns
|
|
} from './polling';
|
|
import { pathTo } from './paths';
|
|
import { liveSettings } from './liveSettings';
|
|
import { createIndex } from './routes/index';
|
|
import { authRoutes } from './routes/auth';
|
|
import api from './routes/api';
|
|
import api1 from './routes/api1';
|
|
import api2 from './routes/api2';
|
|
import apiTools from './routes/api-tools';
|
|
import { createInternalApi } from './api/internal';
|
|
import { createInternalLoginApi } from './api/internal-login';
|
|
import { initLogSwearingAndSpamming } from './api/admin-accounts';
|
|
import { InternalAdminApi } from './api/internal-admin';
|
|
import { AdminService } from './services/adminService';
|
|
import { createEndPoints } from './api/admin';
|
|
import { World } from './world';
|
|
import { ClientActionsTemplate } from '../common/clientActionsTemplate';
|
|
import { ClientAdminActionsTemplate } from '../common/clientAdminActionsTemplate';
|
|
|
|
function getServiceWorker() {
|
|
try {
|
|
return fs.readFileSync(pathTo('build', 'sw.min.js'));
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
mongoose.connect(config.db, {
|
|
reconnectTries: Number.MAX_VALUE,
|
|
useNewUrlParser: true,
|
|
useCreateIndex: true,
|
|
useFindAndModify: false,
|
|
});
|
|
|
|
const MongoStore = connectMongo(expressSession);
|
|
const app = express();
|
|
const production = app.get('env') === 'production';
|
|
const maxAge = production ? YEAR : 0;
|
|
const etag = false;
|
|
const limit = !production || args.tools ? '100mb' : '100kb';
|
|
|
|
Bluebird.config({ warnings: false, longStackTraces: !production });
|
|
|
|
const rollbar = config.rollbar && Rollbar.init({
|
|
accessToken: config.rollbar.serverToken,
|
|
environment: config.rollbar.environment,
|
|
handleUncaughtExceptions: true,
|
|
handleUnhandledRejections: true,
|
|
captureUncaught: true,
|
|
checkIgnore: rollbarCheckIgnore,
|
|
} as any);
|
|
|
|
let assetsPath = pathTo('build', 'assets');
|
|
let adminAssetsPath = pathTo('build', 'assets-admin');
|
|
|
|
ensureDirSync(pathTo('build-copy'));
|
|
|
|
if (production && args.login) {
|
|
const newAssetsPath = pathTo('build-copy', 'assets');
|
|
removeSync(newAssetsPath);
|
|
copySync(assetsPath, newAssetsPath);
|
|
assetsPath = newAssetsPath;
|
|
}
|
|
|
|
if (production && args.admin) {
|
|
const newAssetsPath = pathTo('build-copy', 'assets-admin');
|
|
removeSync(newAssetsPath);
|
|
copySync(adminAssetsPath, newAssetsPath);
|
|
adminAssetsPath = newAssetsPath;
|
|
}
|
|
|
|
app.set('port', port);
|
|
app.set('views', pathTo('views'));
|
|
app.set('view engine', 'pug');
|
|
app.set('view options', { doctype: 'html' });
|
|
app.set('x-powered-by', false);
|
|
app.set('etag', false);
|
|
|
|
if (config.proxy) {
|
|
app.set('trust proxy', config.proxy);
|
|
}
|
|
|
|
if (production) {
|
|
app.use(require('hsts')({ maxAge }));
|
|
app.use(require('frameguard')({ action: 'sameorigin' }));
|
|
// app.use(require('shrink-ray-current')());
|
|
}
|
|
|
|
if (args.login || args.admin) {
|
|
app.use(serveFavicon(pathTo('favicons', 'favicon.ico')));
|
|
}
|
|
|
|
app.use(morgan('dev', { skip: (_, res) => res.statusCode < 500 || res.statusCode === 503 }));
|
|
|
|
const serviceWorker = getServiceWorker();
|
|
|
|
if (serviceWorker) {
|
|
app.get('/sw.js', (_, res) => {
|
|
res.setHeader('Content-Type', 'application/javascript');
|
|
res.setHeader('Cache-Control', 'public, max-age=0');
|
|
res.send(serviceWorker);
|
|
});
|
|
} else {
|
|
app.get('/sw.js', notFound);
|
|
}
|
|
|
|
if (args.login || args.admin) {
|
|
if (production) {
|
|
app.use(inMemoryStaticFiles(assetsPath, '/assets', maxAge));
|
|
}
|
|
|
|
app.use('/assets', blockMaps(DEVELOPMENT, !!args.local), express.static(assetsPath, { maxAge, etag }));
|
|
app.use(express.static(pathTo('public'), { maxAge, etag }));
|
|
app.use(express.static(pathTo('favicons'), { maxAge, etag }));
|
|
}
|
|
|
|
app.use(bodyParser.json({ type: ['json', 'application/csp-report'], limit }));
|
|
app.use(bodyParser.urlencoded({ extended: true, limit }));
|
|
app.use(require('cookie-parser')());
|
|
|
|
if (args.login || args.admin) {
|
|
passport.serializeUser<IAccount, string>((account, done) => done(null, account._id.toString()));
|
|
passport.deserializeUser<IAccount | false, string>((id, done) =>
|
|
Account.findById(id, (err, a) => done(err, a && !isBanned(a) ? a : false)));
|
|
}
|
|
|
|
const ignore = [
|
|
'RangeNotSatisfiableError',
|
|
'PreconditionFailedError',
|
|
];
|
|
|
|
app.use((err: any, req: any, res: express.Response, next: any) => {
|
|
const ignored = err instanceof Error && includes(ignore, err.name);
|
|
return next(ignored ? null : err, req, res);
|
|
});
|
|
|
|
if (rollbar) {
|
|
app.use(rollbar.errorHandler());
|
|
}
|
|
|
|
if (!production) {
|
|
app.use('/assets-admin', express.static(pathTo('assets')));
|
|
app.use('/assets-admin', express.static(pathTo('src')));
|
|
app.use('/assets', express.static(pathTo('assets')));
|
|
app.use('/assets', express.static(pathTo('src')));
|
|
app.use(require('errorhandler')());
|
|
}
|
|
|
|
const httpServer = http.createServer(app);
|
|
const errorHandler = new SocketErrorHandler(rollbar, server);
|
|
const createSession = () => expressSession({
|
|
secret: config.secret,
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
cookie: {
|
|
maxAge: WEEK * 2,
|
|
},
|
|
store: new MongoStore({ mongooseConnection: mongoose.connection }),
|
|
});
|
|
|
|
const statsPath = pathTo('logs', `stats-${server.id}.csv`);
|
|
const stats = new StatsTracker(statsPath);
|
|
const sessionMiddlewares = once(() => [createSession(), passport.initialize(), passport.session()] as express.RequestHandler[]);
|
|
const adminMiddlewares = once(() => [...sessionMiddlewares(), isAdmin(server)]);
|
|
const socketOptionsBase: ServerOptions = {
|
|
ws: { Server: WebSocketServer },
|
|
hash: STAMP,
|
|
};
|
|
|
|
initLogRequest(stats.logRequest);
|
|
initLogSwearingAndSpamming(stats.logSwearing, stats.logSpamming);
|
|
|
|
const host = createServerHost(httpServer, {
|
|
path: args.standaloneadmin && !args.game ? '/admin/ws-admin' : server.path,
|
|
ws: { Server: WebSocketServer },
|
|
perMessageDeflate: false,
|
|
errorHandler,
|
|
});
|
|
|
|
let theWorld: World | undefined = undefined;
|
|
let sent = 0, received = 0;
|
|
let sentPackets = 0, receivedPackets = 0;
|
|
|
|
if (args.game) {
|
|
const getSettings = () => settings.servers[server.id] || {};
|
|
|
|
const { world, createServerActions, hiding } = createServerActionsFactory(
|
|
server, settings, getSettings, {
|
|
stats: () => {
|
|
const result = { sent, received, sentPackets, receivedPackets };
|
|
sent = 0;
|
|
received = 0;
|
|
sentPackets = 0;
|
|
receivedPackets = 0;
|
|
return result;
|
|
}
|
|
});
|
|
|
|
const options = {
|
|
...socketOptionsBase,
|
|
verifyClient: () => !getSettings().isServerOffline && !liveSettings.shutdown,
|
|
forceBinary: true,
|
|
onSend: (packet: Packet) => {
|
|
sent += packet.binary ? packet.binary.byteLength : (packet.json ? packet.json.length : 0);
|
|
sentPackets++;
|
|
stats.logSendStats(packet);
|
|
},
|
|
onRecv: (packet: Packet) => {
|
|
received += packet.binary ? packet.binary.byteLength : (packet.json ? packet.json.length : 0);
|
|
receivedPackets++;
|
|
stats.logRecvStats(packet);
|
|
},
|
|
};
|
|
|
|
const gameSocket = host.socket(ServerActions, ClientActionsTemplate, createServerActions as any, options);
|
|
const tokens = tokenService(gameSocket);
|
|
|
|
start(world, server);
|
|
init(world, tokens);
|
|
|
|
theWorld = world;
|
|
|
|
const apiInternal = createInternalApi(
|
|
world, server, reloadSettings, getSettings, tokens, hiding, stats, liveSettings);
|
|
app.use('/api-internal', internal(config, server), wrapApi(server, apiInternal));
|
|
}
|
|
|
|
const endPoints = args.admin ? createEndPoints() : undefined;
|
|
const adminService = args.admin ? new AdminService() : undefined;
|
|
const removedDocument = createRemovedDocument(endPoints, adminService);
|
|
const index = createIndex(assetsPath, adminAssetsPath);
|
|
|
|
if (args.admin) {
|
|
if (args.standaloneadmin) {
|
|
app.use('/admin/assets-admin', ...adminMiddlewares(), express.static(adminAssetsPath, { maxAge, etag }));
|
|
app.get('/admin/assets-admin/*', (_, res) => res.sendStatus(404));
|
|
|
|
const adminApi = new InternalAdminApi(adminService!, endPoints!);
|
|
app.use('/api-internal-admin', internal(config, server), wrapApi(server, adminApi));
|
|
}
|
|
|
|
const createClient = (client: ClientAdminActionsTemplate & ClientExtensions) =>
|
|
new AdminServerActions(client, server, settings, adminService!, endPoints!, removedDocument);
|
|
|
|
const base = '/admin';
|
|
const assetsBase = args.standaloneadmin ? '/admin' : '';
|
|
const adminSocket = host.socket(AdminServerActions, ClientAdminActionsTemplate, createClient, socketOptionsBase);
|
|
const sendAdminPage = index.admin(production, `${base}/`, assetsBase, 'bootstrap-admin.js', adminSocket);
|
|
|
|
app.get(`${base}`, ...adminMiddlewares(), sendAdminPage);
|
|
app.get(`${base}/*`, ...adminMiddlewares(), sendAdminPage);
|
|
}
|
|
|
|
if (args.tools) {
|
|
const toolsPage = index.user(
|
|
production, '/tools/', 'style-tools.css', 'bootstrap-tools.js', 'bootstrap-tools.js', undefined, true, !!args.local, false);
|
|
|
|
app.get('/tools', ...sessionMiddlewares(), auth, (_, res) => res.send(toolsPage.page));
|
|
app.get('/tools/*', ...sessionMiddlewares(), auth, (_, res) => res.send(toolsPage.page));
|
|
|
|
app.use('/api-tools', ...sessionMiddlewares(), apiTools(server, settings, theWorld));
|
|
app.get('/api-tools/*', (_, res) => res.sendStatus(404));
|
|
}
|
|
|
|
if (args.login) {
|
|
const socketOptions = createClientOptions(ServerActions, ClientActionsTemplate, socketOptionsBase);
|
|
const userPage = index.user(
|
|
production, '/', 'style.css', 'bootstrap.js', 'bootstrap-es.js', socketOptions, false, !!args.local, !production);
|
|
const offlinePage = fs.readFileSync(pathTo('public', 'offline.html'), 'utf8');
|
|
|
|
const script = `${config.host}${index.getRevScript('bootstrap.js')}`;
|
|
const scriptES = `${config.host}${index.getRevScript('bootstrap-es.js')}`;
|
|
const analytics = config.analytics ? 'https://www.google-analytics.com' : '';
|
|
// const workbox = 'https://storage.googleapis.com/workbox-cdn';
|
|
// const rollbarScripts =
|
|
// rollbar ? 'https://d37gvrvc0wt4s1.cloudfront.net https://cdnjs.cloudflare.com/ajax/libs/rollbar.js/' : '';
|
|
const csp = `object-src 'none';`
|
|
+ `frame-src 'self';`
|
|
+ `frame-ancestors 'self';`
|
|
+ `worker-src ${config.host}sw.js;`
|
|
+ `script-src 'unsafe-eval' ${script} ${scriptES} ${analytics};`
|
|
// + `report-uri /api2/csp`
|
|
;
|
|
|
|
const linkPreloads: string[] = [
|
|
...userPage.preload,
|
|
];
|
|
|
|
app.use('/assets-admin', ...adminMiddlewares(), express.static(adminAssetsPath, { maxAge, etag }));
|
|
app.use('/auth', ...sessionMiddlewares(), authRoutes(
|
|
config.host, server, settings, liveSettings, args.local || DEVELOPMENT, removedDocument));
|
|
app.use('/api', ...sessionMiddlewares(), api(
|
|
server, settings, { version, host: config.host, debug: DEVELOPMENT, local: !!args.local }, removedDocument));
|
|
app.use('/api1', ...sessionMiddlewares(), api1(server, settings));
|
|
app.use('/api2', api2(settings, liveSettings, stats));
|
|
|
|
const loginApi = createInternalLoginApi(settings, liveSettings, stats, reloadSettings, removedDocument);
|
|
app.use('/api-internal-login', internal(config, server), wrapApi(server, loginApi));
|
|
|
|
app.get('/assets-admin/*', notFound);
|
|
app.get('/assets/*', notFound);
|
|
app.get('/auth/*', notFound);
|
|
app.get('/api/*', notFound);
|
|
app.get('/api1/*', notFound);
|
|
app.get('/api2/*', notFound);
|
|
|
|
app.get('/*', (req, res) => {
|
|
if (settings.isPageOffline) {
|
|
res.send(offlinePage);
|
|
} else {
|
|
if (production && !args.local) {
|
|
res.setHeader('Content-Security-Policy', csp);
|
|
res.setHeader('Link', linkPreloads);
|
|
}
|
|
|
|
res.setHeader('Referrer-Policy', 'no-referrer');
|
|
// res.setHeader('X-Frame-Options', 'DENY');
|
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
res.send(userPage.page);
|
|
|
|
stats.logRequest(req, userPage.page, '/');
|
|
}
|
|
});
|
|
}
|
|
|
|
app.use((err: any, req: any, res: express.Response, next: any) => {
|
|
if (err instanceof URIError) {
|
|
res.redirect(config.host);
|
|
} else {
|
|
return next(err, req, res);
|
|
}
|
|
});
|
|
|
|
reloadSettings().then(() => {
|
|
if (args.login || args.game) {
|
|
stats.startStatTracking();
|
|
}
|
|
|
|
if (args.login || args.admin) {
|
|
pollServers();
|
|
}
|
|
|
|
if (args.admin && !args.nocleanup) {
|
|
startStrayAuthsCleanup(removedDocument);
|
|
startClearOldIgnores();
|
|
startMergesCleanup();
|
|
startBansCleanup();
|
|
startCollectingUsersVisitedCount();
|
|
startSupporterInvitesCleanup();
|
|
startPotentialDuplicatesCleanup(adminService!);
|
|
startAccountAlertsCleanup();
|
|
startClearTo10Origns(adminService!);
|
|
startClearVeryOldOrigns(adminService!);
|
|
}
|
|
|
|
if (args.admin) {
|
|
pollDiskSpace();
|
|
pollMemoryUsage();
|
|
pollCertificateExpirationDate();
|
|
}
|
|
|
|
httpServer.listen(port, () => {
|
|
const options = compact([
|
|
app.get('env'),
|
|
args.login && 'login',
|
|
args.admin && 'admin',
|
|
args.standaloneadmin && '(standaloneadmin)',
|
|
args.tools && 'tools',
|
|
args.game && `game:${server.id}`,
|
|
]);
|
|
|
|
logger.info(`Listening on port ${port} (${options.join(', ')})`);
|
|
});
|
|
});
|