about summary refs log blame commit diff stats
path: root/src/loader/cgi.nim
blob: 79344b6feb798d16d4aabdae615c5c16420350f2 (plain) (tree)
1
2
3
4
5
6
7
8


                  
                   
 
                   
                     
               



                          
                     


                   








                                                 
                                                                   
                                                      
                       


                                   
                                               



                                    

                         



                                         
                                     



                                                                              


                                                    

                                             
                          
                                       
                      
 
                         
                             
 

                                                                          


                         
                                                 
                  

                                        
                                  

                                                     

                                           
                                    
                       
                                                   

                             
                                                        

                                                     
                                                   





                                  
                                        
                    
                                               
                   
                                                    
                  
                                
                   
               
 
                                                                            
                                        


                         
                  

                                        

                                                     

                                             

                   
                   
               

                                           
                                                                       






                                        
                                                                          
                   
                     
                                       
          




                                                
                                              
                                             





                        
                   







                                                      
                   



                                                           
                                               








                                                        
                   

                         

                                               
                                                             
                                             


                                                 
                                           





                                                                 
                                             




                                     
                                                   

                    

                  
                                           










                                                                    

                                                                               



                                                                               
                                                       
                                                      
                                                                
                                                   








                                                


                                                  
                 











                                                                      

                     
                                                                     





                                              
               
































                                                                             
         


                         
          
 






                                                                     

                                         
import std/options
import std/os
import std/posix
import std/strutils

import io/dynstream
import io/posixstream
import io/stdio
import loader/connecterror
import loader/headers
import loader/loaderhandle
import loader/request
import types/formdata
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, myDir: string;
    request: Request; contentLen: int; prevURL: URL) =
  let url = request.url
  putEnv("SCRIPT_NAME", scriptName)
  putEnv("SCRIPT_FILENAME", cmd)
  putEnv("REQUEST_URI", requestURI)
  putEnv("REQUEST_METHOD", $request.httpMethod)
  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.referrer != nil:
    putEnv("HTTP_REFERER", $request.referrer)
  if request.proxy != nil:
    putEnv("ALL_PROXY", $request.proxy)
  setCurrentDir(myDir)

type ControlResult = enum
  crDone, crContinue, crError

proc handleFirstLine(handle: LoaderHandle; line: string; headers: Headers;
    status: var uint16): ControlResult =
  let k = line.until(':')
  if k.len == line.len:
    # invalid
    handle.sendResult(ERROR_CGI_MALFORMED_HEADER)
    return crError
  let v = line.substr(k.len + 1).strip()
  if k.equalsIgnoreCase("Status"):
    handle.sendResult(0) # success
    status = parseUInt16(v, allowSign = false).get(0)
    return crContinue
  if k.equalsIgnoreCase("Cha-Control"):
    if v.startsWithIgnoreCase("Connected"):
      handle.sendResult(0) # success
      return crContinue
    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 crError
    elif v.startsWithIgnoreCase("ControlDone"):
      return crDone
    handle.sendResult(ERROR_CGI_INVALID_CHA_CONTROL)
    return crError
  handle.sendResult(0) # success
  headers.add(k, v)
  return crDone

proc handleControlLine(handle: LoaderHandle; line: string; headers: Headers;
    status: var uint16): ControlResult =
  let k = line.until(':')
  if k.len == line.len:
    # invalid
    return crError
  let v = line.substr(k.len + 1).strip()
  if k.equalsIgnoreCase("Status"):
    status = parseUInt16(v, allowSign = false).get(0)
    return crContinue
  if k.equalsIgnoreCase("Cha-Control"):
    if v.startsWithIgnoreCase("ControlDone"):
      return crDone
    return crError
  headers.add(k, v)
  return crDone

# 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];
    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
  var myDir: 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
        myDir = dir
        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):
        myDir = dir
        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()
  stdout.flushFile()
  stderr.flushFile()
  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, myDir, request, contentLen,
      prevURL)
    # reset SIGCHLD to the default handler. this is useful if the child process
    # expects SIGCHLD to be untouched. (e.g. git dies a horrible death with
    # SIGCHLD as SIG_IGN)
    signal(SIGCHLD, SIG_DFL)
    discard execl(cstring(cmd), cstring(basename), nil)
    let code = int(ERROR_FAILED_TO_EXECUTE_CGI_SCRIPT)
    stdout.write("Cha-Control: ConnectionError " & $code & " " &
      ($strerror(errno)).deleteChars({'\n', '\r'}))
    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.sclose()
    handle.parser = HeaderParser(headers: newHeaders())
    handle.istream = newPosixStream(pipefd[0])

proc killHandle(handle: LoaderHandle) =
  if handle.parser.state != hpsBeforeLines:
    # not an ideal solution, but better than silently eating malformed
    # headers
    handle.output.ostream.setBlocking(true)
    handle.sendStatus(500)
    handle.sendHeaders(newHeaders())
    const msg = "Error: malformed header in CGI script"
    discard handle.output.ostream.sendData(msg)
  handle.parser = nil

proc parseHeaders0(handle: LoaderHandle; buffer: LoaderBuffer): int =
  let parser = handle.parser
  var s = parser.lineBuffer
  let L = if buffer == nil: 1 else: buffer.len
  for i in 0 ..< L:
    template die =
      handle.killHandle()
      return -1
    let c = if buffer != nil:
      char(buffer.page[i])
    else:
      '\n'
    if parser.crSeen and c != '\n':
      die
    parser.crSeen = false
    if c == '\r':
      parser.crSeen = true
    elif c == '\n':
      if s == "":
        if parser.state == hpsBeforeLines:
          # body comes immediately, so we haven't had a chance to send result
          # yet.
          handle.sendResult(0)
        handle.sendStatus(parser.status)
        handle.sendHeaders(parser.headers)
        handle.parser = nil
        return i + 1 # +1 to skip \n
      case parser.state
      of hpsBeforeLines:
        case handle.handleFirstLine(s, parser.headers, parser.status)
        of crDone: parser.state = hpsControlDone
        of crContinue: parser.state = hpsAfterFirstLine
        of crError: die
      of hpsAfterFirstLine:
        case handle.handleControlLine(s, parser.headers, parser.status)
        of crDone: parser.state = hpsControlDone
        of crContinue: discard
        of crError: die
      of hpsControlDone:
        handle.handleLine(s, parser.headers)
      s = ""
    else:
      s &= c
  if s != "":
    parser.lineBuffer = s
  return L

proc parseHeaders*(handle: LoaderHandle; buffer: LoaderBuffer): int =
  try:
    return handle.parseHeaders0(buffer)
  except ErrorBrokenPipe:
    handle.parser = nil
    return -1

proc finishParse*(handle: LoaderHandle) =
  discard handle.parseHeaders(nil)