import std/options
import std/os
import std/posix
import std/streams
import std/strutils
import extern/stdio
import io/posixstream
import loader/connecterror
import loader/headers
import loader/loaderhandle
import loader/request
import types/formdata
import types/opt
import types/url
import utils/twtstr
proc putMappedURL(url: URL) =
putEnv("MAPPED_URI_SCHEME", url.scheme)
putEnv("MAPPED_URI_USERNAME", url.username)
putEnv("MAPPED_URI_PASSWORD", url.password)
putEnv("MAPPED_URI_HOST", url.hostname)
putEnv("MAPPED_URI_PORT", url.port)
putEnv("MAPPED_URI_PATH", url.path.serialize())
putEnv("MAPPED_URI_QUERY", url.query.get(""))
proc setupEnv(cmd, scriptName, pathInfo, requestURI, libexecPath: string,
request: Request, contentLen: int, prevURL: URL) =
let url = request.url
putEnv("SERVER_SOFTWARE", "Chawan")
putEnv("SERVER_PROTOCOL", "HTTP/1.0")
putEnv("SERVER_NAME", "localhost")
putEnv("SERVER_PORT", "80")
putEnv("REMOTE_HOST", "localhost")
putEnv("REMOTE_ADDR", "127.0.0.1")
putEnv("GATEWAY_INTERFACE", "CGI/1.1")
putEnv("SCRIPT_NAME", scriptName)
putEnv("SCRIPT_FILENAME", cmd)
putEnv("REQUEST_URI", requestURI)
putEnv("REQUEST_METHOD", $request.httpMethod)
putEnv("CHA_LIBEXEC_DIR", libexecPath)
var headers = ""
for k, v in request.headers:
headers &= k & ": " & v & "\r\n"
putEnv("REQUEST_HEADERS", headers)
if prevURL != nil:
putMappedURL(prevURL)
if pathInfo != "":
putEnv("PATH_INFO", pathInfo)
if url.query.isSome:
putEnv("QUERY_STRING", url.query.get)
if request.httpMethod == HTTP_POST:
if request.multipart.isSome:
putEnv("CONTENT_TYPE", request.multipart.get.getContentType())
else:
putEnv("CONTENT_TYPE", request.headers.getOrDefault("Content-Type", ""))
putEnv("CONTENT_LENGTH", $contentLen)
if "Cookie" in request.headers:
putEnv("HTTP_COOKIE", request.headers["Cookie"])
if request.referer != nil:
putEnv("HTTP_REFERER", $request.referer)
if request.proxy != nil:
putEnv("ALL_PROXY", $request.proxy)
type ControlResult = enum
RESULT_CONTROL_DONE, RESULT_CONTROL_CONTINUE, RESULT_ERROR
proc handleFirstLine(handle: LoaderHandle, line: string, headers: Headers,
status: var int): ControlResult =
let k = line.until(':')
if k.len == line.len:
# invalid
handle.sendResult(ERROR_CGI_MALFORMED_HEADER)
return RESULT_ERROR
let v = line.substr(k.len + 1).strip()
if k.equalsIgnoreCase("Status"):
handle.sendResult(0) # success
status = parseInt32(v).get(0)
return RESULT_CONTROL_CONTINUE
if k.equalsIgnoreCase("Cha-Control"):
if v.startsWithIgnoreCase("Connected"):
handle.sendResult(0) # success
return RESULT_CONTROL_CONTINUE
elif v.startsWithIgnoreCase("ConnectionError"):
let errs = v.split(' ')
if errs.len <= 1:
handle.sendResult(ERROR_CGI_INVALID_CHA_CONTROL)
else:
let fb = int32(ERROR_CGI_INVALID_CHA_CONTROL)
let code = int(parseInt32(errs[1]).get(fb))
var message = ""
if errs.len > 2:
message &= errs[2]
for i in 3 ..< errs.len:
message &= ' '
message &= errs[i]
handle.sendResult(code, message)
return RESULT_ERROR
elif v.startsWithIgnoreCase("ControlDone"):
return RESULT_CONTROL_DONE
handle.sendResult(ERROR_CGI_INVALID_CHA_CONTROL)
return RESULT_ERROR
handle.sendResult(0) # success
headers.add(k, v)
return RESULT_CONTROL_DONE
proc handleControlLine(handle: LoaderHandle, line: string, headers: Headers,
status: var int): ControlResult =
let k = line.until(':')
if k.len == line.len:
# invalid
return RESULT_ERROR
let v = line.substr(k.len + 1).strip()
if k.equalsIgnoreCase("Status"):
status = parseInt32(v).get(0)
return RESULT_CONTROL_CONTINUE
if k.equalsIgnoreCase("Cha-Control"):
if v.startsWithIgnoreCase("ControlDone"):
return RESULT_CONTROL_DONE
return RESULT_ERROR
headers.add(k, v)
return RESULT_CONTROL_DONE
# returns false if transfer was interrupted
proc handleLine(handle: LoaderHandle, line: string, headers: Headers) =
let k = line.until(':')
if k.len == line.len:
# invalid
return
let v = line.substr(k.len + 1).strip()
headers.add(k, v)
proc loadCGI*(handle: LoaderHandle, request: Request, cgiDir: seq[string],
libexecPath: string, prevURL: URL) =
if cgiDir.len == 0:
handle.sendResult(ERROR_NO_CGI_DIR)
return
var path = percentDecode(request.url.pathname)
if path.startsWith("/cgi-bin/"):
path.delete(0 .. "/cgi-bin/".high)
elif path.startsWith("/$LIB/"):
path.delete(0 .. "/$LIB/".high)
if path == "" or request.url.hostname != "":
handle.sendResult(ERROR_INVALID_CGI_PATH)
return
var basename: string
var pathInfo: string
var cmd: string
var scriptName: string
var requestURI: string
if path[0] == '/':
for dir in cgiDir:
if path.startsWith(dir):
basename = path.substr(dir.len).until('/')
pathInfo = path.substr(dir.len + basename.len)
cmd = dir / basename
if not fileExists(cmd):
continue
scriptName = path.substr(0, dir.len + basename.len)
requestURI = cmd / pathInfo & request.url.search
break
if cmd == "":
handle.sendResult(ERROR_INVALID_CGI_PATH)
return
else:
basename = path.until('/')
pathInfo = path.substr(basename.len)
scriptName = "/cgi-bin/" & basename
requestURI = "/cgi-bin/" & path & request.url.search
for dir in cgiDir:
cmd = dir / basename
if fileExists(cmd):
break
if not fileExists(cmd):
handle.sendResult(ERROR_CGI_FILE_NOT_FOUND)
return
if basename in ["", ".", ".."] or basename.startsWith("~"):
handle.sendResult(ERROR_INVALID_CGI_PATH)
return
var pipefd: array[0..1, cint] # child -> parent
if pipe(pipefd) == -1:
handle.sendResult(ERROR_FAIL_SETUP_CGI)
return
# Pipe the request body as stdin for POST.
var pipefd_read: array[0..1, cint] # parent -> child
let needsPipe = request.body.isSome or request.multipart.isSome
if needsPipe:
if pipe(pipefd_read) == -1:
handle.sendResult(ERROR_FAIL_SETUP_CGI)
return
var contentLen = 0
if request.body.isSome:
contentLen = request.body.get.len
elif request.multipart.isSome:
contentLen = request.multipart.get.calcLength()
let pid = fork()
if pid == -1:
handle.sendResult(ERROR_FAIL_SETUP_CGI)
elif pid == 0:
discard close(pipefd[0]) # close read
discard dup2(pipefd[1], 1) # dup stdout
if needsPipe:
discard close(pipefd_read[1]) # close write
if pipefd_read[0] != 0:
discard dup2(pipefd_read[0], 0) # dup stdin
discard close(pipefd_read[0])
else:
closeStdin()
# we leave stderr open, so it can be seen in the browser console
setupEnv(cmd, scriptName, pathInfo, requestURI, libexecPath, request,
contentLen, prevURL)
discard execl(cstring(cmd), cstring(basename), nil)
let code = int(ERROR_FAILED_TO_EXECUTE_CGI_SCRIPT)
stdout.write("Cha-Control: ConnectionError " & $code)
quit(1)
else:
discard close(pipefd[1]) # close write
if needsPipe:
discard close(pipefd_read[0]) # close read
let ps = newPosixStream(pipefd_read[1])
if request.body.isSome:
ps.write(request.body.get)
elif request.multipart.isSome:
let multipart = request.multipart.get
for entry in multipart.entries:
ps.writeEntry(entry, multipart.boundary)
ps.close()
let ps = newPosixStream(pipefd[0])
let headers = newHeaders()
var status = 200
if ps.atEnd:
# no data?
handle.sendResult(ERROR_CGI_NO_DATA)
return
let line = ps.readLine()
if line == "": #\r\n
# no headers, body comes immediately
handle.sendResult(0) # success
else:
var res = handle.handleFirstLine(line, headers, status)
if res == RESULT_ERROR:
return
var crlfFound = false
while not ps.atEnd and res == RESULT_CONTROL_CONTINUE:
let line = ps.readLine()
if line == "":
crlfFound = true
break
res = handle.handleControlLine(line, headers, status)
if res == RESULT_ERROR:
return
if not crlfFound:
while not ps.atEnd:
let line = ps.readLine()
if line == "": #\r\n
break
handle.handleLine(line, headers)
handle.sendStatus(status)
handle.sendHeaders(headers)
if not ps.atEnd():
handle.istream = ps