about summary refs log tree commit diff stats
path: root/src/io/http.nim
blob: 1ebcaf72bbf7bb50443aea395bc13a4f6172d8a2 (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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import options
import streams
import strutils

import bindings/curl
import io/headers
import io/request
import ips/serialize
import types/blob
import types/formdata
import types/url
import utils/opt
import utils/twtstr

type
  HandleData* = ref HandleDataObj
  HandleDataObj = object
    curl*: CURL
    statusline: bool
    headers: Headers
    request: Request
    ostream*: Stream
    mime: curl_mime
    slist: curl_slist

func newHandleData(curl: CURL, request: Request, ostream: Stream): HandleData =
  let handleData = HandleData(
    headers: newHeaders(),
    curl: curl,
    ostream: ostream,
    request: request
  )
  return handleData

proc cleanup*(handleData: HandleData) =
  if handleData.mime != nil:
    curl_mime_free(handleData.mime)
  if handleData.slist != nil:
    curl_slist_free_all(handleData.slist)
  curl_easy_cleanup(handleData.curl)

template setopt(curl: CURL, opt: CURLoption, arg: typed) =
  discard curl_easy_setopt(curl, opt, arg)

template setopt(curl: CURL, opt: CURLoption, arg: string) =
  discard curl_easy_setopt(curl, opt, cstring(arg))

template getinfo(curl: CURL, info: CURLINFO, arg: typed) =
  discard curl_easy_getinfo(curl, info, arg)

proc curlWriteHeader(p: cstring, size: csize_t, nitems: csize_t, userdata: pointer): csize_t {.cdecl.} =
  var line = newString(nitems)
  for i in 0..<nitems:
    line[i] = p[i]

  let op = cast[HandleData](userdata)
  if not op.statusline:
    op.statusline = true
    try:
      op.ostream.swrite(int(CURLE_OK))
    except IOError: # Broken pipe
      return 0
    var status: clong
    op.curl.getinfo(CURLINFO_RESPONSE_CODE, addr status)
    op.ostream.swrite(cast[int](status))
    return nitems

  let k = line.until(':')

  if k.len == line.len:
    # empty line (last, before body) or invalid (=> error)
    op.ostream.swrite(op.headers)
    return nitems

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

proc curlWriteBody(p: cstring, size: csize_t, nmemb: csize_t, userdata: pointer): csize_t {.cdecl.} =
  let handleData = cast[HandleData](userdata)
  if nmemb > 0:
    try:
      handleData.ostream.writeData(p, int(nmemb))
    except IOError: # Broken pipe
      return 0
  return nmemb

proc applyPostBody(curl: CURL, request: Request, handleData: HandleData) =
  if request.multipart.isOk:
    handleData.mime = curl_mime_init(curl)
    if handleData.mime == nil:
      # fail (TODO: raise?)
      handleData.ostream.swrite(-1)
      handleData.ostream.flush()
      return
    for entry in request.multipart.get:
      let part = curl_mime_addpart(handleData.mime)
      if part == nil:
        # fail (TODO: raise?)
        handleData.ostream.swrite(-1)
        handleData.ostream.flush()
        return
      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*(curlm: CURLM, request: Request, ostream: Stream): HandleData =
  let curl = curl_easy_init()
  if curl == nil:
    ostream.swrite(-1)
    ostream.flush()
    return # fail
  let surl = request.url.serialize()
  curl.setopt(CURLOPT_URL, surl)
  let handleData = curl.newHandleData(request, ostream)
  curl.setopt(CURLOPT_WRITEDATA, handleData)
  curl.setopt(CURLOPT_WRITEFUNCTION, curlWriteBody)
  curl.setopt(CURLOPT_HEADERDATA, handleData)
  curl.setopt(CURLOPT_HEADERFUNCTION, curlWriteHeader)
  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:
    ostream.swrite(int(res))
    ostream.flush()
    #TODO: raise here?
    return
  return handleData