@api-client/core 0.18.11 → 0.18.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/build/src/browser.d.ts +0 -3
  2. package/build/src/browser.d.ts.map +1 -1
  3. package/build/src/browser.js +0 -3
  4. package/build/src/browser.js.map +1 -1
  5. package/build/src/index.d.ts +2 -5
  6. package/build/src/index.d.ts.map +1 -1
  7. package/build/src/index.js +2 -5
  8. package/build/src/index.js.map +1 -1
  9. package/build/src/lib/logging/DefaultLogger.d.ts +14 -0
  10. package/build/src/lib/logging/DefaultLogger.d.ts.map +1 -1
  11. package/build/src/lib/logging/DefaultLogger.js +27 -0
  12. package/build/src/lib/logging/DefaultLogger.js.map +1 -1
  13. package/build/src/lib/logging/index.d.ts +4 -0
  14. package/build/src/lib/logging/index.d.ts.map +1 -0
  15. package/build/src/lib/logging/index.js +10 -0
  16. package/build/src/lib/logging/index.js.map +1 -0
  17. package/build/src/modeling/DomainModel.d.ts.map +1 -1
  18. package/build/src/modeling/DomainModel.js +11 -4
  19. package/build/src/modeling/DomainModel.js.map +1 -1
  20. package/build/src/models/ClientCertificate.d.ts +1 -1
  21. package/build/src/models/ClientCertificate.js.map +1 -1
  22. package/build/src/models/RequestConfig.d.ts +1 -1
  23. package/build/src/models/RequestConfig.js.map +1 -1
  24. package/build/src/models/SerializableError.d.ts +1 -1
  25. package/build/src/models/SerializableError.d.ts.map +1 -1
  26. package/build/src/models/SerializableError.js.map +1 -1
  27. package/build/src/proxy/RequestProxy.d.ts.map +1 -1
  28. package/build/src/proxy/RequestProxy.js +2 -2
  29. package/build/src/proxy/RequestProxy.js.map +1 -1
  30. package/build/src/runtime/http-engine/CoreEngine.d.ts +218 -139
  31. package/build/src/runtime/http-engine/CoreEngine.d.ts.map +1 -1
  32. package/build/src/runtime/http-engine/CoreEngine.js +716 -870
  33. package/build/src/runtime/http-engine/CoreEngine.js.map +1 -1
  34. package/build/src/runtime/http-engine/PayloadSupport.d.ts.map +1 -1
  35. package/build/src/runtime/http-engine/PayloadSupport.js +2 -1
  36. package/build/src/runtime/http-engine/PayloadSupport.js.map +1 -1
  37. package/build/src/runtime/http-engine/auth/AuthManager.d.ts +73 -0
  38. package/build/src/runtime/http-engine/auth/AuthManager.d.ts.map +1 -0
  39. package/build/src/runtime/http-engine/auth/AuthManager.js +186 -0
  40. package/build/src/runtime/http-engine/auth/AuthManager.js.map +1 -0
  41. package/build/src/runtime/http-engine/auth/index.d.ts +2 -0
  42. package/build/src/runtime/http-engine/auth/index.d.ts.map +1 -0
  43. package/build/src/runtime/http-engine/auth/index.js +2 -0
  44. package/build/src/runtime/http-engine/auth/index.js.map +1 -0
  45. package/build/src/runtime/http-engine/certificates/CertificateManager.d.ts +11 -0
  46. package/build/src/runtime/http-engine/certificates/CertificateManager.d.ts.map +1 -0
  47. package/build/src/runtime/http-engine/certificates/CertificateManager.js +76 -0
  48. package/build/src/runtime/http-engine/certificates/CertificateManager.js.map +1 -0
  49. package/build/src/runtime/http-engine/certificates/index.d.ts +2 -0
  50. package/build/src/runtime/http-engine/certificates/index.d.ts.map +1 -0
  51. package/build/src/runtime/http-engine/certificates/index.js +2 -0
  52. package/build/src/runtime/http-engine/certificates/index.js.map +1 -0
  53. package/build/src/runtime/http-engine/compression/CompressionManager.d.ts +25 -0
  54. package/build/src/runtime/http-engine/compression/CompressionManager.d.ts.map +1 -0
  55. package/build/src/runtime/http-engine/compression/CompressionManager.js +89 -0
  56. package/build/src/runtime/http-engine/compression/CompressionManager.js.map +1 -0
  57. package/build/src/runtime/http-engine/compression/index.d.ts +2 -0
  58. package/build/src/runtime/http-engine/compression/index.d.ts.map +1 -0
  59. package/build/src/runtime/http-engine/compression/index.js +2 -0
  60. package/build/src/runtime/http-engine/compression/index.js.map +1 -0
  61. package/build/src/runtime/http-engine/connections/ConnectionManager.d.ts +57 -0
  62. package/build/src/runtime/http-engine/connections/ConnectionManager.d.ts.map +1 -0
  63. package/build/src/runtime/http-engine/connections/ConnectionManager.js +78 -0
  64. package/build/src/runtime/http-engine/connections/ConnectionManager.js.map +1 -0
  65. package/build/src/runtime/http-engine/connections/DigestAuthHandler.d.ts +70 -0
  66. package/build/src/runtime/http-engine/connections/DigestAuthHandler.d.ts.map +1 -0
  67. package/build/src/runtime/http-engine/connections/DigestAuthHandler.js +184 -0
  68. package/build/src/runtime/http-engine/connections/DigestAuthHandler.js.map +1 -0
  69. package/build/src/runtime/http-engine/connections/DirectConnection.d.ts +22 -0
  70. package/build/src/runtime/http-engine/connections/DirectConnection.d.ts.map +1 -0
  71. package/build/src/runtime/http-engine/connections/DirectConnection.js +105 -0
  72. package/build/src/runtime/http-engine/connections/DirectConnection.js.map +1 -0
  73. package/build/src/runtime/http-engine/connections/ProxyAuthHandler.d.ts +60 -0
  74. package/build/src/runtime/http-engine/connections/ProxyAuthHandler.d.ts.map +1 -0
  75. package/build/src/runtime/http-engine/connections/ProxyAuthHandler.js +138 -0
  76. package/build/src/runtime/http-engine/connections/ProxyAuthHandler.js.map +1 -0
  77. package/build/src/runtime/http-engine/connections/ProxyConnection.d.ts +14 -0
  78. package/build/src/runtime/http-engine/connections/ProxyConnection.d.ts.map +1 -0
  79. package/build/src/runtime/http-engine/connections/ProxyConnection.js +47 -0
  80. package/build/src/runtime/http-engine/connections/ProxyConnection.js.map +1 -0
  81. package/build/src/runtime/http-engine/connections/TunnelConnection.d.ts +13 -0
  82. package/build/src/runtime/http-engine/connections/TunnelConnection.d.ts.map +1 -0
  83. package/build/src/runtime/http-engine/connections/TunnelConnection.js +175 -0
  84. package/build/src/runtime/http-engine/connections/TunnelConnection.js.map +1 -0
  85. package/build/src/runtime/http-engine/connections/index.d.ts +7 -0
  86. package/build/src/runtime/http-engine/connections/index.d.ts.map +1 -0
  87. package/build/src/runtime/http-engine/connections/index.js +7 -0
  88. package/build/src/runtime/http-engine/connections/index.js.map +1 -0
  89. package/build/src/runtime/http-engine/constants.d.ts +69 -0
  90. package/build/src/runtime/http-engine/constants.d.ts.map +1 -0
  91. package/build/src/runtime/http-engine/constants.js +90 -0
  92. package/build/src/runtime/http-engine/constants.js.map +1 -0
  93. package/build/src/runtime/http-engine/cookies/CookieProcessor.d.ts +5 -0
  94. package/build/src/runtime/http-engine/cookies/CookieProcessor.d.ts.map +1 -0
  95. package/build/src/runtime/http-engine/cookies/CookieProcessor.js +20 -0
  96. package/build/src/runtime/http-engine/cookies/CookieProcessor.js.map +1 -0
  97. package/build/src/runtime/http-engine/cookies/index.d.ts +2 -0
  98. package/build/src/runtime/http-engine/cookies/index.d.ts.map +1 -0
  99. package/build/src/runtime/http-engine/cookies/index.js +2 -0
  100. package/build/src/runtime/http-engine/cookies/index.js.map +1 -0
  101. package/build/src/runtime/http-engine/errors/HttpEngineErrors.d.ts +156 -0
  102. package/build/src/runtime/http-engine/errors/HttpEngineErrors.d.ts.map +1 -0
  103. package/build/src/runtime/http-engine/errors/HttpEngineErrors.js +227 -0
  104. package/build/src/runtime/http-engine/errors/HttpEngineErrors.js.map +1 -0
  105. package/build/src/runtime/http-engine/errors/index.d.ts +2 -0
  106. package/build/src/runtime/http-engine/errors/index.d.ts.map +1 -0
  107. package/build/src/runtime/http-engine/errors/index.js +2 -0
  108. package/build/src/runtime/http-engine/errors/index.js.map +1 -0
  109. package/build/src/runtime/http-engine/message/MessageBuilder.d.ts +66 -0
  110. package/build/src/runtime/http-engine/message/MessageBuilder.d.ts.map +1 -0
  111. package/build/src/runtime/http-engine/message/MessageBuilder.js +161 -0
  112. package/build/src/runtime/http-engine/message/MessageBuilder.js.map +1 -0
  113. package/build/src/runtime/http-engine/message/MessageProcessor.d.ts +27 -0
  114. package/build/src/runtime/http-engine/message/MessageProcessor.d.ts.map +1 -0
  115. package/build/src/runtime/http-engine/message/MessageProcessor.js +51 -0
  116. package/build/src/runtime/http-engine/message/MessageProcessor.js.map +1 -0
  117. package/build/src/runtime/http-engine/message/index.d.ts +3 -0
  118. package/build/src/runtime/http-engine/message/index.d.ts.map +1 -0
  119. package/build/src/runtime/http-engine/message/index.js +3 -0
  120. package/build/src/runtime/http-engine/message/index.js.map +1 -0
  121. package/build/src/runtime/http-engine/ntlm/NtlmAuth.d.ts +2 -8
  122. package/build/src/runtime/http-engine/ntlm/NtlmAuth.d.ts.map +1 -1
  123. package/build/src/runtime/http-engine/ntlm/NtlmAuth.js +11 -5
  124. package/build/src/runtime/http-engine/ntlm/NtlmAuth.js.map +1 -1
  125. package/build/src/runtime/http-engine/ntlm/NtlmMessage.js +6 -6
  126. package/build/src/runtime/http-engine/ntlm/NtlmMessage.js.map +1 -1
  127. package/build/src/runtime/http-engine/parsers/BodyParser.d.ts +39 -0
  128. package/build/src/runtime/http-engine/parsers/BodyParser.d.ts.map +1 -0
  129. package/build/src/runtime/http-engine/parsers/BodyParser.js +145 -0
  130. package/build/src/runtime/http-engine/parsers/BodyParser.js.map +1 -0
  131. package/build/src/runtime/http-engine/parsers/HeadersParser.d.ts +29 -0
  132. package/build/src/runtime/http-engine/parsers/HeadersParser.d.ts.map +1 -0
  133. package/build/src/runtime/http-engine/parsers/HeadersParser.js +88 -0
  134. package/build/src/runtime/http-engine/parsers/HeadersParser.js.map +1 -0
  135. package/build/src/runtime/http-engine/parsers/HttpResponseParser.d.ts +91 -0
  136. package/build/src/runtime/http-engine/parsers/HttpResponseParser.d.ts.map +1 -0
  137. package/build/src/runtime/http-engine/parsers/HttpResponseParser.js +236 -0
  138. package/build/src/runtime/http-engine/parsers/HttpResponseParser.js.map +1 -0
  139. package/build/src/runtime/http-engine/parsers/StatusParser.d.ts +20 -0
  140. package/build/src/runtime/http-engine/parsers/StatusParser.d.ts.map +1 -0
  141. package/build/src/runtime/http-engine/parsers/StatusParser.js +51 -0
  142. package/build/src/runtime/http-engine/parsers/StatusParser.js.map +1 -0
  143. package/build/src/runtime/http-engine/parsers/index.d.ts +5 -0
  144. package/build/src/runtime/http-engine/parsers/index.d.ts.map +1 -0
  145. package/build/src/runtime/http-engine/parsers/index.js +5 -0
  146. package/build/src/runtime/http-engine/parsers/index.js.map +1 -0
  147. package/build/src/runtime/http-engine/response/ResponseProcessor.d.ts +22 -0
  148. package/build/src/runtime/http-engine/response/ResponseProcessor.d.ts.map +1 -0
  149. package/build/src/runtime/http-engine/response/ResponseProcessor.js +25 -0
  150. package/build/src/runtime/http-engine/response/ResponseProcessor.js.map +1 -0
  151. package/build/src/runtime/http-engine/response/index.d.ts +2 -0
  152. package/build/src/runtime/http-engine/response/index.d.ts.map +1 -0
  153. package/build/src/runtime/http-engine/response/index.js +2 -0
  154. package/build/src/runtime/http-engine/response/index.js.map +1 -0
  155. package/build/src/runtime/http-engine/statistics/StatisticsProcessor.d.ts +7 -0
  156. package/build/src/runtime/http-engine/statistics/StatisticsProcessor.d.ts.map +1 -0
  157. package/build/src/runtime/http-engine/statistics/StatisticsProcessor.js +40 -0
  158. package/build/src/runtime/http-engine/statistics/StatisticsProcessor.js.map +1 -0
  159. package/build/src/runtime/http-engine/statistics/index.d.ts +2 -0
  160. package/build/src/runtime/http-engine/statistics/index.d.ts.map +1 -0
  161. package/build/src/runtime/http-engine/statistics/index.js +2 -0
  162. package/build/src/runtime/http-engine/statistics/index.js.map +1 -0
  163. package/build/src/runtime/http-engine/url/UrlProcessor.d.ts +24 -0
  164. package/build/src/runtime/http-engine/url/UrlProcessor.d.ts.map +1 -0
  165. package/build/src/runtime/http-engine/url/UrlProcessor.js +50 -0
  166. package/build/src/runtime/http-engine/url/UrlProcessor.js.map +1 -0
  167. package/build/src/runtime/http-engine/url/index.d.ts +2 -0
  168. package/build/src/runtime/http-engine/url/index.d.ts.map +1 -0
  169. package/build/src/runtime/http-engine/url/index.js +2 -0
  170. package/build/src/runtime/http-engine/url/index.js.map +1 -0
  171. package/build/src/runtime/http-runner/HttpRequestRunner.d.ts +3 -3
  172. package/build/src/runtime/http-runner/HttpRequestRunner.d.ts.map +1 -1
  173. package/build/src/runtime/http-runner/HttpRequestRunner.js.map +1 -1
  174. package/build/src/runtime/node/InteropInterfaces.d.ts +3 -3
  175. package/build/src/runtime/node/InteropInterfaces.d.ts.map +1 -1
  176. package/build/src/runtime/node/InteropInterfaces.js.map +1 -1
  177. package/build/src/runtime/node/ProjectRequestRunner.d.ts +2 -2
  178. package/build/src/runtime/node/ProjectRequestRunner.d.ts.map +1 -1
  179. package/build/src/runtime/node/ProjectRequestRunner.js.map +1 -1
  180. package/build/src/runtime/node/ProjectRunner.d.ts.map +1 -1
  181. package/build/src/runtime/node/ProjectRunner.js +2 -2
  182. package/build/src/runtime/node/ProjectRunner.js.map +1 -1
  183. package/build/tsconfig.tsbuildinfo +1 -1
  184. package/data/models/example-generator-api.json +24 -24
  185. package/package.json +2 -2
  186. package/src/lib/logging/DefaultLogger.ts +32 -0
  187. package/src/modeling/DomainModel.ts +11 -4
  188. package/src/models/ClientCertificate.ts +1 -1
  189. package/src/models/RequestConfig.ts +1 -1
  190. package/src/models/SerializableError.ts +1 -1
  191. package/src/proxy/RequestProxy.ts +2 -2
  192. package/src/runtime/http-engine/CoreEngine.ts +858 -893
  193. package/src/runtime/http-engine/PayloadSupport.ts +2 -1
  194. package/src/runtime/http-engine/auth/AuthManager.ts +242 -0
  195. package/src/runtime/http-engine/certificates/CertificateManager.ts +74 -0
  196. package/src/runtime/http-engine/compression/CompressionManager.ts +99 -0
  197. package/src/runtime/http-engine/connections/ConnectionManager.ts +123 -0
  198. package/src/runtime/http-engine/connections/DigestAuthHandler.ts +238 -0
  199. package/src/runtime/http-engine/connections/DirectConnection.ts +134 -0
  200. package/src/runtime/http-engine/connections/ProxyAuthHandler.ts +179 -0
  201. package/src/runtime/http-engine/connections/ProxyConnection.ts +55 -0
  202. package/src/runtime/http-engine/connections/TunnelConnection.ts +192 -0
  203. package/src/runtime/http-engine/constants.ts +103 -0
  204. package/src/runtime/http-engine/cookies/CookieProcessor.ts +25 -0
  205. package/src/runtime/http-engine/errors/HttpEngineErrors.ts +319 -0
  206. package/src/runtime/http-engine/message/MessageBuilder.ts +201 -0
  207. package/src/runtime/http-engine/message/MessageProcessor.ts +73 -0
  208. package/src/runtime/http-engine/ntlm/NtlmAuth.ts +16 -13
  209. package/src/runtime/http-engine/ntlm/NtlmMessage.ts +6 -6
  210. package/src/runtime/http-engine/parsers/BodyParser.ts +171 -0
  211. package/src/runtime/http-engine/parsers/HeadersParser.ts +103 -0
  212. package/src/runtime/http-engine/parsers/HttpResponseParser.ts +280 -0
  213. package/src/runtime/http-engine/parsers/StatusParser.ts +69 -0
  214. package/src/runtime/http-engine/response/ResponseProcessor.ts +46 -0
  215. package/src/runtime/http-engine/statistics/StatisticsProcessor.ts +52 -0
  216. package/src/runtime/http-engine/url/UrlProcessor.ts +55 -0
  217. package/src/runtime/http-runner/HttpRequestRunner.ts +3 -3
  218. package/src/runtime/node/InteropInterfaces.ts +3 -3
  219. package/src/runtime/node/ProjectRequestRunner.ts +2 -2
  220. package/src/runtime/node/ProjectRunner.ts +2 -2
  221. package/tests/servers/ProxyServer.ts +32 -19
  222. package/tests/servers/express-routes/ApiEndpoint.ts +24 -0
  223. package/tests/servers/express-routes/BasicAuthRoute.ts +36 -0
  224. package/tests/servers/express-routes/BearerAuthRoute.ts +35 -0
  225. package/tests/servers/express-routes/NTLMRoute.ts +2 -3
  226. package/tests/servers/express-routes/PostApi.ts +15 -2
  227. package/tests/servers/express-routes/RedirectsApi.ts +12 -1
  228. package/tests/servers/express-routes/ResponsesApi.ts +1 -1
  229. package/tests/servers/express-routes/StreamApi.ts +19 -0
  230. package/tests/servers/oauth2mock/ServerMock.js +1 -1
  231. package/tests/unit/modeling/domain_model_entities.spec.ts +306 -1
  232. package/tests/unit/runtime/http-engine/HttpResponseParser.spec.ts +337 -0
  233. package/tests/unit/runtime/http-engine/abort.spec.ts +4 -5
  234. package/tests/unit/runtime/http-engine/auth.spec.ts +7 -58
  235. package/tests/unit/runtime/http-engine/certificates/CertificateManager.spec.ts +482 -0
  236. package/tests/unit/runtime/http-engine/certificates.spec.ts +2 -2
  237. package/tests/unit/runtime/http-engine/compression/CompressionManager.spec.ts +498 -0
  238. package/tests/unit/runtime/http-engine/compression.spec.ts +3 -72
  239. package/tests/unit/runtime/http-engine/connections/ConnectionManager.spec.ts +379 -0
  240. package/tests/unit/runtime/http-engine/connections/DigestAuthHandler.spec.ts +164 -0
  241. package/tests/unit/runtime/http-engine/core_engine.spec.ts +561 -0
  242. package/tests/unit/runtime/http-engine/engine_statuses.spec.ts +2 -2
  243. package/tests/unit/runtime/http-engine/events.spec.ts +2 -2
  244. package/tests/unit/runtime/http-engine/headers.spec.ts +2 -88
  245. package/tests/unit/runtime/http-engine/hosts.spec.ts +2 -2
  246. package/tests/unit/runtime/http-engine/http-get.spec.ts +2 -2
  247. package/tests/unit/runtime/http-engine/http-post.spec.ts +2 -2
  248. package/tests/unit/runtime/http-engine/logger.spec.ts +0 -8
  249. package/tests/unit/runtime/http-engine/message.spec.ts +2 -194
  250. package/tests/unit/runtime/http-engine/params.spec.ts +4 -4
  251. package/tests/unit/runtime/http-engine/proxy.spec.ts +15 -14
  252. package/tests/unit/runtime/http-engine/redirects.spec.ts +2 -2
  253. package/tests/unit/runtime/http-engine/responses.spec.ts +170 -277
  254. package/tests/unit/runtime/http-engine/timeout.spec.ts +3 -3
  255. package/tests/unit/runtime/http-engine/timings.spec.ts +2 -2
  256. package/tests/unit/runtime/proxy/HttpProjectProxy.spec.ts +25 -28
  257. package/tests/unit/runtime/runners/project_runner.spec.ts +2 -2
  258. package/tests/unit/runtime/runners/request_runner.spec.ts +2 -2
  259. package/build/src/runtime/http-engine/HttpEngine.d.ts +0 -311
  260. package/build/src/runtime/http-engine/HttpEngine.d.ts.map +0 -1
  261. package/build/src/runtime/http-engine/HttpEngine.js +0 -802
  262. package/build/src/runtime/http-engine/HttpEngine.js.map +0 -1
  263. package/src/runtime/http-engine/HttpEngine.ts +0 -952
  264. package/tests/unit/runtime/http-engine/connecting.spec.ts +0 -140
