EffectiveURL.ts 9.2 KB

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