From 290a377ef9aeb61e16ef9ae04b4010ad8065224d Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 13 Oct 2023 16:33:47 -0500 Subject: [PATCH] Update:Remove local cover path input & replace with url from web input, include SSRF request filter --- client/components/modals/item/tabs/Cover.vue | 102 +++++++++---------- client/strings/de.json | 1 + client/strings/en-us.json | 1 + client/strings/es.json | 1 + client/strings/fr.json | 1 + client/strings/gu.json | 1 + client/strings/hi.json | 1 + client/strings/hr.json | 1 + client/strings/it.json | 1 + client/strings/lt.json | 1 + client/strings/nl.json | 1 + client/strings/no.json | 1 + client/strings/pl.json | 1 + client/strings/ru.json | 1 + client/strings/zh-cn.json | 1 + package-lock.json | 34 ++++++- package.json | 3 +- server/controllers/LibraryItemController.js | 12 +-- server/managers/CoverManager.js | 9 +- server/utils/fileUtils.js | 9 +- 20 files changed, 117 insertions(+), 66 deletions(-) diff --git a/client/components/modals/item/tabs/Cover.vue b/client/components/modals/item/tabs/Cover.vue index 09329482..056fdcb3 100644 --- a/client/components/modals/item/tabs/Cover.vue +++ b/client/components/modals/item/tabs/Cover.vue @@ -7,7 +7,7 @@
-
+
delete @@ -16,15 +16,16 @@
-
+
upload
+
- - {{ $strings.ButtonSave }} + + {{ $strings.ButtonSubmit }}
@@ -64,7 +65,7 @@

{{ $strings.MessageNoCoversFound }}

