EffectiveURL.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. import * as A from "fp-ts/Array"
  2. import * as E from "fp-ts/Either"
  3. import * as O from "fp-ts/Option"
  4. import * as RA from "fp-ts/ReadonlyArray"
  5. import * as S from "fp-ts/string"
  6. import qs from "qs"
  7. import { flow, pipe } from "fp-ts/function"
  8. import { combineLatest, Observable } from "rxjs"
  9. import { map } from "rxjs/operators"
  10. import {
  11. FormDataKeyValue,
  12. HoppRESTReqBody,
  13. HoppRESTRequest,
  14. parseTemplateString,
  15. parseBodyEnvVariables,
  16. Environment,
  17. HoppRESTHeader,
  18. HoppRESTParam,
  19. parseRawKeyValueEntriesE,
  20. parseTemplateStringE,
  21. } from "@hoppscotch/data"
  22. import { arrayFlatMap, arraySort } from "../functional/array"
  23. import { toFormData } from "../functional/formData"
  24. import { tupleWithSameKeysToRecord } from "../functional/record"
  25. import { getGlobalVariables } from "~/newstore/environments"
  26. export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
  27. /**
  28. * The effective final URL.
  29. *
  30. * This contains path, params and environment variables all applied to it
  31. */
  32. effectiveFinalURL: string
  33. effectiveFinalHeaders: { key: string; value: string }[]
  34. effectiveFinalParams: { key: string; value: string }[]
  35. effectiveFinalBody: FormData | string | null
  36. effectiveFinalVars: { key: string; value: string }[]
  37. }
  38. /**
  39. * Get headers that can be generated by authorization config of the request
  40. * @param req Request to check
  41. * @param envVars Currently active environment variables
  42. * @returns The list of headers
  43. */
  44. const getComputedAuthHeaders = (
  45. req: HoppRESTRequest,
  46. envVars: Environment["variables"]
  47. ) => {
  48. // If Authorization header is also being user-defined, that takes priority
  49. if (req.headers.find((h) => h.key.toLowerCase() === "authorization"))
  50. return []
  51. if (!req.auth.authActive) return []
  52. const headers: HoppRESTHeader[] = []
  53. // TODO: Support a better b64 implementation than btoa ?
  54. if (req.auth.authType === "basic") {
  55. const username = parseTemplateString(req.auth.username, envVars)
  56. const password = parseTemplateString(req.auth.password, envVars)
  57. headers.push({
  58. active: true,
  59. key: "Authorization",
  60. value: `Basic ${btoa(`${username}:${password}`)}`,
  61. })
  62. } else if (
  63. req.auth.authType === "bearer" ||
  64. req.auth.authType === "oauth-2"
  65. ) {
  66. headers.push({
  67. active: true,
  68. key: "Authorization",
  69. value: `Bearer ${parseTemplateString(req.auth.token, envVars)}`,
  70. })
  71. } else if (req.auth.authType === "api-key") {
  72. const { key, value, addTo } = req.auth
  73. if (addTo === "Headers") {
  74. headers.push({
  75. active: true,
  76. key: parseTemplateString(key, envVars),
  77. value: parseTemplateString(value, envVars),
  78. })
  79. }
  80. }
  81. return headers
  82. }
  83. /**
  84. * Get headers that can be generated by body config of the request
  85. * @param req Request to check
  86. * @returns The list of headers
  87. */
  88. export const getComputedBodyHeaders = (
  89. req: HoppRESTRequest
  90. ): HoppRESTHeader[] => {
  91. // If a content-type is already defined, that will override this
  92. if (
  93. req.headers.find(
  94. (req) => req.active && req.key.toLowerCase() === "content-type"
  95. )
  96. )
  97. return []
  98. // Body should have a non-null content-type
  99. if (req.body.contentType === null) return []
  100. return [
  101. {
  102. active: true,
  103. key: "content-type",
  104. value: req.body.contentType,
  105. },
  106. ]
  107. }
  108. export type ComputedHeader = {
  109. source: "auth" | "body"
  110. header: HoppRESTHeader
  111. }
  112. /**
  113. * Returns a list of headers that will be added during execution of the request
  114. * For e.g, Authorization headers maybe added if an Auth Mode is defined on REST
  115. * @param req The request to check
  116. * @param envVars The environment variables active
  117. * @returns The headers that are generated along with the source of that header
  118. */
  119. export const getComputedHeaders = (
  120. req: HoppRESTRequest,
  121. envVars: Environment["variables"]
  122. ): ComputedHeader[] => [
  123. ...getComputedAuthHeaders(req, envVars).map((header) => ({
  124. source: "auth" as const,
  125. header,
  126. })),
  127. ...getComputedBodyHeaders(req).map((header) => ({
  128. source: "body" as const,
  129. header,
  130. })),
  131. ]
  132. export type ComputedParam = {
  133. source: "auth"
  134. param: HoppRESTParam
  135. }
  136. /**
  137. * Returns a list of params that will be added during execution of the request
  138. * For e.g, Authorization params (like API-key) maybe added if an Auth Mode is defined on REST
  139. * @param req The request to check
  140. * @param envVars The environment variables active
  141. * @returns The params that are generated along with the source of that header
  142. */
  143. export const getComputedParams = (
  144. req: HoppRESTRequest,
  145. envVars: Environment["variables"]
  146. ): ComputedParam[] => {
  147. // When this gets complex, its best to split this function off (like with getComputedHeaders)
  148. // API-key auth can be added to query params
  149. if (!req.auth.authActive) return []
  150. if (req.auth.authType !== "api-key") return []
  151. if (req.auth.addTo !== "Query params") return []
  152. return [
  153. {
  154. source: "auth",
  155. param: {
  156. active: true,
  157. key: parseTemplateString(req.auth.key, envVars),
  158. value: parseTemplateString(req.auth.value, envVars),
  159. },
  160. },
  161. ]
  162. }
  163. // Resolves environment variables in the body
  164. export const resolvesEnvsInBody = (
  165. body: HoppRESTReqBody,
  166. env: Environment
  167. ): HoppRESTReqBody => {
  168. if (!body.contentType) return body
  169. if (body.contentType === "multipart/form-data") {
  170. return {
  171. contentType: "multipart/form-data",
  172. body: body.body.map(
  173. (entry) =>
  174. <FormDataKeyValue>{
  175. active: entry.active,
  176. isFile: entry.isFile,
  177. key: parseTemplateString(entry.key, env.variables),
  178. value: entry.isFile
  179. ? entry.value
  180. : parseTemplateString(entry.value, env.variables),
  181. }
  182. ),
  183. }
  184. } else {
  185. return {
  186. contentType: body.contentType,
  187. body: parseTemplateString(body.body, env.variables),
  188. }
  189. }
  190. }
  191. function getFinalBodyFromRequest(
  192. request: HoppRESTRequest,
  193. envVariables: Environment["variables"]
  194. ): FormData | string | null {
  195. if (request.body.contentType === null) {
  196. return null
  197. }
  198. if (request.body.contentType === "application/x-www-form-urlencoded") {
  199. const parsedBodyRecord = pipe(
  200. request.body.body,
  201. parseRawKeyValueEntriesE,
  202. E.map(
  203. flow(
  204. RA.toArray,
  205. /**
  206. * Filtering out empty keys and non-active pairs.
  207. */
  208. A.filter(({ active, key }) => active && !S.isEmpty(key)),
  209. /**
  210. * Mapping each key-value to template-string-parser with either on array,
  211. * which will be resolved in further steps.
  212. */
  213. A.map(({ key, value }) => [
  214. parseTemplateStringE(key, envVariables),
  215. parseTemplateStringE(value, envVariables),
  216. ]),
  217. /**
  218. * Filtering and mapping only right-eithers for each key-value as [string, string].
  219. */
  220. A.filterMap(([key, value]) =>
  221. E.isRight(key) && E.isRight(value)
  222. ? O.some([key.right, value.right] as [string, string])
  223. : O.none
  224. ),
  225. tupleWithSameKeysToRecord,
  226. (obj) => qs.stringify(obj, { indices: false })
  227. )
  228. )
  229. )
  230. return E.isRight(parsedBodyRecord) ? parsedBodyRecord.right : null
  231. }
  232. if (request.body.contentType === "multipart/form-data") {
  233. return pipe(
  234. request.body.body,
  235. A.filter((x) => x.key !== "" && x.active), // Remove empty keys
  236. // Sort files down
  237. arraySort((a, b) => {
  238. if (a.isFile) return 1
  239. if (b.isFile) return -1
  240. return 0
  241. }),
  242. // FormData allows only a single blob in an entry,
  243. // we split array blobs into separate entries (FormData will then join them together during exec)
  244. arrayFlatMap((x) =>
  245. x.isFile
  246. ? x.value.map((v) => ({
  247. key: parseTemplateString(x.key, envVariables),
  248. value: v as string | Blob,
  249. }))
  250. : [
  251. {
  252. key: parseTemplateString(x.key, envVariables),
  253. value: parseTemplateString(x.value, envVariables),
  254. },
  255. ]
  256. ),
  257. toFormData
  258. )
  259. } else return parseBodyEnvVariables(request.body.body, envVariables)
  260. }
  261. /**
  262. * Outputs an executable request format with environment variables applied
  263. *
  264. * @param request The request to source from
  265. * @param environment The environment to apply
  266. *
  267. * @returns An object with extra fields defining a complete request
  268. */
  269. export function getEffectiveRESTRequest(
  270. request: HoppRESTRequest,
  271. environment: Environment
  272. ): EffectiveHoppRESTRequest {
  273. const envVariables = [...environment.variables, ...getGlobalVariables()]
  274. const effectiveFinalHeaders = pipe(
  275. getComputedHeaders(request, envVariables).map((h) => h.header),
  276. A.concat(request.headers),
  277. A.filter((x) => x.active && x.key !== ""),
  278. A.map((x) => ({
  279. active: true,
  280. key: parseTemplateString(x.key, envVariables),
  281. value: parseTemplateString(x.value, envVariables),
  282. }))
  283. )
  284. const effectiveFinalParams = pipe(
  285. getComputedParams(request, envVariables).map((p) => p.param),
  286. A.concat(request.params),
  287. A.filter((x) => x.active && x.key !== ""),
  288. A.map((x) => ({
  289. active: true,
  290. key: parseTemplateString(x.key, envVariables),
  291. value: parseTemplateString(x.value, envVariables),
  292. }))
  293. )
  294. const effectiveFinalVars = request.vars
  295. const effectiveFinalBody = getFinalBodyFromRequest(request, envVariables)
  296. return {
  297. ...request,
  298. effectiveFinalURL: parseTemplateString(
  299. request.endpoint,
  300. envVariables,
  301. request.vars
  302. ),
  303. effectiveFinalHeaders,
  304. effectiveFinalParams,
  305. effectiveFinalBody,
  306. effectiveFinalVars,
  307. }
  308. }
  309. /**
  310. * Creates an Observable Stream that emits HoppRESTRequests whenever
  311. * the input streams emit a value
  312. *
  313. * @param request$ The request stream containing request data
  314. * @param environment$ The environment stream containing environment data to apply
  315. *
  316. * @returns Observable Stream for the Effective Request Object
  317. */
  318. export function getEffectiveRESTRequestStream(
  319. request$: Observable<HoppRESTRequest>,
  320. environment$: Observable<Environment>
  321. ): Observable<EffectiveHoppRESTRequest> {
  322. return combineLatest([request$, environment$]).pipe(
  323. map(([request, env]) => getEffectiveRESTRequest(request, env))
  324. )
  325. }