@@ -1,61 +1,364 @@
1
1
  import net from 'net'
2
+ import { URL } from 'url'
2
3
  import tls from 'tls'
3
- import http from 'http'
4
- import https from 'https'
5
- import { HttpEngine, HttpEngineOptions, HeadersReceivedDetail } from './HttpEngine.js'
4
+ import { EventEmitter } from 'events'
5
+ import { Logger, type ILogObj } from 'tslog'
6
6
  import { IRequestLog } from '../../models/RequestLog.js'
7
- import { IHttpRequest } from '../../models/HttpRequest.js'
7
+ import { IHttpRequest, HttpRequest } from '../../models/HttpRequest.js'
8
+ import { IRequestBaseConfig } from '../../models/RequestConfig.js'
9
+ import { IRequestAuthorization } from '../../models/RequestAuthorization.js'
10
+
11
+ import { HttpCertificate } from '../../models/ClientCertificate.js'
12
+ import { SentRequest } from '../../models/SentRequest.js'
8
13
  import { Response } from '../../models/Response.js'
14
+ import { ErrorResponse } from '../../models/ErrorResponse.js'
15
+ import { RequestsSize } from '../../models/RequestsSize.js'
16
+ import { HttpResponse } from '../../models/HttpResponse.js'
17
+ import { ResponseRedirect } from '../../models/ResponseRedirect.js'
18
+ import { RequestLog } from '../../models/RequestLog.js'
19
+ import { RequestTime } from '../../models/RequestTime.js'
9
20
  import { SerializableError } from '../../models/SerializableError.js'
