]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/helpers/requests.ts
Translated using Weblate (Chinese (Traditional))
[github/Chocobozzz/PeerTube.git] / server / helpers / requests.ts
CommitLineData
bfe2ef6b 1import { createWriteStream, remove } from 'fs-extra'
3455c265 2import got, { CancelableRequest, NormalizedOptions, Options as GotOptions, RequestError, Response } from 'got'
8729a870 3import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'
db4b15f2
C
4import { join } from 'path'
5import { CONFIG } from '../initializers/config'
62549e6c 6import { ACTIVITY_PUB, BINARY_CONTENT_TYPES, PEERTUBE_VERSION, REQUEST_TIMEOUT, WEBSERVER } from '../initializers/constants'
db4b15f2 7import { pipelinePromise } from './core-utils'
58d515e3 8import { processImage } from './image-utils'
ac036180 9import { logger, loggerTagsFactory } from './logger'
8729a870 10import { getProxy, isProxyEnabled } from './proxy'
11
ac036180
C
12const lTags = loggerTagsFactory('request')
13
5842a854 14const httpSignature = require('@peertube/http-signature')
da854ddd 15
b5c36108
C
16export interface PeerTubeRequestError extends Error {
17 statusCode?: number
18 responseBody?: any
3455c265 19 responseHeaders?: any
b5c36108
C
20}
21
db4b15f2
C
22type PeerTubeRequestOptions = {
23 activityPub?: boolean
24 bodyKBLimit?: number // 1MB
25 httpSignature?: {
26 algorithm: string
27 authorizationHeaderName: string
28 keyId: string
29 key: string
30 headers: string[]
31 }
32 jsonResponse?: boolean
33} & Pick<GotOptions, 'headers' | 'json' | 'method' | 'searchParams'>
34
35const peertubeGot = got.extend({
8729a870 36 ...getAgent(),
37
db4b15f2
C
38 headers: {
39 'user-agent': getUserAgent()
40 },
41
42 handlers: [
43 (options, next) => {
44 const promiseOrStream = next(options) as CancelableRequest<any>
b329abc2 45 const bodyKBLimit = options.context?.bodyKBLimit as number
db4b15f2
C
46 if (!bodyKBLimit) throw new Error('No KB limit for this request')
47
b329abc2
C
48 const bodyLimit = bodyKBLimit * 1000
49
db4b15f2
C
50 /* eslint-disable @typescript-eslint/no-floating-promises */
51 promiseOrStream.on('downloadProgress', progress => {
b329abc2
C
52 if (progress.transferred > bodyLimit && progress.percent !== 1) {
53 const message = `Exceeded the download limit of ${bodyLimit} B`
ac036180 54 logger.warn(message, lTags())
b329abc2
C
55
56 // CancelableRequest
57 if (promiseOrStream.cancel) {
58 promiseOrStream.cancel()
59 return
60 }
61
62 // Stream
63 (promiseOrStream as any).destroy()
db4b15f2
C
64 }
65 })
e0ce715a 66
db4b15f2
C
67 return promiseOrStream
68 }
69 ],
70
71 hooks: {
72 beforeRequest: [
73 options => {
74 const headers = options.headers || {}
75 headers['host'] = options.url.host
76 },
77
78 options => {
79 const httpSignatureOptions = options.context?.httpSignature
80
81 if (httpSignatureOptions) {
82 const method = options.method ?? 'GET'
83 const path = options.path ?? options.url.pathname
84
85 if (!method || !path) {
86 throw new Error(`Cannot sign request without method (${method}) or path (${path}) ${options}`)
87 }
88
89 httpSignature.signRequest({
90 getHeader: function (header) {
91 return options.headers[header]
92 },
93
94 setHeader: function (header, value) {
95 options.headers[header] = value
96 },
97
98 method,
99 path
100 }, httpSignatureOptions)
101 }
102 }
3455c265
C
103 ],
104
105 beforeRetry: [
106 (_options: NormalizedOptions, error: RequestError, retryCount: number) => {
107 logger.debug('Retrying request to %s.', error.request.requestUrl, { retryCount, error: buildRequestError(error), ...lTags() })
108 }
db4b15f2 109 ]
da854ddd 110 }
db4b15f2 111})
e4f97bab 112
db4b15f2
C
113function doRequest (url: string, options: PeerTubeRequestOptions = {}) {
114 const gotOptions = buildGotOptions(options)
115
116 return peertubeGot(url, gotOptions)
117 .catch(err => { throw buildRequestError(err) })
118}
119
120function doJSONRequest <T> (url: string, options: PeerTubeRequestOptions = {}) {
121 const gotOptions = buildGotOptions(options)
122
123 return peertubeGot<T>(url, { ...gotOptions, responseType: 'json' })
124 .catch(err => { throw buildRequestError(err) })
e4f97bab 125}
dac0a531 126
db4b15f2
C
127async function doRequestAndSaveToFile (
128 url: string,
bfe2ef6b 129 destPath: string,
db4b15f2 130 options: PeerTubeRequestOptions = {}
bfe2ef6b 131) {
db4b15f2 132 const gotOptions = buildGotOptions(options)
02988fdc 133
db4b15f2 134 const outFile = createWriteStream(destPath)
bfe2ef6b 135
db4b15f2
C
136 try {
137 await pipelinePromise(
138 peertubeGot.stream(url, gotOptions),
139 outFile
140 )
141 } catch (err) {
142 remove(destPath)
ac036180 143 .catch(err => logger.error('Cannot remove %s after request failure.', destPath, { err, ...lTags() }))
bfe2ef6b 144
db4b15f2
C
145 throw buildRequestError(err)
146 }
0d0e8dd0
C
147}
148
6040f87d
C
149async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) {
150 const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName)
db4b15f2 151 await doRequestAndSaveToFile(url, tmpPath)
58d515e3 152
6040f87d 153 const destPath = join(destDir, destName)
4ee7a4c9
C
154
155 try {
2fb5b3a5 156 await processImage(tmpPath, destPath, size)
4ee7a4c9
C
157 } catch (err) {
158 await remove(tmpPath)
159
160 throw err
161 }
58d515e3
C
162}
163
8729a870 164function getAgent () {
165 if (!isProxyEnabled()) return {}
166
167 const proxy = getProxy()
168
ac036180 169 logger.info('Using proxy %s.', proxy, lTags())
8729a870 170
171 const proxyAgentOptions = {
172 keepAlive: true,
173 keepAliveMsecs: 1000,
174 maxSockets: 256,
175 maxFreeSockets: 256,
176 scheduling: 'lifo' as 'lifo',
177 proxy
178 }
179
180 return {
181 agent: {
182 http: new HttpProxyAgent(proxyAgentOptions),
183 https: new HttpsProxyAgent(proxyAgentOptions)
184 }
185 }
186}
187
e0ce715a 188function getUserAgent () {
66170ca8 189 return `PeerTube/${PEERTUBE_VERSION} (+${WEBSERVER.URL})`
e0ce715a
C
190}
191
62549e6c
C
192function isBinaryResponse (result: Response<any>) {
193 return BINARY_CONTENT_TYPES.has(result.headers['content-type'])
194}
195
dedcd583
C
196async function findLatestRedirection (url: string, options: PeerTubeRequestOptions, iteration = 1) {
197 if (iteration > 10) throw new Error('Too much iterations to find final URL ' + url)
198
199 const { headers } = await peertubeGot(url, { followRedirect: false, ...buildGotOptions(options) })
200
201 if (headers.location) return findLatestRedirection(headers.location, options, iteration + 1)
202
203 return url
204}
205
9f10b292 206// ---------------------------------------------------------------------------
dac0a531 207
65fcc311 208export {
e4f97bab 209 doRequest,
db4b15f2 210 doJSONRequest,
58d515e3 211 doRequestAndSaveToFile,
62549e6c 212 isBinaryResponse,
b3fc41a1 213 downloadImage,
dedcd583 214 findLatestRedirection,
b3fc41a1 215 peertubeGot
65fcc311 216}
bfe2ef6b
C
217
218// ---------------------------------------------------------------------------
219
db4b15f2
C
220function buildGotOptions (options: PeerTubeRequestOptions) {
221 const { activityPub, bodyKBLimit = 1000 } = options
bfe2ef6b 222
db4b15f2 223 const context = { bodyKBLimit, httpSignature: options.httpSignature }
bfe2ef6b 224
db4b15f2
C
225 let headers = options.headers || {}
226
e7053b1d
C
227 if (!headers.date) {
228 headers = { ...headers, date: new Date().toUTCString() }
229 }
db4b15f2 230
e7053b1d 231 if (activityPub && !headers.accept) {
db4b15f2 232 headers = { ...headers, accept: ACTIVITY_PUB.ACCEPT_HEADER }
bfe2ef6b 233 }
db4b15f2
C
234
235 return {
236 method: options.method,
bbb3be68 237 dnsCache: true,
e81f6ccf 238 timeout: REQUEST_TIMEOUT,
db4b15f2
C
239 json: options.json,
240 searchParams: options.searchParams,
ac036180 241 retry: 2,
db4b15f2
C
242 headers,
243 context
244 }
245}
246
b5c36108
C
247function buildRequestError (error: RequestError) {
248 const newError: PeerTubeRequestError = new Error(error.message)
db4b15f2
C
249 newError.name = error.name
250 newError.stack = error.stack
251
b5c36108
C
252 if (error.response) {
253 newError.responseBody = error.response.body
3455c265 254 newError.responseHeaders = error.response.headers
b5c36108 255 newError.statusCode = error.response.statusCode
db4b15f2
C
256 }
257
b5c36108 258 return newError
bfe2ef6b 259}