summary refs log tree commit diff stats
path: root/lib/pure/cgi.nim
blob: 31fb24eef49c162023ff4ced48fc011c7e091263 (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
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Mu - 102keyboard.subx</title>
<meta name="Generator" content="Vim/8.2">
<meta name="plugin-version" content="vim8.1_v2">
<meta name="syntax" content="none">
<meta name="settings" content="number_lines,use_css,pre_wrap,no_foldcolumn,expand_tabs,line_ids,prevent_copy=,use_input_for_pc=fallback">
<meta name="colorscheme" content="minimal-light">
<style>
<!--
pre { white-space: pre-wrap; font-family: monospace; color: #000000; background-color: #ffffd7; }
body { font-size:12pt; font-family: monospace; color: #000000; background-color: #ffffd7; }
a 
#
#
#            Nimrod's Runtime Library
#        (c) Copyright 2012 Andreas Rumpf
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

## This module implements helper procs for CGI applications. Example:
##
## .. code-block:: Nimrod
##
##    import strtabs, cgi
##
##    # Fill the values when debugging:
##    when debug:
##      setTestData("name", "Klaus", "password", "123456")
##    # read the data into `myData`
##    var myData = readData()
##    # check that the data's variable names are "name" or "password"
##    validateData(myData, "name", "password")
##    # start generating content:
##    writeContentType()
##    # generate content:
##    write(stdout, "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n")
##    write(stdout, "<html><head><title>Test</title></head><body>\n")
##    writeln(stdout, "your name: " & myData["name"])
##    writeln(stdout, "your password: " & myData["password"])
##    writeln(stdout, "</body></html>")

import strutils, os, strtabs, cookies

proc URLencode*(s: string): string =
  ## Encodes a value to be HTTP safe: This means that characters in the set
  ## ``{'A'..'Z', 'a'..'z', '0'..'9', '_'}`` are carried over to the result,
  ## a space is converted to ``'+'`` and every other character is encoded as
  ## ``'%xx'`` where ``xx`` denotes its hexadecimal value.
  result = newStringOfCap(s.len + s.len shr 2) # assume 12% non-alnum-chars
  for i in 0..s.len-1:
    case s[i]
    of 'a'..'z', 'A'..'Z', '0'..'9', '_': add(result, s[i])
    of ' ': add(result, '+')
    else:
      add(result, '%')
      add(result, toHex(ord(s[i]), 2))

proc handleHexChar(c: char, x: var int) {.inline.} =
  case c
  of '0'..'9': x = (x shl 4) or (ord(c) - ord('0'))
  of 'a'..'f': x = (x shl 4) or (ord(c) - ord('a') + 10)
  of 'A'..'F': x = (x shl 4) or (ord(c) - ord('A') + 10)
  else: assert(false)

proc URLdecode*(s: string): string =
  ## Decodes a value from its HTTP representation: This means that a ``'+'``
  ## is converted to a space, ``'%xx'`` (where ``xx`` denotes a hexadecimal
  ## value) is converted to the character with ordinal number ``xx``, and
  ## and every other character is carried over.
  result = newString(s.len)
  var i = 0
  var j = 0
  while i < s.len:
    case s[i]
    of '%':
      var x = 0
      handleHexChar(s[i+1], x)
      handleHexChar(s[i+2], x)
      inc(i, 2)
      result[j] = chr(x)
    of '+': result[j] = ' '
    else: result[j] = s[i]
    inc(i)
    inc(j)
  setLen(result, j)

proc addXmlChar(dest: var string, c: char) {.inline.} =
  case c
  of '&': add(dest, "&amp;")
  of '<': add(dest, "&lt;")
  of '>': add(dest, "&gt;")
  of '\"': add(dest, "&quot;")
  else: add(dest, c)

proc XMLencode*(s: string): string =
  ## Encodes a value to be XML safe:
  ## * ``"`` is replaced by ``&quot;``
  ## * ``<`` is replaced by ``&lt;``
  ## * ``>`` is replaced by ``&gt;``
  ## * ``&`` is replaced by ``&amp;``
  ## * every other character is carried over.
  result = newStringOfCap(s.len + s.len shr 2)
  for i in 0..len(s)-1: addXmlChar(result, s[i])

type
  ECgi* = object of EIO  ## the exception that is raised, if a CGI error occurs
  TRequestMethod* = enum ## the used request method
    methodNone,          ## no REQUEST_METHOD environment variable
    methodPost,          ## query uses the POST method
    methodGet            ## query uses the GET method

proc cgiError*(msg: string) {.noreturn.} =
  ## raises an ECgi exception with message `msg`.
  var e: ref ECgi
  new(e)
  e.msg = msg
  raise e

proc getEncodedData(allowedMethods: set[TRequestMethod]): string =
  case getenv("REQUEST_METHOD").string
  of "POST":
    if methodPost notin allowedMethods:
      cgiError("'REQUEST_METHOD' 'POST' is not supported")
    var L = parseInt(getenv("CONTENT_LENGTH").string)
    result = newString(L)
    if readBuffer(stdin, addr(result[0]), L) != L:
      cgiError("cannot read from stdin")
  of "GET":
    if methodGet notin allowedMethods:
      cgiError("'REQUEST_METHOD' 'GET' is not supported")
    result = getenv("QUERY_STRING").string
  else:
    if methodNone notin allowedMethods:
      cgiError("'REQUEST_METHOD' must be 'POST' or 'GET'")

iterator decodeData*(data: string): tuple[key, value: TaintedString] =
  ## Reads and decodes CGI data and yields the (name, value) pairs the
  ## data consists of.
  var i = 0
  var name = ""
  var value = ""
  # decode everything in one pass:
  while data[i] != '\0':
    setLen(name, 0) # reuse memory
    while true:
      case data[i]
      of '\0': break
      of '%':
        var x = 0
        handleHexChar(data[i+1], x)
        handleHexChar(data[i+2], x)
        inc(i, 2)
        add(name, chr(x))
      of '+': add(name, ' ')
      of '=', '&': break
      else: add(name, data[i])
      inc(i)
    if data[i] != '=': cgiError("'=' expected")
    inc(i) # skip '='
    setLen(value, 0) # reuse memory
    while true:
      case data[i]
      of '%':
        var x = 0
        handleHexChar(data[i+1], x)
        handleHexChar(data[i+2], x)
        inc(i, 2)
        add(value, chr(x))
      of '+': add(value, ' ')
      of '&', '\0': break
      else: add(value, data[i])
      inc(i)
    yield (name.TaintedString, value.TaintedString)
    if data[i] == '&': inc(i)
    elif data[i] == '\0': break
    else: cgiError("'&' expected")

iterator decodeData*(allowedMethods: set[TRequestMethod] =
       {methodNone, methodPost, methodGet}): tuple[key, value: TaintedString] =
  ## Reads and decodes CGI data and yields the (name, value) pairs the
  ## data consists of. If the client does not use a method listed in the
  ## `allowedMethods` set, an `ECgi` exception is raised.
  var data = getEncodedData(allowedMethods)
  if not isNil(data):
    for key, value in decodeData(data):
      yield (key, value)

proc readData*(allowedMethods: set[TRequestMethod] =
               {methodNone, methodPost, methodGet}): PStringTable =
  ## Read CGI data. If the client does not use a method listed in the
  ## `allowedMethods` set, an `ECgi` exception is raised.
  result = newStringTable()
  for name, value in decodeData(allowedMethods):
    result[name.string] = value.string

proc validateData*(data: PStringTable, validKeys: varargs[string]) =
  ## validates data; raises `ECgi` if this fails. This checks that each variable
  ## name of the CGI `data` occurs in the `validKeys` array.
  for key, val in pairs(data):
    if find(validKeys, key) < 0:
      cgiError("unknown variable name: " & key)

proc getContentLength*(): string =
  ## returns contents of the ``CONTENT_LENGTH`` environment variable
  return getenv("CONTENT_LENGTH").string

proc getContentType*(): string =
  ## returns contents of the ``CONTENT_TYPE`` environment variable
  return getenv("CONTENT_Type").string

proc getDocumentRoot*(): string =
  ## returns contents of the ``DOCUMENT_ROOT`` environment variable
  return getenv("DOCUMENT_ROOT").string

proc getGatewayInterface*(): string =
  ## returns contents of the ``GATEWAY_INTERFACE`` environment variable
  return getenv("GATEWAY_INTERFACE").string

proc getHttpAccept*(): string =
  ## returns contents of the ``HTTP_ACCEPT`` environment variable
  return getenv("HTTP_ACCEPT").string

proc getHttpAcceptCharset*(): string =
  ## returns contents of the ``HTTP_ACCEPT_CHARSET`` environment variable
  return getenv("HTTP_ACCEPT_CHARSET").string

proc getHttpAcceptEncoding*(): string =
  ## returns contents of the ``HTTP_ACCEPT_ENCODING`` environment variable
  return getenv("HTTP_ACCEPT_ENCODING").string

proc getHttpAcceptLanguage*(): string =
  ## returns contents of the ``HTTP_ACCEPT_LANGUAGE`` environment variable
  return getenv("HTTP_ACCEPT_LANGUAGE").string

proc getHttpConnection*(): string =
  ## returns contents of the ``HTTP_CONNECTION`` environment variable
  return getenv("HTTP_CONNECTION").string

proc getHttpCookie*(): string =
  ## returns contents of the ``HTTP_COOKIE`` environment variable
  return getenv("HTTP_COOKIE").string

proc getHttpHost*(): string =
  ## returns contents of the ``HTTP_HOST`` environment variable
  return getenv("HTTP_HOST").string

proc getHttpReferer*(): string =
  ## returns contents of the ``HTTP_REFERER`` environment variable
  return getenv("HTTP_REFERER").string

proc getHttpUserAgent*(): string =
  ## returns contents of the ``HTTP_USER_AGENT`` environment variable
  return getenv("HTTP_USER_AGENT").string

proc getPathInfo*(): string =
  ## returns contents of the ``PATH_INFO`` environment variable
  return getenv("PATH_INFO").string

proc getPathTranslated*(): string =
  ## returns contents of the ``PATH_TRANSLATED`` environment variable
  return getenv("PATH_TRANSLATED").string

proc getQueryString*(): string =
  ## returns contents of the ``QUERY_STRING`` environment variable
  return getenv("QUERY_STRING").string

proc getRemoteAddr*(): string =
  ## returns contents of the ``REMOTE_ADDR`` environment variable
  return getenv("REMOTE_ADDR").string

proc getRemoteHost*(): string =
  ## returns contents of the ``REMOTE_HOST`` environment variable
  return getenv("REMOTE_HOST").string

proc getRemoteIdent*(): string =
  ## returns contents of the ``REMOTE_IDENT`` environment variable
  return getenv("REMOTE_IDENT").string

proc getRemotePort*(): string =
  ## returns contents of the ``REMOTE_PORT`` environment variable
  return getenv("REMOTE_PORT").string

proc getRemoteUser*(): string =
  ## returns contents of the ``REMOTE_USER`` environment variable
  return getenv("REMOTE_USER").string

proc getRequestMethod*(): string =
  ## returns contents of the ``REQUEST_METHOD`` environment variable
  return getenv("REQUEST_METHOD").string

proc getRequestURI*(): string =
  ## returns contents of the ``REQUEST_URI`` environment variable
  return getenv("REQUEST_URI").string

proc getScriptFilename*(): string =
  ## returns contents of the ``SCRIPT_FILENAME`` environment variable
  return getenv("SCRIPT_FILENAME").string

proc getScriptName*(): string =
  ## returns contents of the ``SCRIPT_NAME`` environment variable
  return getenv("SCRIPT_NAME").string

proc getServerAddr*(): string =
  ## returns contents of the ``SERVER_ADDR`` environment variable
  return getenv("SERVER_ADDR").string

proc getServerAdmin*(): string =
  ## returns contents of the ``SERVER_ADMIN`` environment variable
  return getenv("SERVER_ADMIN").string

proc getServerName*(): string =
  ## returns contents of the ``SERVER_NAME`` environment variable
  return getenv("SERVER_NAME").string

proc getServerPort*(): string =
  ## returns contents of the ``SERVER_PORT`` environment variable
  return getenv("SERVER_PORT").string

proc getServerProtocol*(): string =
  ## returns contents of the ``SERVER_PROTOCOL`` environment variable
  return getenv("SERVER_PROTOCOL").string

proc getServerSignature*(): string =
  ## returns contents of the ``SERVER_SIGNATURE`` environment variable
  return getenv("SERVER_SIGNATURE").string

proc getServerSoftware*(): string =
  ## returns contents of the ``SERVER_SOFTWARE`` environment variable
  return getenv("SERVER_SOFTWARE").string

proc setTestData*(keysvalues: varargs[string]) =
  ## fills the appropriate environment variables to test your CGI application.
  ## This can only simulate the 'GET' request method. `keysvalues` should
  ## provide embedded (name, value)-pairs. Example:
  ##
  ## .. code-block:: Nimrod
  ##    setTestData("name", "Hanz", "password", "12345")
  putenv("REQUEST_METHOD", "GET")
  var i = 0
  var query = ""
  while i < keysvalues.len:
    add(query, URLencode(keysvalues[i]))
    add(query, '=')
    add(query, URLencode(keysvalues[i+1]))
    add(query, '&')
    inc(i, 2)
  putenv("QUERY_STRING", query)

proc writeContentType*() =
  ## call this before starting to send your HTML data to `stdout`. This
  ## implements this part of the CGI protocol:
  ##
  ## .. code-block:: Nimrod
  ##     write(stdout, "Content-type: text/html\n\n")
  write(stdout, "Content-type: text/html\n\n")

proc resetForStacktrace() =
  stdout.write """<!--: spam
Content-Type: text/html

<body bgcolor=#f0f0f8><font color=#f0f0f8 size=-5> -->
<body bgcolor=#f0f0f8><font color=#f0f0f8 size=-5> --> -->
</font> </font> </font> </script> </object> </blockquote> </pre>
</table> </table> </table> </table> </table> </font> </font> </font>
"""

proc writeErrorMessage*(data: string) =
  ## Tries to reset browser state and writes `data` to stdout in
  ## <plaintext> tag.
  resetForStacktrace()
  # We use <plaintext> here, instead of escaping, so stacktrace can
  # be understood by human looking at source.
  stdout.write("<plaintext>\n")
  stdout.write(data)

proc setStackTraceStdout*() =
  ## Makes Nimrod output stacktraces to stdout, instead of server log.
  errorMessageWriter = writeErrorMessage

proc setStackTraceNewLine*() {.deprecated.} =
  ## Makes Nimrod output stacktraces to stdout, instead of server log.
  ## Depracated alias for setStackTraceStdout.
  setStackTraceStdout()

proc setCookie*(name, value: string) =
  ## Sets a cookie.
  write(stdout, "Set-Cookie: ", name, "=", value, "\n")

var
  gcookies {.threadvar.}: PStringTable

proc getCookie*(name: string): TaintedString =
  ## Gets a cookie. If no cookie of `name` exists, "" is returned.
  if gcookies == nil: gcookies = parseCookies(getHttpCookie())
  result = TaintedString(gcookies[name])

proc existsCookie*(name: string): bool =
  ## Checks if a cookie of `name` exists.
  if gcookies == nil: gcookies = parseCookies(getHttpCookie())
  result = hasKey(gcookies, name)

when isMainModule:
  const test1 = "abc\L+def xyz"
  assert UrlEncode(test1) == "abc%0A%2Bdef+xyz"
  assert UrlDecode(UrlEncode(test1)) == test1