21
+ import { ResponseAuthorization } from '../../models/ResponseAuthorization.js'
10
22
  import { Headers } from '../../lib/headers/Headers.js'
23
+ import { getPort } from './RequestUtils.js'
24
+
25
+ import { HttpResponseParser, ParserCallbacks, RequestState } from './parsers/index.js'
26
+ import { ConnectionManager } from './connections/index.js'
27
+ import { AuthManager, type ProxyAuthCredentials } from './auth/index.js'
28
+ import { MessageBuilder } from './message/index.js'
29
+ import { HttpEngineErrorFactory } from './errors/index.js'
30
+ import { isMethodWithoutBody, HEADER_PROXY_AUTHORIZATION } from './constants.js'
31
+ import { decompress } from './compression/index.js'
32
+ import { checkServerIdentity } from './certificates/index.js'
33
+ import { readUrl, getHostHeader, getRedirectLocation, isRedirectLoop } from './url/index.js'
34
+ import { processRedirectCookies } from './cookies/index.js'
35
+ import { computeStats } from './statistics/index.js'
36
+ import { processResponse } from './response/index.js'
37
+ import { getCodeMessage } from './HttpErrorCodes.js'
38
+ import { createLogger } from '../../lib/logging/index.js'
11
39
  import * as PayloadSupport from './PayloadSupport.js'
12
- import { addContentLength, getPort } from './RequestUtils.js'
13
- import { INtlmAuthorization } from '../../models/Authorization.js'
14
- import { NtlmAuth, INtlmAuthConfig } from './ntlm/NtlmAuth.js'
40
+ import { addContentLength } from './RequestUtils.js'
15
41
  import { PayloadSerializer } from '../../lib/transformers/PayloadSerializer.js'
16
- import { ResponseRedirect } from '../../models/ResponseRedirect.js'
42
+ import { ProxyAuthenticationError } from './errors/HttpEngineErrors.js'
17
43
 
18
- const nlBuffer = Buffer.from([13, 10])
19
- const nlNlBuffer = Buffer.from([13, 10, 13, 10])
44
+ export interface HttpEngineOptions extends IRequestBaseConfig {
45
+ /**
46
+ * The authorization configuration to apply to the request.
47
+ */
48
+ authorization?: IRequestAuthorization[]
49
+ /**
50
+ * Logger object.
51
+ */
52
+ logger?: Logger<ILogObj>
53
+ /**
54
+ * A certificate to use with the request.
55
+ */
56
+ certificates?: HttpCertificate[]
57
+ }
20
58
 
