X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fhelpers%2Frequests.ts;h=fd2a56f30c717e6ce892b1fd90deca6af228a327;hb=ca4b4b2e5590c1b37cff1fe1be7f797b93351229;hp=3fc776f1a53299ceb2630313938d8c4757b24b33;hpb=2a8c5d0af13f3ccb9a505e1fbc9d324b9d33ba1f;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts index 3fc776f1a..fd2a56f30 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts @@ -1,46 +1,201 @@ -import * as Bluebird from 'bluebird' -import { createWriteStream } from 'fs-extra' -import * as request from 'request' -import { ACTIVITY_PUB, CONFIG } from '../initializers' -import { processImage } from './image-utils' +import { createWriteStream, remove } from 'fs-extra' +import got, { CancelableRequest, Options as GotOptions, RequestError } from 'got' import { join } from 'path' +import { CONFIG } from '../initializers/config' +import { ACTIVITY_PUB, PEERTUBE_VERSION, WEBSERVER } from '../initializers/constants' +import { pipelinePromise } from './core-utils' +import { processImage } from './image-utils' +import { logger } from './logger' + +export interface PeerTubeRequestError extends Error { + statusCode?: number + responseBody?: any +} -function doRequest ( - requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } -): Bluebird<{ response: request.RequestResponse, body: any }> { - if (requestOptions.activityPub === true) { - if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {} - requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER +const httpSignature = require('http-signature') + +type PeerTubeRequestOptions = { + activityPub?: boolean + bodyKBLimit?: number // 1MB + httpSignature?: { + algorithm: string + authorizationHeaderName: string + keyId: string + key: string + headers: string[] } + jsonResponse?: boolean +} & Pick + +const peertubeGot = got.extend({ + headers: { + 'user-agent': getUserAgent() + }, + + handlers: [ + (options, next) => { + const promiseOrStream = next(options) as CancelableRequest + const bodyKBLimit = options.context?.bodyKBLimit as number + if (!bodyKBLimit) throw new Error('No KB limit for this request') + + const bodyLimit = bodyKBLimit * 1000 + + /* eslint-disable @typescript-eslint/no-floating-promises */ + promiseOrStream.on('downloadProgress', progress => { + if (progress.transferred > bodyLimit && progress.percent !== 1) { + const message = `Exceeded the download limit of ${bodyLimit} B` + logger.warn(message) + + // CancelableRequest + if (promiseOrStream.cancel) { + promiseOrStream.cancel() + return + } + + // Stream + (promiseOrStream as any).destroy() + } + }) + + return promiseOrStream + } + ], + + hooks: { + beforeRequest: [ + options => { + const headers = options.headers || {} + headers['host'] = options.url.host + }, - return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => { - request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) - }) + options => { + const httpSignatureOptions = options.context?.httpSignature + + if (httpSignatureOptions) { + const method = options.method ?? 'GET' + const path = options.path ?? options.url.pathname + + if (!method || !path) { + throw new Error(`Cannot sign request without method (${method}) or path (${path}) ${options}`) + } + + httpSignature.signRequest({ + getHeader: function (header) { + return options.headers[header] + }, + + setHeader: function (header, value) { + options.headers[header] = value + }, + + method, + path + }, httpSignatureOptions) + } + } + ] + } +}) + +function doRequest (url: string, options: PeerTubeRequestOptions = {}) { + const gotOptions = buildGotOptions(options) + + return peertubeGot(url, gotOptions) + .catch(err => { throw buildRequestError(err) }) } -function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.UriOptions, destPath: string) { - return new Bluebird((res, rej) => { - const file = createWriteStream(destPath) - file.on('finish', () => res()) +function doJSONRequest (url: string, options: PeerTubeRequestOptions = {}) { + const gotOptions = buildGotOptions(options) - request(requestOptions) - .on('error', err => rej(err)) - .pipe(file) - }) + return peertubeGot(url, { ...gotOptions, responseType: 'json' }) + .catch(err => { throw buildRequestError(err) }) +} + +async function doRequestAndSaveToFile ( + url: string, + destPath: string, + options: PeerTubeRequestOptions = {} +) { + const gotOptions = buildGotOptions(options) + + const outFile = createWriteStream(destPath) + + try { + await pipelinePromise( + peertubeGot.stream(url, gotOptions), + outFile + ) + } catch (err) { + remove(destPath) + .catch(err => logger.error('Cannot remove %s after request failure.', destPath, { err })) + + throw buildRequestError(err) + } } async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) { const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName) - await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath) + await doRequestAndSaveToFile(url, tmpPath) const destPath = join(destDir, destName) - await processImage({ path: tmpPath }, destPath, size) + + try { + await processImage(tmpPath, destPath, size) + } catch (err) { + await remove(tmpPath) + + throw err + } +} + +function getUserAgent () { + return `PeerTube/${PEERTUBE_VERSION} (+${WEBSERVER.URL})` } // --------------------------------------------------------------------------- export { doRequest, + doJSONRequest, doRequestAndSaveToFile, downloadImage } + +// --------------------------------------------------------------------------- + +function buildGotOptions (options: PeerTubeRequestOptions) { + const { activityPub, bodyKBLimit = 1000 } = options + + const context = { bodyKBLimit, httpSignature: options.httpSignature } + + let headers = options.headers || {} + + if (!headers.date) { + headers = { ...headers, date: new Date().toUTCString() } + } + + if (activityPub && !headers.accept) { + headers = { ...headers, accept: ACTIVITY_PUB.ACCEPT_HEADER } + } + + return { + method: options.method, + json: options.json, + searchParams: options.searchParams, + headers, + context + } +} + +function buildRequestError (error: RequestError) { + const newError: PeerTubeRequestError = new Error(error.message) + newError.name = error.name + newError.stack = error.stack + + if (error.response) { + newError.responseBody = error.response.body + newError.statusCode = error.response.statusCode + } + + return newError +}