mirror of
https://github.com/zoriya/flood.git
synced 2025-12-06 07:16:18 +00:00
refactor: use fastify (#661)
This commit is contained in:
2979
package-lock.json
generated
2979
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -81,6 +81,8 @@
|
||||
"@emotion/babel-plugin": "^11.11.0",
|
||||
"@emotion/css": "^11.11.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@fastify/express": "^2.3.0",
|
||||
"@fastify/static": "^6.10.2",
|
||||
"@lingui/loader": "^3.17.2",
|
||||
"@lingui/react": "^3.17.2",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
||||
@@ -92,7 +94,6 @@
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/create-torrent": "^5.0.0",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/debug": "^4.1.8",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/geoip-country": "^4.0.0",
|
||||
@@ -104,7 +105,7 @@
|
||||
"@types/node": "^12.20.55",
|
||||
"@types/parse-torrent": "^5.8.4",
|
||||
"@types/passport": "^1.0.12",
|
||||
"@types/passport-jwt": "^3.0.8",
|
||||
"@types/passport-jwt": "^3.0.9",
|
||||
"@types/react": "^18.2.11",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@types/react-measure": "^2.0.8",
|
||||
@@ -136,8 +137,6 @@
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-shape": "^3.2.0",
|
||||
"debug": "^4.3.4",
|
||||
"esbuild-jest": "^0.5.0",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
@@ -149,6 +148,8 @@
|
||||
"express-rate-limit": "^6.7.0",
|
||||
"fast-json-patch": "^3.1.1",
|
||||
"fast-sort": "^3.4.0",
|
||||
"fastify": "^4.21.0",
|
||||
"fastify-type-provider-zod": "^1.1.9",
|
||||
"feedsub": "^0.7.8",
|
||||
"file-loader": "^6.2.0",
|
||||
"form-data": "^4.0.0",
|
||||
@@ -159,6 +160,7 @@
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"http-errors": "^2.0.0",
|
||||
"jest": "^28.1.3",
|
||||
"jest-esbuild": "^0.3.0",
|
||||
"js-file-download": "^0.4.12",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -176,7 +178,6 @@
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-loader": "^7.3.3",
|
||||
"prettier": "^2.8.8",
|
||||
"promise": "^8.3.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@@ -3,7 +3,7 @@ module.exports = {
|
||||
transform: {
|
||||
// transform ESM only package to CommonJS
|
||||
'^.+\\.(t|j)sx?$': [
|
||||
'esbuild-jest',
|
||||
'jest-esbuild',
|
||||
{
|
||||
format: 'cjs',
|
||||
},
|
||||
|
||||
129
server/app.ts
129
server/app.ts
@@ -1,129 +0,0 @@
|
||||
import bodyParser from 'body-parser';
|
||||
import compression from 'compression';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import express from 'express';
|
||||
import fs from 'fs';
|
||||
import morgan from 'morgan';
|
||||
import passport from 'passport';
|
||||
import path from 'path';
|
||||
import {Strategy} from 'passport-jwt';
|
||||
|
||||
import type {Request} from 'express';
|
||||
|
||||
import {authTokenSchema} from '@shared/schema/Auth';
|
||||
import paths from '@shared/config/paths';
|
||||
|
||||
import type {UserInDatabase} from '@shared/schema/Auth';
|
||||
|
||||
import apiRoutes from './routes/api';
|
||||
import config from '../config';
|
||||
import Users from './models/Users';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Express {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface User extends UserInDatabase {}
|
||||
}
|
||||
}
|
||||
|
||||
Users.bootstrapServicesForAllUsers();
|
||||
|
||||
const app = express();
|
||||
const servedPath = config.baseURI.endsWith('/') ? config.baseURI : `${config.baseURI}/`;
|
||||
|
||||
// Remove Express header
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
app.disable('x-powered-by');
|
||||
}
|
||||
|
||||
app.set('strict routing', true);
|
||||
app.set('trust proxy', 'loopback');
|
||||
|
||||
app.use(morgan('dev'));
|
||||
|
||||
if (config.serveAssets !== false) {
|
||||
// Disable ETag
|
||||
app.set('etag', false);
|
||||
|
||||
// Enable compression
|
||||
app.use(compression());
|
||||
|
||||
// Static assets
|
||||
app.use(servedPath, express.static(paths.appDist));
|
||||
|
||||
// Client app routes, serve index.html and client js will figure it out
|
||||
const html = fs.readFileSync(path.join(paths.appDist, 'index.html'), {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
// Prohibit caching of index.html as browser can use a fully cached asset
|
||||
// tree in some cases, which defeats cache busting by asset hashes.
|
||||
const headers = {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
Pragma: 'no-cache',
|
||||
Expires: '0',
|
||||
};
|
||||
|
||||
app.get(`${servedPath}login`, (_req, res) => {
|
||||
res.set(headers);
|
||||
res.send(html);
|
||||
});
|
||||
|
||||
app.get(`${servedPath}register`, (_req, res) => {
|
||||
res.set(headers);
|
||||
res.send(html);
|
||||
});
|
||||
|
||||
app.get(`${servedPath}overview`, (_req, res) => {
|
||||
res.set(headers);
|
||||
res.send(html);
|
||||
});
|
||||
} else {
|
||||
// no-op res.flush() as compression is not handled by Express
|
||||
app.use((_req, res, next) => {
|
||||
res.flush = () => {
|
||||
// do nothing.
|
||||
};
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
app.use(passport.initialize());
|
||||
app.use(bodyParser.json({limit: '50mb'}));
|
||||
app.use(bodyParser.urlencoded({extended: false, limit: '50mb'}));
|
||||
app.use(cookieParser());
|
||||
|
||||
passport.use(
|
||||
new Strategy(
|
||||
{
|
||||
jwtFromRequest: (req: Request) => req?.cookies?.jwt,
|
||||
secretOrKey: config.secret,
|
||||
},
|
||||
(payload, callback) => {
|
||||
const parsedResult = authTokenSchema.safeParse(payload);
|
||||
|
||||
if (!parsedResult.success) {
|
||||
callback(parsedResult.error, false);
|
||||
return;
|
||||
}
|
||||
|
||||
Users.lookupUser(parsedResult.data.username).then(
|
||||
(user) => {
|
||||
if (user?.timestamp <= parsedResult.data.iat + 10) {
|
||||
callback(null, user);
|
||||
} else {
|
||||
callback(new Error(), false);
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
callback(err, false);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
app.use(`${servedPath}api`, apiRoutes);
|
||||
|
||||
export default app;
|
||||
7
server/bin/start.ts
Executable file → Normal file
7
server/bin/start.ts
Executable file → Normal file
@@ -4,6 +4,7 @@ import chalk from 'chalk';
|
||||
|
||||
import enforcePrerequisites from './enforce-prerequisites';
|
||||
import migrateData from './migrations/run';
|
||||
import startWebServer from './web-server';
|
||||
|
||||
if (process.env.NODE_ENV == 'production') {
|
||||
// Catch unhandled rejections and exceptions
|
||||
@@ -26,11 +27,7 @@ if (process.env.NODE_ENV == 'production') {
|
||||
|
||||
enforcePrerequisites()
|
||||
.then(migrateData)
|
||||
.then(() => {
|
||||
// We do this because we don't want the side effects of importing server functions before migration is completed.
|
||||
const startWebServer = require('./web-server').default; // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
return startWebServer();
|
||||
})
|
||||
.then(startWebServer)
|
||||
.catch((error) => {
|
||||
console.log(chalk.red('Failed to start Flood:'));
|
||||
console.trace(error);
|
||||
|
||||
@@ -1,106 +1,51 @@
|
||||
import chalk from 'chalk';
|
||||
import debug from 'debug';
|
||||
import fastify from 'fastify';
|
||||
import fs from 'fs';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
|
||||
import app from '../app';
|
||||
import type {FastifyInstance} from 'fastify';
|
||||
import type {Http2SecureServer} from 'http2';
|
||||
import type {Server} from 'http';
|
||||
|
||||
import config from '../../config';
|
||||
import constructRoutes from '../routes';
|
||||
import packageJSON from '../../package.json';
|
||||
|
||||
const debugFloodServer = debug('flood:server');
|
||||
const startWebServer = async () => {
|
||||
const {ssl = false, floodServerHost: host, floodServerPort: port} = config;
|
||||
|
||||
// Normalize a port into a number, string, or false.
|
||||
const normalizePort = (val: string | number): string | number => {
|
||||
const port = parseInt(val as string, 10);
|
||||
let instance: FastifyInstance<Http2SecureServer> | FastifyInstance<Server>;
|
||||
|
||||
// Named pipe.
|
||||
if (Number.isNaN(port)) {
|
||||
return val;
|
||||
}
|
||||
|
||||
// Port number.
|
||||
if (port >= 0) {
|
||||
return port;
|
||||
}
|
||||
|
||||
console.error('Unexpected port or pipe');
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
const startWebServer = () => {
|
||||
const port = normalizePort(config.floodServerPort);
|
||||
const host = config.floodServerHost;
|
||||
const useSSL = config.ssl ?? false;
|
||||
|
||||
app.set('port', port);
|
||||
app.set('host', host);
|
||||
|
||||
// Create HTTP or HTTPS server.
|
||||
let server: http.Server | https.Server;
|
||||
|
||||
if (useSSL) {
|
||||
if (ssl) {
|
||||
if (!config.sslKey || !config.sslCert) {
|
||||
console.error('Cannot start HTTPS server, `sslKey` or `sslCert` is missing in config.js.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
server = https.createServer(
|
||||
{
|
||||
instance = fastify({
|
||||
bodyLimit: 100 * 1024 * 1024,
|
||||
trustProxy: 'loopback',
|
||||
http2: true,
|
||||
https: {
|
||||
allowHTTP1: true,
|
||||
key: fs.readFileSync(config.sslKey),
|
||||
cert: fs.readFileSync(config.sslCert),
|
||||
},
|
||||
app,
|
||||
);
|
||||
} else {
|
||||
server = http.createServer(app);
|
||||
}
|
||||
|
||||
const handleError = (error: NodeJS.ErrnoException) => {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`;
|
||||
|
||||
// Handle specific listen errors with friendly messages.
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(`${bind} requires elevated privileges`);
|
||||
process.exit(1);
|
||||
case 'EADDRINUSE':
|
||||
console.error(`${bind} is already in use`);
|
||||
process.exit(1);
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Event listener for HTTP server "listening" event.
|
||||
const handleListening = () => {
|
||||
const addr = server.address();
|
||||
if (addr == null) {
|
||||
console.error('Unable to get listening address.');
|
||||
process.exit(1);
|
||||
}
|
||||
const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr.port}`;
|
||||
debugFloodServer(`Listening on ${bind}`);
|
||||
};
|
||||
|
||||
// Listen on provided port, on all network interfaces.
|
||||
if (typeof port === 'string') {
|
||||
server.listen(port);
|
||||
} else {
|
||||
server.listen(port, host);
|
||||
}
|
||||
|
||||
server.on('error', handleError);
|
||||
server.on('listening', handleListening);
|
||||
process.on('exit', () => {
|
||||
server.close();
|
||||
});
|
||||
} else {
|
||||
instance = fastify({
|
||||
bodyLimit: 100 * 1024 * 1024,
|
||||
trustProxy: 'loopback',
|
||||
});
|
||||
}
|
||||
|
||||
const address = chalk.underline(typeof port === 'string' ? port : `${useSSL ? 'https' : 'http'}://${host}:${port}`);
|
||||
await constructRoutes(instance as FastifyInstance);
|
||||
|
||||
if (typeof port === 'string') {
|
||||
await instance.listen({path: port});
|
||||
} else {
|
||||
await instance.listen({port, host});
|
||||
}
|
||||
const address = chalk.underline(`${ssl ? 'https' : 'http'}://${host}:${port}`);
|
||||
|
||||
console.log(chalk.green(`Flood server ${packageJSON.version} starting on ${address}\n`));
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import crypto from 'crypto';
|
||||
import fastify from 'fastify';
|
||||
import supertest from 'supertest';
|
||||
|
||||
import {AccessLevel} from '../../../shared/schema/constants/Auth';
|
||||
|
||||
import app from '../../app';
|
||||
import constructRoutes from '..';
|
||||
import {getAuthToken} from '../../util/authUtil';
|
||||
|
||||
import type {
|
||||
@@ -13,8 +14,6 @@ import type {
|
||||
} from '../../../shared/schema/api/auth';
|
||||
import type {ClientConnectionSettings} from '../../../shared/schema/ClientConnectionSettings';
|
||||
|
||||
const request = supertest(app);
|
||||
|
||||
const testConnectionSettings: ClientConnectionSettings = {
|
||||
client: 'rTorrent',
|
||||
type: 'socket',
|
||||
@@ -38,6 +37,19 @@ const testNonAdminUser = {
|
||||
} as const;
|
||||
let testNonAdminUserToken = '';
|
||||
|
||||
const app = fastify({disableRequestLogging: true, logger: false});
|
||||
let request: supertest.SuperTest<supertest.Test>;
|
||||
|
||||
beforeAll(async () => {
|
||||
await constructRoutes(app);
|
||||
await app.ready();
|
||||
request = supertest(app.server);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('GET /api/auth/verify (initial)', () => {
|
||||
it('Verify without credential', (done) => {
|
||||
request
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
import fastify from 'fastify';
|
||||
import supertest from 'supertest';
|
||||
|
||||
import app from '../../app';
|
||||
import constructRoutes from '..';
|
||||
import {getAuthToken} from '../../util/authUtil';
|
||||
|
||||
import type {ClientSettings} from '../../../shared/types/ClientSettings';
|
||||
|
||||
const request = supertest(app);
|
||||
const app = fastify({disableRequestLogging: true, logger: true});
|
||||
let request: supertest.SuperTest<supertest.Test>;
|
||||
|
||||
beforeAll(async () => {
|
||||
await constructRoutes(app);
|
||||
await app.ready();
|
||||
request = supertest(app.server);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
const authToken = `jwt=${getAuthToken('_config')}`;
|
||||
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import fastify from 'fastify';
|
||||
import fs from 'fs';
|
||||
import supertest from 'supertest';
|
||||
|
||||
import app from '../../app';
|
||||
import constructRoutes from '..';
|
||||
import {getAuthToken} from '../../util/authUtil';
|
||||
import {getTempPath} from '../../models/TemporaryStorage';
|
||||
|
||||
import type {AddFeedOptions, AddRuleOptions, ModifyFeedOptions} from '../../../shared/types/api/feed-monitor';
|
||||
import type {Feed, Rule} from '../../../shared/types/Feed';
|
||||
|
||||
const request = supertest(app);
|
||||
const app = fastify({disableRequestLogging: true, logger: false});
|
||||
let request: supertest.SuperTest<supertest.Test>;
|
||||
|
||||
beforeAll(async () => {
|
||||
await constructRoutes(app);
|
||||
await app.ready();
|
||||
request = supertest(app.server);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
const authToken = `jwt=${getAuthToken('_config')}`;
|
||||
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
import fastify from 'fastify';
|
||||
import supertest from 'supertest';
|
||||
|
||||
import app from '../../app';
|
||||
import constructRoutes from '..';
|
||||
import {getAuthToken} from '../../util/authUtil';
|
||||
|
||||
import type {FloodSettings} from '../../../shared/types/FloodSettings';
|
||||
|
||||
const request = supertest(app);
|
||||
const app = fastify({disableRequestLogging: true, logger: false});
|
||||
let request: supertest.SuperTest<supertest.Test>;
|
||||
|
||||
beforeAll(async () => {
|
||||
await constructRoutes(app);
|
||||
await app.ready();
|
||||
request = supertest(app.server);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
const authToken = `jwt=${getAuthToken('_config')}`;
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import axios from 'axios';
|
||||
import crypto from 'crypto';
|
||||
import fastify from 'fastify';
|
||||
import fs from 'fs';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import readline from 'readline';
|
||||
import stream from 'stream';
|
||||
import supertest from 'supertest';
|
||||
|
||||
import app from '../../app';
|
||||
import constructRoutes from '..';
|
||||
import {getAuthToken} from '../../util/authUtil';
|
||||
import {getTempPath} from '../../models/TemporaryStorage';
|
||||
import paths from '../../../shared/config/paths';
|
||||
@@ -20,9 +19,20 @@ import type {TorrentList} from '../../../shared/types/Torrent';
|
||||
import type {TorrentStatus} from '../../../shared/constants/torrentStatusMap';
|
||||
import type {TorrentTracker} from '../../../shared/types/TorrentTracker';
|
||||
|
||||
const request = supertest(app);
|
||||
const app = fastify({bodyLimit: 100 * 1024 * 1024 * 1024, disableRequestLogging: true, forceCloseConnections: true});
|
||||
let request: supertest.SuperTest<supertest.Test>;
|
||||
|
||||
const activityStream = new stream.PassThrough();
|
||||
const authToken = `jwt=${getAuthToken('_config')}`;
|
||||
const rl = readline.createInterface({input: activityStream});
|
||||
|
||||
beforeAll(async () => {
|
||||
console.time('before all');
|
||||
await constructRoutes(app);
|
||||
await app.ready();
|
||||
request = supertest(app.server);
|
||||
request.get('/api/activity-stream').send().set('Cookie', [authToken]).pipe(activityStream);
|
||||
});
|
||||
|
||||
const tempDirectory = getTempPath('download');
|
||||
|
||||
@@ -33,19 +43,9 @@ const torrentFiles = [
|
||||
path.join(paths.appSrc, 'fixtures/multi.torrent'),
|
||||
].map((torrentPath) => Buffer.from(fs.readFileSync(torrentPath)).toString('base64')) as [string, ...string[]];
|
||||
|
||||
const mock = new MockAdapter(axios, {onNoMatch: 'passthrough'});
|
||||
|
||||
mock
|
||||
.onGet('https://www.torrents/single.torrent')
|
||||
.reply(200, fs.readFileSync(path.join(paths.appSrc, 'fixtures/single.torrent')));
|
||||
|
||||
mock
|
||||
.onGet('https://www.torrents/multi.torrent')
|
||||
.reply(200, fs.readFileSync(path.join(paths.appSrc, 'fixtures/multi.torrent')));
|
||||
|
||||
const torrentURLs: [string, ...string[]] = [
|
||||
'https://www.torrents/single.torrent',
|
||||
'https://www.torrents/multi.torrent',
|
||||
'https://releases.ubuntu.com/focal/ubuntu-20.04.6-live-server-amd64.iso.torrent',
|
||||
'https://flood.js.org/api/test-cookie',
|
||||
];
|
||||
|
||||
const torrentCookies = {
|
||||
@@ -58,12 +58,8 @@ const testTrackers = [
|
||||
`http://${crypto.randomBytes(8).toString('hex')}.com/announce.php?key=test`,
|
||||
];
|
||||
|
||||
const torrentHashes: string[] = [];
|
||||
const createdTorrentHashes: string[] = [];
|
||||
|
||||
const activityStream = new stream.PassThrough();
|
||||
const rl = readline.createInterface({input: activityStream});
|
||||
request.get('/api/activity-stream').send().set('Cookie', [authToken]).pipe(activityStream);
|
||||
let torrentHash = '';
|
||||
let createdTorrentHash = '';
|
||||
|
||||
const watchTorrentList = (op: 'add' | 'remove' | 'replace' | 'move' | 'copy' | 'test'): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
@@ -79,23 +75,6 @@ const watchTorrentList = (op: 'add' | 'remove' | 'replace' | 'move' | 'copy' | '
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll((done) => {
|
||||
request
|
||||
.get('/api/client/connection-test')
|
||||
.send()
|
||||
.set('Cookie', [authToken])
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.end((err, res) => {
|
||||
if (err) done(err);
|
||||
|
||||
expect(res.body).toMatchObject({isConnected: true});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/torrents/add-urls', () => {
|
||||
const addTorrentByURLOptions: AddTorrentByURLOptions = {
|
||||
urls: torrentURLs,
|
||||
@@ -199,7 +178,7 @@ describe('POST /api/torrents/add-urls', () => {
|
||||
: ['stopped', 'inactive'];
|
||||
expect(torrent.status).toEqual(expect.arrayContaining(expectedStatuses));
|
||||
|
||||
torrentHashes.push(torrent.hash);
|
||||
torrentHash = torrent.hash;
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -208,31 +187,6 @@ describe('POST /api/torrents/add-urls', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/torrents/delete', () => {
|
||||
const torrentDeleted = watchTorrentList('remove');
|
||||
it('Deletes added torrents', (done) => {
|
||||
request
|
||||
.post('/api/torrents/delete')
|
||||
.send({hashes: torrentHashes, deleteData: true})
|
||||
.set('Cookie', [authToken])
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.end((err, _res) => {
|
||||
if (err) done(err);
|
||||
|
||||
Promise.race([torrentDeleted, new Promise((r) => setTimeout(r, 1000 * 15))])
|
||||
.then(async () => {
|
||||
// Wait a while
|
||||
await new Promise((r) => setTimeout(r, 1000 * 3));
|
||||
})
|
||||
.then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/torrents/add-files', () => {
|
||||
const addTorrentByFileOptions: AddTorrentByFileOptions = {
|
||||
files: torrentFiles,
|
||||
@@ -415,7 +369,7 @@ describe('POST /api/torrents/create', () => {
|
||||
|
||||
await Promise.all(
|
||||
addedTorrents.map(async (torrent) => {
|
||||
createdTorrentHashes.push(torrent.hash);
|
||||
createdTorrentHash = torrent.hash;
|
||||
expect(torrent.isPrivate).toBe(false);
|
||||
|
||||
if (process.argv.includes('--trurl')) {
|
||||
@@ -435,7 +389,7 @@ describe('POST /api/torrents/create', () => {
|
||||
describe('PATCH /api/torrents/trackers', () => {
|
||||
it('Sets single tracker', (done) => {
|
||||
const setTrackersOptions: SetTorrentsTrackersOptions = {
|
||||
hashes: [torrentHashes[0]],
|
||||
hashes: [torrentHash],
|
||||
trackers: [testTrackers[0]],
|
||||
};
|
||||
|
||||
@@ -455,7 +409,7 @@ describe('PATCH /api/torrents/trackers', () => {
|
||||
|
||||
it('Sets multiple trackers', (done) => {
|
||||
const setTrackersOptions: SetTorrentsTrackersOptions = {
|
||||
hashes: [torrentHashes[0]],
|
||||
hashes: [torrentHash],
|
||||
trackers: testTrackers,
|
||||
};
|
||||
|
||||
@@ -475,7 +429,7 @@ describe('PATCH /api/torrents/trackers', () => {
|
||||
|
||||
it('GET /api/torrents/{hash}/trackers', (done) => {
|
||||
request
|
||||
.get(`/api/torrents/${torrentHashes[0]}/trackers`)
|
||||
.get(`/api/torrents/${torrentHash}/trackers`)
|
||||
.send()
|
||||
.set('Cookie', [authToken])
|
||||
.set('Accept', 'application/json')
|
||||
@@ -497,7 +451,7 @@ describe('PATCH /api/torrents/trackers', () => {
|
||||
describe('GET /api/torrents/{hash}/contents', () => {
|
||||
it('Gets contents of torrents', (done) => {
|
||||
request
|
||||
.get(`/api/torrents/${torrentHashes[0]}/contents`)
|
||||
.get(`/api/torrents/${torrentHash}/contents`)
|
||||
.send()
|
||||
.set('Cookie', [authToken])
|
||||
.set('Accept', 'application/json')
|
||||
@@ -520,7 +474,7 @@ describe('POST /api/torrents/move', () => {
|
||||
|
||||
it('Moves torrent', (done) => {
|
||||
const moveTorrentsOptions: MoveTorrentsOptions = {
|
||||
hashes: [createdTorrentHashes[createdTorrentHashes.length - 1]],
|
||||
hashes: [createdTorrentHash],
|
||||
destination: destDirectory,
|
||||
moveFiles: true,
|
||||
isBasePath: true,
|
||||
@@ -558,7 +512,7 @@ describe('POST /api/torrents/move', () => {
|
||||
|
||||
expect(res.body.torrents == null).toBe(false);
|
||||
const torrentList: TorrentList = res.body.torrents;
|
||||
const torrent = torrentList[createdTorrentHashes[createdTorrentHashes.length - 1]];
|
||||
const torrent = torrentList[createdTorrentHash];
|
||||
|
||||
expect(torrent).not.toBe(null);
|
||||
expect(torrent.directory.startsWith(destDirectory)).toBe(true);
|
||||
|
||||
137
server/routes/index.ts
Normal file
137
server/routes/index.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import type {FastifyInstance} from 'fastify';
|
||||
|
||||
import paths from '@shared/config/paths';
|
||||
import {Strategy} from 'passport-jwt';
|
||||
|
||||
import apiRoutes from './api';
|
||||
import config from '../../config';
|
||||
import Users from '../models/Users';
|
||||
import {authTokenSchema, UserInDatabase} from '@shared/schema/Auth';
|
||||
import express from 'express';
|
||||
import morgan from 'morgan';
|
||||
import compression from 'compression';
|
||||
import passport from 'passport';
|
||||
import bodyParser from 'body-parser';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import {fastifyExpress} from '@fastify/express';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Express {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface User extends UserInDatabase {}
|
||||
}
|
||||
}
|
||||
|
||||
const constructRoutes = async (fastify: FastifyInstance) => {
|
||||
await Users.bootstrapServicesForAllUsers();
|
||||
|
||||
const app = express();
|
||||
const servedPath = config.baseURI.endsWith('/') ? config.baseURI : `${config.baseURI}/`;
|
||||
|
||||
// Remove Express header
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
app.disable('x-powered-by');
|
||||
}
|
||||
|
||||
app.set('strict routing', true);
|
||||
app.set('trust proxy', 'loopback');
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
app.use(morgan('dev'));
|
||||
}
|
||||
|
||||
if (config.serveAssets !== false) {
|
||||
// Disable ETag
|
||||
app.set('etag', false);
|
||||
|
||||
// Enable compression
|
||||
app.use(compression());
|
||||
|
||||
// Static assets
|
||||
app.use(servedPath, express.static(paths.appDist));
|
||||
|
||||
// Client app routes, serve index.html and client js will figure it out
|
||||
const html = fs.readFileSync(path.join(paths.appDist, 'index.html'), {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
// Prohibit caching of index.html as browser can use a fully cached asset
|
||||
// tree in some cases, which defeats cache busting by asset hashes.
|
||||
const headers = {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
Pragma: 'no-cache',
|
||||
Expires: '0',
|
||||
};
|
||||
|
||||
app.get(`${servedPath}login`, (_req, res) => {
|
||||
res.set(headers);
|
||||
res.send(html);
|
||||
});
|
||||
|
||||
app.get(`${servedPath}register`, (_req, res) => {
|
||||
res.set(headers);
|
||||
res.send(html);
|
||||
});
|
||||
|
||||
app.get(`${servedPath}overview`, (_req, res) => {
|
||||
res.set(headers);
|
||||
res.send(html);
|
||||
});
|
||||
} else {
|
||||
// no-op res.flush() as compression is not handled by Express
|
||||
app.use((_req, res, next) => {
|
||||
res.flush = () => {
|
||||
// do nothing.
|
||||
};
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
app.use(passport.initialize());
|
||||
app.use(bodyParser.json({limit: '50mb'}));
|
||||
app.use(bodyParser.urlencoded({extended: false, limit: '50mb'}));
|
||||
app.use(cookieParser());
|
||||
|
||||
passport.use(
|
||||
new Strategy(
|
||||
{
|
||||
jwtFromRequest: (req: express.Request) => req?.cookies?.jwt,
|
||||
secretOrKey: config.secret,
|
||||
},
|
||||
(payload, callback) => {
|
||||
const parsedResult = authTokenSchema.safeParse(payload);
|
||||
|
||||
if (!parsedResult.success) {
|
||||
callback(parsedResult.error, false);
|
||||
return;
|
||||
}
|
||||
|
||||
Users.lookupUser(parsedResult.data.username).then(
|
||||
(user) => {
|
||||
if (user?.timestamp <= parsedResult.data.iat + 10) {
|
||||
callback(null, user);
|
||||
} else {
|
||||
callback(new Error(), false);
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
callback(err, false);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
app.use(`${servedPath}api`, apiRoutes);
|
||||
|
||||
await fastify.register(fastifyExpress);
|
||||
fastify.use(app);
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
export default constructRoutes;
|
||||
Reference in New Issue
Block a user