httpclient.nim 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361
  1. #
  2. #
  3. # Nim's Runtime Library
  4. # (c) Copyright 2019 Nim Contributors
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. ## This module implements a simple HTTP client that can be used to retrieve
  10. ## webpages and other data.
  11. ##
  12. ## .. warning:: Validate untrusted inputs: URI parsers and getters are not detecting malicious URIs.
  13. ##
  14. ## Retrieving a website
  15. ## ====================
  16. ##
  17. ## This example uses HTTP GET to retrieve
  18. ## `http://google.com`:
  19. ##
  20. ## ```Nim
  21. ## import std/httpclient
  22. ## var client = newHttpClient()
  23. ## try:
  24. ## echo client.getContent("http://google.com")
  25. ## finally:
  26. ## client.close()
  27. ## ```
  28. ##
  29. ## The same action can also be performed asynchronously, simply use the
  30. ## `AsyncHttpClient`:
  31. ##
  32. ## ```Nim
  33. ## import std/[asyncdispatch, httpclient]
  34. ##
  35. ## proc asyncProc(): Future[string] {.async.} =
  36. ## var client = newAsyncHttpClient()
  37. ## try:
  38. ## return await client.getContent("http://google.com")
  39. ## finally:
  40. ## client.close()
  41. ##
  42. ## echo waitFor asyncProc()
  43. ## ```
  44. ##
  45. ## The functionality implemented by `HttpClient` and `AsyncHttpClient`
  46. ## is the same, so you can use whichever one suits you best in the examples
  47. ## shown here.
  48. ##
  49. ## **Note:** You need to run asynchronous examples in an async proc
  50. ## otherwise you will get an `Undeclared identifier: 'await'` error.
  51. ##
  52. ## **Note:** An asynchronous client instance can only deal with one
  53. ## request at a time. To send multiple requests in parallel, use
  54. ## multiple client instances.
  55. ##
  56. ## Using HTTP POST
  57. ## ===============
  58. ##
  59. ## This example demonstrates the usage of the W3 HTML Validator, it
  60. ## uses `multipart/form-data` as the `Content-Type` to send the HTML to be
  61. ## validated to the server.
  62. ##
  63. ## ```Nim
  64. ## var client = newHttpClient()
  65. ## var data = newMultipartData()
  66. ## data["output"] = "soap12"
  67. ## data["uploaded_file"] = ("test.html", "text/html",
  68. ## "<html><head></head><body><p>test</p></body></html>")
  69. ## try:
  70. ## echo client.postContent("http://validator.w3.org/check", multipart=data)
  71. ## finally:
  72. ## client.close()
  73. ## ```
  74. ##
  75. ## To stream files from disk when performing the request, use `addFiles`.
  76. ##
  77. ## **Note:** This will allocate a new `Mimetypes` database every time you call
  78. ## it, you can pass your own via the `mimeDb` parameter to avoid this.
  79. ##
  80. ## ```Nim
  81. ## let mimes = newMimetypes()
  82. ## var client = newHttpClient()
  83. ## var data = newMultipartData()
  84. ## data.addFiles({"uploaded_file": "test.html"}, mimeDb = mimes)
  85. ## try:
  86. ## echo client.postContent("http://validator.w3.org/check", multipart=data)
  87. ## finally:
  88. ## client.close()
  89. ## ```
  90. ##
  91. ## You can also make post requests with custom headers.
  92. ## This example sets `Content-Type` to `application/json`
  93. ## and uses a json object for the body
  94. ##
  95. ## ```Nim
  96. ## import std/[httpclient, json]
  97. ##
  98. ## let client = newHttpClient()
  99. ## client.headers = newHttpHeaders({ "Content-Type": "application/json" })
  100. ## let body = %*{
  101. ## "data": "some text"
  102. ## }
  103. ## try:
  104. ## let response = client.request("http://some.api", httpMethod = HttpPost, body = $body)
  105. ## echo response.status
  106. ## finally:
  107. ## client.close()
  108. ## ```
  109. ##
  110. ## Progress reporting
  111. ## ==================
  112. ##
  113. ## You may specify a callback procedure to be called during an HTTP request.
  114. ## This callback will be executed every second with information about the
  115. ## progress of the HTTP request.
  116. ##
  117. ## ```Nim
  118. ## import std/[asyncdispatch, httpclient]
  119. ##
  120. ## proc onProgressChanged(total, progress, speed: BiggestInt) {.async.} =
  121. ## echo("Downloaded ", progress, " of ", total)
  122. ## echo("Current rate: ", speed div 1000, "kb/s")
  123. ##
  124. ## proc asyncProc() {.async.} =
  125. ## var client = newAsyncHttpClient()
  126. ## client.onProgressChanged = onProgressChanged
  127. ## try:
  128. ## discard await client.getContent("http://speedtest-ams2.digitalocean.com/100mb.test")
  129. ## finally:
  130. ## client.close()
  131. ##
  132. ## waitFor asyncProc()
  133. ## ```
  134. ##
  135. ## If you would like to remove the callback simply set it to `nil`.
  136. ##
  137. ## ```Nim
  138. ## client.onProgressChanged = nil
  139. ## ```
  140. ##
  141. ## .. warning:: The `total` reported by httpclient may be 0 in some cases.
  142. ##
  143. ##
  144. ## SSL/TLS support
  145. ## ===============
  146. ## This requires the OpenSSL library. Fortunately it's widely used and installed
  147. ## on many operating systems. httpclient will use SSL automatically if you give
  148. ## any of the functions a url with the `https` schema, for example:
  149. ## `https://github.com/`.
  150. ##
  151. ## You will also have to compile with `ssl` defined like so:
  152. ## `nim c -d:ssl ...`.
  153. ##
  154. ## Certificate validation is performed by default.
  155. ##
  156. ## A set of directories and files from the `ssl_certs <ssl_certs.html>`_
  157. ## module are scanned to locate CA certificates.
  158. ##
  159. ## Example of setting SSL verification parameters in a new client:
  160. ##
  161. ## ```Nim
  162. ## import httpclient
  163. ## var client = newHttpClient(sslContext=newContext(verifyMode=CVerifyPeer))
  164. ## ```
  165. ##
  166. ## There are three options for verify mode:
  167. ##
  168. ## * ``CVerifyNone``: certificates are not verified;
  169. ## * ``CVerifyPeer``: certificates are verified;
  170. ## * ``CVerifyPeerUseEnvVars``: certificates are verified and the optional
  171. ## environment variables SSL_CERT_FILE and SSL_CERT_DIR are also used to
  172. ## locate certificates
  173. ##
  174. ## See `newContext <net.html#newContext.string,string,string,string>`_ to tweak or disable certificate validation.
  175. ##
  176. ## Timeouts
  177. ## ========
  178. ##
  179. ## Currently only the synchronous functions support a timeout.
  180. ## The timeout is
  181. ## measured in milliseconds, once it is set any call on a socket which may
  182. ## block will be susceptible to this timeout.
  183. ##
  184. ## It may be surprising but the
  185. ## function as a whole can take longer than the specified timeout, only
  186. ## individual internal calls on the socket are affected. In practice this means
  187. ## that as long as the server is sending data an exception will not be raised,
  188. ## if however data does not reach the client within the specified timeout a
  189. ## `TimeoutError` exception will be raised.
  190. ##
  191. ## Here is how to set a timeout when creating an `HttpClient` instance:
  192. ##
  193. ## ```Nim
  194. ## import std/httpclient
  195. ##
  196. ## let client = newHttpClient(timeout = 42)
  197. ## ```
  198. ##
  199. ## Proxy
  200. ## =====
  201. ##
  202. ## A proxy can be specified as a param to any of the procedures defined in
  203. ## this module. To do this, use the `newProxy` constructor. Unfortunately,
  204. ## only basic authentication is supported at the moment.
  205. ##
  206. ## Some examples on how to configure a Proxy for `HttpClient`:
  207. ##
  208. ## ```Nim
  209. ## import std/httpclient
  210. ##
  211. ## let myProxy = newProxy("http://myproxy.network")
  212. ## let client = newHttpClient(proxy = myProxy)
  213. ## ```
  214. ##
  215. ## Use proxies with basic authentication:
  216. ##
  217. ## ```Nim
  218. ## import std/httpclient
  219. ##
  220. ## let myProxy = newProxy("http://myproxy.network", auth="user:password")
  221. ## let client = newHttpClient(proxy = myProxy)
  222. ## ```
  223. ##
  224. ## Get Proxy URL from environment variables:
  225. ##
  226. ## ```Nim
  227. ## import std/httpclient
  228. ##
  229. ## var url = ""
  230. ## try:
  231. ## if existsEnv("http_proxy"):
  232. ## url = getEnv("http_proxy")
  233. ## elif existsEnv("https_proxy"):
  234. ## url = getEnv("https_proxy")
  235. ## except ValueError:
  236. ## echo "Unable to parse proxy from environment variables."
  237. ##
  238. ## let myProxy = newProxy(url = url)
  239. ## let client = newHttpClient(proxy = myProxy)
  240. ## ```
  241. ##
  242. ## Redirects
  243. ## =========
  244. ##
  245. ## The maximum redirects can be set with the `maxRedirects` of `int` type,
  246. ## it specifies the maximum amount of redirects to follow,
  247. ## it defaults to `5`, you can set it to `0` to disable redirects.
  248. ##
  249. ## Here you can see an example about how to set the `maxRedirects` of `HttpClient`:
  250. ##
  251. ## ```Nim
  252. ## import std/httpclient
  253. ##
  254. ## let client = newHttpClient(maxRedirects = 0)
  255. ## ```
  256. ##
  257. import std/private/since
  258. import std/[
  259. net, strutils, uri, parseutils, base64, os, mimetypes,
  260. math, random, httpcore, times, tables, streams, monotimes,
  261. asyncnet, asyncdispatch, asyncfile, nativesockets,
  262. ]
  263. when defined(nimPreviewSlimSystem):
  264. import std/[assertions, syncio]
  265. export httpcore except parseHeader # TODO: The `except` doesn't work
  266. type
  267. Response* = ref object
  268. version*: string
  269. status*: string
  270. headers*: HttpHeaders
  271. body: string
  272. bodyStream*: Stream
  273. AsyncResponse* = ref object
  274. version*: string
  275. status*: string
  276. headers*: HttpHeaders
  277. body: string
  278. bodyStream*: FutureStream[string]
  279. proc code*(response: Response | AsyncResponse): HttpCode
  280. {.raises: [ValueError, OverflowDefect].} =
  281. ## Retrieves the specified response's `HttpCode`.
  282. ##
  283. ## Raises a `ValueError` if the response's `status` does not have a
  284. ## corresponding `HttpCode`.
  285. return response.status[0 .. 2].parseInt.HttpCode
  286. proc contentType*(response: Response | AsyncResponse): string {.inline.} =
  287. ## Retrieves the specified response's content type.
  288. ##
  289. ## This is effectively the value of the "Content-Type" header.
  290. response.headers.getOrDefault("content-type")
  291. proc contentLength*(response: Response | AsyncResponse): int =
  292. ## Retrieves the specified response's content length.
  293. ##
  294. ## This is effectively the value of the "Content-Length" header.
  295. ##
  296. ## A `ValueError` exception will be raised if the value is not an integer.
  297. ## If the Content-Length header is not set in the response, ContentLength is set to the value -1.
  298. var contentLengthHeader = response.headers.getOrDefault("Content-Length", HttpHeaderValues(@["-1"]))
  299. result = contentLengthHeader.parseInt()
  300. proc lastModified*(response: Response | AsyncResponse): DateTime =
  301. ## Retrieves the specified response's last modified time.
  302. ##
  303. ## This is effectively the value of the "Last-Modified" header.
  304. ##
  305. ## Raises a `ValueError` if the parsing fails or the value is not a correctly
  306. ## formatted time.
  307. var lastModifiedHeader = response.headers.getOrDefault("last-modified")
  308. result = parse(lastModifiedHeader, "ddd, dd MMM yyyy HH:mm:ss 'GMT'", utc())
  309. proc body*(response: Response): string =
  310. ## Retrieves the specified response's body.
  311. ##
  312. ## The response's body stream is read synchronously.
  313. if response.body.len == 0:
  314. response.body = response.bodyStream.readAll()
  315. return response.body
  316. proc body*(response: AsyncResponse): Future[string] {.async.} =
  317. ## Reads the response's body and caches it. The read is performed only
  318. ## once.
  319. if response.body.len == 0:
  320. response.body = await readAll(response.bodyStream)
  321. return response.body
  322. type
  323. Proxy* = ref object
  324. url*: Uri
  325. auth*: string
  326. MultipartEntry = object
  327. name, content: string
  328. case isFile: bool
  329. of true:
  330. filename, contentType: string
  331. fileSize: int64
  332. isStream: bool
  333. else: discard
  334. MultipartEntries* = openArray[tuple[name, content: string]]
  335. MultipartData* = ref object
  336. content: seq[MultipartEntry]
  337. ProtocolError* = object of IOError ## exception that is raised when server
  338. ## does not conform to the implemented
  339. ## protocol
  340. HttpRequestError* = object of IOError ## Thrown in the `getContent` proc
  341. ## and `postContent` proc,
  342. ## when the server returns an error
  343. const defUserAgent* = "Nim-httpclient/" & NimVersion
  344. proc httpError(msg: string) =
  345. var e: ref ProtocolError
  346. new(e)
  347. e.msg = msg
  348. raise e
  349. proc fileError(msg: string) =
  350. var e: ref IOError
  351. new(e)
  352. e.msg = msg
  353. raise e
  354. when not defined(ssl):
  355. type SslContext = ref object
  356. var defaultSslContext {.threadvar.}: SslContext
  357. proc getDefaultSSL(): SslContext =
  358. result = defaultSslContext
  359. when defined(ssl):
  360. if result == nil:
  361. defaultSslContext = newContext(verifyMode = CVerifyPeer)
  362. result = defaultSslContext
  363. doAssert result != nil, "failure to initialize the SSL context"
  364. proc newProxy*(url: string; auth = ""): Proxy =
  365. ## Constructs a new `TProxy` object.
  366. result = Proxy(url: parseUri(url), auth: auth)
  367. proc newProxy*(url: Uri; auth = ""): Proxy =
  368. ## Constructs a new `TProxy` object.
  369. result = Proxy(url: url, auth: auth)
  370. proc newMultipartData*: MultipartData {.inline.} =
  371. ## Constructs a new `MultipartData` object.
  372. MultipartData()
  373. proc `$`*(data: MultipartData): string {.since: (1, 1).} =
  374. ## convert MultipartData to string so it's human readable when echo
  375. ## see https://github.com/nim-lang/Nim/issues/11863
  376. const sep = "-".repeat(30)
  377. for pos, entry in data.content:
  378. result.add(sep & center($pos, 3) & sep)
  379. result.add("\nname=\"" & entry.name & "\"")
  380. if entry.isFile:
  381. result.add("; filename=\"" & entry.filename & "\"\n")
  382. result.add("Content-Type: " & entry.contentType)
  383. result.add("\n\n" & entry.content & "\n")
  384. proc add*(p: MultipartData, name, content: string, filename: string = "",
  385. contentType: string = "", useStream = true) =
  386. ## Add a value to the multipart data.
  387. ##
  388. ## When `useStream` is `false`, the file will be read into memory.
  389. ##
  390. ## Raises a `ValueError` exception if
  391. ## `name`, `filename` or `contentType` contain newline characters.
  392. if {'\c', '\L'} in name:
  393. raise newException(ValueError, "name contains a newline character")
  394. if {'\c', '\L'} in filename:
  395. raise newException(ValueError, "filename contains a newline character")
  396. if {'\c', '\L'} in contentType:
  397. raise newException(ValueError, "contentType contains a newline character")
  398. var entry = MultipartEntry(
  399. name: name,
  400. content: content,
  401. isFile: filename.len > 0
  402. )
  403. if entry.isFile:
  404. entry.isStream = useStream
  405. entry.filename = filename
  406. entry.contentType = contentType
  407. p.content.add(entry)
  408. proc add*(p: MultipartData, xs: MultipartEntries): MultipartData
  409. {.discardable.} =
  410. ## Add a list of multipart entries to the multipart data `p`. All values are
  411. ## added without a filename and without a content type.
  412. ##
  413. ## ```Nim
  414. ## data.add({"action": "login", "format": "json"})
  415. ## ```
  416. for name, content in xs.items:
  417. p.add(name, content)
  418. result = p
  419. proc newMultipartData*(xs: MultipartEntries): MultipartData =
  420. ## Create a new multipart data object and fill it with the entries `xs`
  421. ## directly.
  422. ##
  423. ## ```Nim
  424. ## var data = newMultipartData({"action": "login", "format": "json"})
  425. ## ```
  426. result = MultipartData()
  427. for entry in xs:
  428. result.add(entry.name, entry.content)
  429. proc addFiles*(p: MultipartData, xs: openArray[tuple[name, file: string]],
  430. mimeDb = newMimetypes(), useStream = true):
  431. MultipartData {.discardable.} =
  432. ## Add files to a multipart data object. The files will be streamed from disk
  433. ## when the request is being made. When `stream` is `false`, the files are
  434. ## instead read into memory, but beware this is very memory ineffecient even
  435. ## for small files. The MIME types will automatically be determined.
  436. ## Raises an `IOError` if the file cannot be opened or reading fails. To
  437. ## manually specify file content, filename and MIME type, use `[]=` instead.
  438. ##
  439. ## ```Nim
  440. ## data.addFiles({"uploaded_file": "public/test.html"})
  441. ## ```
  442. for name, file in xs.items:
  443. var contentType: string
  444. let (_, fName, ext) = splitFile(file)
  445. if ext.len > 0:
  446. contentType = mimeDb.getMimetype(ext[1..ext.high], "")
  447. let content = if useStream: file else: readFile(file)
  448. p.add(name, content, fName & ext, contentType, useStream = useStream)
  449. result = p
  450. proc `[]=`*(p: MultipartData, name, content: string) {.inline.} =
  451. ## Add a multipart entry to the multipart data `p`. The value is added
  452. ## without a filename and without a content type.
  453. ##
  454. ## ```Nim
  455. ## data["username"] = "NimUser"
  456. ## ```
  457. p.add(name, content)
  458. proc `[]=`*(p: MultipartData, name: string,
  459. file: tuple[name, contentType, content: string]) {.inline.} =
  460. ## Add a file to the multipart data `p`, specifying filename, contentType
  461. ## and content manually.
  462. ##
  463. ## ```Nim
  464. ## data["uploaded_file"] = ("test.html", "text/html",
  465. ## "<html><head></head><body><p>test</p></body></html>")
  466. ## ```
  467. p.add(name, file.content, file.name, file.contentType, useStream = false)
  468. proc getBoundary(p: MultipartData): string =
  469. if p == nil or p.content.len == 0: return
  470. while true:
  471. result = $rand(int.high)
  472. for i, entry in p.content:
  473. if result in entry.content: break
  474. elif i == p.content.high: return
  475. proc sendFile(socket: Socket | AsyncSocket,
  476. entry: MultipartEntry) {.multisync.} =
  477. const chunkSize = 2^18
  478. let file =
  479. when socket is AsyncSocket: openAsync(entry.content)
  480. else: newFileStream(entry.content, fmRead)
  481. var buffer: string
  482. while true:
  483. buffer =
  484. when socket is AsyncSocket: (await read(file, chunkSize))
  485. else: readStr(file, chunkSize)
  486. if buffer.len == 0: break
  487. await socket.send(buffer)
  488. file.close()
  489. proc getNewLocation(lastURL: Uri, headers: HttpHeaders): Uri =
  490. let newLocation = headers.getOrDefault"Location"
  491. if newLocation == "": httpError("location header expected")
  492. # Relative URLs. (Not part of the spec, but soon will be.)
  493. let parsedLocation = parseUri(newLocation)
  494. if parsedLocation.hostname == "" and parsedLocation.path != "":
  495. result = lastURL
  496. result.path = parsedLocation.path
  497. result.query = parsedLocation.query
  498. result.anchor = parsedLocation.anchor
  499. else:
  500. result = parsedLocation
  501. proc generateHeaders(requestUrl: Uri, httpMethod: HttpMethod, headers: HttpHeaders,
  502. proxy: Proxy): string =
  503. # GET
  504. result = $httpMethod
  505. result.add ' '
  506. if proxy.isNil or requestUrl.scheme == "https":
  507. # /path?query
  508. if not requestUrl.path.startsWith("/"): result.add '/'
  509. result.add(requestUrl.path)
  510. if requestUrl.query.len > 0:
  511. result.add("?" & requestUrl.query)
  512. else:
  513. # Remove the 'http://' from the URL for CONNECT requests for TLS connections.
  514. var modifiedUrl = requestUrl
  515. if requestUrl.scheme == "https": modifiedUrl.scheme = ""
  516. result.add($modifiedUrl)
  517. # HTTP/1.1\c\l
  518. result.add(" HTTP/1.1" & httpNewLine)
  519. # Host header.
  520. if not headers.hasKey("Host"):
  521. if requestUrl.port == "":
  522. add(result, "Host: " & requestUrl.hostname & httpNewLine)
  523. else:
  524. add(result, "Host: " & requestUrl.hostname & ":" & requestUrl.port & httpNewLine)
  525. # Connection header.
  526. if not headers.hasKey("Connection"):
  527. add(result, "Connection: Keep-Alive" & httpNewLine)
  528. # Proxy auth header.
  529. if not proxy.isNil and proxy.auth != "":
  530. let auth = base64.encode(proxy.auth)
  531. add(result, "Proxy-Authorization: Basic " & auth & httpNewLine)
  532. for key, val in headers:
  533. add(result, key & ": " & val & httpNewLine)
  534. add(result, httpNewLine)
  535. type
  536. ProgressChangedProc*[ReturnType] =
  537. proc (total, progress, speed: BiggestInt):
  538. ReturnType {.closure, gcsafe.}
  539. HttpClientBase*[SocketType] = ref object
  540. socket: SocketType
  541. connected: bool
  542. currentURL: Uri ## Where we are currently connected.
  543. headers*: HttpHeaders ## Headers to send in requests.
  544. maxRedirects: Natural ## Maximum redirects, set to `0` to disable.
  545. userAgent: string
  546. timeout*: int ## Only used for blocking HttpClient for now.
  547. proxy: Proxy
  548. ## `nil` or the callback to call when request progress changes.
  549. when SocketType is Socket:
  550. onProgressChanged*: ProgressChangedProc[void]
  551. else:
  552. onProgressChanged*: ProgressChangedProc[Future[void]]
  553. when defined(ssl):
  554. sslContext: net.SslContext
  555. contentTotal: BiggestInt
  556. contentProgress: BiggestInt
  557. oneSecondProgress: BiggestInt
  558. lastProgressReport: MonoTime
  559. when SocketType is AsyncSocket:
  560. bodyStream: FutureStream[string]
  561. parseBodyFut: Future[void]
  562. else:
  563. bodyStream: Stream
  564. getBody: bool ## When `false`, the body is never read in requestAux.
  565. type
  566. HttpClient* = HttpClientBase[Socket]
  567. proc newHttpClient*(userAgent = defUserAgent, maxRedirects = 5,
  568. sslContext = getDefaultSSL(), proxy: Proxy = nil,
  569. timeout = -1, headers = newHttpHeaders()): HttpClient =
  570. ## Creates a new HttpClient instance.
  571. ##
  572. ## `userAgent` specifies the user agent that will be used when making
  573. ## requests.
  574. ##
  575. ## `maxRedirects` specifies the maximum amount of redirects to follow,
  576. ## default is 5.
  577. ##
  578. ## `sslContext` specifies the SSL context to use for HTTPS requests.
  579. ## See `SSL/TLS support <#sslslashtls-support>`_
  580. ##
  581. ## `proxy` specifies an HTTP proxy to use for this HTTP client's
  582. ## connections.
  583. ##
  584. ## `timeout` specifies the number of milliseconds to allow before a
  585. ## `TimeoutError` is raised.
  586. ##
  587. ## `headers` specifies the HTTP Headers.
  588. runnableExamples:
  589. import std/strutils
  590. let exampleHtml = newHttpClient().getContent("http://example.com")
  591. assert "Example Domain" in exampleHtml
  592. assert "Pizza" notin exampleHtml
  593. new result
  594. result.headers = headers
  595. result.userAgent = userAgent
  596. result.maxRedirects = maxRedirects
  597. result.proxy = proxy
  598. result.timeout = timeout
  599. result.onProgressChanged = nil
  600. result.bodyStream = newStringStream()
  601. result.getBody = true
  602. when defined(ssl):
  603. result.sslContext = sslContext
  604. type
  605. AsyncHttpClient* = HttpClientBase[AsyncSocket]
  606. proc newAsyncHttpClient*(userAgent = defUserAgent, maxRedirects = 5,
  607. sslContext = getDefaultSSL(), proxy: Proxy = nil,
  608. headers = newHttpHeaders()): AsyncHttpClient =
  609. ## Creates a new AsyncHttpClient instance.
  610. ##
  611. ## `userAgent` specifies the user agent that will be used when making
  612. ## requests.
  613. ##
  614. ## `maxRedirects` specifies the maximum amount of redirects to follow,
  615. ## default is 5.
  616. ##
  617. ## `sslContext` specifies the SSL context to use for HTTPS requests.
  618. ##
  619. ## `proxy` specifies an HTTP proxy to use for this HTTP client's
  620. ## connections.
  621. ##
  622. ## `headers` specifies the HTTP Headers.
  623. runnableExamples:
  624. import std/[asyncdispatch, strutils]
  625. proc asyncProc(): Future[string] {.async.} =
  626. let client = newAsyncHttpClient()
  627. result = await client.getContent("http://example.com")
  628. let exampleHtml = waitFor asyncProc()
  629. assert "Example Domain" in exampleHtml
  630. assert "Pizza" notin exampleHtml
  631. new result
  632. result.headers = headers
  633. result.userAgent = userAgent
  634. result.maxRedirects = maxRedirects
  635. result.proxy = proxy
  636. result.timeout = -1 # TODO
  637. result.onProgressChanged = nil
  638. result.bodyStream = newFutureStream[string]("newAsyncHttpClient")
  639. result.getBody = true
  640. when defined(ssl):
  641. result.sslContext = sslContext
  642. proc close*(client: HttpClient | AsyncHttpClient) =
  643. ## Closes any connections held by the HTTP client.
  644. if client.connected:
  645. client.socket.close()
  646. client.connected = false
  647. proc getSocket*(client: HttpClient): Socket {.inline.} =
  648. ## Get network socket, useful if you want to find out more details about the connection.
  649. ##
  650. ## This example shows info about local and remote endpoints:
  651. ##
  652. ## ```Nim
  653. ## if client.connected:
  654. ## echo client.getSocket.getLocalAddr
  655. ## echo client.getSocket.getPeerAddr
  656. ## ```
  657. return client.socket
  658. proc getSocket*(client: AsyncHttpClient): AsyncSocket {.inline.} =
  659. return client.socket
  660. proc reportProgress(client: HttpClient | AsyncHttpClient,
  661. progress: BiggestInt) {.multisync.} =
  662. client.contentProgress += progress
  663. client.oneSecondProgress += progress
  664. if (getMonoTime() - client.lastProgressReport).inSeconds >= 1:
  665. if not client.onProgressChanged.isNil:
  666. await client.onProgressChanged(client.contentTotal,
  667. client.contentProgress,
  668. client.oneSecondProgress)
  669. client.oneSecondProgress = 0
  670. client.lastProgressReport = getMonoTime()
  671. proc recvFull(client: HttpClient | AsyncHttpClient, size: int, timeout: int,
  672. keep: bool): Future[int] {.multisync.} =
  673. ## Ensures that all the data requested is read and returned.
  674. var readLen = 0
  675. while true:
  676. if size == readLen: break
  677. let remainingSize = size - readLen
  678. let sizeToRecv = min(remainingSize, net.BufferSize)
  679. when client.socket is Socket:
  680. let data = client.socket.recv(sizeToRecv, timeout)
  681. else:
  682. let data = await client.socket.recv(sizeToRecv)
  683. if data == "":
  684. client.close()
  685. break # We've been disconnected.
  686. readLen.inc(data.len)
  687. if keep:
  688. await client.bodyStream.write(data)
  689. await reportProgress(client, data.len)
  690. return readLen
  691. proc parseChunks(client: HttpClient | AsyncHttpClient): Future[void]
  692. {.multisync.} =
  693. while true:
  694. var chunkSize = 0
  695. var chunkSizeStr = await client.socket.recvLine()
  696. var i = 0
  697. if chunkSizeStr == "":
  698. httpError("Server terminated connection prematurely")
  699. while i < chunkSizeStr.len:
  700. case chunkSizeStr[i]
  701. of '0'..'9':
  702. chunkSize = chunkSize shl 4 or (ord(chunkSizeStr[i]) - ord('0'))
  703. of 'a'..'f':
  704. chunkSize = chunkSize shl 4 or (ord(chunkSizeStr[i]) - ord('a') + 10)
  705. of 'A'..'F':
  706. chunkSize = chunkSize shl 4 or (ord(chunkSizeStr[i]) - ord('A') + 10)
  707. of ';':
  708. # http://tools.ietf.org/html/rfc2616#section-3.6.1
  709. # We don't care about chunk-extensions.
  710. break
  711. else:
  712. httpError("Invalid chunk size: " & chunkSizeStr)
  713. inc(i)
  714. if chunkSize <= 0:
  715. discard await recvFull(client, 2, client.timeout, false) # Skip \c\L
  716. break
  717. var bytesRead = await recvFull(client, chunkSize, client.timeout, true)
  718. if bytesRead != chunkSize:
  719. httpError("Server terminated connection prematurely")
  720. bytesRead = await recvFull(client, 2, client.timeout, false) # Skip \c\L
  721. if bytesRead != 2:
  722. httpError("Server terminated connection prematurely")
  723. # Trailer headers will only be sent if the request specifies that we want
  724. # them: http://tools.ietf.org/html/rfc2616#section-3.6.1
  725. proc parseBody(client: HttpClient | AsyncHttpClient, headers: HttpHeaders,
  726. httpVersion: string): Future[void] {.multisync.} =
  727. # Reset progress from previous requests.
  728. client.contentTotal = 0
  729. client.contentProgress = 0
  730. client.oneSecondProgress = 0
  731. client.lastProgressReport = MonoTime()
  732. when client is AsyncHttpClient:
  733. assert(not client.bodyStream.finished)
  734. if headers.getOrDefault"Transfer-Encoding" == "chunked":
  735. await parseChunks(client)
  736. else:
  737. # -REGION- Content-Length
  738. # (http://tools.ietf.org/html/rfc2616#section-4.4) NR.3
  739. var contentLengthHeader = headers.getOrDefault"Content-Length"
  740. if contentLengthHeader != "":
  741. var length = contentLengthHeader.parseInt()
  742. client.contentTotal = length
  743. if length > 0:
  744. let recvLen = await client.recvFull(length, client.timeout, true)
  745. if recvLen == 0:
  746. client.close()
  747. httpError("Got disconnected while trying to read body.")
  748. if recvLen != length:
  749. httpError("Received length doesn't match expected length. Wanted " &
  750. $length & " got: " & $recvLen)
  751. else:
  752. # (http://tools.ietf.org/html/rfc2616#section-4.4) NR.4 TODO
  753. # -REGION- Connection: Close
  754. # (http://tools.ietf.org/html/rfc2616#section-4.4) NR.5
  755. let implicitConnectionClose =
  756. httpVersion == "1.0" or
  757. # This doesn't match the HTTP spec, but it fixes issues for non-conforming servers.
  758. (httpVersion == "1.1" and headers.getOrDefault"Connection" == "")
  759. if headers.getOrDefault"Connection" == "close" or implicitConnectionClose:
  760. while true:
  761. let recvLen = await client.recvFull(4000, client.timeout, true)
  762. if recvLen != 4000:
  763. client.close()
  764. break
  765. when client is AsyncHttpClient:
  766. client.bodyStream.complete()
  767. else:
  768. client.bodyStream.setPosition(0)
  769. # If the server will close our connection, then no matter the method of
  770. # reading the body, we need to close our socket.
  771. if headers.getOrDefault"Connection" == "close":
  772. client.close()
  773. proc parseResponse(client: HttpClient | AsyncHttpClient,
  774. getBody: bool): Future[Response | AsyncResponse]
  775. {.multisync.} =
  776. new result
  777. var parsedStatus = false
  778. var linei = 0
  779. var fullyRead = false
  780. var lastHeaderName = ""
  781. var line = ""
  782. result.headers = newHttpHeaders()
  783. while true:
  784. linei = 0
  785. when client is HttpClient:
  786. line = await client.socket.recvLine(client.timeout)
  787. else:
  788. line = await client.socket.recvLine()
  789. if line == "":
  790. # We've been disconnected.
  791. client.close()
  792. break
  793. if line == httpNewLine:
  794. fullyRead = true
  795. break
  796. if not parsedStatus:
  797. # Parse HTTP version info and status code.
  798. var le = skipIgnoreCase(line, "HTTP/", linei)
  799. if le <= 0:
  800. httpError("invalid http version, `" & line & "`")
  801. inc(linei, le)
  802. le = skipIgnoreCase(line, "1.1", linei)
  803. if le > 0: result.version = "1.1"
  804. else:
  805. le = skipIgnoreCase(line, "1.0", linei)
  806. if le <= 0: httpError("unsupported http version")
  807. result.version = "1.0"
  808. inc(linei, le)
  809. # Status code
  810. linei.inc skipWhitespace(line, linei)
  811. result.status = line[linei .. ^1]
  812. parsedStatus = true
  813. else:
  814. # Parse headers
  815. # There's at least one char because empty lines are handled above (with client.close)
  816. if line[0] in {' ', '\t'}:
  817. # Check if it's a multiline header value, if so, append to the header we're currently parsing
  818. # This works because a line with a header must start with the header name without any leading space
  819. # See https://datatracker.ietf.org/doc/html/rfc7230, section 3.2 and 3.2.4
  820. # Multiline headers are deprecated in the spec, but it's better to parse them than crash
  821. if lastHeaderName == "":
  822. # Some extra unparsable lines in the HTTP output - we ignore them
  823. discard
  824. else:
  825. result.headers.table[result.headers.toCaseInsensitive(lastHeaderName)][^1].add "\n" & line
  826. else:
  827. var name = ""
  828. var le = parseUntil(line, name, ':', linei)
  829. if le <= 0: httpError("Invalid headers - received empty header name")
  830. if line.len == le: httpError("Invalid headers - no colon after header name")
  831. inc(linei, le) # Skip the parsed header name
  832. inc(linei) # Skip :
  833. # If we want to be HTTP spec compliant later, error on linei == line.len (for empty header value)
  834. lastHeaderName = name # Remember the header name for the possible multi-line header
  835. result.headers.add(name, line[linei .. ^1].strip())
  836. if result.headers.len > headerLimit:
  837. httpError("too many headers")
  838. if not fullyRead:
  839. httpError("Connection was closed before full request has been made")
  840. when client is HttpClient:
  841. result.bodyStream = newStringStream()
  842. else:
  843. result.bodyStream = newFutureStream[string]("parseResponse")
  844. if getBody and result.code != Http204:
  845. client.bodyStream = result.bodyStream
  846. when client is HttpClient:
  847. parseBody(client, result.headers, result.version)
  848. else:
  849. assert(client.parseBodyFut.isNil or client.parseBodyFut.finished)
  850. # do not wait here for the body request to complete
  851. client.parseBodyFut = parseBody(client, result.headers, result.version)
  852. client.parseBodyFut.addCallback do():
  853. if client.parseBodyFut.failed:
  854. client.bodyStream.fail(client.parseBodyFut.error)
  855. else:
  856. when client is AsyncHttpClient:
  857. result.bodyStream.complete()
  858. proc newConnection(client: HttpClient | AsyncHttpClient,
  859. url: Uri) {.multisync.} =
  860. if client.currentURL.hostname != url.hostname or
  861. client.currentURL.scheme != url.scheme or
  862. client.currentURL.port != url.port or
  863. (not client.connected):
  864. # Connect to proxy if specified
  865. let connectionUrl =
  866. if client.proxy.isNil: url else: client.proxy.url
  867. let isSsl = connectionUrl.scheme.toLowerAscii() == "https"
  868. if isSsl and not defined(ssl):
  869. raise newException(HttpRequestError,
  870. "SSL support is not available. Cannot connect over SSL. Compile with -d:ssl to enable.")
  871. if client.connected:
  872. client.close()
  873. client.connected = false
  874. # TODO: I should be able to write 'net.Port' here...
  875. let port =
  876. if connectionUrl.port == "":
  877. if isSsl:
  878. nativesockets.Port(443)
  879. else:
  880. nativesockets.Port(80)
  881. else: nativesockets.Port(connectionUrl.port.parseInt)
  882. when client is HttpClient:
  883. client.socket = await net.dial(connectionUrl.hostname, port)
  884. elif client is AsyncHttpClient:
  885. client.socket = await asyncnet.dial(connectionUrl.hostname, port)
  886. else: {.fatal: "Unsupported client type".}
  887. when defined(ssl):
  888. if isSsl:
  889. try:
  890. client.sslContext.wrapConnectedSocket(
  891. client.socket, handshakeAsClient, connectionUrl.hostname)
  892. except:
  893. client.socket.close()
  894. raise getCurrentException()
  895. # If need to CONNECT through proxy
  896. if url.scheme == "https" and not client.proxy.isNil:
  897. when defined(ssl):
  898. # Pass only host:port for CONNECT
  899. var connectUrl = initUri()
  900. connectUrl.hostname = url.hostname
  901. connectUrl.port = if url.port != "": url.port else: "443"
  902. let proxyHeaderString = generateHeaders(connectUrl, HttpConnect,
  903. newHttpHeaders(), client.proxy)
  904. await client.socket.send(proxyHeaderString)
  905. let proxyResp = await parseResponse(client, false)
  906. if not proxyResp.status.startsWith("200"):
  907. raise newException(HttpRequestError,
  908. "The proxy server rejected a CONNECT request, " &
  909. "so a secure connection could not be established.")
  910. client.sslContext.wrapConnectedSocket(
  911. client.socket, handshakeAsClient, url.hostname)
  912. else:
  913. raise newException(HttpRequestError,
  914. "SSL support is not available. Cannot connect over SSL. Compile with -d:ssl to enable.")
  915. # May be connected through proxy but remember actual URL being accessed
  916. client.currentURL = url
  917. client.connected = true
  918. proc readFileSizes(client: HttpClient | AsyncHttpClient,
  919. multipart: MultipartData) {.multisync.} =
  920. for entry in multipart.content.mitems():
  921. if not entry.isFile: continue
  922. if not entry.isStream:
  923. entry.fileSize = entry.content.len
  924. continue
  925. # TODO: look into making getFileSize work with async
  926. let fileSize = getFileSize(entry.content)
  927. entry.fileSize = fileSize
  928. proc format(entry: MultipartEntry, boundary: string): string =
  929. result = "--" & boundary & httpNewLine
  930. result.add("Content-Disposition: form-data; name=\"" & entry.name & "\"")
  931. if entry.isFile:
  932. result.add("; filename=\"" & entry.filename & "\"" & httpNewLine)
  933. result.add("Content-Type: " & entry.contentType & httpNewLine)
  934. else:
  935. result.add(httpNewLine & httpNewLine & entry.content)
  936. proc format(client: HttpClient | AsyncHttpClient,
  937. multipart: MultipartData): Future[seq[string]] {.multisync.} =
  938. let bound = getBoundary(multipart)
  939. client.headers["Content-Type"] = "multipart/form-data; boundary=" & bound
  940. await client.readFileSizes(multipart)
  941. var length: int64
  942. for entry in multipart.content:
  943. result.add(format(entry, bound) & httpNewLine)
  944. if entry.isFile:
  945. length += entry.fileSize + httpNewLine.len
  946. result.add "--" & bound & "--" & httpNewLine
  947. for s in result: length += s.len
  948. client.headers["Content-Length"] = $length
  949. proc override(fallback, override: HttpHeaders): HttpHeaders =
  950. # Right-biased map union for `HttpHeaders`
  951. result = newHttpHeaders()
  952. # Copy by value
  953. result.table[] = fallback.table[]
  954. if override.isNil:
  955. # Return the copy of fallback so it does not get modified
  956. return result
  957. for k, vs in override.table:
  958. result[k] = vs
  959. proc requestAux(client: HttpClient | AsyncHttpClient, url: Uri,
  960. httpMethod: HttpMethod, body = "", headers: HttpHeaders = nil,
  961. multipart: MultipartData = nil): Future[Response | AsyncResponse]
  962. {.multisync.} =
  963. # Helper that actually makes the request. Does not handle redirects.
  964. if url.scheme == "":
  965. raise newException(ValueError, "No uri scheme supplied.")
  966. when client is AsyncHttpClient:
  967. if not client.parseBodyFut.isNil:
  968. # let the current operation finish before making another request
  969. await client.parseBodyFut
  970. client.parseBodyFut = nil
  971. await newConnection(client, url)
  972. var newHeaders: HttpHeaders
  973. var data: seq[string]
  974. if multipart != nil and multipart.content.len > 0:
  975. # `format` modifies `client.headers`, see
  976. # https://github.com/nim-lang/Nim/pull/18208#discussion_r647036979
  977. data = await client.format(multipart)
  978. newHeaders = client.headers.override(headers)
  979. else:
  980. newHeaders = client.headers.override(headers)
  981. # Only change headers if they have not been specified already
  982. if not newHeaders.hasKey("Content-Length"):
  983. if body.len != 0:
  984. newHeaders["Content-Length"] = $body.len
  985. elif httpMethod notin {HttpGet, HttpHead}:
  986. newHeaders["Content-Length"] = "0"
  987. if not newHeaders.hasKey("user-agent") and client.userAgent.len > 0:
  988. newHeaders["User-Agent"] = client.userAgent
  989. let headerString = generateHeaders(url, httpMethod, newHeaders,
  990. client.proxy)
  991. await client.socket.send(headerString)
  992. if data.len > 0:
  993. var buffer: string
  994. for i, entry in multipart.content:
  995. buffer.add data[i]
  996. if not entry.isFile: continue
  997. if buffer.len > 0:
  998. await client.socket.send(buffer)
  999. buffer.setLen(0)
  1000. if entry.isStream:
  1001. await client.socket.sendFile(entry)
  1002. else:
  1003. await client.socket.send(entry.content)
  1004. buffer.add httpNewLine
  1005. # send the rest and the last boundary
  1006. await client.socket.send(buffer & data[^1])
  1007. elif body.len > 0:
  1008. await client.socket.send(body)
  1009. let getBody = httpMethod notin {HttpHead, HttpConnect} and
  1010. client.getBody
  1011. result = await parseResponse(client, getBody)
  1012. proc request*(client: HttpClient | AsyncHttpClient, url: Uri | string,
  1013. httpMethod: HttpMethod | string = HttpGet, body = "",
  1014. headers: HttpHeaders = nil,
  1015. multipart: MultipartData = nil): Future[Response | AsyncResponse]
  1016. {.multisync.} =
  1017. ## Connects to the hostname specified by the URL and performs a request
  1018. ## using the custom method string specified by `httpMethod`.
  1019. ##
  1020. ## Connection will be kept alive. Further requests on the same `client` to
  1021. ## the same hostname will not require a new connection to be made. The
  1022. ## connection can be closed by using the `close` procedure.
  1023. ##
  1024. ## This procedure will follow redirects up to a maximum number of redirects
  1025. ## specified in `client.maxRedirects`.
  1026. ##
  1027. ## You need to make sure that the `url` doesn't contain any newline
  1028. ## characters. Failing to do so will raise `AssertionDefect`.
  1029. ##
  1030. ## `headers` are HTTP headers that override the `client.headers` for
  1031. ## this specific request only and will not be persisted.
  1032. ##
  1033. ## **Deprecated since v1.5**: use HttpMethod enum instead; string parameter httpMethod is deprecated
  1034. when url is string:
  1035. doAssert(not url.contains({'\c', '\L'}), "url shouldn't contain any newline characters")
  1036. let url = parseUri(url)
  1037. when httpMethod is string:
  1038. {.warning:
  1039. "Deprecated since v1.5; use HttpMethod enum instead; string parameter httpMethod is deprecated".}
  1040. let httpMethod = case httpMethod
  1041. of "HEAD":
  1042. HttpHead
  1043. of "GET":
  1044. HttpGet
  1045. of "POST":
  1046. HttpPost
  1047. of "PUT":
  1048. HttpPut
  1049. of "DELETE":
  1050. HttpDelete
  1051. of "TRACE":
  1052. HttpTrace
  1053. of "OPTIONS":
  1054. HttpOptions
  1055. of "CONNECT":
  1056. HttpConnect
  1057. of "PATCH":
  1058. HttpPatch
  1059. else:
  1060. raise newException(ValueError, "Invalid HTTP method name: " & httpMethod)
  1061. result = await client.requestAux(url, httpMethod, body, headers, multipart)
  1062. var lastURL = url
  1063. for i in 1..client.maxRedirects:
  1064. let statusCode = result.code
  1065. if statusCode notin {Http301, Http302, Http303, Http307, Http308}:
  1066. break
  1067. let redirectTo = getNewLocation(lastURL, result.headers)
  1068. var redirectMethod: HttpMethod
  1069. var redirectBody: string
  1070. # For more informations about the redirect methods see:
  1071. # https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections
  1072. case statusCode
  1073. of Http301, Http302, Http303:
  1074. # The method is changed to GET unless it is GET or HEAD (RFC2616)
  1075. if httpMethod notin {HttpGet, HttpHead}:
  1076. redirectMethod = HttpGet
  1077. else:
  1078. redirectMethod = httpMethod
  1079. # The body is stripped away
  1080. redirectBody = ""
  1081. # Delete any header value associated with the body
  1082. if not headers.isNil():
  1083. headers.del("Content-Length")
  1084. headers.del("Content-Type")
  1085. headers.del("Transfer-Encoding")
  1086. of Http307, Http308:
  1087. # The method and the body are unchanged
  1088. redirectMethod = httpMethod
  1089. redirectBody = body
  1090. else:
  1091. # Unreachable
  1092. doAssert(false)
  1093. # Check if the redirection is to the same domain or a sub-domain (foo.com
  1094. # -> sub.foo.com)
  1095. if redirectTo.hostname != lastURL.hostname and
  1096. not redirectTo.hostname.endsWith("." & lastURL.hostname):
  1097. # Perform some cleanup of the header values
  1098. if headers != nil:
  1099. # Delete the Host header
  1100. headers.del("Host")
  1101. # Do not send any sensitive info to a unknown host
  1102. headers.del("Authorization")
  1103. result = await client.requestAux(redirectTo, redirectMethod, redirectBody,
  1104. headers, multipart)
  1105. lastURL = redirectTo
  1106. proc responseContent(resp: Response | AsyncResponse): Future[string] {.multisync.} =
  1107. ## Returns the content of a response as a string.
  1108. ##
  1109. ## A `HttpRequestError` will be raised if the server responds with a
  1110. ## client error (status code 4xx) or a server error (status code 5xx).
  1111. if resp.code.is4xx or resp.code.is5xx:
  1112. raise newException(HttpRequestError, resp.status.move)
  1113. else:
  1114. return await resp.bodyStream.readAll()
  1115. proc head*(client: HttpClient | AsyncHttpClient,
  1116. url: Uri | string): Future[Response | AsyncResponse] {.multisync.} =
  1117. ## Connects to the hostname specified by the URL and performs a HEAD request.
  1118. ##
  1119. ## This procedure uses httpClient values such as `client.maxRedirects`.
  1120. result = await client.request(url, HttpHead)
  1121. proc get*(client: HttpClient | AsyncHttpClient,
  1122. url: Uri | string): Future[Response | AsyncResponse] {.multisync.} =
  1123. ## Connects to the hostname specified by the URL and performs a GET request.
  1124. ##
  1125. ## This procedure uses httpClient values such as `client.maxRedirects`.
  1126. result = await client.request(url, HttpGet)
  1127. proc getContent*(client: HttpClient | AsyncHttpClient,
  1128. url: Uri | string): Future[string] {.multisync.} =
  1129. ## Connects to the hostname specified by the URL and returns the content of a GET request.
  1130. let resp = await get(client, url)
  1131. return await responseContent(resp)
  1132. proc delete*(client: HttpClient | AsyncHttpClient,
  1133. url: Uri | string): Future[Response | AsyncResponse] {.multisync.} =
  1134. ## Connects to the hostname specified by the URL and performs a DELETE request.
  1135. ## This procedure uses httpClient values such as `client.maxRedirects`.
  1136. result = await client.request(url, HttpDelete)
  1137. proc deleteContent*(client: HttpClient | AsyncHttpClient,
  1138. url: Uri | string): Future[string] {.multisync.} =
  1139. ## Connects to the hostname specified by the URL and returns the content of a DELETE request.
  1140. let resp = await delete(client, url)
  1141. return await responseContent(resp)
  1142. proc post*(client: HttpClient | AsyncHttpClient, url: Uri | string, body = "",
  1143. multipart: MultipartData = nil): Future[Response | AsyncResponse]
  1144. {.multisync.} =
  1145. ## Connects to the hostname specified by the URL and performs a POST request.
  1146. ## This procedure uses httpClient values such as `client.maxRedirects`.
  1147. result = await client.request(url, HttpPost, body, multipart=multipart)
  1148. proc postContent*(client: HttpClient | AsyncHttpClient, url: Uri | string, body = "",
  1149. multipart: MultipartData = nil): Future[string]
  1150. {.multisync.} =
  1151. ## Connects to the hostname specified by the URL and returns the content of a POST request.
  1152. let resp = await post(client, url, body, multipart)
  1153. return await responseContent(resp)
  1154. proc put*(client: HttpClient | AsyncHttpClient, url: Uri | string, body = "",
  1155. multipart: MultipartData = nil): Future[Response | AsyncResponse]
  1156. {.multisync.} =
  1157. ## Connects to the hostname specified by the URL and performs a PUT request.
  1158. ## This procedure uses httpClient values such as `client.maxRedirects`.
  1159. result = await client.request(url, HttpPut, body, multipart=multipart)
  1160. proc putContent*(client: HttpClient | AsyncHttpClient, url: Uri | string, body = "",
  1161. multipart: MultipartData = nil): Future[string] {.multisync.} =
  1162. ## Connects to the hostname specified by the URL andreturns the content of a PUT request.
  1163. let resp = await put(client, url, body, multipart)
  1164. return await responseContent(resp)
  1165. proc patch*(client: HttpClient | AsyncHttpClient, url: Uri | string, body = "",
  1166. multipart: MultipartData = nil): Future[Response | AsyncResponse]
  1167. {.multisync.} =
  1168. ## Connects to the hostname specified by the URL and performs a PATCH request.
  1169. ## This procedure uses httpClient values such as `client.maxRedirects`.
  1170. result = await client.request(url, HttpPatch, body, multipart=multipart)
  1171. proc patchContent*(client: HttpClient | AsyncHttpClient, url: Uri | string, body = "",
  1172. multipart: MultipartData = nil): Future[string]
  1173. {.multisync.} =
  1174. ## Connects to the hostname specified by the URL and returns the content of a PATCH request.
  1175. let resp = await patch(client, url, body, multipart)
  1176. return await responseContent(resp)
  1177. proc downloadFile*(client: HttpClient, url: Uri | string, filename: string) =
  1178. ## Downloads `url` and saves it to `filename`.
  1179. client.getBody = false
  1180. defer:
  1181. client.getBody = true
  1182. let resp = client.get(url)
  1183. if resp.code.is4xx or resp.code.is5xx:
  1184. raise newException(HttpRequestError, resp.status)
  1185. client.bodyStream = newFileStream(filename, fmWrite)
  1186. if client.bodyStream.isNil:
  1187. fileError("Unable to open file")
  1188. parseBody(client, resp.headers, resp.version)
  1189. client.bodyStream.close()
  1190. proc downloadFileEx(client: AsyncHttpClient,
  1191. url: Uri | string, filename: string): Future[void] {.async.} =
  1192. ## Downloads `url` and saves it to `filename`.
  1193. client.getBody = false
  1194. let resp = await client.get(url)
  1195. if resp.code.is4xx or resp.code.is5xx:
  1196. raise newException(HttpRequestError, resp.status)
  1197. client.bodyStream = newFutureStream[string]("downloadFile")
  1198. var file = openAsync(filename, fmWrite)
  1199. defer: file.close()
  1200. # Let `parseBody` write response data into client.bodyStream in the
  1201. # background.
  1202. let parseBodyFut = parseBody(client, resp.headers, resp.version)
  1203. parseBodyFut.addCallback do():
  1204. if parseBodyFut.failed:
  1205. client.bodyStream.fail(parseBodyFut.error)
  1206. # The `writeFromStream` proc will complete once all the data in the
  1207. # `bodyStream` has been written to the file.
  1208. await file.writeFromStream(client.bodyStream)
  1209. proc downloadFile*(client: AsyncHttpClient, url: Uri | string,
  1210. filename: string): Future[void] =
  1211. result = newFuture[void]("downloadFile")
  1212. try:
  1213. result = downloadFileEx(client, url, filename)
  1214. except Exception as exc:
  1215. result.fail(exc)
  1216. finally:
  1217. result.addCallback(
  1218. proc () = client.getBody = true
  1219. )