mirror of
https://github.com/zoriya/flood.git
synced 2026-06-04 11:35:11 +00:00
server: redirect to a portable link for /data endpoint
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import express from 'express';
|
||||
import passport from 'passport';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
|
||||
import {contentTokenSchema} from '@shared/schema/api/torrents';
|
||||
|
||||
import type {FloodSettings} from '@shared/types/FloodSettings';
|
||||
import type {HistorySnapshot} from '@shared/constants/historySnapshotTypes';
|
||||
@@ -12,6 +15,7 @@ import clientRoutes from './client';
|
||||
import clientActivityStream from '../../middleware/clientActivityStream';
|
||||
import eventStream from '../../middleware/eventStream';
|
||||
import feedMonitorRoutes from './feed-monitor';
|
||||
import {getAuthToken, verifyToken} from '../../util/authUtil';
|
||||
import {getDirectoryList} from '../../util/fileUtil';
|
||||
import {getResponseFn} from '../../util/ajaxUtil';
|
||||
import torrentsRoutes from './torrents';
|
||||
@@ -20,6 +24,51 @@ const router = express.Router();
|
||||
|
||||
router.use('/auth', authRoutes);
|
||||
|
||||
// Special routes that may bypass authentication when conditions matched
|
||||
|
||||
/**
|
||||
* GET /api/torrents/{hash}/contents/{indices}/data
|
||||
* @summary Gets downloaded data of contents of a torrent. Allows unauthenticated
|
||||
* access if a valid content token is found in the query.
|
||||
* @see torrents.ts
|
||||
*/
|
||||
router.get<{hash: string; indices: string}, unknown, unknown, {token: string}>(
|
||||
'/torrents/:hash/contents/:indices/data',
|
||||
rateLimit({
|
||||
windowMs: 5 * 60 * 1000,
|
||||
max: 60,
|
||||
}),
|
||||
async (req, _res, next) => {
|
||||
const {token} = req.query;
|
||||
|
||||
if (typeof token === 'string' && token !== '') {
|
||||
const payload = await verifyToken(token).catch(() => undefined);
|
||||
|
||||
if (payload != null) {
|
||||
const parsedResult = contentTokenSchema.safeParse(payload);
|
||||
|
||||
if (parsedResult.success) {
|
||||
const {username, hash: authorizedHash, indices: authorizedIndices, iat} = parsedResult.data;
|
||||
|
||||
if (
|
||||
typeof username === 'string' &&
|
||||
typeof authorizedHash === 'string' &&
|
||||
typeof authorizedIndices === 'string'
|
||||
) {
|
||||
const {hash: requestedHash, indices: requestedIndices} = req.params;
|
||||
|
||||
if (requestedHash === authorizedHash && requestedIndices === authorizedIndices) {
|
||||
req.cookies = {jwt: getAuthToken(username, iat)};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
);
|
||||
|
||||
// All subsequent routes need authentication
|
||||
router.use('/', passport.authenticate('jwt', {session: false}), appendUserServices);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import tar, {Pack} from 'tar-fs';
|
||||
import type {
|
||||
AddTorrentByFileOptions,
|
||||
AddTorrentByURLOptions,
|
||||
ContentToken,
|
||||
SetTorrentsTagsOptions,
|
||||
} from '@shared/schema/api/torrents';
|
||||
import type {
|
||||
@@ -35,6 +36,7 @@ import {
|
||||
import {accessDeniedError, fileNotFoundError, isAllowedPath, sanitizePath} from '../../util/fileUtil';
|
||||
import {getResponseFn, validationError} from '../../util/ajaxUtil';
|
||||
import {getTempPath} from '../../models/TemporaryStorage';
|
||||
import {getToken} from '../../util/authUtil';
|
||||
|
||||
const getDestination = async (
|
||||
services: Express.Request['services'],
|
||||
@@ -670,7 +672,7 @@ router.patch<{hash: string}, unknown, SetTorrentContentsPropertiesOptions>('/:ha
|
||||
* @param {string} indices.path - 'all' or indices of selected contents separated by ','
|
||||
* @return {object} 200 - contents archived in .tar - application/x-tar
|
||||
*/
|
||||
router.get(
|
||||
router.get<{hash: string; indices: string}, unknown, unknown, {token: string}>(
|
||||
'/:hash/contents/:indices/data',
|
||||
// This operation is resource-intensive
|
||||
// Limit each IP to 60 requests every 5 minutes
|
||||
@@ -681,6 +683,17 @@ router.get(
|
||||
(req, res) => {
|
||||
const {hash, indices: stringIndices} = req.params;
|
||||
|
||||
if (req.user != null && req.query.token == null) {
|
||||
res.redirect(
|
||||
`?token=${getToken<ContentToken>({
|
||||
username: req.user.username,
|
||||
hash,
|
||||
indices: stringIndices,
|
||||
})}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedTorrent = req.services?.torrentService.getTorrent(hash);
|
||||
if (!selectedTorrent) {
|
||||
res.status(404).json({error: 'Torrent not found.'});
|
||||
|
||||
+14
-3
@@ -1,6 +1,8 @@
|
||||
import {CookieOptions} from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
import type {AuthToken} from '@shared/schema/Auth';
|
||||
|
||||
import config from '../../config';
|
||||
|
||||
const EXPIRATION_SECONDS = 60 * 60 * 24 * 7; // one week
|
||||
@@ -11,12 +13,21 @@ export const getCookieOptions = (): CookieOptions => ({
|
||||
sameSite: 'strict',
|
||||
});
|
||||
|
||||
export const getAuthToken = (username: string): string =>
|
||||
jwt.sign({username}, config.secret, {
|
||||
export const getAuthToken = (username: string, iat?: number): string => {
|
||||
const authTokenPayload: Partial<AuthToken> = {
|
||||
username,
|
||||
};
|
||||
|
||||
if (iat != null) {
|
||||
authTokenPayload.iat = iat;
|
||||
}
|
||||
|
||||
return jwt.sign(authTokenPayload, config.secret, {
|
||||
expiresIn: EXPIRATION_SECONDS,
|
||||
});
|
||||
};
|
||||
|
||||
export const getToken = (payload: Record<string, unknown>) =>
|
||||
export const getToken = <T extends Record<string, unknown>>(payload: Omit<T, 'iat' | 'exp'>) =>
|
||||
jwt.sign(payload, config.secret, {
|
||||
expiresIn: EXPIRATION_SECONDS,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {array, boolean, object, record, string} from 'zod';
|
||||
import {array, boolean, number, object, record, string} from 'zod';
|
||||
import {noComma} from '../../util/regEx';
|
||||
|
||||
import type {infer as zodInfer} from 'zod';
|
||||
@@ -62,3 +62,16 @@ export const setTorrentsTagsSchema = object({
|
||||
});
|
||||
|
||||
export type SetTorrentsTagsOptions = zodInfer<typeof setTorrentsTagsSchema>;
|
||||
|
||||
// GET /api/torrents/{hash}/contents/{indices}/data
|
||||
export const contentTokenSchema = object({
|
||||
username: string(),
|
||||
hash: string(),
|
||||
indices: string(),
|
||||
// issued at
|
||||
iat: number(),
|
||||
// expiration
|
||||
exp: number(),
|
||||
});
|
||||
|
||||
export type ContentToken = zodInfer<typeof contentTokenSchema>;
|
||||
|
||||
Reference in New Issue
Block a user