about summary refs log tree commit diff stats
path: root/adapter/protocol/http.nim
blob: 25e3933097615668a5935009751323cbbddbdb25 (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
when NimMajor >= 2:
  import std/envvars
else:
  import std/os
import std/posix
import std/strutils

import curl
import curlerrors
import curlwrap

import utils/twtstr

type
  EarlyHintState = enum
    NO_EARLY_HINT, EARLY_HINT_STARTED, EARLY_HINT_DONE

  HttpHandle = ref object
    curl: CURL
    statusline: bool
    connectreport: bool
    earlyhint: EarlyHintState
    slist: curl_slist

proc puts(s: string) =
  discard write(1, unsafeAddr s[0], s.len)

proc curlWriteHeader(p: cstring, size, 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
    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:
      op.connectreport = true
      puts("Status: " & $status & "\nCha-Control: ControlDone\n")
    return nitems

  if line == "":
    # empty line (last, before body)
    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
    return nitems

  if op.earlyhint != EARLY_HINT_STARTED:
    # Regrettably, we can only write early hint headers after the status
    # code is already known.
    # For now, it seems easiest to just ignore them all.
    puts(line)
  return nitems

# From the documentation: size is always 1.
proc curlWriteBody(p: cstring, size, nmemb: csize_t, userdata: pointer):
    csize_t {.cdecl.} =
  return csize_t(write(stdout.getFileHandle(), p, int(nmemb)))

# From the documentation: size is always 1.
proc readFromStdin(buffer: cstring, size, nitems: csize_t, userdata: pointer):
    csize_t {.cdecl.} =
  return csize_t(stdin.readBuffer(buffer, nitems))

proc curlPreRequest(clientp: pointer, conn_primary_ip, conn_local_ip: cstring,
    conn_primary_port, conn_local_port: cint): cint {.cdecl.} =
  let op = cast[HttpHandle](clientp)
  op.connectreport = true
  puts("Cha-Control: Connected\n")
  return 0 # ok

proc main() =
  let curl = curl_easy_init()
  doAssert curl != nil
  let url = curl_url()
  const flags = cuint(CURLU_PATH_AS_IS)
  url.set(CURLUPART_SCHEME, getEnv("MAPPED_URI_SCHEME"), flags)
  let username = getEnv("MAPPED_URI_USERNAME")
  if username != "":
    url.set(CURLUPART_USER, username, flags)
  let password = getEnv("MAPPED_URI_PASSWORD")
  if password != "":
    url.set(CURLUPART_PASSWORD, password, flags)
  url.set(CURLUPART_HOST, getEnv("MAPPED_URI_HOST"), flags)
  let port = getEnv("MAPPED_URI_PORT")
  if port != "":
    url.set(CURLUPART_PORT, port, flags)
  let path = getEnv("MAPPED_URI_PATH")
  if path != "":
    url.set(CURLUPART_PATH, path, flags)
  let query = getEnv("MAPPED_URI_QUERY")
  if query != "":
    url.set(CURLUPART_QUERY, query, flags)
  curl.setopt(CURLOPT_CURLU, url)
  let op = HttpHandle(curl: curl)
  curl.setopt(CURLOPT_WRITEFUNCTION, curlWriteBody)
  curl.setopt(CURLOPT_HEADERDATA, op)
  curl.setopt(CURLOPT_HEADERFUNCTION, curlWriteHeader)
  curl.setopt(CURLOPT_PREREQDATA, op)
  curl.setopt(CURLOPT_PREREQFUNCTION, curlPreRequest)
  let proxy = getEnv("ALL_PROXY")
  if proxy != "":
    curl.setopt(CURLOPT_PROXY, proxy)
  case getEnv("REQUEST_METHOD")
  of "GET":
    curl.setopt(CURLOPT_HTTPGET, 1)
  of "POST":
    curl.setopt(CURLOPT_POST, 1)
    let len = parseInt(getEnv("CONTENT_LENGTH"))
    # > For any given platform/compiler curl_off_t must be typedef'ed to
    # a 64-bit
    # > wide signed integral data type. The width of this data type must remain
    # > constant and independent of any possible large file support settings.
    # >
    # > As an exception to the above, curl_off_t shall be typedef'ed to
    # a 32-bit
    # > wide signed integral data type if there is no 64-bit type.
    # It seems safe to assume that if the platform has no uint64 then Nim won't
    # compile either. In return, we are allowed to post >2G of data.
    curl.setopt(CURLOPT_POSTFIELDSIZE_LARGE, uint64(len))
    curl.setopt(CURLOPT_READFUNCTION, readFromStdin)
  else: discard #TODO
  let headers = getEnv("REQUEST_HEADERS")
  for line in headers.split("\r\n"):
    if line.startsWithNoCase("Accept-Encoding: "):
      let s = line.after(' ')
      # From the CURLOPT_ACCEPT_ENCODING manpage:
      # > The application does not have to keep the string around after
      # > setting this option.
      curl.setopt(CURLOPT_ACCEPT_ENCODING, cstring(s))
    # This is OK, because curl_slist_append strdup's line.
    op.slist = curl_slist_append(op.slist, cstring(line))
  if op.slist != nil:
    curl.setopt(CURLOPT_HTTPHEADER, op.slist)
  let res = curl_easy_perform(curl)
  if res != CURLE_OK and not op.connectreport:
    puts(getCurlConnectionError(res))
    op.connectreport = true
  curl_easy_cleanup(curl)

main()