diff options
Diffstat (limited to 'server/helpers')
-rw-r--r-- | server/helpers/activitypub.ts | 5 | ||||
-rw-r--r-- | server/helpers/core-utils.ts | 6 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/activity.ts | 2 | ||||
-rw-r--r-- | server/helpers/peertube-crypto.ts | 2 | ||||
-rw-r--r-- | server/helpers/requests.ts | 179 | ||||
-rw-r--r-- | server/helpers/youtube-dl.ts | 71 |
6 files changed, 165 insertions, 100 deletions
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts index 08aef2908..e0754b501 100644 --- a/server/helpers/activitypub.ts +++ b/server/helpers/activitypub.ts | |||
@@ -3,7 +3,6 @@ import { URL } from 'url' | |||
3 | import validator from 'validator' | 3 | import validator from 'validator' |
4 | import { ContextType } from '@shared/models/activitypub/context' | 4 | import { ContextType } from '@shared/models/activitypub/context' |
5 | import { ResultList } from '../../shared/models' | 5 | import { ResultList } from '../../shared/models' |
6 | import { Activity } from '../../shared/models/activitypub' | ||
7 | import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants' | 6 | import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants' |
8 | import { MActor, MVideoWithHost } from '../types/models' | 7 | import { MActor, MVideoWithHost } from '../types/models' |
9 | import { pageToStartAndCount } from './core-utils' | 8 | import { pageToStartAndCount } from './core-utils' |
@@ -182,10 +181,10 @@ async function activityPubCollectionPagination ( | |||
182 | 181 | ||
183 | } | 182 | } |
184 | 183 | ||
185 | function buildSignedActivity (byActor: MActor, data: Object, contextType?: ContextType) { | 184 | function buildSignedActivity <T> (byActor: MActor, data: T, contextType?: ContextType) { |
186 | const activity = activityPubContextify(data, contextType) | 185 | const activity = activityPubContextify(data, contextType) |
187 | 186 | ||
188 | return signJsonLDObject(byActor, activity) as Promise<Activity> | 187 | return signJsonLDObject(byActor, activity) |
189 | } | 188 | } |
190 | 189 | ||
191 | function getAPId (activity: string | { id: string }) { | 190 | function getAPId (activity: string | { id: string }) { |
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index 935fd22d9..7ba7d865a 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts | |||
@@ -10,7 +10,9 @@ import { BinaryToTextEncoding, createHash, randomBytes } from 'crypto' | |||
10 | import { truncate } from 'lodash' | 10 | import { truncate } from 'lodash' |
11 | import { basename, isAbsolute, join, resolve } from 'path' | 11 | import { basename, isAbsolute, join, resolve } from 'path' |
12 | import * as pem from 'pem' | 12 | import * as pem from 'pem' |
13 | import { pipeline } from 'stream' | ||
13 | import { URL } from 'url' | 14 | import { URL } from 'url' |
15 | import { promisify } from 'util' | ||
14 | 16 | ||
15 | const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => { | 17 | const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => { |
16 | if (!oldObject || typeof oldObject !== 'object') { | 18 | if (!oldObject || typeof oldObject !== 'object') { |
@@ -254,6 +256,7 @@ const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKe | |||
254 | const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey) | 256 | const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey) |
255 | const execPromise2 = promisify2<string, any, string>(exec) | 257 | const execPromise2 = promisify2<string, any, string>(exec) |
256 | const execPromise = promisify1<string, string>(exec) | 258 | const execPromise = promisify1<string, string>(exec) |
259 | const pipelinePromise = promisify(pipeline) | ||
257 | 260 | ||
258 | // --------------------------------------------------------------------------- | 261 | // --------------------------------------------------------------------------- |
259 | 262 | ||
@@ -284,5 +287,6 @@ export { | |||
284 | createPrivateKey, | 287 | createPrivateKey, |
285 | getPublicKey, | 288 | getPublicKey, |
286 | execPromise2, | 289 | execPromise2, |
287 | execPromise | 290 | execPromise, |
291 | pipelinePromise | ||
288 | } | 292 | } |
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts index 46126da57..69558e358 100644 --- a/server/helpers/custom-validators/activitypub/activity.ts +++ b/server/helpers/custom-validators/activitypub/activity.ts | |||
@@ -41,7 +41,7 @@ const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean | |||
41 | } | 41 | } |
42 | 42 | ||
43 | function isActivityValid (activity: any) { | 43 | function isActivityValid (activity: any) { |
44 | const checker = activityCheckers[activity.tswype] | 44 | const checker = activityCheckers[activity.type] |
45 | // Unknown activity type | 45 | // Unknown activity type |
46 | if (!checker) return false | 46 | if (!checker) return false |
47 | 47 | ||
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index 994f725d8..bc6f1d074 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts | |||
@@ -84,7 +84,7 @@ async function isJsonLDRSA2017Verified (fromActor: MActor, signedDocument: any) | |||
84 | return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64') | 84 | return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64') |
85 | } | 85 | } |
86 | 86 | ||
87 | async function signJsonLDObject (byActor: MActor, data: any) { | 87 | async function signJsonLDObject <T> (byActor: MActor, data: T) { |
88 | const signature = { | 88 | const signature = { |
89 | type: 'RsaSignature2017', | 89 | type: 'RsaSignature2017', |
90 | creator: byActor.url, | 90 | creator: byActor.url, |
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts index b556c392e..2c9da213c 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts | |||
@@ -1,58 +1,124 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { createWriteStream, remove } from 'fs-extra' | 1 | import { createWriteStream, remove } from 'fs-extra' |
3 | import * as request from 'request' | 2 | import got, { CancelableRequest, Options as GotOptions } from 'got' |
3 | import { join } from 'path' | ||
4 | import { CONFIG } from '../initializers/config' | ||
4 | import { ACTIVITY_PUB, PEERTUBE_VERSION, WEBSERVER } from '../initializers/constants' | 5 | import { ACTIVITY_PUB, PEERTUBE_VERSION, WEBSERVER } from '../initializers/constants' |
6 | import { pipelinePromise } from './core-utils' | ||
5 | import { processImage } from './image-utils' | 7 | import { processImage } from './image-utils' |
6 | import { join } from 'path' | ||
7 | import { logger } from './logger' | 8 | import { logger } from './logger' |
8 | import { CONFIG } from '../initializers/config' | ||
9 | 9 | ||
10 | function doRequest <T> ( | 10 | const httpSignature = require('http-signature') |
11 | requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }, | 11 | |
12 | bodyKBLimit = 1000 // 1MB | 12 | type PeerTubeRequestOptions = { |
13 | ): Bluebird<{ response: request.RequestResponse, body: T }> { | 13 | activityPub?: boolean |
14 | if (!(requestOptions.headers)) requestOptions.headers = {} | 14 | bodyKBLimit?: number // 1MB |
15 | requestOptions.headers['User-Agent'] = getUserAgent() | 15 | httpSignature?: { |
16 | algorithm: string | ||
17 | authorizationHeaderName: string | ||
18 | keyId: string | ||
19 | key: string | ||
20 | headers: string[] | ||
21 | } | ||
22 | jsonResponse?: boolean | ||
23 | } & Pick<GotOptions, 'headers' | 'json' | 'method' | 'searchParams'> | ||
24 | |||
25 | const peertubeGot = got.extend({ | ||
26 | headers: { | ||
27 | 'user-agent': getUserAgent() | ||
28 | }, | ||
29 | |||
30 | handlers: [ | ||
31 | (options, next) => { | ||
32 | const promiseOrStream = next(options) as CancelableRequest<any> | ||
33 | const bodyKBLimit = options.context?.bodyKBLimit | ||
34 | if (!bodyKBLimit) throw new Error('No KB limit for this request') | ||
35 | |||
36 | /* eslint-disable @typescript-eslint/no-floating-promises */ | ||
37 | promiseOrStream.on('downloadProgress', progress => { | ||
38 | if (progress.transferred * 1000 > bodyKBLimit && progress.percent !== 1) { | ||
39 | promiseOrStream.cancel(`Exceeded the download limit of ${bodyKBLimit} bytes`) | ||
40 | } | ||
41 | }) | ||
16 | 42 | ||
17 | if (requestOptions.activityPub === true) { | 43 | return promiseOrStream |
18 | requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER | 44 | } |
45 | ], | ||
46 | |||
47 | hooks: { | ||
48 | beforeRequest: [ | ||
49 | options => { | ||
50 | const headers = options.headers || {} | ||
51 | headers['host'] = options.url.host | ||
52 | }, | ||
53 | |||
54 | options => { | ||
55 | const httpSignatureOptions = options.context?.httpSignature | ||
56 | |||
57 | if (httpSignatureOptions) { | ||
58 | const method = options.method ?? 'GET' | ||
59 | const path = options.path ?? options.url.pathname | ||
60 | |||
61 | if (!method || !path) { | ||
62 | throw new Error(`Cannot sign request without method (${method}) or path (${path}) ${options}`) | ||
63 | } | ||
64 | |||
65 | httpSignature.signRequest({ | ||
66 | getHeader: function (header) { | ||
67 | return options.headers[header] | ||
68 | }, | ||
69 | |||
70 | setHeader: function (header, value) { | ||
71 | options.headers[header] = value | ||
72 | }, | ||
73 | |||
74 | method, | ||
75 | path | ||
76 | }, httpSignatureOptions) | ||
77 | } | ||
78 | } | ||
79 | ] | ||
19 | } | 80 | } |
81 | }) | ||
20 | 82 | ||
21 | return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => { | 83 | function doRequest (url: string, options: PeerTubeRequestOptions = {}) { |
22 | request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) | 84 | const gotOptions = buildGotOptions(options) |
23 | .on('data', onRequestDataLengthCheck(bodyKBLimit)) | 85 | |
24 | }) | 86 | return peertubeGot(url, gotOptions) |
87 | .catch(err => { throw buildRequestError(err) }) | ||
88 | } | ||
89 | |||
90 | function doJSONRequest <T> (url: string, options: PeerTubeRequestOptions = {}) { | ||
91 | const gotOptions = buildGotOptions(options) | ||
92 | |||
93 | return peertubeGot<T>(url, { ...gotOptions, responseType: 'json' }) | ||
94 | .catch(err => { throw buildRequestError(err) }) | ||
25 | } | 95 | } |
26 | 96 | ||
27 | function doRequestAndSaveToFile ( | 97 | async function doRequestAndSaveToFile ( |
28 | requestOptions: request.CoreOptions & request.UriOptions, | 98 | url: string, |
29 | destPath: string, | 99 | destPath: string, |
30 | bodyKBLimit = 10000 // 10MB | 100 | options: PeerTubeRequestOptions = {} |
31 | ) { | 101 | ) { |
32 | if (!requestOptions.headers) requestOptions.headers = {} | 102 | const gotOptions = buildGotOptions(options) |
33 | requestOptions.headers['User-Agent'] = getUserAgent() | ||
34 | |||
35 | return new Bluebird<void>((res, rej) => { | ||
36 | const file = createWriteStream(destPath) | ||
37 | file.on('finish', () => res()) | ||
38 | 103 | ||
39 | request(requestOptions) | 104 | const outFile = createWriteStream(destPath) |
40 | .on('data', onRequestDataLengthCheck(bodyKBLimit)) | ||
41 | .on('error', err => { | ||
42 | file.close() | ||
43 | 105 | ||
44 | remove(destPath) | 106 | try { |
45 | .catch(err => logger.error('Cannot remove %s after request failure.', destPath, { err })) | 107 | await pipelinePromise( |
108 | peertubeGot.stream(url, gotOptions), | ||
109 | outFile | ||
110 | ) | ||
111 | } catch (err) { | ||
112 | remove(destPath) | ||
113 | .catch(err => logger.error('Cannot remove %s after request failure.', destPath, { err })) | ||
46 | 114 | ||
47 | return rej(err) | 115 | throw buildRequestError(err) |
48 | }) | 116 | } |
49 | .pipe(file) | ||
50 | }) | ||
51 | } | 117 | } |
52 | 118 | ||
53 | async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) { | 119 | async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) { |
54 | const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName) | 120 | const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName) |
55 | await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath) | 121 | await doRequestAndSaveToFile(url, tmpPath) |
56 | 122 | ||
57 | const destPath = join(destDir, destName) | 123 | const destPath = join(destDir, destName) |
58 | 124 | ||
@@ -73,24 +139,43 @@ function getUserAgent () { | |||
73 | 139 | ||
74 | export { | 140 | export { |
75 | doRequest, | 141 | doRequest, |
142 | doJSONRequest, | ||
76 | doRequestAndSaveToFile, | 143 | doRequestAndSaveToFile, |
77 | downloadImage | 144 | downloadImage |
78 | } | 145 | } |
79 | 146 | ||
80 | // --------------------------------------------------------------------------- | 147 | // --------------------------------------------------------------------------- |
81 | 148 | ||
82 | // Thanks to https://github.com/request/request/issues/2470#issuecomment-268929907 <3 | 149 | function buildGotOptions (options: PeerTubeRequestOptions) { |
83 | function onRequestDataLengthCheck (bodyKBLimit: number) { | 150 | const { activityPub, bodyKBLimit = 1000 } = options |
84 | let bufferLength = 0 | ||
85 | const bytesLimit = bodyKBLimit * 1000 | ||
86 | 151 | ||
87 | return function (chunk) { | 152 | const context = { bodyKBLimit, httpSignature: options.httpSignature } |
88 | bufferLength += chunk.length | ||
89 | if (bufferLength > bytesLimit) { | ||
90 | this.abort() | ||
91 | 153 | ||
92 | const error = new Error(`Response was too large - aborted after ${bytesLimit} bytes.`) | 154 | let headers = options.headers || {} |
93 | this.emit('error', error) | 155 | |
94 | } | 156 | headers = { ...headers, date: new Date().toUTCString() } |
157 | |||
158 | if (activityPub) { | ||
159 | headers = { ...headers, accept: ACTIVITY_PUB.ACCEPT_HEADER } | ||
95 | } | 160 | } |
161 | |||
162 | return { | ||
163 | method: options.method, | ||
164 | json: options.json, | ||
165 | searchParams: options.searchParams, | ||
166 | headers, | ||
167 | context | ||
168 | } | ||
169 | } | ||
170 | |||
171 | function buildRequestError (error: any) { | ||
172 | const newError = new Error(error.message) | ||
173 | newError.name = error.name | ||
174 | newError.stack = error.stack | ||
175 | |||
176 | if (error.response?.body) { | ||
177 | error.responseBody = error.response.body | ||
178 | } | ||
179 | |||
180 | return newError | ||
96 | } | 181 | } |
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts index 8537a5772..9d2e54fb5 100644 --- a/server/helpers/youtube-dl.ts +++ b/server/helpers/youtube-dl.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import { createWriteStream } from 'fs' | 1 | import { createWriteStream } from 'fs' |
2 | import { ensureDir, move, pathExists, remove, writeFile } from 'fs-extra' | 2 | import { ensureDir, move, pathExists, remove, writeFile } from 'fs-extra' |
3 | import got from 'got' | ||
3 | import { join } from 'path' | 4 | import { join } from 'path' |
4 | import * as request from 'request' | ||
5 | import { CONFIG } from '@server/initializers/config' | 5 | import { CONFIG } from '@server/initializers/config' |
6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' |
7 | import { VideoResolution } from '../../shared/models/videos' | 7 | import { VideoResolution } from '../../shared/models/videos' |
8 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants' | 8 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants' |
9 | import { getEnabledResolutions } from '../lib/video-transcoding' | 9 | import { getEnabledResolutions } from '../lib/video-transcoding' |
10 | import { peertubeTruncate, root } from './core-utils' | 10 | import { peertubeTruncate, pipelinePromise, root } from './core-utils' |
11 | import { isVideoFileExtnameValid } from './custom-validators/videos' | 11 | import { isVideoFileExtnameValid } from './custom-validators/videos' |
12 | import { logger } from './logger' | 12 | import { logger } from './logger' |
13 | import { generateVideoImportTmpPath } from './utils' | 13 | import { generateVideoImportTmpPath } from './utils' |
@@ -195,55 +195,32 @@ async function updateYoutubeDLBinary () { | |||
195 | 195 | ||
196 | await ensureDir(binDirectory) | 196 | await ensureDir(binDirectory) |
197 | 197 | ||
198 | return new Promise<void>(res => { | 198 | try { |
199 | request.get(url, { followRedirect: false }, (err, result) => { | 199 | const result = await got(url, { followRedirect: false }) |
200 | if (err) { | ||
201 | logger.error('Cannot update youtube-dl.', { err }) | ||
202 | return res() | ||
203 | } | ||
204 | |||
205 | if (result.statusCode !== HttpStatusCode.FOUND_302) { | ||
206 | logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode) | ||
207 | return res() | ||
208 | } | ||
209 | |||
210 | const url = result.headers.location | ||
211 | const downloadFile = request.get(url) | ||
212 | const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(url)[1] | ||
213 | |||
214 | downloadFile.on('response', result => { | ||
215 | if (result.statusCode !== HttpStatusCode.OK_200) { | ||
216 | logger.error('Cannot update youtube-dl: new version response is not 200, it\'s %d.', result.statusCode) | ||
217 | return res() | ||
218 | } | ||
219 | |||
220 | const writeStream = createWriteStream(bin, { mode: 493 }).on('error', err => { | ||
221 | logger.error('youtube-dl update error in write stream', { err }) | ||
222 | return res() | ||
223 | }) | ||
224 | 200 | ||
225 | downloadFile.pipe(writeStream) | 201 | if (result.statusCode !== HttpStatusCode.FOUND_302) { |
226 | }) | 202 | logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode) |
203 | return | ||
204 | } | ||
227 | 205 | ||
228 | downloadFile.on('error', err => { | 206 | const newUrl = result.headers.location |
229 | logger.error('youtube-dl update error.', { err }) | 207 | const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(newUrl)[1] |
230 | return res() | ||
231 | }) | ||
232 | 208 | ||
233 | downloadFile.on('end', () => { | 209 | const downloadFileStream = got.stream(newUrl) |
234 | const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' }) | 210 | const writeStream = createWriteStream(bin, { mode: 493 }) |
235 | writeFile(detailsPath, details, { encoding: 'utf8' }, err => { | ||
236 | if (err) { | ||
237 | logger.error('youtube-dl update error: cannot write details.', { err }) | ||
238 | return res() | ||
239 | } | ||
240 | 211 | ||
241 | logger.info('youtube-dl updated to version %s.', newVersion) | 212 | await pipelinePromise( |
242 | return res() | 213 | downloadFileStream, |
243 | }) | 214 | writeStream |
244 | }) | 215 | ) |
245 | }) | 216 | |
246 | }) | 217 | const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' }) |
218 | await writeFile(detailsPath, details, { encoding: 'utf8' }) | ||
219 | |||
220 | logger.info('youtube-dl updated to version %s.', newVersion) | ||
221 | } catch (err) { | ||
222 | logger.error('Cannot update youtube-dl.', { err }) | ||
223 | } | ||
247 | } | 224 | } |
248 | 225 | ||
249 | async function safeGetYoutubeDL () { | 226 | async function safeGetYoutubeDL () { |