]>
Commit | Line | Data |
---|---|---|
bfe2ef6b | 1 | import { createWriteStream, remove } from 'fs-extra' |
b5c36108 | 2 | import got, { CancelableRequest, Options as GotOptions, RequestError } from 'got' |
db4b15f2 C |
3 | import { join } from 'path' |
4 | import { CONFIG } from '../initializers/config' | |
7500d6c9 | 5 | import { ACTIVITY_PUB, PEERTUBE_VERSION, REQUEST_TIMEOUT, WEBSERVER } from '../initializers/constants' |
db4b15f2 | 6 | import { pipelinePromise } from './core-utils' |
58d515e3 | 7 | import { processImage } from './image-utils' |
bfe2ef6b | 8 | import { logger } from './logger' |
da854ddd | 9 | |
b5c36108 C |
10 | export interface PeerTubeRequestError extends Error { |
11 | statusCode?: number | |
12 | responseBody?: any | |
13 | } | |
14 | ||
db4b15f2 C |
15 | const httpSignature = require('http-signature') |
16 | ||
17 | type PeerTubeRequestOptions = { | |
18 | activityPub?: boolean | |
19 | bodyKBLimit?: number // 1MB | |
20 | httpSignature?: { | |
21 | algorithm: string | |
22 | authorizationHeaderName: string | |
23 | keyId: string | |
24 | key: string | |
25 | headers: string[] | |
26 | } | |
7500d6c9 | 27 | timeout?: number |
db4b15f2 C |
28 | jsonResponse?: boolean |
29 | } & Pick<GotOptions, 'headers' | 'json' | 'method' | 'searchParams'> | |
30 | ||
31 | const peertubeGot = got.extend({ | |
32 | headers: { | |
33 | 'user-agent': getUserAgent() | |
34 | }, | |
35 | ||
36 | handlers: [ | |
37 | (options, next) => { | |
38 | const promiseOrStream = next(options) as CancelableRequest<any> | |
b329abc2 | 39 | const bodyKBLimit = options.context?.bodyKBLimit as number |
db4b15f2 C |
40 | if (!bodyKBLimit) throw new Error('No KB limit for this request') |
41 | ||
b329abc2 C |
42 | const bodyLimit = bodyKBLimit * 1000 |
43 | ||
db4b15f2 C |
44 | /* eslint-disable @typescript-eslint/no-floating-promises */ |
45 | promiseOrStream.on('downloadProgress', progress => { | |
b329abc2 C |
46 | if (progress.transferred > bodyLimit && progress.percent !== 1) { |
47 | const message = `Exceeded the download limit of ${bodyLimit} B` | |
48 | logger.warn(message) | |
49 | ||
50 | // CancelableRequest | |
51 | if (promiseOrStream.cancel) { | |
52 | promiseOrStream.cancel() | |
53 | return | |
54 | } | |
55 | ||
56 | // Stream | |
57 | (promiseOrStream as any).destroy() | |
db4b15f2 C |
58 | } |
59 | }) | |
e0ce715a | 60 | |
db4b15f2 C |
61 | return promiseOrStream |
62 | } | |
63 | ], | |
64 | ||
65 | hooks: { | |
66 | beforeRequest: [ | |
67 | options => { | |
68 | const headers = options.headers || {} | |
69 | headers['host'] = options.url.host | |
70 | }, | |
71 | ||
72 | options => { | |
73 | const httpSignatureOptions = options.context?.httpSignature | |
74 | ||
75 | if (httpSignatureOptions) { | |
76 | const method = options.method ?? 'GET' | |
77 | const path = options.path ?? options.url.pathname | |
78 | ||
79 | if (!method || !path) { | |
80 | throw new Error(`Cannot sign request without method (${method}) or path (${path}) ${options}`) | |
81 | } | |
82 | ||
83 | httpSignature.signRequest({ | |
84 | getHeader: function (header) { | |
85 | return options.headers[header] | |
86 | }, | |
87 | ||
88 | setHeader: function (header, value) { | |
89 | options.headers[header] = value | |
90 | }, | |
91 | ||
92 | method, | |
93 | path | |
94 | }, httpSignatureOptions) | |
95 | } | |
7500d6c9 C |
96 | }, |
97 | ||
98 | (options: GotOptions) => { | |
99 | options.timeout = REQUEST_TIMEOUT | |
db4b15f2 C |
100 | } |
101 | ] | |
da854ddd | 102 | } |
db4b15f2 | 103 | }) |
e4f97bab | 104 | |
db4b15f2 C |
105 | function doRequest (url: string, options: PeerTubeRequestOptions = {}) { |
106 | const gotOptions = buildGotOptions(options) | |
107 | ||
108 | return peertubeGot(url, gotOptions) | |
109 | .catch(err => { throw buildRequestError(err) }) | |
110 | } | |
111 | ||
112 | function doJSONRequest <T> (url: string, options: PeerTubeRequestOptions = {}) { | |
113 | const gotOptions = buildGotOptions(options) | |
114 | ||
115 | return peertubeGot<T>(url, { ...gotOptions, responseType: 'json' }) | |
116 | .catch(err => { throw buildRequestError(err) }) | |
e4f97bab | 117 | } |
dac0a531 | 118 | |
db4b15f2 C |
119 | async function doRequestAndSaveToFile ( |
120 | url: string, | |
bfe2ef6b | 121 | destPath: string, |
db4b15f2 | 122 | options: PeerTubeRequestOptions = {} |
bfe2ef6b | 123 | ) { |
db4b15f2 | 124 | const gotOptions = buildGotOptions(options) |
02988fdc | 125 | |
db4b15f2 | 126 | const outFile = createWriteStream(destPath) |
bfe2ef6b | 127 | |
db4b15f2 C |
128 | try { |
129 | await pipelinePromise( | |
130 | peertubeGot.stream(url, gotOptions), | |
131 | outFile | |
132 | ) | |
133 | } catch (err) { | |
134 | remove(destPath) | |
135 | .catch(err => logger.error('Cannot remove %s after request failure.', destPath, { err })) | |
bfe2ef6b | 136 | |
db4b15f2 C |
137 | throw buildRequestError(err) |
138 | } | |
0d0e8dd0 C |
139 | } |
140 | ||
6040f87d C |
141 | async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) { |
142 | const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName) | |
db4b15f2 | 143 | await doRequestAndSaveToFile(url, tmpPath) |
58d515e3 | 144 | |
6040f87d | 145 | const destPath = join(destDir, destName) |
4ee7a4c9 C |
146 | |
147 | try { | |
2fb5b3a5 | 148 | await processImage(tmpPath, destPath, size) |
4ee7a4c9 C |
149 | } catch (err) { |
150 | await remove(tmpPath) | |
151 | ||
152 | throw err | |
153 | } | |
58d515e3 C |
154 | } |
155 | ||
e0ce715a | 156 | function getUserAgent () { |
66170ca8 | 157 | return `PeerTube/${PEERTUBE_VERSION} (+${WEBSERVER.URL})` |
e0ce715a C |
158 | } |
159 | ||
9f10b292 | 160 | // --------------------------------------------------------------------------- |
dac0a531 | 161 | |
65fcc311 | 162 | export { |
e4f97bab | 163 | doRequest, |
db4b15f2 | 164 | doJSONRequest, |
58d515e3 C |
165 | doRequestAndSaveToFile, |
166 | downloadImage | |
65fcc311 | 167 | } |
bfe2ef6b C |
168 | |
169 | // --------------------------------------------------------------------------- | |
170 | ||
db4b15f2 C |
171 | function buildGotOptions (options: PeerTubeRequestOptions) { |
172 | const { activityPub, bodyKBLimit = 1000 } = options | |
bfe2ef6b | 173 | |
db4b15f2 | 174 | const context = { bodyKBLimit, httpSignature: options.httpSignature } |
bfe2ef6b | 175 | |
db4b15f2 C |
176 | let headers = options.headers || {} |
177 | ||
e7053b1d C |
178 | if (!headers.date) { |
179 | headers = { ...headers, date: new Date().toUTCString() } | |
180 | } | |
db4b15f2 | 181 | |
e7053b1d | 182 | if (activityPub && !headers.accept) { |
db4b15f2 | 183 | headers = { ...headers, accept: ACTIVITY_PUB.ACCEPT_HEADER } |
bfe2ef6b | 184 | } |
db4b15f2 C |
185 | |
186 | return { | |
187 | method: options.method, | |
188 | json: options.json, | |
189 | searchParams: options.searchParams, | |
7500d6c9 | 190 | timeout: options.timeout ?? REQUEST_TIMEOUT, |
db4b15f2 C |
191 | headers, |
192 | context | |
193 | } | |
194 | } | |
195 | ||
b5c36108 C |
196 | function buildRequestError (error: RequestError) { |
197 | const newError: PeerTubeRequestError = new Error(error.message) | |
db4b15f2 C |
198 | newError.name = error.name |
199 | newError.stack = error.stack | |
200 | ||
b5c36108 C |
201 | if (error.response) { |
202 | newError.responseBody = error.response.body | |
203 | newError.statusCode = error.response.statusCode | |
db4b15f2 C |
204 | } |
205 | ||
b5c36108 | 206 | return newError |
bfe2ef6b | 207 | } |