about summary refs log tree commit diff stats
path: root/src/loader/http.nim
blob: d7bc3a8fdff676a097bf9af361fd143793e7d10d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import options
import strutils

import bindings/curl
import loader/curlhandle
import loader/curlwrap
import loader/headers
import loader/loaderhandle
import loader/request
import types/blob
import types/formdata
import types/opt
import types/url
import utils/twtstr

type
  EarlyHintState = enum
    NO_EARLY_HINT, EARLY_HINT_STARTED, EARLY_HINT_DONE

  HttpHandle = ref object of CurlHandle
    earlyhint: EarlyHintState

func newHttpHandle(curl: CURL, request: Request, handle: LoaderHandle):
    HttpHandle =
  return HttpHandle(
    headers: newHeaders(),
    curl: curl,
    handle: handle,
    request: request
  )

proc curlWriteHeader(p: cstring, size: csize_t, nitems: csize_t,
    userdata: pointer): csize_t {.cdecl.} =
  var line = newString(nitems)
  if nitems > 0:
    prepareMutation(line)
    copyMem(addr line[0], p, nitems)

  let op = cast[HttpHandle](userdata)
  if not op.statusline:
    op.statusline = true
    if op.earlyhint == NO_EARLY_HINT:
      if not op.handle.sendResult(int(CURLE_OK)):
        return 0
    var status: clong
    op.curl.getinfo(CURLINFO_RESPONSE_CODE, addr status)
    if status == 103 and op.earlyhint == NO_EARLY_HINT:
      op.earlyhint = EARLY_HINT_STARTED
    else:
      if not op.handle.sendStatus(cast[int](status)):
        return 0
    return nitems

  let k = line.until(':')

  if k.len == line.len:
    # empty line (last, before body) or invalid (=> error)
    if op.earlyhint == EARLY_HINT_STARTED:
      # ignore; we do not have a way to stream headers yet.
      op.earlyhint = EARLY_HINT_DONE
      # reset statusline; we are awaiting the next line.
      op.statusline = false
      return nitems
    if not op.handle.sendHeaders(op.headers):
      return 0
    return nitems

  let v = line.substr(k.len + 1).strip()
  op.headers.add(k, v)
  return nitems

# From the documentation: size is always 1.
proc curlWriteBody(p: cstring, size: csize_t, nmemb: csize_t,
    userdata: pointer): csize_t {.cdecl.} =
  let handleData = cast[HttpHandle](userdata)
  if nmemb > 0:
    if not handleData.handle.sendData(p, int(nmemb)):
      return 0
  return nmemb

proc applyPostBody(curl: CURL, request: Request, handleData: HttpHandle) =
  if request.multipart.isOk:
    handleData.mime = curl_mime_init(curl)
    doAssert handleData.mime != nil
    for entry in request.multipart.get:
      let part = curl_mime_addpart(handleData.mime)
      doAssert part != nil
      curl_mime_name(part, cstring(entry.name))
      if entry.isstr:
        curl_mime_data(part, cstring(entry.svalue), csize_t(entry.svalue.len))
      else:
        let blob = entry.value
        if blob.isfile: #TODO ?
          curl_mime_filedata(part, cstring(WebFile(blob).path))
        else:
          curl_mime_data(part, blob.buffer, csize_t(blob.size))
        # may be overridden by curl_mime_filedata, so set it here
        curl_mime_filename(part, cstring(entry.filename))
    curl.setopt(CURLOPT_MIMEPOST, handleData.mime)
  elif request.body.issome:
    curl.setopt(CURLOPT_POSTFIELDS, cstring(request.body.get))
    curl.setopt(CURLOPT_POSTFIELDSIZE, request.body.get.len)

proc loadHttp*(handle: LoaderHandle, curlm: CURLM,
    request: Request): HttpHandle =
  let curl = curl_easy_init()
  doAssert curl != nil
  let surl = request.url.serialize()
  curl.setopt(CURLOPT_URL, surl)
  let handleData = curl.newHttpHandle(request, handle)
  curl.setopt(CURLOPT_WRITEDATA, handleData)
  curl.setopt(CURLOPT_WRITEFUNCTION, curlWriteBody)
  curl.setopt(CURLOPT_HEADERDATA, handleData)
  curl.setopt(CURLOPT_HEADERFUNCTION, curlWriteHeader)
  if "Accept-Encoding" in request.headers:
    let s = request.headers["Accept-Encoding"]
    curl.setopt(CURLOPT_ACCEPT_ENCODING, cstring(s))
  if request.proxy != nil:
    let purl = request.proxy.serialize()
    curl.setopt(CURLOPT_PROXY, purl)
  case request.httpmethod
  of HTTP_GET:
    curl.setopt(CURLOPT_HTTPGET, 1)
  of HTTP_POST:
    curl.setopt(CURLOPT_POST, 1)
    curl.applyPostBody(request, handleData)
  else: discard #TODO
  for k, v in request.headers:
    let header = k & ": " & v
    handleData.slist = curl_slist_append(handleData.slist, cstring(header))
  if handleData.slist != nil:
    curl.setopt(CURLOPT_HTTPHEADER, handleData.slist)
  let res = curl_multi_add_handle(curlm, curl)
  if res != CURLM_OK:
    discard handle.sendResult(int(res))
    return nil
  return handleData