21
- export enum RequestState {
22
- Status,
23
- Headers,
24
- Body,
25
- Done,
59
+ export interface RequestStats {
60
+ firstReceiveTime?: number
61
+ lastReceivedTime?: number
62
+ messageStart?: number
63
+ sentTime?: number
64
+ connectionTime?: number
65
+ lookupTime?: number
66
+ connectedTime?: number
67
+ secureStartTime?: number
68
+ secureConnectedTime?: number
69
+ startTime?: number
70
+ responseTime?: number
71
+ receivingTime?: number
26
72
  }
27
73
 
28
- interface ResponseInfo {
29
- contentLength?: number
30
- chunked: boolean
31
- body?: Buffer
32
- chunk?: Buffer
33
- chunkSize?: number
74
+ export interface ResponseErrorInit {
75
+ code?: number | string
76
+ message?: string
34
77
  }
35
78
 
79
+ export interface BeforeRedirectDetail {
80
+ location: string
81
+ returnValue: boolean
82
+ }
83
+
84
+ export interface HeadersReceivedDetail {
85
+ value: string
86
+ returnValue: boolean
87
+ }
88
+
89
+ export interface IRequestAuthState {
90
+ method: 'ntlm'
91
+ state: number
92
+ headers?: string
93
+ challengeHeader?: string
94
+ }
95
+
96
+ export const mainPromiseSymbol = Symbol('mainPromise')
97
+
36
98
  /**
37
- * API Client's HTTP engine.
99
+ * API Client's HTTP engine with refactored parsing and connection handling.
38
100
  * An HTTP 1.1 engine working directly on the socket. It communicates with the remote machine and
39
101
  * collects stats about the request and response.
40
102
  */
41
- export class CoreEngine extends HttpEngine {
42
- state = RequestState.Status
43
- rawHeaders?: Buffer
44
- _hostTestReg = /^\s*host\s*:/im
103
+ export class CoreEngine extends EventEmitter {
104
+ request: HttpRequest
105
+ opts: HttpEngineOptions
106
+ logger: Logger<ILogObj>
107
+
108
+ /**
109
+ * The current sent request
110
+ */
111
+ sentRequest: SentRequest
112
+
113
+ redirects: ResponseRedirect[] = []
114
+ /**
115
+ * When true the request has been aborted.
116
+ */
117
+ aborted = false
118
+ /**
119
+ * Parsed value of the request URL.
120
+ */
121
+ uri: URL
122
+
123
+ socket?: net.Socket
124
+ /**
125
+ * Host header can be different than registered URL because of
126
+ * `hosts` rules.
127
+ * If a rule changes host value of the URL the original URL host value
128
+ * is used when generating the request and not overwritten one.
129
+ * This way virtual hosts can be tested using hosts.
130
+ */
131
+ hostHeader: string | undefined
132
+
133
+ protected hostTestReg = /^\s*host\s*:/im
134
+ /**
135
+ * Set when the request is redirected.
136
+ */
137
+ redirecting = false
138
+
139
+ /**
140
+ * The response headers.
141
+ * The object may be empty when the response is not set.
142
+ */
143
+ currentHeaders = new Headers()
144
+
145
+ /**
146
+ * The response object build during the execution.
147
+ */
148
+ currentResponse?: Response
149
+
150
+ /**
151
+ * Keeps the raw body in a temporary buffer while processing the response.
152
+ */
153
+ _rawBody?: Buffer
154
+
155
+ stats: RequestStats = {}
156
+
157
+ auth?: IRequestAuthState
158
+
159
+ protected mainResolver?: (log: IRequestLog) => void
160
+ protected mainRejecter?: (err: SerializableError) => void;
161
+ [mainPromiseSymbol]?: Promise<IRequestLog>
162
+
163
+ protected _signal?: AbortSignal
164
+
165
+ // CoreEngine specific properties
166
+ private parser: HttpResponseParser
167
+ private connectionManager: ConnectionManager
168
+ private authManager: AuthManager
169
+ private messageBuilder: MessageBuilder
45
170
 
46
- responseInfo: ResponseInfo
47
171
  hasProxy: boolean
48
172
  isProxyTunnel: boolean
49
173
  isProxySsl: boolean
50
174
 
51
- constructor(request: IHttpRequest, opts: HttpEngineOptions = {}) {
52
- super(request, opts)
53
- this.responseInfo = {
54
- chunked: false,
175
+ /**
176
+ * @return True if following redirects is allowed.
177
+ */
178
+ get followRedirects(): boolean {
179
+ const { opts } = this
180
+ if (typeof opts.followRedirects === 'boolean') {
181
+ return opts.followRedirects
55
182
  }
183
+ return true
184
+ }
185
+
186
+ /**
187
+ * The request timeout.
188
+ */
189
+ get timeout(): number {
190
+ const { opts } = this
191
+ if (typeof opts.timeout === 'number') {
192
+ return opts.timeout
193
+ }
194
+ return 0
195
+ }
196
+
197
+ /**
198
+ * The abort signal to set on this request.
199
+ * Aborts the request when the signal fires.
200
+ */
201
+ get signal(): AbortSignal | undefined {
202
+ return this._signal
203
+ }
204
+
205
+ set signal(value: AbortSignal | undefined) {
206
+ const old = this._signal
207
+ if (old === value) {
208
+ return
209
+ }
210
+ this._signal = value
211
+ if (old) {
212
+ old.removeEventListener('abort', this._abortHandler)
213
+ }
214
+ if (value) {
215
+ value.addEventListener('abort', this._abortHandler)
216
+ }
217
+ }
218
+
219
+ constructor(request: IHttpRequest, opts: HttpEngineOptions = {}) {
220
+ super()
221
+ this.request = new HttpRequest({ ...request })
222
+ this.opts = opts
223
+ this.logger = this.setupLogger(opts)
224
+ this.sentRequest = new SentRequest({ ...request, startTime: Date.now() })
225
+ this.uri = this.readUrl(request.url)
226
+ this.hostHeader = this.getHostHeader(request.url)
227
+
228
+ // CoreEngine specific initialization
56
229
  this.hasProxy = !!this.opts.proxy
57
230
  this.isProxyTunnel = this.hasProxy && this.request.url.startsWith('https:')
58
231
  this.isProxySsl = !!this.opts.proxy && this.opts.proxy.startsWith('https:')
232
+
233
+ // Initialize the HTTP response parser
234
+ this.parser = new HttpResponseParser(this.logger, this.createParserCallbacks())
235
+
236
+ // Initialize the connection manager
237
+ this.connectionManager = new ConnectionManager(this.logger, this.stats)
238
+
239
+ // Initialize the auth manager
240
+ const proxyCredentials: ProxyAuthCredentials = {
241
+ proxyUsername: this.opts.proxyUsername,
242
+ proxyPassword: this.opts.proxyPassword,
243
+ proxyAuthorization: this.opts.proxyAuthorization,
244
+ }
245
+
246
+ this.authManager = new AuthManager({
247
+ logger: this.logger,
248
+ proxyCredentials,
249
+ })
250
+
251
+ // Initialize the message builder
252
+ this.messageBuilder = new MessageBuilder({
253
+ logger: this.logger,
254
+ request: this.request as IHttpRequest,
255
+ hasProxy: this.hasProxy,
256
+ isProxyTunnel: this.isProxyTunnel,
257
+ hostHeader: this.hostHeader,
258
+ })
259
+
260
+ this._abortHandler = this._abortHandler.bind(this)
261
+ if (opts.signal) {
262
+ this.signal = opts.signal
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Creates a logger object to log debug output.
268
+ */
269
+ setupLogger(opts: HttpEngineOptions = {}): Logger<ILogObj> {
270
+ if (opts.logger) {
271
+ return opts.logger
272
+ }
273
+ return createLogger()
274
+ }
275
+
276
+ /**
277
+ * Updates the `uri` property from current request URL
278
+ */
279
+ readUrl(value: string): URL {
280
+ return readUrl(value, { hosts: this.opts.hosts })
281
+ }
282
+
283
+ /**
284
+ * Get host header for the request
285
+ */
286
+ getHostHeader(url: string): string | undefined {
287
+ return getHostHeader(url, { hosts: this.opts.hosts })
288
+ }
289
+
290
+ /**
291
+ * Aborts current request.
292
+ * It emits `error` event
293
+ */
294
+ abort(): void {
295
+ this.logger.debug('Aborting the request...')
296
+ this.aborted = true
297
+ if (!this.socket) {
298
+ return
299
+ }
300
+ this.socket.removeAllListeners()
301
+ if (this.socket.destroyed) {
302
+ this.socket = undefined
303
+ return
304
+ }
305
+ this.socket.pause()
306
+ this.socket.destroy()
307
+ this.socket = undefined
308
+ }
309
+
310
+ /**
311
+ * Handler for the `abort` event on the `AbortSignal`.
312
+ */
313
+ protected _abortHandler(): void {
314
+ const e = new SerializableError('Request aborted', 3)
315
+ this._errorRequest(e)
316
+ this.abort()
317
+ }
318
+
319
+ /**
320
+ * Create parser callbacks to handle parsed data
321
+ */
322
+ private createParserCallbacks(): ParserCallbacks {
323
+ return {
324
+ onStatusParsed: (status: number, statusText: string) => {
325
+ this.logger.debug('onStatusParsed called', status, statusText)
326
+ const response = Response.fromValues(status, statusText)
327
+ response.loadingTime = 0
328
+ this.currentResponse = response
329
+ },
330
+ onHeadersParsed: (headers: Headers) => {
331
+ this.logger.debug('onHeadersParsed called')
332
+ this.currentHeaders = headers
333
+ if (this.currentResponse) {
334
+ this.currentResponse.headers = headers.toString()
335
+ }
336
+ },
337
+ onBodyComplete: (body: Buffer) => {
338
+ this.logger.debug('onBodyComplete called')
339
+ this._rawBody = body
340
+ this._reportResponse().catch((error) => {
341
+ this.logger.error('Error in _reportResponse:', error)
342
+ this._errorRequest({
343
+ message: `Response processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
344
+ })
345
+ })
346
+ },
347
+ onError: (error: Error) => {
348
+ this.logger.debug('onError called', error)
349
+ this._errorRequest({
350
+ message: error.message || 'Unknown error occurred',
351
+ })
352
+ },
353
+ onAbort: () => {
354
+ this.logger.debug('onAbort called')
355
+ this.abort()
356
+ },
357
+ emit: (event: string, detail?: unknown) => {
358
+ this.logger.debug('emit called', event, detail)
359
+ this.emit(event, detail)
360
+ },
361
+ }
59
362
  }
60
363
 
61
364
  /**
@@ -69,17 +372,31 @@ export class CoreEngine extends HttpEngine {
69
372
 
70
373
  private async sendRequest(): Promise<void> {
71
374
  try {
72
- if (this.hasProxy) {
73
- await this.connectProxy()
74
- } else {
75
- await this.connect()
76
- }
77
- if (!this.socket || this.aborted) {
375
+ // Use the connection manager to establish the connection
376
+ const socket = await this.establishConnection()
377
+ if (!socket || this.aborted) {
78
378
  return
79
379
  }
380
+
80
381
  const message = await this.prepareMessage()
81
382
  await this.writeMessage(message)
82
383
  } catch (cause) {
384
+ if (cause instanceof ProxyAuthenticationError) {
385
+ this.currentHeaders = new Headers(cause.response.headers)
386
+ const currentResponse = Response.fromValues(
387
+ cause.response.statusCode || 401,
388
+ cause.response.statusMessage,
389
+ this.currentHeaders.toString()
390
+ )
391
+ currentResponse.loadingTime = 0
392
+ this.currentResponse = currentResponse
393
+ if (cause.head.length) {
394
+ this._rawBody = cause.head
395
+ currentResponse.payload = PayloadSerializer.stringifyBuffer(cause.head)
396
+ }
397
+ this._publishResponse()
398
+ return
399
+ }
83
400
  const e = cause as SerializableError
84
401
  const err = new SerializableError(e.message, { cause: e })
85
402
  if (e.code || e.code === 0) {
@@ -90,341 +407,491 @@ export class CoreEngine extends HttpEngine {
90
407
  message: err.message,
91
408
  code: err.code,
92
409
  })
93
- throw cause
94
410
  }
95
411
  }
96
412
 
97
413
  /**
98
- * Cleans the state after finished.
414
+ * Establish connection using the connection manager
99
415
  */
100
- override _cleanUp(): void {
101
- super._cleanUp()
102
- this.state = RequestState.Status
103
- this.rawHeaders = undefined
104
- this.responseInfo = {
105
- chunked: false,
416
+ private async establishConnection(): Promise<net.Socket> {
417
+ const port = getPort(this.uri.port, this.uri.protocol)
418
+ const host = this.uri.hostname
419
+ const protocol = this.uri.protocol
420
+
421
+ const socket = await this.connectionManager.createConnection(host, port, protocol, this.opts)
422
+ const { timeout } = this
423
+ if (timeout > 0) {
424
+ socket.setTimeout(timeout)
106
425
  }
426
+ this.socket = socket
427
+ this._addSocketListeners(socket)
428
+ socket.resume()
429
+ return socket
430
+ }
431
+
432
+ /**
433
+ * Decompresses received body if `content-encoding` header is set.
434
+ */
435
+ async decompress(body?: Buffer): Promise<Buffer | undefined> {
436
+ return decompress(body, {
437
+ aborted: this.aborted,
438
+ headers: this.currentHeaders,
439
+ })
440
+ }
441
+
442
+ /**
443
+ * Prepares headers list to be send to the remote machine.
444
+ * If `defaultHeaders` option is set then it adds `user-agent` and `accept`
445
+ * headers.
446
+ */
447
+ prepareHeaders(headers: Headers, withPayload = false): void {
448
+ if (this.opts.defaultHeaders) {
449
+ if (!headers.has('user-agent')) {
450
+ if (this.opts.defaultUserAgent) {
451
+ headers.set('user-agent', this.opts.defaultUserAgent)
452
+ } else {
453
+ headers.set('user-agent', 'api client')
454
+ }
455
+ }
456
+ if (!headers.has('accept')) {
457
+ if (this.opts.defaultAccept) {
458
+ headers.set('accept', this.opts.defaultAccept)
459
+ } else {
460
+ headers.set('accept', '*/*')
461
+ }
462
+ }
463
+ }
464
+ if (withPayload) {
465
+ if (!headers.has('content-type') && this.request.payloadOptions) {
466
+ if (this.request.payloadOptions.mime) {
467
+ headers.set('content-type', this.request.payloadOptions.mime)
468
+ }
469
+ }
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Cleans the state after finished.
475
+ */
476
+ _cleanUp(): void {
477
+ this.redirects = []
478
+ this.currentHeaders = new Headers()
479
+ this.currentResponse = undefined
480
+ this._rawBody = undefined
481
+ this.redirecting = false
482
+ this.stats = {}
483
+ this.mainRejecter = undefined
484
+ this.mainResolver = undefined
485
+ this._clearSocketEventListeners()
486
+ this.parser.reset()
107
487
  }
108
488
 
109
489
  /**
110
490
  * Cleans up the state for redirect.
111
491
  */
112
- override _cleanUpRedirect(): void {
113
- super._cleanUpRedirect()
114
- this.state = RequestState.Status
115
- this.rawHeaders = undefined
116
- this.responseInfo = {
117
- chunked: false,
492
+ _cleanUpRedirect(): void {
493
+ this.currentHeaders = new Headers()
494
+ this.currentResponse = undefined
495
+ this._rawBody = undefined
496
+ this.stats = {}
497
+ this._clearSocketEventListeners()
498
+ this.parser.reset()
499
+ }
500
+
501
+ /**
502
+ * Reports response when redirected.
503
+ */
504
+ _reportRedirect(status: number): boolean {
505
+ const { request, currentHeaders } = this
506
+ if (!currentHeaders) {
507
+ return false
508
+ }
509
+ const rerUrl = currentHeaders.get('location')
510
+ // https://github.com/jarrodek/socket-fetch/issues/13
511
+ const redirectOptions = this.getRedirectOptions(status, request.method, rerUrl)
512
+ if (!redirectOptions.redirect) {
513
+ return false
514
+ }
515
+ this.redirecting = true
516
+ setTimeout(() => this._redirectRequest(redirectOptions))
517
+ return true
518
+ }
519
+
520
+ /**
521
+ * Get redirect options based on status and method
522
+ */
523
+ private getRedirectOptions(
524
+ status: number,
525
+ method?: string,
526
+ location?: string
527
+ ): { redirect: boolean; location?: string; forceGet?: boolean } {
528
+ if (!location) {
529
+ return { redirect: false }
118
530
  }
531
+
532
+ // 301, 302, 303, 307, 308 are redirect status codes
533
+ if (status >= 300 && status < 400) {
534
+ const forceGet = status === 301 || status === 302 || status === 303
535
+ return { redirect: true, location, forceGet }
536
+ }
537
+
538
+ return { redirect: false }
119
539
  }
120
540
 
121
541
  /**
122
- * Prepares an HTTP message from API Client's request object.
123
- *
124
- * @returns Resolved promise to an `ArrayBuffer`.
542
+ * Creates a response and adds it to the redirects list and redirects
543
+ * the request to the new location.
125
544
  */
126
- async prepareMessage(): Promise<Buffer> {
127
- let payload = this.request.payload
128
- if (['get', 'head'].includes(this.request.method.toLowerCase())) {
129
- payload = undefined
545
+ async _redirectRequest(options: { redirect: boolean; location?: string; forceGet?: boolean }): Promise<void> {
546
+ if (this.followRedirects === false) {
547
+ this._publishResponse()
548
+ return
130
549
  }
131
- const headers = new Headers(this.request.toHeadersString())
132
- this.prepareHeaders(headers, !!payload)
133
- const auth = this.hasProxy && !this.isProxyTunnel ? this._proxyAuthHeader() : undefined
134
- if (auth) {
135
- headers.set('proxy-authorization', auth)
550
+ const location = options.location && this.getRedirectLocation(options.location, this.request.url)
551
+ if (!location) {
552
+ this._errorRequest({ code: 302 })
553
+ return
136
554
  }
137
- const buffer = PayloadSupport.payloadToBuffer(headers, payload)
138
- if (buffer) {
139
- addContentLength(this.request.method || 'GET', buffer, headers)
555
+
556
+ // check if this is infinite loop
557
+ if (this.isRedirectLoop(location, this.redirects)) {
558
+ this._errorRequest({ code: 310 })
559
+ return
140
560
  }
141
561
 
142
- this._handleAuthorization(headers)
143
- this.sentRequest.headers = headers.toString()
144
- const message = this._prepareMessage(headers, buffer)
145
- if (this.auth) {
146
- // This restores altered by authorization original headers
147
- // so it can be safe to use when redirecting
148
- if (this.auth.headers) {
149
- this.request.headers = this.auth.headers
150
- delete this.auth.headers
562
+ const detail: BeforeRedirectDetail = {
563
+ location,
564
+ returnValue: true,
565
+ }
566
+ this.emit('beforeredirect', detail)
567
+ if (!detail.returnValue) {
568
+ this._publishResponse()
569
+ return
570
+ }
571
+ try {
572
+ const responseCookies = this.currentHeaders.get('set-cookie')
573
+ const response = await this._createRedirectResponse(location)
574
+ this.redirects.push(response)
575
+ this._cleanUpRedirect()
576
+ if (responseCookies) {
577
+ this._processRedirectCookies(responseCookies, location)
151
578
  }
579
+ this.redirecting = false
580
+
581
+ this.request.url = location
582
+ this.sentRequest.url = location
583
+ if (options.forceGet) {
584
+ this.request.method = 'GET'
585
+ }
586
+ this.uri = this.readUrl(location)
587
+ this.hostHeader = this.getHostHeader(location)
588
+ // No idea why but without setTimeout the program loses it's
589
+ // scope after calling the function.
590
+ setTimeout(() => this.send())
591
+ } catch (e) {
592
+ const error = e as Error
593
+ this._errorRequest({
594
+ message: (error && error.message) || 'Unknown error occurred',
595
+ })
152
596
  }
153
- return message
154
597
  }
155
598
 
156
599
  /**
157
- * Sends a data to a socket.
158
- *
159
- * @param buffer HTTP message to send
600
+ * Get redirect location
160
601
  */
161
- writeMessage(buffer: Buffer): Promise<void> {
162
- this.logger.debug(`Writing the message to the socket...`)
163
- let msg = buffer.toString()
164
- const limit = this.opts.sentMessageLimit
165
- if (limit && limit > 0 && msg.length >= limit) {
166
- msg = msg.substr(0, limit)
167
- msg += ' ...'
602
+ private getRedirectLocation(location: string, currentUrl: string): string {
603
+ return getRedirectLocation(location, currentUrl)
604
+ }
605
+
606
+ /**
607
+ * Check if redirect is a loop
608
+ */
609
+ private isRedirectLoop(location: string, redirects: ResponseRedirect[]): boolean {
610
+ return isRedirectLoop(location, redirects)
611
+ }
612
+
613
+ /**
614
+ * @param location The redirect location.
615
+ * @return Redirect response object
616
+ */
617
+ async _createRedirectResponse(location: string): Promise<ResponseRedirect> {
618
+ const { currentResponse = new Response() } = this
619
+
620
+ const response = HttpResponse.fromValues(
621
+ currentResponse.status,
622
+ currentResponse.statusText,
623
+ currentResponse.headers
624
+ )
625
+ if (currentResponse.payload) {
626
+ response.payload = currentResponse.payload
168
627
  }
169
- this.sentRequest.httpMessage = msg
170
- const startTime = Date.now()
171
- this.stats.startTime = startTime
172
- this.sentRequest.startTime = startTime
173
628
 
174
- this.stats.messageStart = Date.now()
175
- return new Promise((resolve) => {
176
- this.socket?.write(buffer, () => {
177
- this.logger.debug(`The message has been sent.`)
178
- this.stats.sentTime = Date.now()
179
- try {
180
- this.emit('loadstart')
181
- } catch {
182
- //
183
- }
184
- resolve()
185
- })
186
- })
629
+ const body = await this.decompress(this._rawBody)
630
+ if (body) {
631
+ await response.writePayload(body)
632
+ currentResponse.payload = response.payload
633
+ }
634
+
635
+ const redirect = ResponseRedirect.fromValues(
636
+ location,
637
+ response.toJSON(),
638
+ this.stats.startTime,
639
+ this.stats.responseTime
640
+ )
641
+ redirect.timings = this._computeStats(this.stats)
642
+
643
+ return redirect
187
644
  }
188
645
 
189
646
  /**
190
- * Connects to a server and sends the message.
191
- *
192
- * @returns Promise resolved when socket is connected.
647
+ * Creates a response object
193
648
  */
194
- async connect(): Promise<net.Socket> {
195
- const port = getPort(this.uri.port, this.uri.protocol)
196
- const host = this.uri.hostname
197
- let socket
198
- if (port === 443 || this.uri.protocol === 'https:') {
199
- socket = await this._connectTls(port, host)
200
- } else {
201
- socket = await this._connect(port, host)
649
+ async _createResponse(): Promise<Response> {
650
+ if (this.aborted) {
651
+ throw new Error(`Request aborted.`)
202
652
  }
203
- const { timeout } = this
204
- if (timeout > 0) {
205
- socket.setTimeout(timeout)
653
+ const { currentResponse } = this
654
+ if (!currentResponse) {
655
+ throw new Error(`Tried to create a response but no response data is set.`)
206
656
  }
207
- this.socket = socket
208
- this._addSocketListeners(socket)
209
- socket.resume()
210
- return socket
657
+ const { status } = currentResponse
658
+ if (status === undefined) {
659
+ throw new Error(`The response status is empty.
660
+ It means that the successful connection wasn't made.
661
+ Check your request parameters.`)
662
+ }
663
+ const body = await this.decompress(this._rawBody)
664
+ const response = Response.fromValues(status, currentResponse.statusText, currentResponse.headers)
665
+ response.timings = this._computeStats(this.stats)
666
+ response.loadingTime = response.timings.total()
667
+ if (body) {
668
+ await response.writePayload(body)
669
+ currentResponse.payload = response.payload
670
+ }
671
+ if (status === 401) {
672
+ this.logger.silly('Setting auth on the response...')
673
+ response.auth = this._getAuth()
674
+ }
675
+ return response
211
676
  }
212
677
 
213
678
  /**
214
- * Connects to a server and writes a message using insecure connection.
215
- *
216
- * @param port A port number to connect to.
217
- * @param host A host name to connect to
218
- * @returns A promise resolved when the message was sent to a server
679
+ * Finishes the response with error message.
219
680
  */
220
- _connect(port: number, host: string): Promise<net.Socket> {
221
- this.logger.debug('Opening an HTTP connection...')
222
- return new Promise((resolve, reject) => {
223
- this.stats.connectionTime = Date.now()
224
- const isIp = net.isIP(host)
225
- if (isIp) {
226
- this.stats.lookupTime = Date.now()
227
- }
228
- const client = net.createConnection(port, host, () => {
229
- this.logger.debug('HTTP connection established.')
230
- this.stats.connectedTime = Date.now()
231
- resolve(client)
232
- })
233
- client.pause()
234
- if (!isIp) {
235
- client.once('lookup', () => {
236
- this.stats.lookupTime = Date.now()
237
- })
238
- }
239
- client.once('error', (err: Error) => reject(err))
240
- })
681
+ _errorRequest(opts: ResponseErrorInit): void {
682
+ const { currentResponse } = this
683
+ this.aborted = true
684
+ let message
685
+ if (opts.code && !opts.message) {
686
+ message = getCodeMessage(opts.code)
687
+ } else if (opts.message) {
688
+ message = opts.message
689
+ }
690
+ message = message || 'Unknown error occurred'
691
+ const error = new SerializableError(message, opts.code)
692
+ const log = RequestLog.fromRequest(this.sentRequest.toJSON())
693
+ const response = ErrorResponse.fromError(error)
694
+ log.response = response
695
+ if (currentResponse && currentResponse.status) {
696
+ response.status = currentResponse.status
697
+ response.statusText = currentResponse.statusText
698
+ response.headers = currentResponse.headers
699
+ response.payload = currentResponse.payload
700
+ }
701
+ this.finalizeRequest(log)
702
+ this._cleanUp()
241
703
  }
242
704
 
243
705
  /**
244
- * Connects to a server and writes a message using secured connection.
245
- *
246
- * @param port A port number to connect to.
247
- * @param host A host name to connect to
248
- * @returns A promise resolved when the message was sent to a server
706
+ * Generates authorization info object from response.
249
707
  */
250
- _connectTls(port: number, host: string): Promise<tls.TLSSocket> {
251
- this.logger.debug('Opening an SSL connection...')
252
- const { opts } = this
253
- const options: tls.ConnectionOptions = {}
254
- const isIp = net.isIP(host)
255
- if (!isIp) {
256
- options.servername = host
257
- }
258
- if (opts.validateCertificates) {
259
- options.checkServerIdentity = this._checkServerIdentity.bind(this)
260
- } else {
261
- options.rejectUnauthorized = false
262
- options.checkServerIdentity = (): Error | undefined => {
263
- return undefined
708
+ _getAuth(): ResponseAuthorization {
709
+ this.logger.debug('Getting auth from response headers')
710
+ let auth = this.currentHeaders.get('www-authenticate')
711
+ const result = new ResponseAuthorization()
712
+ if (auth) {
713
+ this.logger.silly('Auth header found', auth)
714
+ auth = auth.toLowerCase()
715
+ if (auth.indexOf('ntlm') !== -1) {
716
+ this.logger.silly('Detected NTLM authorization')
717
+ result.method = 'ntlm'
718
+ } else if (auth.indexOf('basic') !== -1) {
719
+ result.method = 'basic'
720
+ this.logger.silly('Detected basic authorization')
721
+ } else if (auth.indexOf('digest') !== -1) {
722
+ result.method = 'digest'
723
+ this.logger.silly('Detected digest authorization')
264
724
  }
265
- // target.requestOCSP = false;
266
725
  }
267
- const certs = this.opts.certificates
268
- if (Array.isArray(certs)) {
269
- certs.forEach((cert) => this._addClientCertificate(cert, options))
726
+ return result
727
+ }
728
+
729
+ /**
730
+ * Creates HAR 1.2 timings object from stats.
731
+ */
732
+ _computeStats(stats: RequestStats): RequestTime {
733
+ return computeStats(stats)
734
+ }
735
+
736
+ /**
737
+ * Handles cookie exchange when redirecting the request.
738
+ */
739
+ _processRedirectCookies(responseCookies: string, location: string): void {
740
+ this.request.headers = processRedirectCookies(
741
+ responseCookies,
742
+ location,
743
+ this.request.url,
744
+ this.request.toHeadersString()
745
+ )
746
+ }
747
+
748
+ /**
749
+ * Checks certificate identity using TLS api.
750
+ */
751
+ _checkServerIdentity(host: string, cert: tls.PeerCertificate): Error | undefined {
752
+ return checkServerIdentity(host, cert)
753
+ }
754
+
755
+ /**
756
+ * Clears event listeners of the socket object,
757
+ */
758
+ _clearSocketEventListeners(): void {
759
+ if (!this.socket) {
760
+ return
270
761
  }
271
- return new Promise((resolve, reject) => {
272
- const time = Date.now()
273
- this.stats.connectionTime = time
274
- if (isIp) {
275
- this.stats.lookupTime = time
276
- }
277
- const client = tls.connect(port, host, options, () => {
278
- this.logger.debug('SSL connection established.')
279
- const connectTime = Date.now()
280
- this.stats.connectedTime = connectTime
281
- this.stats.secureStartTime = connectTime
282
- resolve(client)
283
- })
284
- client.pause()
285
- client.once('error', (e: Error) => reject(e))
286
- if (!isIp) {
287
- client.once('lookup', () => {
288
- this.stats.lookupTime = Date.now()
289
- })
290
- }
291
- client.once('secureConnect', () => {
292
- this.stats.secureConnectedTime = Date.now()
293
- })
294
- })
762
+ this.socket.removeAllListeners('error')
763
+ this.socket.removeAllListeners('timeout')
764
+ this.socket.removeAllListeners('end')
295
765
  }
296
766
 
297
767
  /**
298
- * Prepares a full HTTP message body
299
- *
300
- * @param httpHeaders The list ogf headers to append.
301
- * @param buffer The buffer with the HTTP message
302
- * @returns The Buffer of the HTTP message
768
+ * Called with the `send()` function to initialize the main promise returned by the send function.
769
+ * The send function returns a promise that is resolved when the request finish.
303
770
  */
304
- _prepareMessage(httpHeaders: Headers, buffer?: Buffer): Buffer {
305
- this.logger.debug('Preparing an HTTP message...')
306
- const headers = []
307
- // const search = this.uri.search;
308
- // let path = this.uri.pathname;
309
- // if (search) {
310
- // path += search;
311
- // }
312
- // headers.push(`${this.arcRequest.method} ${path} HTTP/1.1`);
313
- const status = this._createHttpStatus()
314
- this.logger.debug(`Created message status: ${status}`)
315
- headers.push(status)
316
- if (this._hostRequired()) {
317
- this.logger.debug(`Adding the "host" header: ${this.hostHeader}`)
318
- headers.push(`Host: ${this.hostHeader}`)
771
+ protected wrapExecution(): Promise<IRequestLog> {
772
+ let promise: Promise<IRequestLog>
773
+ if (this[mainPromiseSymbol]) {
774
+ promise = this[mainPromiseSymbol] as Promise<IRequestLog>
775
+ } else {
776
+ promise = new Promise((resolve, reject) => {
777
+ this.mainResolver = resolve
778
+ this.mainRejecter = reject
779
+ })
780
+ this[mainPromiseSymbol] = promise
319
781
  }
320
- let str = headers.join('\r\n')
321
- const addHeaders = httpHeaders.toString()
322
- if (addHeaders) {
323
- this.logger.debug(`Adding headers to the request...`)
324
- str += '\r\n'
325
- str += PayloadSupport.normalizeString(addHeaders)
782
+ return promise
783
+ }
784
+
785
+ /**
786
+ * Called by the request finalizer or error finalized to report the response.
787
+ */
788
+ protected finalizeRequest(log: RequestLog | SerializableError): void {
789
+ const { mainRejecter, mainResolver } = this
790
+ if (!mainRejecter || !mainResolver) {
791
+ return
326
792
  }
327
- const startBuffer = Buffer.from(str, 'utf8')
328
- const endBuffer = Buffer.from(new Uint8Array([13, 10, 13, 10]))
329
- let body
330
- let sum = startBuffer.length + endBuffer.length
331
- if (buffer) {
332
- sum += buffer.length
333
- body = Buffer.concat([startBuffer, endBuffer, buffer], sum)
793
+
794
+ if (log instanceof SerializableError) {
795
+ mainRejecter(log)
334
796
  } else {
335
- body = Buffer.concat([startBuffer, endBuffer], sum)
797
+ mainResolver(log.toJSON())
336
798
  }
337
- this.logger.debug(`The message is ready.`)
338
- return body
799
+ this.mainRejecter = undefined
800
+ this.mainResolver = undefined
801
+ this[mainPromiseSymbol] = undefined
339
802
  }
340
803
 
341
804
  /**
342
- * Creates an HTTP status line for the message.
343
- * For proxy connections it, depending whether target is SSL or not, sets the path
344
- * as the full URL or just the authority.
345
- * @returns The generates status message.
805
+ * Prepares an HTTP message from API Client's request object.
346
806
  */
347
- _createHttpStatus(): string {
348
- const { request, uri, hasProxy, isProxyTunnel } = this
349
- const parts = [request.method]
350
- if (hasProxy && !isProxyTunnel) {
351
- // if (isProxyTunnel) {
352
- // // when a tunnel then the target is over SSL so the default port is 443.
353
- // parts.push(`${uri.hostname}:${uri.port || 443}`);
354
- // } else {
355
- // parts.push(arcRequest.url);
356
- // }
357
- parts.push(request.url)
358
- } else {
359
- let path = uri.pathname
360
- if (uri.search) {
361
- path += uri.search
362
- }
363
- parts.push(path)
807
+ async prepareMessage(): Promise<Buffer> {
808
+ let payload = this.request.payload
809
+ if (isMethodWithoutBody(this.request.method)) {
810
+ payload = undefined
364
811
  }
812
+ const headers = new Headers(this.request.toHeadersString())
813
+ this.prepareHeaders(headers, !!payload)
814
+ const auth = this.hasProxy && !this.isProxyTunnel ? this._proxyAuthHeader() : undefined
815
+ if (auth) {
816
+ headers.set(HEADER_PROXY_AUTHORIZATION, auth)
817
+ }
818
+ const buffer = PayloadSupport.payloadToBuffer(headers, payload)
819
+ if (buffer) {
820
+ addContentLength(this.request.method || 'GET', buffer, headers)
821
+ }
822
+
823
+ this._handleAuthorization(headers)
824
+ this.sentRequest.headers = headers.toString()
825
+
826
+ // Update message builder with current host header
827
+ this.messageBuilder.updateHostHeader(this.hostHeader || '')
365
828
 
366
- parts.push('HTTP/1.1')
367
- return parts.join(' ')
829
+ const message = this.messageBuilder.buildMessage(headers, buffer)
830
+ if (this.auth) {
831
+ // This restores altered by authorization original headers
832
+ // so it can be safe to use when redirecting
833
+ if (this.auth.headers) {
834
+ this.request.headers = this.auth.headers
835
+ delete this.auth.headers
836
+ }
837
+ }
838
+ return message
368
839
  }
369
840
 
370
841
  /**
371
- * Tests if current connection is required to add `host` header.
372
- * It returns `false` only if `host` header has been already provided.
373
- *
374
- * @returns True when the `host` header should be added to the headers list.
842
+ * Sends a data to a socket.
375
843
  */
376
- _hostRequired(): boolean {
377
- const headers = this.request.headers
378
- if (typeof headers !== 'string') {
379
- return true
844
+ writeMessage(buffer: Buffer): Promise<void> {
845
+ this.logger.debug(`Writing the message to the socket...`)
846
+ let msg = buffer.toString()
847
+ const limit = this.opts.sentMessageLimit
848
+ if (limit && limit > 0 && msg.length >= limit) {
849
+ msg = msg.substring(0, limit)
850
+ msg += ' ...'
380
851
  }
381
- return !this._hostTestReg.test(headers)
852
+ this.sentRequest.httpMessage = msg
853
+ const startTime = Date.now()
854
+ this.stats.startTime = startTime
855
+ this.sentRequest.startTime = startTime
856
+
857
+ this.stats.messageStart = Date.now()
858
+ return new Promise((resolve) => {
859
+ this.socket?.write(buffer, () => {
860
+ this.logger.debug(`The message has been sent.`)
861
+ this.stats.sentTime = Date.now()
862
+ try {
863
+ this.emit('loadstart')
864
+ } catch {
865
+ //
866
+ }
867
+ resolve()
868
+ })
869
+ })
382
870
  }
383
871
 
384
872
  /**
385
873
  * Alters authorization header depending on the `auth` object
386
- * @param headers A headers object where to append headers when needed
387
874
  */
388
875
  _handleAuthorization(headers: Headers): void {
389
876
  const { authorization } = this.opts
390
- const enabled = Array.isArray(authorization) ? authorization.filter((i) => i.enabled) : []
391
- if (!enabled.length) {
392
- return
393
- }
394
- const ntlm = enabled.find((i) => i.type === 'ntlm')
395
- if (ntlm) {
396
- this._authorizeNtlm(ntlm.config as INtlmAuthorization, headers)
877
+ this.authManager.handleRequestAuthorization(headers, authorization, this.uri.host)
878
+
879
+ // Sync the auth state from AuthManager to parent class for compatibility
880
+ const authState = this.authManager.getAuthState()
881
+ if (authState) {
882
+ this.auth = authState
397
883
  }
398
884
  }
399
885
 
400
886
  /**
401
- * Authorize the request with NTLM
402
- * @param authData Credentials to use
403
- * @param headers A headers object where to append headers if needed
887
+ * Generate proxy authorization header
404
888
  */
405
- _authorizeNtlm(authData: INtlmAuthorization, headers: Headers): void {
406
- const init = { ...authData, url: this.request.url } as INtlmAuthConfig
407
- const auth = new NtlmAuth(init)
408
- if (!this.auth) {
409
- this.auth = {
410
- method: 'ntlm',
411
- state: 0,
412
- headers: headers.toString(),
413
- }
414
- const msg = auth.createMessage1(this.uri.host)
415
- headers.set('Authorization', `NTLM ${msg.toBase64()}`)
416
- headers.set('Connection', 'keep-alive')
417
- } else if (this.auth && this.auth.state === 1) {
418
- const msg = auth.createMessage3(this.auth.challengeHeader as string, this.uri.host)
419
- this.auth.state = 2
420
- headers.set('Authorization', `NTLM ${msg.toBase64()}`)
421
- }
889
+ _proxyAuthHeader(): string | undefined {
890
+ return this.authManager.generateProxyAuthHeader()
422
891
  }
423
892
 
424
893
  /**
425
894
  * Add event listeners to existing socket.
426
- * @param socket An instance of the socket.
427
- * @return The same socket. Used for chaining.
428
895
  */
429
896
  _addSocketListeners(socket: net.Socket): net.Socket {
430
897
  let received = false
@@ -438,8 +905,11 @@ export class CoreEngine extends HttpEngine {
438
905
  }
439
906
  data = Buffer.from(data)
440
907
  try {
441
- this._processSocketMessage(data)
908
+ // Use the new parser instead of the old parsing methods
909
+ this.parser.processData(data)
910
+ this.logger.debug('Parser state:', this.parser.getState())
442
911
  } catch (e) {
912
+ this.logger.error('Error in _addSocketListeners:', e)
443
913
  const err = e as Error
444
914
  this._errorRequest({
445
915
  message: err.message || 'Unknown error occurred',
@@ -448,51 +918,44 @@ export class CoreEngine extends HttpEngine {
448
918
  }
449
919
  })
450
920
  socket.once('timeout', () => {
451
- this.state = RequestState.Done
452
- this._errorRequest(new Error('Connection timeout.'))
921
+ this.logger.debug(`Socket timeout occurred after ${this.timeout}ms.`)
922
+ this.parser.abort()
923
+ this._errorRequest(HttpEngineErrorFactory.connectionTimeout(this.timeout))
453
924
  socket.destroy()
454
925
  })
455
926
  socket.on('end', () => {
927
+ const endTime = Date.now()
456
928
  this.logger.debug(`Server connection ended.`)
457
929
  socket.removeAllListeners('timeout')
458
930
  socket.removeAllListeners('error')
459
- const endTime = Date.now()
931
+ socket.removeAllListeners('data')
460
932
  this.stats.lastReceivedTime = endTime
461
933
  this.sentRequest.endTime = endTime
462
- if (this.state !== RequestState.Done) {
934
+ // console.log(`Socket ended at ${endTime} in state`, this.parser.getState())
935
+ if (this.parser.getState() !== RequestState.Done) {
463
936
  if (!this.currentResponse) {
464
- this.logger.error(`Connection closed without receiving any data.`)
465
- // The parser haven't found the end of message so there was no message!
466
- const e = new SerializableError('Connection closed without receiving any data', 100)
937
+ const message = `Connection closed without receiving any data.`
938
+ this.logger.error(message)
939
+ const e = HttpEngineErrorFactory.connectionError(message, 100)
467
940
  this._errorRequest(e)
468
941
  } else {
469
- // There is an issue with the response. Size mismatch? Anyway,
470
- // it tries to create a response from current data.
471
942
  this.emit('loadend')
472
943
  this._publishResponse()
473
944
  }
474
945
  }
475
946
  })
476
947
  socket.once('error', (err) => {
948
+ this.logger.error(`Socket error occurred: ${err.message}`)
477
949
  socket.removeAllListeners('timeout')
478
950
  this._errorRequest(err)
479
951
  })
480
952
  return socket
481
953
  }
482
954
 
483
- /**
484
- * Processes response message chunk
485
- * @param buffer Message buffer
486
- */
487
- _processResponse(buffer: Buffer): void {
488
- this._processSocketMessage(buffer)
489
- this._reportResponse()
490
- }
491
-
492
955
  /**
493
956
  * Reports response after processing it.
494
957
  */
495
- _reportResponse(): void {
958
+ async _reportResponse(): Promise<void> {
496
959
  this._clearSocketEventListeners()
497
960
  const { aborted, currentResponse } = this
498
961
  if (aborted || !currentResponse) {
@@ -504,617 +967,119 @@ export class CoreEngine extends HttpEngine {
504
967
  this.stats.lastReceivedTime = endTime
505
968
  this.sentRequest.endTime = endTime
506
969
 
507
- if (status >= 300 && status < 400) {
508
- if (this.followRedirects && this._reportRedirect(status)) {
509
- this.closeClient()
510
- return
511
- }
512
- } else if (status === 401 && this.auth) {
513
- switch (this.auth.method) {
514
- case 'ntlm':
515
- this.handleNtlmResponse()
516
- return
517
- }
518
- }
519
- this.closeClient()
520
- this.emit('loadend')
521
- this._publishResponse()
522
- }
523
-
524
- /**
525
- * Generate response object and publish it to the listeners.
526
- */
527
- override _publishResponse(): Promise<void> {
528
- this.state = RequestState.Done
529
- if (!this._rawBody) {
530
- if (this.responseInfo.body) {
531
- this._rawBody = this.responseInfo.body
532
- } else if (this.responseInfo.chunk) {
533
- this._rawBody = this.responseInfo.chunk
534
- }
535
- }
536
- return super._publishResponse()
537
- }
538
-
539
- /**
540
- * @param location The redirect location.
541
- * @return Redirect response object
542
- */
543
- override async _createRedirectResponse(location: string): Promise<ResponseRedirect> {
544
- const { currentResponse = new Response() } = this
545
- this.currentResponse = currentResponse
546
- if (!this.currentResponse.payload) {
547
- if (this._rawBody) {
548
- this.currentResponse.payload = PayloadSerializer.stringifyBuffer(this._rawBody)
549
- } else if (this.responseInfo.body) {
550
- this.currentResponse.payload = PayloadSerializer.stringifyBuffer(this.responseInfo.body)
551
- } else if (this.responseInfo.chunk) {
552
- this.currentResponse.payload = PayloadSerializer.stringifyBuffer(this.responseInfo.chunk)
553
- }
554
- }
555
- return super._createRedirectResponse(location)
556
- }
557
-
558
- /**
559
- * closes the connection, if any
560
- */
561
- closeClient(): void {
562
- if (this.socket && !this.socket.destroyed) {
563
- this.socket.destroy()
564
- }
565
- }
566
-
567
- /**
568
- * Handles the response with NTLM authorization
569
- */
570
- handleNtlmResponse(): void {
571
- const { auth } = this
572
- if (!auth) {
573
- throw new Error('No auth data.')
574
- }
575
- if (auth.state === 0) {
576
- if (this.currentHeaders.has('www-authenticate')) {
577
- auth.state = 1
578
- auth.challengeHeader = this.currentHeaders.get('www-authenticate')
579
- this._cleanUpRedirect()
580
- this.prepareMessage().then((message) => this.writeMessage(message))
581
- return
582
- }
583
- }
584
- delete this.auth
585
- this.emit('loadend')
586
- this._publishResponse()
587
- }
588
-
589
- /**
590
- * Process received message.
591
- *
592
- * @param data Received message.
593
- */
594
- _processSocketMessage(data: Buffer): void {
595
- if (this.aborted) {
596
- return
597
- }
598
- if (this.state === RequestState.Done) {
599
- return
600
- }
601
- let remaining: Buffer | undefined = data
602
- if (this.state === RequestState.Status) {
603
- remaining = this._processStatus(remaining)
604
- if (!remaining) {
605
- return
606
- }
607
- }
608
- if (this.state === RequestState.Headers) {
609
- remaining = this._processHeaders(remaining)
610
- if (!remaining) {
611
- return
612
- }
613
- }
614
- if (this.state === RequestState.Body) {
615
- this._processBody(remaining)
616
- return
617
- }
618
- }
619
-
620
- /**
621
- * Read status line from the response.
622
- * This function will set `status` and `statusText` fields
623
- * and then will set `state` to HEADERS.
624
- *
625
- * @param data The received data
626
- */
627
- _processStatus(data?: Buffer): Buffer | undefined {
628
- if (this.aborted) {
629
- return
630
- }
631
- const response = Response.fromValues(0)
632
- response.loadingTime = 0
633
- this.currentResponse = response
634
- if (!data) {
635
- return
636
- }
637
-
638
- this.logger.info('Processing status')
639
- const index = data.indexOf(nlBuffer)
640
- let statusLine = data.slice(0, index).toString()
641
- data = data.slice(index + 2)
642
- statusLine = statusLine.replace(/HTTP\/\d(\.\d)?\s/, '')
643
- const delimiterPos = statusLine.indexOf(' ')
644
- let status
645
- let msg = ''
646
- if (delimiterPos === -1) {
647
- status = statusLine
648
- } else {
649
- status = statusLine.substr(0, delimiterPos)
650
- msg = statusLine.substr(delimiterPos + 1)
651
- }
652
- let typedStatus = Number(status)
653
- if (Number.isNaN(typedStatus)) {
654
- typedStatus = 0
655
- }
656
- if (msg && msg.indexOf('\n') !== -1) {
657
- msg = msg.split('\n')[0]
658
- }
659
- const cr = this.currentResponse as Response
660
- cr.status = typedStatus
661
- cr.statusText = msg
662
- this.logger.info('Received status', typedStatus, msg)
663
- this.state = RequestState.Headers
664
- return data
665
- }
666
-
667
- /**
668
- * Read headers from the received data.
669
- *
670
- * @param data Received data
671
- * @returns Remaining data in the buffer.
672
- */
673
- _processHeaders(data?: Buffer): Buffer | undefined {
674
- if (this.aborted) {
675
- return
676
- }
677
- if (!data) {
678
- this._parseHeaders()
679
- return
680
- }
681
- this.logger.info('Processing headers')
682
- // Looking for end of headers section
683
- let index = data.indexOf(nlNlBuffer)
684
- let padding = 4
685
- if (index === -1) {
686
- // It can also be 2x ASCII 10
687
- const _index = data.indexOf(Buffer.from([10, 10]))
688
- if (_index !== -1) {
689
- index = _index
690
- padding = 2
691
- }
692
- }
693
-
694
- // https://github.com/jarrodek/socket-fetch/issues/3
695
- const enterIndex = data.indexOf(nlBuffer)
696
- if (index === -1 && enterIndex !== 0) {
697
- // end in next chunk
698
- if (!this.rawHeaders) {
699
- this.rawHeaders = data
700
- } else {
701
- const sum = this.rawHeaders.length + data.length
702
- this.rawHeaders = Buffer.concat([this.rawHeaders, data], sum)
703
- }
704
- return
705
- }
706
- if (enterIndex !== 0) {
707
- const headersArray = data.slice(0, index)
708
- if (!this.rawHeaders) {
709
- this.rawHeaders = headersArray
710
- } else {
711
- const sum = this.rawHeaders.length + headersArray.length
712
- this.rawHeaders = Buffer.concat([this.rawHeaders, headersArray], sum)
713
- }
714
- }
715
- this._parseHeaders(this.rawHeaders)
716
- delete this.rawHeaders
717
- this.state = RequestState.Body
718
- const start = index === -1 ? 0 : index
719
- const move = enterIndex === 0 ? 2 : padding
720
- data = data.slice(start + move)
721
- return this._postHeaders(data)
722
- }
723
-
724
- /**
725
- * Check the response headers and end the request if necessary.
726
- * @param data Current response data buffer
727
- */
728
- _postHeaders(data: Buffer): Buffer | undefined {
729
- if (this.request.method === 'HEAD') {
730
- this._reportResponse()
731
- return
732
- }
733
- if (data.length === 0) {
734
- if (this.currentResponse?.status === 204) {
735
- this._reportResponse()
736
- return
737
- }
738
- if (this.currentHeaders.has('Content-Length')) {
739
- // If the server do not close the connection and clearly indicate that
740
- // there are no further data to receive the app can close the connection
741
- // and prepare the response.
742
- const length = Number(this.currentHeaders.get('Content-Length'))
743
- if (!Number.isNaN(length) && length === 0) {
744
- this._reportResponse()
745
- return
746
- }
747
- }
748
- // See: https://github.com/advanced-rest-client/arc-electron/issues/106
749
- // The client should wait until the connection is closed instead of assuming it should end the request.
750
-
751
- // else if (!this.currentHeaders.has('Transfer-Encoding') || !this.currentHeaders.get('Transfer-Encoding')) {
752
- // // Fix for https://github.com/jarrodek/socket-fetch/issues/6
753
- // // There is no body in the response.
754
- // // this._reportResponse();
755
- // return;
756
- // }
757
- return
758
- }
759
- return data
760
- }
761
-
762
- /**
763
- * This function assumes that all headers has been read and it's
764
- * just before changing the status to BODY.
765
- */
766
- _parseHeaders(buffer?: Buffer): void {
767
- let raw = ''
768
- if (buffer) {
769
- raw = buffer.toString()
770
- }
771
- ;(this.currentResponse as Response).headers = raw
772
- this.logger.info('Received headers list', raw)
773
- const headers = new Headers(raw)
774
- this.currentHeaders = headers
775
- if (headers.has('Content-Length')) {
776
- this.responseInfo.contentLength = Number(headers.get('Content-Length'))
777
- }
778
- if (headers.has('Transfer-Encoding')) {
779
- const tr = headers.get('Transfer-Encoding')
780
- if (tr === 'chunked') {
781
- this.responseInfo.chunked = true
782
- }
783
- }
784
- const rawHeaders = headers.toString()
785
- const detail: HeadersReceivedDetail = {
786
- returnValue: true,
787
- value: rawHeaders,
788
- }
789
- this.emit('headersreceived', detail)
790
- if (!detail.returnValue) {
791
- this.abort()
792
- }
793
- }
794
-
795
- /**
796
- * @param data A data to process
797
- */
798
- _processBody(data?: Buffer): void {
799
- if (this.aborted || !data) {
800
- return
801
- }
802
- if (this.responseInfo.chunked) {
803
- this._processBodyChunked(data)
804
- } else {
805
- this._processBodyContentLength(data)
806
- }
807
- }
808
-
809
- _processBodyContentLength(data: Buffer): void {
810
- if (typeof this.responseInfo.contentLength === 'undefined') {
811
- this._errorRequest(new Error(`The content-length header of the response is missing.`))
812
- return
813
- }
814
- if (!this.responseInfo.body) {
815
- this.responseInfo.body = data
816
- if (data.length >= this.responseInfo.contentLength) {
817
- this._reportResponse()
818
- return
819
- }
820
- return
821
- }
822
- const sum = this.responseInfo.body.length + data.length
823
- this.responseInfo.body = Buffer.concat([this.responseInfo.body, data], sum)
824
- if (this.responseInfo.body.length >= this.responseInfo.contentLength) {
825
- this._reportResponse()
826
- return
827
- }
828
- }
829
-
830
- /**
831
- * @param data A latest data to process
832
- */
833
- _processBodyChunked(data?: Buffer): void {
834
- if (!data) {
835
- return
836
- }
837
- if (this.responseInfo.chunk) {
838
- data = Buffer.concat([this.responseInfo.chunk, data], this.responseInfo.chunk.length + data.length)
839
- this.responseInfo.chunk = undefined
840
- }
970
+ const result = processResponse({
971
+ status,
972
+ currentResponse,
973
+ currentHeaders: this.currentHeaders,
974
+ followRedirects: this.followRedirects,
975
+ auth: this.auth,
976
+ aborted: this.aborted,
977
+ })
841
978
 
842
- while (true) {
843
- if (this.responseInfo.chunkSize === 0 && data.indexOf(nlNlBuffer) === 0) {
844
- this._reportResponse()
845
- return
846
- }
847
- if (!this.responseInfo.chunkSize) {
848
- data = this.readChunkSize(data)
849
- if (!data) {
979
+ switch (result.action) {
980
+ case 'redirect':
981
+ if (this._reportRedirect(status)) {
982
+ this.closeClient()
850
983
  return
851
984
  }
852
- if (!this.responseInfo.chunkSize && this.responseInfo.chunkSize !== 0) {
853
- // It may happen that node's buffer cuts the data
854
- // just before the chunk size.
855
- // It should proceed it in next portion of the data.
856
- this.responseInfo.chunk = data
985
+ break
986
+ case 'auth':
987
+ if (result.data?.method === 'ntlm') {
988
+ await this.handleNtlmResponse()
857
989
  return
858
990
  }
859
- if (!this.responseInfo.chunkSize) {
860
- this._reportResponse()
861
- return
862
- }
863
- }
864
- const size = Math.min(this.responseInfo.chunkSize, data.length)
865
- const sliced = data.slice(0, size)
866
- if (!this._rawBody) {
867
- this._rawBody = sliced
868
- } else {
869
- const sum = size + this._rawBody.length
870
- this._rawBody = Buffer.concat([this._rawBody, sliced], sum)
871
- }
872
-
873
- this.responseInfo.chunkSize -= size
874
- if (data.length === 0) {
875
- // this.logger.warn('Next chunk will start with CRLF!');
876
- return
877
- }
878
- data = data.slice(size + 2) // + CR
879
- if (data.length === 0) {
880
- // this.logger.info('No more data here. Waiting for new chunk');
881
- return
882
- }
991
+ break
992
+ case 'publish':
993
+ this.closeClient()
994
+ this.emit('loadend')
995
+ this._publishResponse()
996
+ break
997
+ case 'abort':
998
+ // Already aborted, do nothing
999
+ break
883
1000
  }
884
1001
  }
885
1002
 
886
1003
  /**
887
- * If response's Transfer-Encoding is 'chunked' read until next CR.
888
- * Everything before it is a chunk size.
1004
+ * Generate response object and publish it to the listeners.
889
1005
  */
890
- readChunkSize(array: Buffer): Buffer | undefined {
1006
+ async _publishResponse(): Promise<void> {
891
1007
  if (this.aborted) {
892
1008
  return
893
1009
  }
894
- let index = array.indexOf(nlBuffer)
895
- if (index === -1) {
896
- // not found in this portion of data.
897
- return array
898
- }
899
- if (index === 0) {
900
- // Node's buffer cuts CRLF after the end of chunk data, without last CLCR,
901
- // here's to fix it.
902
- // It can be either new line from the last chunk or end of
903
- // the message where
904
- // the rest of the array is [13, 10, 48, 13, 10, 13, 10]
905
- if (array.indexOf(nlNlBuffer) === 0) {
906
- this.responseInfo.chunkSize = 0
907
- return Buffer.alloc(0)
1010
+ try {
1011
+ const response = await this._createResponse()
1012
+ const result = RequestLog.fromRequestResponse(this.sentRequest.toJSON(), response.toJSON())
1013
+ if (this.redirects.length) {
1014
+ result.redirects = this.redirects
908
1015
  }
909
- array = array.slice(index + 2)
910
- index = array.indexOf(nlBuffer)
911
- }
912
- // this.logger.info('Size index: ', index);
913
- const chunkSize = parseInt(array.slice(0, index).toString(), 16)
914
- if (Number.isNaN(chunkSize)) {
915
- this.responseInfo.chunkSize = undefined
916
- return array.slice(index + 2)
1016
+ result.size = new RequestsSize()
1017
+ if (this.sentRequest.httpMessage) {
1018
+ result.size.request = Buffer.from(this.sentRequest.httpMessage).length
1019
+ }
1020
+ if (response.payload) {
1021
+ if (typeof response.payload === 'string') {
1022
+ result.size.response = response.payload.length
1023
+ } else {
1024
+ result.size.response = response.payload.data.length
1025
+ }
1026
+ }
1027
+ this.finalizeRequest(result)
1028
+ } catch (e) {
1029
+ const error = e as Error
1030
+ this.logger.error('Error in _publishResponse:', error)
1031
+ this._errorRequest({
1032
+ message: (error && error.message) || 'Unknown error occurred',
1033
+ })
917
1034
  }
918
- this.responseInfo.chunkSize = chunkSize
919
- return array.slice(index + 2)
1035
+ this.abort()
1036
+ this._cleanUp()
920
1037
  }
921
1038
 
922
1039
  /**
923
- * Connects to a server through a proxy. Depending on the proxy type the returned socket
924
- * is a socket created after creating a tunnel (SSL) or the proxy socket.
925
- *
926
- * @returns Promise resolved when socket is connected.
1040
+ * closes the connection, if any
927
1041
  */
928
- async connectProxy(): Promise<net.Socket | undefined> {
929
- let socket
930
- if (this.isProxyTunnel) {
931
- socket = await this.connectTunnel(this.isProxySsl)
932
- } else {
933
- socket = await this.proxyHttp(this.isProxySsl)
934
- }
935
- if (!socket) {
936
- return
937
- }
938
- const { timeout } = this
939
- if (timeout > 0) {
940
- socket.setTimeout(timeout)
1042
+ closeClient(): void {
1043
+ if (this.socket && !this.socket.destroyed) {
1044
+ this.logger.debug('Closing the socket connection...')
1045
+ this.socket.destroy()
941
1046
  }
942
- this.socket = socket
943
- this._addSocketListeners(socket)
944
- socket.resume()
945
- return socket
946
1047
  }
947
1048
 
948
1049
  /**
949
- * Creates a tunnel to a Proxy for SSL connections.
950
- * The returned socket is the one created after the tunnel is established.
951
- * @param proxyIsSsl Whether the proxy is an SSL connection.
952
- * @returns Promise resolved when socket is connected.
1050
+ * Handles the response with NTLM authorization
953
1051
  */
954
- async connectTunnel(proxyIsSsl = false): Promise<net.Socket | undefined> {
955
- this.logger.debug(`Creating a tunnel through the proxy...`)
956
- const { proxy } = this.opts
957
- const { url } = this.request
958
- if (!proxy) {
959
- throw new Error(`No proxy configuration found.`)
960
- }
961
- let proxyUrl = proxy
962
- if (proxyIsSsl && !proxyUrl.startsWith('https:')) {
963
- proxyUrl = `https://${proxyUrl}`
964
- } else if (!proxyIsSsl && !proxyUrl.startsWith('http:')) {
965
- proxyUrl = `http://${proxyUrl}`
966
- }
967
- const proxyUri = new URL(proxyUrl)
968
- const targetUrl = new URL(url)
969
- const proxyPort = proxyUri.port || (proxyIsSsl ? 443 : 80)
970
- const targetPort = targetUrl.port || 443 // target is always SSL so 443.
971
- const authority = `${targetUrl.hostname}:${targetPort}`
972
- const connectOptions: https.RequestOptions = {
973
- host: proxyUri.hostname,
974
- port: proxyPort,
975
- method: 'CONNECT',
976
- path: authority,
977
- headers: {
978
- host: authority,
979
- },
980
- }
981
- if (proxyIsSsl) {
982
- connectOptions.rejectUnauthorized = false
983
- // @ts-expect-error This is correct!
984
- connectOptions.requestOCSP = false
985
- }
986
- const auth = this._proxyAuthHeader()
987
- if (auth) {
988
- this.logger.debug(`Adding proxy authorization.`)
989
- ;(connectOptions.headers as http.OutgoingHttpHeaders)['proxy-authorization'] = auth
990
- }
991
- const lib = proxyIsSsl ? https : http
992
- return new Promise((resolve, reject) => {
993
- this.stats.connectionTime = Date.now()
994
- const connectRequest = lib.request(connectOptions)
995
- connectRequest.once('socket', (socket: net.Socket) => {
996
- socket.on('lookup', () => {
997
- this.stats.lookupTime = Date.now()
998
- })
999
- })
1052
+ async handleNtlmResponse(): Promise<void> {
1053
+ this.logger.debug('Handling NTLM response...')
1054
+ this.logger.debug('Current headers:', this.currentHeaders?.toString())
1055
+ this.logger.debug('Auth state:', this.authManager.getAuthState())
1000
1056
 
1001
- connectRequest.on('error', (err) => {
1002
- this.logger.error('Error establishing proxy tunnel:', err)
1003
- reject(
1004
- new SerializableError(
1005
- `Failed to establish proxy tunnel: ${err.message}`,
1006
- (err as Error & { code: string }).code ? Number((err as Error & { code: string }).code) : 111
1007
- )
1008
- )
1009
- })
1057
+ const shouldRetry = this.authManager.handleNtlmResponse(this.currentHeaders)
1058
+ this.logger.debug('Should retry NTLM:', shouldRetry)
1010
1059
 
1011
- connectRequest.on('connect', async (res: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
1012
- const time = Date.now()
1013
- this.stats.connectedTime = time
1014
- this.stats.secureStartTime = time
1015
- if (typeof this.stats.lookupTime === 'undefined') {
1016
- this.stats.lookupTime = time
1017
- }
1018
- if (res.statusCode === 401) {
1019
- this.currentHeaders = new Headers(res.headers)
1020
- const currentResponse = Response.fromValues(res.statusCode, res.statusMessage, this.currentHeaders.toString())
1021
- currentResponse.loadingTime = 0
1022
- this.currentResponse = currentResponse
1023
- if (head.length) {
1024
- this._rawBody = head
1025
- currentResponse.payload = PayloadSerializer.stringifyBuffer(head)
1026
- }
1027
- connectRequest.destroy()
1028
- resolve(undefined)
1029
- setTimeout(() => {
1030
- // const e = new NetError('The proxy requires authentication.', 127);
1031
- this._publishResponse()
1032
- })
1033
- } else if (res.statusCode === 407) {
1034
- await this.handleProxyAuthentication(res, socket)
1035
- } else if (res.statusCode !== 200) {
1036
- this.logger.debug(`The proxy tunnel ended with ${res.statusCode} status code. Erroring request.`)
1037
- connectRequest.destroy()
1038
- const errorMessage =
1039
- res.statusMessage || `Proxy tunnel establishment failed with status code ${res.statusCode}`
1040
- const e = new SerializableError(errorMessage, res.statusCode)
1041
- reject(e)
1042
- } else {
1043
- this.logger.debug(`Established a proxy tunnel.`)
1044
- this.logger.debug(`Upgrading connection to SSL...`)
1045
- const tlsSocket = tls.connect({ socket, rejectUnauthorized: false }, () => {
1046
- this.logger.debug(`Connection upgraded to SSL.`)
1047
- resolve(tlsSocket)
1048
- })
1049
- tlsSocket.once('secureConnect', () => {
1050
- this.stats.secureConnectedTime = Date.now()
1051
- })
1052
- }
1053
- })
1054
- connectRequest.end()
1055
- })
1056
- }
1060
+ if (shouldRetry) {
1061
+ // Update the parent auth state to match AuthManager state
1062
+ this.auth = this.authManager.getAuthState()
1063
+ this._cleanUpRedirect()
1057
1064
 
1058
- async handleProxyAuthentication(res: http.IncomingMessage, socket: net.Socket): Promise<void> {
1059
- if (res.statusCode === 407) {
1060
- // Proxy Authentication Required
1061
- this.currentHeaders = new Headers(res.headers)
1062
- const authHeader = this.currentHeaders.get('proxy-authenticate')
1063
-
1064
- if (authHeader && authHeader.startsWith('Digest')) {
1065
- // Parse the Digest challenge and update _proxyAuthHeader to generate the correct response.
1066
- this._errorRequest(new SerializableError(`Unsupported proxy authentication scheme: ${authHeader}`, 127))
1067
- socket.destroy()
1068
- // this._cleanUpRedirect(); // Might need adjustments
1069
- // const message = await this.prepareMessage();
1070
- // this.writeMessage(message);
1071
- } else if (authHeader && authHeader.startsWith('Basic')) {
1072
- // Basic auth is usually handled directly in _proxyAuthHeader, so this might be redundant.
1073
- this._cleanUpRedirect() // Might need adjustments
1065
+ try {
1074
1066
  const message = await this.prepareMessage()
1075
- this.writeMessage(message)
1076
- } else {
1077
- // Unsupported authentication scheme
1078
- this._errorRequest(new SerializableError(`Unsupported proxy authentication scheme: ${authHeader}`, 127))
1079
- socket.destroy()
1067
+ await this.writeMessage(message)
1068
+ // Don't return here - let the socket listeners handle the response
1069
+ } catch (error) {
1070
+ this.logger.error('Failed to retry NTLM authentication:', error)
1071
+ this._errorRequest({
1072
+ message: `NTLM retry failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
1073
+ })
1080
1074
  }
1075
+ return
1081
1076
  }
1082
- }
1083
1077
 
1084
- /**
1085
- * Creates connection to a proxy for an HTTP (non-SSL) transport.
1086
- * This is the same as calling _connect or _connectTls but the target is the proxy and not the
1087
- * target URL. The message sent to the proxy server is different than the one sent
1088
- * to the target.
1089
- * @param proxyIsSsl
1090
- * @returns Promise resolved when socket is connected.
1091
- */
1092
- async proxyHttp(proxyIsSsl = false): Promise<net.Socket> {
1093
- this.logger.debug('Proxying an HTTP request...')
1094
- const { proxy } = this.opts
1095
- if (!proxy) {
1096
- throw new Error(`No proxy configuration found.`)
1097
- }
1098
- let proxyUrl = proxy
1099
- if (proxyIsSsl && !proxyUrl.startsWith('https:')) {
1100
- proxyUrl = `https://${proxyUrl}`
1101
- } else if (!proxyIsSsl && !proxyUrl.startsWith('http:')) {
1102
- proxyUrl = `http://${proxyUrl}`
1103
- }
1104
- const proxyUri = new URL(proxyUrl)
1105
- const port = Number(proxyUri.port || 443)
1106
- const host = proxyUri.hostname
1107
- let socket
1108
- try {
1109
- if (proxyIsSsl) {
1110
- socket = await this._connectTls(port, host)
1111
- } else {
1112
- socket = await this._connect(port, host)
1113
- }
1114
- } catch (error) {
1115
- const err = error as Error & { code?: string }
1116
- throw new SerializableError(`Failed to connect to proxy: ${err.message}`, err.code ? Number(err.code) : 112)
1117
- }
1118
- return socket
1078
+ this.logger.debug('NTLM authentication completed')
1079
+ // Clear both auth states
1080
+ this.authManager.clearAuthState()
1081
+ delete this.auth
1082
+ this.emit('loadend')
1083
+ this._publishResponse()
1119
1084
  }
1120
1085
  }