@@ -165,6 +166,9 @@ export default { userCanUpload() { return this.$store.getters['user/getUserCanUpload'] }, + userCanDelete() { + return this.$store.getters['user/getUserCanDelete'] + }, userToken() { return this.$store.getters['user/getToken'] }, @@ -222,71 +226,53 @@ export default { this.coversFound = [] this.hasSearched = false } - this.imageUrl = this.media.coverPath || '' + this.imageUrl = '' this.searchTitle = this.mediaMetadata.title || '' this.searchAuthor = this.mediaMetadata.authorName || '' if (this.isPodcast) this.provider = 'itunes' else this.provider = localStorage.getItem('book-cover-provider') || localStorage.getItem('book-provider') || 'google' }, removeCover() { - if (!this.media.coverPath) { - this.imageUrl = '' + if (!this.coverPath) { return } - this.updateCover('') + this.isProcessing = true + this.$axios + .$delete(`/api/items/${this.libraryItemId}/cover`) + .then(() => {}) + .catch((error) => { + console.error('Failed to remove cover', error) + if (error.response?.data) { + this.$toast.error(error.response.data) + } + }) + .finally(() => { + this.isProcessing = false + }) }, submitForm() { this.updateCover(this.imageUrl) }, async updateCover(cover) { - if (cover === this.coverPath) { - console.warn('Cover has not changed..', cover) + if (!cover.startsWith('http:') && !cover.startsWith('https:')) { + this.$toast.error('Invalid URL') return } this.isProcessing = true - var success = false - - if (!cover) { - // Remove cover - success = await this.$axios - .$delete(`/api/items/${this.libraryItemId}/cover`) - .then(() => true) - .catch((error) => { - console.error('Failed to remove cover', error) - if (error.response && error.response.data) { - this.$toast.error(error.response.data) - } - return false - }) - } else if (cover.startsWith('http:') || cover.startsWith('https:')) { - // Download cover from url and use - success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover }).catch((error) => { - console.error('Failed to download cover from url', error) - if (error.response && error.response.data) { - this.$toast.error(error.response.data) - } - return false + this.$axios + .$post(`/api/items/${this.libraryItemId}/cover`, { url: cover }) + .then(() => { + this.imageUrl = '' + this.$toast.success('Update Successful') }) - } else { - // Update local cover url - const updatePayload = { - cover - } - success = await this.$axios.$patch(`/api/items/${this.libraryItemId}/cover`, updatePayload).catch((error) => { - console.error('Failed to update', error) - if (error.response && error.response.data) { - this.$toast.error(error.response.data) - } - return false + .catch((error) => { + console.error('Failed to update cover', error) + this.$toast.error(error.response?.data || 'Failed to update cover') + }) + .finally(() => { + this.isProcessing = false }) - } - if (success) { - this.$toast.success('Update Successful') - } else if (this.media.coverPath) { - this.imageUrl = this.media.coverPath - } - this.isProcessing = false }, getSearchQuery() { var searchQuery = `provider=${this.provider}&title=${this.searchTitle}` @@ -319,7 +305,19 @@ export default { this.hasSearched = true }, setCover(coverFile) { - this.updateCover(coverFile.metadata.path) + this.isProcessing = true + this.$axios + .$patch(`/api/items/${this.libraryItemId}/cover`, { cover: coverFile.metadata.path }) + .then(() => { + this.$toast.success('Update Successful') + }) + .catch((error) => { + console.error('Failed to set local cover', error) + this.$toast.error(error.response?.data || 'Failed to set cover') + }) + .finally(() => { + this.isProcessing = false + }) } } } diff --git a/client/strings/de.json b/client/strings/de.json index ccd42ede..b72df02f 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -266,6 +266,7 @@ "LabelHost": "Host", "LabelHour": "Stunde", "LabelIcon": "Symbol", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "In die Titelliste aufnehmen", "LabelIncomplete": "Unvollständig", "LabelInProgress": "In Bearbeitung", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 37478bf0..9195265e 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -266,6 +266,7 @@ "LabelHost": "Host", "LabelHour": "Hour", "LabelIcon": "Icon", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Include in Tracklist", "LabelIncomplete": "Incomplete", "LabelInProgress": "In Progress", diff --git a/client/strings/es.json b/client/strings/es.json index f7d548ba..f03c6352 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -266,6 +266,7 @@ "LabelHost": "Host", "LabelHour": "Hora", "LabelIcon": "Icono", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Incluir en Tracklist", "LabelIncomplete": "Incompleto", "LabelInProgress": "En Proceso", diff --git a/client/strings/fr.json b/client/strings/fr.json index 5d0e4b3a..031462b2 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -266,6 +266,7 @@ "LabelHost": "Hôte", "LabelHour": "Heure", "LabelIcon": "Icone", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Inclure dans la liste des pistes", "LabelIncomplete": "Incomplet", "LabelInProgress": "En cours", diff --git a/client/strings/gu.json b/client/strings/gu.json index 5018cf4d..0803ccf4 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -266,6 +266,7 @@ "LabelHost": "Host", "LabelHour": "Hour", "LabelIcon": "Icon", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Include in Tracklist", "LabelIncomplete": "Incomplete", "LabelInProgress": "In Progress", diff --git a/client/strings/hi.json b/client/strings/hi.json index 21ed9893..1eea8495 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -266,6 +266,7 @@ "LabelHost": "Host", "LabelHour": "Hour", "LabelIcon": "Icon", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Include in Tracklist", "LabelIncomplete": "Incomplete", "LabelInProgress": "In Progress", diff --git a/client/strings/hr.json b/client/strings/hr.json index b0e0db91..47908b18 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -266,6 +266,7 @@ "LabelHost": "Host", "LabelHour": "Sat", "LabelIcon": "Ikona", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Dodaj u Tracklist", "LabelIncomplete": "Nepotpuno", "LabelInProgress": "U tijeku", diff --git a/client/strings/it.json b/client/strings/it.json index 96a24392..b60a87c1 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -266,6 +266,7 @@ "LabelHost": "Host", "LabelHour": "Ora", "LabelIcon": "Icona", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Includi nella Tracklist", "LabelIncomplete": "Incompleta", "LabelInProgress": "In Corso", diff --git a/client/strings/lt.json b/client/strings/lt.json index 4f7bf2ed..31d259e6 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -266,6 +266,7 @@ "LabelHost": "Serveris", "LabelHour": "Valanda", "LabelIcon": "Piktograma", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Įtraukti į takelių sąrašą", "LabelIncomplete": "Nebaigta", "LabelInProgress": "Vyksta", diff --git a/client/strings/nl.json b/client/strings/nl.json index ac61de96..eb6b35b3 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -266,6 +266,7 @@ "LabelHost": "Host", "LabelHour": "Uur", "LabelIcon": "Icoon", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Includeer in tracklijst", "LabelIncomplete": "Incompleet", "LabelInProgress": "Bezig", diff --git a/client/strings/no.json b/client/strings/no.json index d1f51aac..f4fe316c 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -266,6 +266,7 @@ "LabelHost": "Tjener", "LabelHour": "Time", "LabelIcon": "Ikon", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Inkluder i sporliste", "LabelIncomplete": "Ufullstendig", "LabelInProgress": "I gang", diff --git a/client/strings/pl.json b/client/strings/pl.json index c4e6ae84..a645877b 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -266,6 +266,7 @@ "LabelHost": "Host", "LabelHour": "Godzina", "LabelIcon": "Ikona", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Dołącz do listy odtwarzania", "LabelIncomplete": "Nieukończone", "LabelInProgress": "W trakcie", diff --git a/client/strings/ru.json b/client/strings/ru.json index 3c95affa..f7f56965 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -266,6 +266,7 @@ "LabelHost": "Хост", "LabelHour": "Часы", "LabelIcon": "Иконка", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Включать в список воспроизведения", "LabelIncomplete": "Не завершен", "LabelInProgress": "В процессе", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 09eb6708..1d7f90dd 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -266,6 +266,7 @@ "LabelHost": "主机", "LabelHour": "小时", "LabelIcon": "图标", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "包含在音轨列表中", "LabelIncomplete": "未听完", "LabelInProgress": "正在听", diff --git a/package-lock.json b/package-lock.json index 77948004..7178ac98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "sequelize": "^6.32.1", "socket.io": "^4.5.4", "sqlite3": "^5.1.6", + "ssrf-req-filter": "^1.1.0", "xml2js": "^0.5.0" }, "bin": { @@ -2387,6 +2388,22 @@ } } }, + "node_modules/ssrf-req-filter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ssrf-req-filter/-/ssrf-req-filter-1.1.0.tgz", + "integrity": "sha512-YUyTinAEm52NsoDvkTFN9BQIa5nURNr2aN0BwOiJxHK3tlyGUczHa+2LjcibKNugAk/losB6kXOfcRzy0LQ4uA==", + "dependencies": { + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/ssrf-req-filter/node_modules/ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", + "engines": { + "node": ">= 10" + } + }, "node_modules/ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", @@ -4437,6 +4454,21 @@ "tar": "^6.1.11" } }, + "ssrf-req-filter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ssrf-req-filter/-/ssrf-req-filter-1.1.0.tgz", + "integrity": "sha512-YUyTinAEm52NsoDvkTFN9BQIa5nURNr2aN0BwOiJxHK3tlyGUczHa+2LjcibKNugAk/losB6kXOfcRzy0LQ4uA==", + "requires": { + "ipaddr.js": "^2.1.0" + }, + "dependencies": { + "ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==" + } + } + }, "ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", @@ -4672,4 +4704,4 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 4a00fa59..e76147d8 100644 --- a/package.json +++ b/package.json @@ -39,9 +39,10 @@ "sequelize": "^6.32.1", "socket.io": "^4.5.4", "sqlite3": "^5.1.6", + "ssrf-req-filter": "^1.1.0", "xml2js": "^0.5.0" }, "devDependencies": { "nodemon": "^2.0.20" } -} \ No newline at end of file +} diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index ac019a96..b0ecf446 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -182,22 +182,22 @@ class LibraryItemController { return res.sendStatus(403) } - var libraryItem = req.libraryItem + let libraryItem = req.libraryItem - var result = null - if (req.body && req.body.url) { + let result = null + if (req.body?.url) { Logger.debug(`[LibraryItemController] Requesting download cover from url "${req.body.url}"`) result = await CoverManager.downloadCoverFromUrl(libraryItem, req.body.url) - } else if (req.files && req.files.cover) { + } else if (req.files?.cover) { Logger.debug(`[LibraryItemController] Handling uploaded cover`) result = await CoverManager.uploadCover(libraryItem, req.files.cover) } else { return res.status(400).send('Invalid request no file or url') } - if (result && result.error) { + if (result?.error) { return res.status(400).send(result.error) - } else if (!result || !result.cover) { + } else if (!result?.cover) { return res.status(500).send('Unknown error occurred') } diff --git a/server/managers/CoverManager.js b/server/managers/CoverManager.js index f30c9c6d..934deaff 100644 --- a/server/managers/CoverManager.js +++ b/server/managers/CoverManager.js @@ -120,13 +120,16 @@ class CoverManager { await fs.ensureDir(coverDirPath) var temppath = Path.posix.join(coverDirPath, 'cover') - var success = await downloadFile(url, temppath).then(() => true).catch((err) => { - Logger.error(`[CoverManager] Download image file failed for "${url}"`, err) + + let errorMsg = '' + let success = await downloadFile(url, temppath).then(() => true).catch((err) => { + errorMsg = err.message || 'Unknown error' + Logger.error(`[CoverManager] Download image file failed for "${url}"`, errorMsg) return false }) if (!success) { return { - error: 'Failed to download image from url' + error: 'Failed to download image from url: ' + errorMsg } } diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 966c7a93..37e89029 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -1,7 +1,8 @@ -const fs = require('../libs/fsExtra') -const rra = require('../libs/recursiveReaddirAsync') const axios = require('axios') const Path = require('path') +const ssrfFilter = require('ssrf-req-filter') +const fs = require('../libs/fsExtra') +const rra = require('../libs/recursiveReaddirAsync') const Logger = require('../Logger') const { AudioMimeType } = require('./constants') @@ -210,7 +211,9 @@ module.exports.downloadFile = (url, filepath) => { url, method: 'GET', responseType: 'stream', - timeout: 30000 + timeout: 30000, + httpAgent: ssrfFilter(url), + httpsAgent: ssrfFilter(url) }).then((response) => { const writer = fs.createWriteStream(filepath) response.data.pipe(writer)