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