From 20fc3ac6a830960301d58310a0a9a07eb9dd98c1 Mon Sep 17 00:00:00 2001 From: Jesse Chan Date: Tue, 15 Dec 2020 20:31:29 +0800 Subject: [PATCH] server: torrents: disallow comma in tag --- .../src/javascript/actions/TorrentActions.ts | 7 +- .../modals/set-tags-modal/SetTagsModal.tsx | 12 ++- client/src/javascript/util/validators.ts | 4 +- scripts/testsetup.js | 1 + server/routes/api/torrents.ts | 37 +++++--- .../Transmission/clientGatewayService.ts | 85 +++++++++++-------- .../interfaces/clientGatewayService.ts | 7 +- .../qBittorrent/clientGatewayService.ts | 7 +- .../services/rTorrent/clientGatewayService.ts | 7 +- server/util/feedUtil.ts | 4 +- server/util/torrentPropertiesUtil.ts | 4 +- shared/schema/api/torrents.ts | 19 ++++- shared/types/api/torrents.ts | 8 -- shared/util/regEx.ts | 11 +-- 14 files changed, 129 insertions(+), 84 deletions(-) diff --git a/client/src/javascript/actions/TorrentActions.ts b/client/src/javascript/actions/TorrentActions.ts index 3efc550b..6e7d9e1c 100644 --- a/client/src/javascript/actions/TorrentActions.ts +++ b/client/src/javascript/actions/TorrentActions.ts @@ -1,7 +1,11 @@ import axios, {CancelToken} from 'axios'; import download from 'js-file-download'; -import type {AddTorrentByFileOptions, AddTorrentByURLOptions} from '@shared/schema/api/torrents'; +import type { + AddTorrentByFileOptions, + AddTorrentByURLOptions, + SetTorrentsTagsOptions, +} from '@shared/schema/api/torrents'; import type { CheckTorrentsOptions, CreateTorrentOptions, @@ -9,7 +13,6 @@ import type { MoveTorrentsOptions, SetTorrentContentsPropertiesOptions, SetTorrentsPriorityOptions, - SetTorrentsTagsOptions, SetTorrentsTrackersOptions, StartTorrentsOptions, StopTorrentsOptions, diff --git a/client/src/javascript/components/modals/set-tags-modal/SetTagsModal.tsx b/client/src/javascript/components/modals/set-tags-modal/SetTagsModal.tsx index c81d5815..28a4f99d 100644 --- a/client/src/javascript/components/modals/set-tags-modal/SetTagsModal.tsx +++ b/client/src/javascript/components/modals/set-tags-modal/SetTagsModal.tsx @@ -52,14 +52,18 @@ const SetTagsModal: FC = () => { return; } + const {selectedTorrents} = TorrentStore; const formData = formRef.current.getFormData() as {tags: string}; const tags = formData.tags ? formData.tags.split(',') : []; setIsSettingTags(true); - TorrentActions.setTags({ - hashes: TorrentStore.selectedTorrents, - tags, - }); + + if (selectedTorrents?.length > 0) { + TorrentActions.setTags({ + hashes: selectedTorrents as [string, ...string[]], + tags, + }); + } }, isLoading: isSettingTags, triggerDismiss: false, diff --git a/client/src/javascript/util/validators.ts b/client/src/javascript/util/validators.ts index b905c195..dccb82d9 100644 --- a/client/src/javascript/util/validators.ts +++ b/client/src/javascript/util/validators.ts @@ -1,4 +1,4 @@ -import regEx from '@shared/util/regEx'; +import {url as matchURL} from '@shared/util/regEx'; export const isNotEmpty = (value: string) => value != null && value !== ''; @@ -13,7 +13,7 @@ export const isRegExValid = (regExToCheck: string) => { return true; }; -export const isURLValid = (url: string) => url != null && url !== '' && url.match(regEx.url) !== null; +export const isURLValid = (url: string) => url != null && url !== '' && url.match(matchURL) !== null; export const isPositiveInteger = (value: number | string) => { if (value === null || value === '') return false; diff --git a/scripts/testsetup.js b/scripts/testsetup.js index 2158171a..ccfad7b2 100644 --- a/scripts/testsetup.js +++ b/scripts/testsetup.js @@ -17,6 +17,7 @@ fs.mkdirSync(rTorrentSession, {recursive: true}); fs.writeFileSync( `${temporaryRuntimeDirectory}/rtorrent.rc`, ` +execute.nothrow = rm,-rf,${rTorrentSession}/rtorrent.lock directory.default.set = "${temporaryRuntimeDirectory}" session.path.set = "${rTorrentSession}" network.scgi.open_local = "${rTorrentSocket}" diff --git a/server/routes/api/torrents.ts b/server/routes/api/torrents.ts index a4dbab78..7f92b1d4 100644 --- a/server/routes/api/torrents.ts +++ b/server/routes/api/torrents.ts @@ -8,7 +8,11 @@ import rateLimit from 'express-rate-limit'; import sanitize from 'sanitize-filename'; import tar from 'tar'; -import type {AddTorrentByFileOptions, AddTorrentByURLOptions} from '@shared/schema/api/torrents'; +import type { + AddTorrentByFileOptions, + AddTorrentByURLOptions, + SetTorrentsTagsOptions, +} from '@shared/schema/api/torrents'; import type { CheckTorrentsOptions, CreateTorrentOptions, @@ -16,13 +20,16 @@ import type { MoveTorrentsOptions, SetTorrentContentsPropertiesOptions, SetTorrentsPriorityOptions, - SetTorrentsTagsOptions, SetTorrentsTrackersOptions, StartTorrentsOptions, StopTorrentsOptions, } from '@shared/types/api/torrents'; -import {addTorrentByFileSchema, addTorrentByURLSchema} from '../../../shared/schema/api/torrents'; +import { + addTorrentByFileSchema, + addTorrentByURLSchema, + setTorrentsTagsSchema, +} from '../../../shared/schema/api/torrents'; import {accessDeniedError, isAllowedPath, sanitizePath} from '../../util/fileUtil'; import {getResponseFn, validationError} from '../../util/ajaxUtil'; import {getTempPath} from '../../models/TemporaryStorage'; @@ -434,18 +441,22 @@ router.patch('/priority', (req, re * @return {Error} 500 - failure response - application/json */ router.patch('/tags', (req, res) => { - const callback = getResponseFn(res); + const parsedResult = setTorrentsTagsSchema.safeParse(req.body); - req.services?.clientGatewayService - ?.setTorrentsTags(req.body) - .then((response) => { + if (!parsedResult.success) { + validationError(res, parsedResult.error); + return; + } + + req.services?.clientGatewayService?.setTorrentsTags(parsedResult.data).then( + (response) => { req.services?.torrentService.fetchTorrentList(); - return response; - }) - .then(callback) - .catch((err) => { - callback(null, err); - }); + res.status(200).json(response); + }, + (err) => { + res.status(500).json(err); + }, + ); }); /** diff --git a/server/services/Transmission/clientGatewayService.ts b/server/services/Transmission/clientGatewayService.ts index 438010a8..6f8223a0 100644 --- a/server/services/Transmission/clientGatewayService.ts +++ b/server/services/Transmission/clientGatewayService.ts @@ -1,13 +1,16 @@ import geoip from 'geoip-country'; -import type {AddTorrentByFileOptions, AddTorrentByURLOptions} from '@shared/schema/api/torrents'; +import type { + AddTorrentByFileOptions, + AddTorrentByURLOptions, + SetTorrentsTagsOptions, +} from '@shared/schema/api/torrents'; import type { CheckTorrentsOptions, DeleteTorrentsOptions, MoveTorrentsOptions, SetTorrentContentsPropertiesOptions, SetTorrentsPriorityOptions, - SetTorrentsTagsOptions, SetTorrentsTrackersOptions, StartTorrentsOptions, StopTorrentsOptions, @@ -40,22 +43,27 @@ class TransmissionClientGatewayService extends ClientGatewayService { isCompleted, start, }: Required): Promise { - const addedTorrents: Array = ( - await Promise.all( - files.map(async (file) => { - const {hashString} = - (await this.clientRequestManager - .addTorrent({ - metainfo: file, - 'download-dir': destination, - paused: !start, - }) - .then(this.processClientRequestSuccess, this.processClientRequestError) - .catch(() => undefined)) || {}; - return hashString; - }), - ) - ).filter((hash) => hash != null) as Array; + const addedTorrents: [string, ...string[]] = await Promise.all( + files.map(async (file) => { + const {hashString} = + (await this.clientRequestManager + .addTorrent({ + metainfo: file, + 'download-dir': destination, + paused: !start, + }) + .then(this.processClientRequestSuccess, this.processClientRequestError) + .catch(() => undefined)) || {}; + return hashString; + }), + ) + .then((results) => results.filter((hash) => hash != null) as string[]) + .then((hashes) => { + if (hashes.length < 1) { + throw new Error(); + } + return hashes as [string, ...string[]]; + }); if (tags.length > 0) { await this.setTorrentsTags({hashes: addedTorrents, tags}); @@ -75,24 +83,29 @@ class TransmissionClientGatewayService extends ClientGatewayService { isCompleted, start, }: Required): Promise { - const addedTorrents: Array = ( - await Promise.all( - urls.map(async (url) => { - const domain = url.split('/')[2]; - const {hashString} = - (await this.clientRequestManager - .addTorrent({ - filename: url, - cookies: cookies[domain] != null ? `${cookies[domain].join('; ')};` : undefined, - 'download-dir': destination, - paused: !start, - }) - .then(this.processClientRequestSuccess, this.processClientRequestError) - .catch(() => undefined)) || {}; - return hashString; - }), - ) - ).filter((hash) => hash != null) as Array; + const addedTorrents: [string, ...string[]] = await Promise.all( + urls.map(async (url) => { + const domain = url.split('/')[2]; + const {hashString} = + (await this.clientRequestManager + .addTorrent({ + filename: url, + cookies: cookies[domain] != null ? `${cookies[domain].join('; ')};` : undefined, + 'download-dir': destination, + paused: !start, + }) + .then(this.processClientRequestSuccess, this.processClientRequestError) + .catch(() => undefined)) || {}; + return hashString; + }), + ) + .then((results) => results.filter((hash) => hash != null) as string[]) + .then((hashes) => { + if (hashes.length < 1) { + throw new Error(); + } + return hashes as [string, ...string[]]; + }); if (tags.length > 0) { await this.setTorrentsTags({hashes: addedTorrents, tags}); diff --git a/server/services/interfaces/clientGatewayService.ts b/server/services/interfaces/clientGatewayService.ts index d5fb49ec..fbc34172 100644 --- a/server/services/interfaces/clientGatewayService.ts +++ b/server/services/interfaces/clientGatewayService.ts @@ -1,11 +1,14 @@ -import type {AddTorrentByFileOptions, AddTorrentByURLOptions} from '@shared/schema/api/torrents'; +import type { + AddTorrentByFileOptions, + AddTorrentByURLOptions, + SetTorrentsTagsOptions, +} from '@shared/schema/api/torrents'; import type { CheckTorrentsOptions, DeleteTorrentsOptions, MoveTorrentsOptions, SetTorrentContentsPropertiesOptions, SetTorrentsPriorityOptions, - SetTorrentsTagsOptions, SetTorrentsTrackersOptions, StartTorrentsOptions, StopTorrentsOptions, diff --git a/server/services/qBittorrent/clientGatewayService.ts b/server/services/qBittorrent/clientGatewayService.ts index e24e0ea7..8ca9960e 100644 --- a/server/services/qBittorrent/clientGatewayService.ts +++ b/server/services/qBittorrent/clientGatewayService.ts @@ -1,11 +1,14 @@ -import type {AddTorrentByFileOptions, AddTorrentByURLOptions} from '@shared/schema/api/torrents'; +import type { + AddTorrentByFileOptions, + AddTorrentByURLOptions, + SetTorrentsTagsOptions, +} from '@shared/schema/api/torrents'; import type { CheckTorrentsOptions, DeleteTorrentsOptions, MoveTorrentsOptions, SetTorrentContentsPropertiesOptions, SetTorrentsPriorityOptions, - SetTorrentsTagsOptions, SetTorrentsTrackersOptions, StartTorrentsOptions, StopTorrentsOptions, diff --git a/server/services/rTorrent/clientGatewayService.ts b/server/services/rTorrent/clientGatewayService.ts index 185c07e2..547a1986 100644 --- a/server/services/rTorrent/clientGatewayService.ts +++ b/server/services/rTorrent/clientGatewayService.ts @@ -4,14 +4,17 @@ import {moveSync} from 'fs-extra'; import path from 'path'; import sanitize from 'sanitize-filename'; -import type {AddTorrentByFileOptions, AddTorrentByURLOptions} from '@shared/schema/api/torrents'; +import type { + AddTorrentByFileOptions, + AddTorrentByURLOptions, + SetTorrentsTagsOptions, +} from '@shared/schema/api/torrents'; import type { CheckTorrentsOptions, DeleteTorrentsOptions, MoveTorrentsOptions, SetTorrentContentsPropertiesOptions, SetTorrentsPriorityOptions, - SetTorrentsTagsOptions, SetTorrentsTrackersOptions, StartTorrentsOptions, StopTorrentsOptions, diff --git a/server/util/feedUtil.ts b/server/util/feedUtil.ts index ee9854f7..42a9cb29 100644 --- a/server/util/feedUtil.ts +++ b/server/util/feedUtil.ts @@ -1,6 +1,6 @@ import type {FeedItem} from 'feedsub'; -import regEx from '../../shared/util/regEx'; +import {cdata as matchCDATA} from '../../shared/util/regEx'; import type {AddTorrentByURLOptions} from '../../shared/schema/api/torrents'; import type {Rule} from '../../shared/types/Feed'; @@ -42,7 +42,7 @@ export const getTorrentUrlsFromFeedItem = (feedItem: FeedItem): Array => // If there are no enclosures, then use the link tag instead if (feedItem.link) { // remove CDATA tags around links - const cdata = regEx.cdata.exec(feedItem.link as string); + const cdata = matchCDATA.exec(feedItem.link as string); if (cdata && cdata[1]) { return [cdata[1]]; diff --git a/server/util/torrentPropertiesUtil.ts b/server/util/torrentPropertiesUtil.ts index 59dc5764..b6e8e2a1 100644 --- a/server/util/torrentPropertiesUtil.ts +++ b/server/util/torrentPropertiesUtil.ts @@ -1,4 +1,4 @@ -import regEx from '../../shared/util/regEx'; +import {domainName as matchDomainName} from '../../shared/util/regEx'; import type {TorrentProperties} from '../../shared/types/Torrent'; @@ -25,7 +25,7 @@ export const getDomainsFromURLs = (urls: Array): Array => { const domains: Array = []; urls.forEach((url) => { - const regexMatched = regEx.domainName.exec(url); + const regexMatched = matchDomainName.exec(url); if (regexMatched != null && regexMatched[1]) { let domain = regexMatched[1]; diff --git a/shared/schema/api/torrents.ts b/shared/schema/api/torrents.ts index c60522d7..54cd6f80 100644 --- a/shared/schema/api/torrents.ts +++ b/shared/schema/api/torrents.ts @@ -1,7 +1,12 @@ import {array, boolean, object, record, string} from 'zod'; +import {noComma} from '../../util/regEx'; import type {infer as zodInfer} from 'zod'; +const TAG_NO_COMMA_MESSAGE = { + message: 'Tag must not contain comma', +}; + // POST /api/torrents/add-urls export const addTorrentByURLSchema = object({ // URLs to download torrents from @@ -11,7 +16,7 @@ export const addTorrentByURLSchema = object({ // Path of destination destination: string().optional(), // Tags - tags: array(string()).optional(), + tags: array(string().regex(noComma, TAG_NO_COMMA_MESSAGE)).optional(), // Whether destination is the base path [default: false] isBasePath: boolean().optional(), // Whether destination contains completed contents [default: false] @@ -29,7 +34,7 @@ export const addTorrentByFileSchema = object({ // Path of destination destination: string().optional(), // Tags - tags: array(string()).optional(), + tags: array(string().regex(noComma, TAG_NO_COMMA_MESSAGE)).optional(), // Whether destination is the base path [default: false] isBasePath: boolean().optional(), // Whether destination contains completed contents [default: false] @@ -39,3 +44,13 @@ export const addTorrentByFileSchema = object({ }); export type AddTorrentByFileOptions = zodInfer; + +// PATCH /api/torrents/tags +export const setTorrentsTagsSchema = object({ + // An array of string representing hashes of torrents to operate on + hashes: array(string()).nonempty(), + // An array of string representing tags + tags: array(string().regex(noComma, TAG_NO_COMMA_MESSAGE)), +}); + +export type SetTorrentsTagsOptions = zodInfer; diff --git a/shared/types/api/torrents.ts b/shared/types/api/torrents.ts index 5f5f9d6e..266a55e0 100644 --- a/shared/types/api/torrents.ts +++ b/shared/types/api/torrents.ts @@ -71,14 +71,6 @@ export interface SetTorrentsPriorityOptions { priority: TorrentPriority; } -// PATCH /api/torrents/tags -export interface SetTorrentsTagsOptions { - // An array of string representing hashes of torrents to operate on - hashes: Array; - // An array of string representing tags - tags: TorrentProperties['tags']; -} - // PATCH /api/torrents/trackers export interface SetTorrentsTrackersOptions { // An array of string representing hashes of torrents to operate on diff --git a/shared/util/regEx.ts b/shared/util/regEx.ts index 9bdfd185..df78eb36 100644 --- a/shared/util/regEx.ts +++ b/shared/util/regEx.ts @@ -1,7 +1,4 @@ -const regEx = { - url: /^(?:https?|ftp):\/\/.{1,}\.{1}.{1,}/, - domainName: /(?:https?|udp):\/\/(?:www\.)?([-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,18}\b)*(\/[/\d\w.-]*)*(?:[?])*(.+)*/i, - cdata: //, -} as const; - -export default regEx; +export const url = /^(?:https?|ftp):\/\/.{1,}\.{1}.{1,}/; +export const domainName = /(?:https?|udp):\/\/(?:www\.)?([-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,18}\b)*(\/[/\d\w.-]*)*(?:[?])*(.+)*/i; +export const cdata = //; +export const noComma = /^[^,]+$/;