about summary refs log blame commit diff stats
path: root/src/io/loader.nim
blob: 7e6faab4ec8d3229e103077bf0de7541fa32c57c (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13












                                                                               

              
             
          

                    
 
                    
              
                 

                   
                      
                 
                
                   
 
                         
                         
                                     

                                
                              

                                                    
 

                             
               
 





                                                        
                    
                              
                   
       
                     
                            
                                   






                                                             



                                            






                                                                          



                              
                                                  
                                                   
                                                               

                                                                              






                                                              
             
                                           
        
                                        
                                              
                                         
                                      
                                          



                                    
                  
                       
               

         
                                                                 
             



                                                  
                     

                                

                                                                             
         
                                                              



                                                                     
                        
 
                                                             
             
                                        
                      

                                 




                                                                  
                     
                                           
                                     

                          

                                            
                     
                                                     
                                                                   


                                          
 
                                   
                               
# A file loader server (?)
# The idea here is that we receive requests with a socket, then respond to each
# with a response (ideally a document.)
# For now, the protocol looks like:
# C: Request
# S: res (0 => success, _ => error)
# if success:
#  S: status code
#  S: headers
#  S: response body
#
# The body is passed to the stream as-is, so effectively nothing can follow it.

import options
import streams
import tables
import net
when defined(posix):
  import posix

import bindings/curl
import io/http
import io/process
import io/request
import io/serialize
import io/socketstream
import types/mime
import types/url
import utils/twtstr

const DefaultHeaders0 = {
  "User-Agent": "chawan",
  "Accept": "text/html,text/*;q=0.5",
  "Accept-Language": "en;q=1.0",
  "Pragma": "no-cache",
  "Cache-Control": "no-cache",
}.toTable()
let DefaultHeaders = DefaultHeaders0.newHeaderList()

type FileLoader* = ref object
  defaultHeaders*: HeaderList
  process*: Pid

proc loadFile(url: Url, ostream: Stream) =
  when defined(windows) or defined(OS2) or defined(DOS):
    let path = url.path.serialize_unicode_dos()
  else:
    let path = url.path.serialize_unicode()
  let istream = newFileStream(path, fmRead)
  if istream == nil:
    ostream.swrite(-1) # error
    ostream.flush()
  else:
    ostream.swrite(0)
    ostream.swrite(200) # ok
    ostream.swrite(newHeaderList())
    while not istream.atEnd:
      const bufferSize = 4096
      var buffer {.noinit.}: array[bufferSize, char]
      while true:
        let n = readData(istream, addr buffer[0], bufferSize)
        if n == 0:
          break
        ostream.writeData(addr buffer[0], n)
        ostream.flush()
        if n < bufferSize:
          break

proc loadResource(loader: FileLoader, request: Request, ostream: Stream) =
  case request.url.scheme
  of "file":
    loadFile(request.url, ostream)
  of "http", "https":
    loadHttp(request, ostream)
  else:
    ostream.swrite(-1) # error
    ostream.flush()

proc runFileLoader(loader: FileLoader, fd: cint) =
  if curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK:
    raise newException(Defect, "Failed to initialize libcurl.")
  let ssock = initServerSocket(getpid())
  # The server has been initialized, so the main process can resume execution.
  var writef: File
  if not open(writef, FileHandle(fd), fmWrite):
    raise newException(Defect, "Failed to open input handle.")
  writef.write(char(0u8))
  writef.flushFile()
  close(writef)
  discard close(fd)
  while true:
    let stream = ssock.acceptSocketStream()
    try:
      let request = stream.readRequest()
      for k, v in loader.defaultHeaders.table:
        if k notin request.headers.table:
          request.headers.table[k] = v
      loader.loadResource(request, stream)
    except IOError:
      # End-of-file, quit.
      # TODO this should be EOFError
      break
    stream.close()
  curl_global_cleanup()
  ssock.close()
  quit(0)

proc doRequest*(loader: FileLoader, request: Request): Response =
  new(result)
  let stream = connectSocketStream(loader.process)
  stream.swrite(request)
  stream.flush()
  stream.sread(result.res)
  if result.res == 0:
    stream.sread(result.status)
    stream.sread(result.headers)
    if "Content-Type" in result.headers.table:
      result.contenttype = result.headers.table["Content-Type"][0].until(';')
    else:
      result.contenttype = guessContentType($request.url.path)
    if "Location" in result.headers.table:
      let location = result.headers.table["Location"][0]
      result.redirect = parseUrl(location, some(request.url))
    # Only a stream of the response body may arrive after this point.
    result.body = stream

proc newFileLoader*(defaultHeaders: HeaderList): FileLoader =
  new(result)
  result.defaultHeaders = defaultHeaders
  when defined(posix):
    var pipefd: array[0..1, cint]
    if pipe(pipefd) == -1:
      raise newException(Defect, "Failed to open pipe.")
    let pid = fork()
    if pid == -1:
      raise newException(Defect, "Failed to fork network process")
    elif pid == 0:
      # child process
      discard close(pipefd[0]) # close read
      result.runFileLoader(pipefd[1])
    else:
      result.process = pid
      let readfd = pipefd[0] # get read
      discard close(pipefd[1]) # close write
      var readf: File
      if not open(readf, FileHandle(readfd), fmRead):
        raise newException(Defect, "Failed to open output handle.")
      assert readf.readChar() == char(0u8)
      close(readf)
      discard close(pipefd[0])

proc newFileLoader*(): FileLoader =
  newFileLoader(DefaultHeaders)