ニジカ投稿局 https://tv.nizika.tv
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

259 lines
6.7 KiB

  1. import httpSignature from '@peertube/http-signature'
  2. import { createWriteStream } from 'fs'
  3. import { remove } from 'fs-extra/esm'
  4. import got, { CancelableRequest, OptionsInit, OptionsOfTextResponseBody, OptionsOfUnknownResponseBody, RequestError, Response } from 'got'
  5. import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'
  6. import { ACTIVITY_PUB, BINARY_CONTENT_TYPES, PEERTUBE_VERSION, REQUEST_TIMEOUTS, WEBSERVER } from '../initializers/constants.js'
  7. import { pipelinePromise } from './core-utils.js'
  8. import { logger, loggerTagsFactory } from './logger.js'
  9. import { getProxy, isProxyEnabled } from './proxy.js'
  10. const lTags = loggerTagsFactory('request')
  11. export interface PeerTubeRequestError extends Error {
  12. statusCode?: number
  13. responseBody?: any
  14. responseHeaders?: any
  15. requestHeaders?: any
  16. }
  17. type PeerTubeRequestOptions = {
  18. timeout?: number
  19. activityPub?: boolean
  20. bodyKBLimit?: number // 1MB
  21. httpSignature?: {
  22. algorithm: string
  23. authorizationHeaderName: string
  24. keyId: string
  25. key: string
  26. headers: string[]
  27. }
  28. jsonResponse?: boolean
  29. followRedirect?: boolean
  30. } & Pick<OptionsInit, 'headers' | 'json' | 'method' | 'searchParams'>
  31. const peertubeGot = got.extend({
  32. ...getAgent(),
  33. headers: {
  34. 'user-agent': getUserAgent()
  35. },
  36. handlers: [
  37. (options, next) => {
  38. const promiseOrStream = next(options) as CancelableRequest<any>
  39. const bodyKBLimit = options.context?.bodyKBLimit as number
  40. if (!bodyKBLimit) throw new Error('No KB limit for this request')
  41. const bodyLimit = bodyKBLimit * 1000
  42. /* eslint-disable @typescript-eslint/no-floating-promises */
  43. promiseOrStream.on('downloadProgress', progress => {
  44. if (progress.transferred > bodyLimit && progress.percent !== 1) {
  45. const message = `Exceeded the download limit of ${bodyLimit} B`
  46. logger.warn(message, lTags())
  47. // CancelableRequest
  48. if (promiseOrStream.cancel) {
  49. promiseOrStream.cancel()
  50. return
  51. }
  52. // Stream
  53. (promiseOrStream as any).destroy()
  54. }
  55. })
  56. return promiseOrStream
  57. }
  58. ],
  59. hooks: {
  60. beforeRequest: [
  61. options => {
  62. const headers = options.headers || {}
  63. headers['host'] = buildUrl(options.url).host
  64. },
  65. options => {
  66. const httpSignatureOptions = options.context?.httpSignature
  67. if (httpSignatureOptions) {
  68. const method = options.method ?? 'GET'
  69. const path = buildUrl(options.url).pathname
  70. if (!method || !path) {
  71. throw new Error(`Cannot sign request without method (${method}) or path (${path}) ${options}`)
  72. }
  73. httpSignature.signRequest({
  74. getHeader: function (header: string) {
  75. const value = options.headers[header.toLowerCase()]
  76. if (!value) logger.warn('Unknown header requested by http-signature.', { headers: options.headers, header })
  77. return value
  78. },
  79. setHeader: function (header: string, value: string) {
  80. options.headers[header] = value
  81. },
  82. method,
  83. path
  84. }, httpSignatureOptions)
  85. }
  86. }
  87. ],
  88. beforeRetry: [
  89. (error: RequestError, retryCount: number) => {
  90. logger.debug('Retrying request to %s.', error.request.requestUrl, { retryCount, error: buildRequestError(error), ...lTags() })
  91. }
  92. ]
  93. }
  94. })
  95. function doRequest (url: string, options: PeerTubeRequestOptions = {}) {
  96. const gotOptions = buildGotOptions(options) as OptionsOfTextResponseBody
  97. return peertubeGot(url, gotOptions)
  98. .catch(err => { throw buildRequestError(err) })
  99. }
  100. function doJSONRequest <T> (url: string, options: PeerTubeRequestOptions = {}) {
  101. const gotOptions = buildGotOptions(options)
  102. return peertubeGot<T>(url, { ...gotOptions, responseType: 'json' })
  103. .catch(err => { throw buildRequestError(err) })
  104. }
  105. async function doRequestAndSaveToFile (
  106. url: string,
  107. destPath: string,
  108. options: PeerTubeRequestOptions = {}
  109. ) {
  110. const gotOptions = buildGotOptions({ ...options, timeout: options.timeout ?? REQUEST_TIMEOUTS.FILE })
  111. const outFile = createWriteStream(destPath)
  112. try {
  113. await pipelinePromise(
  114. peertubeGot.stream(url, { ...gotOptions, isStream: true }),
  115. outFile
  116. )
  117. } catch (err) {
  118. remove(destPath)
  119. .catch(err => logger.error('Cannot remove %s after request failure.', destPath, { err, ...lTags() }))
  120. throw buildRequestError(err)
  121. }
  122. }
  123. function getAgent () {
  124. if (!isProxyEnabled()) return {}
  125. const proxy = getProxy()
  126. logger.info('Using proxy %s.', proxy, lTags())
  127. const proxyAgentOptions = {
  128. keepAlive: true,
  129. keepAliveMsecs: 1000,
  130. maxSockets: 256,
  131. maxFreeSockets: 256,
  132. scheduling: 'lifo' as 'lifo',
  133. proxy
  134. }
  135. return {
  136. agent: {
  137. http: new HttpProxyAgent(proxyAgentOptions),
  138. https: new HttpsProxyAgent(proxyAgentOptions)
  139. }
  140. }
  141. }
  142. function getUserAgent () {
  143. return `PeerTube/${PEERTUBE_VERSION} (+${WEBSERVER.URL})`
  144. }
  145. function isBinaryResponse (result: Response<any>) {
  146. return BINARY_CONTENT_TYPES.has(result.headers['content-type'])
  147. }
  148. // ---------------------------------------------------------------------------
  149. export {
  150. type PeerTubeRequestOptions,
  151. doRequest,
  152. doJSONRequest,
  153. doRequestAndSaveToFile,
  154. isBinaryResponse,
  155. getAgent,
  156. peertubeGot
  157. }
  158. // ---------------------------------------------------------------------------
  159. function buildGotOptions (options: PeerTubeRequestOptions): OptionsOfUnknownResponseBody {
  160. const { activityPub, bodyKBLimit = 1000 } = options
  161. const context = { bodyKBLimit, httpSignature: options.httpSignature }
  162. let headers = options.headers || {}
  163. if (!headers.date) {
  164. headers = { ...headers, date: new Date().toUTCString() }
  165. }
  166. if (activityPub && !headers.accept) {
  167. headers = { ...headers, accept: ACTIVITY_PUB.ACCEPT_HEADER }
  168. }
  169. return {
  170. method: options.method,
  171. dnsCache: true,
  172. timeout: {
  173. request: options.timeout ?? REQUEST_TIMEOUTS.DEFAULT
  174. },
  175. json: options.json,
  176. searchParams: options.searchParams,
  177. followRedirect: options.followRedirect,
  178. retry: {
  179. limit: 2
  180. },
  181. headers,
  182. context
  183. }
  184. }
  185. function buildRequestError (error: RequestError) {
  186. const newError: PeerTubeRequestError = new Error(error.message)
  187. newError.name = error.name
  188. newError.stack = error.stack
  189. if (error.response) {
  190. newError.responseBody = error.response.body
  191. newError.responseHeaders = error.response.headers
  192. newError.statusCode = error.response.statusCode
  193. }
  194. if (error.options) {
  195. newError.requestHeaders = error.options.headers
  196. }
  197. return newError
  198. }
  199. function buildUrl (url: string | URL) {
  200. if (typeof url === 'string') {
  201. return new URL(url)
  202. }
  203. return url
  204. }