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


                 
               
 
                    





                          
                         



                        


                          
 



                           

                         
                                     




                                
                                                       
             
                          
 





                                    
                                                












                                     



























                                                                                                        








                                                           





                                                                                                                                                                     
                                

                            

                                                   


                                                


                                                      



                           
                                             
              
                                
















                                                                                  
                                         
                    

                                                    










                                                     
                                          

                                   
                                          







                                                                 






                                                                               






                              
                                                                                                                                                          
                          
                                                          
                                                 

                                             

                                               
                                                    
                                                     
                                                                                                    





                                                                                                                                                             
import httpclient
import options
import streams
import strutils

import bindings/curl
import types/mime
import types/url
import utils/twtstr

type
  FileLoader* = ref object
    headers*: HttpHeaders

  LoadResult* = object
    s*: Stream
    contenttype*: string
    status*: int
    headers*: HttpHeaders
    redirect*: Option[Url]

  HeaderResult = ref object
    statusline: bool
    headers: HttpHeaders

const DefaultHeaders = {
  "User-Agent": "chawan",
  "Accept": "text/html,text/*;q=0.5",
  "Accept-Language": "en;q=1.0",
  "Pragma": "no-cache",
  "Cache-control": "no-cache",
}

proc newFileLoader*(headers: HttpHeaders): FileLoader =
  new(result)
  result.headers = headers

proc newFileLoader*(): FileLoader =
  var headers = newHttpHeaders(true)
  for header in DefaultHeaders:
    headers[header[0]] = header[1]
  newFileLoader(headers)

#TODO replace this with our own multipart object
type
  MultipartEntryClone* = object
    name, content: string
    case isFile: bool
    of true:
      filename, contentType: string
      fileSize: int64
      isStream: bool
    else: discard

  MultipartDataClone* = ref object
    content: seq[MultipartEntryClone]

proc curlWriteHeader(p: cstring, size: csize_t, nitems: csize_t, userdata: pointer): csize_t {.cdecl.} =
  var line = newString(nitems)
  for i in 0..<nitems:
    line[i] = p[i]

  let headers = cast[HeaderResult](userdata)
  if not headers.statusline:
    headers.statusline = true
    return nitems #TODO handle status line

  let k = line.until(':')

  if k.len == line.len:
    return nitems # probably empty line, or invalid

  let v = line.substr(k.len + 1).strip()
  headers.headers.add(k, v)
  return nitems

proc curlWriteBody(p: cstring, size: csize_t, nmemb: csize_t, userdata: pointer): csize_t {.cdecl.} =
  var s = newString(nmemb)
  for i in 0..<nmemb:
    s[i] = p[i]
  let stream = cast[Stream](userdata)
  stream.write(s)
  stream.flush()
  return nmemb

template setopt(curl: CURL, opt: CURLoption, arg: typed) =
  discard curl_easy_setopt(curl, opt, arg)

template setopt(curl: CURL, opt: CURLoption, arg: string) =
  discard curl_easy_setopt(curl, opt, cstring(arg))

template getinfo(curl: CURL, info: CURLINFO, arg: typed) =
  discard curl_easy_getinfo(curl, info, arg)

proc getPageLibcurl(loader: FileLoader, url: Url, smethod: HttpMethod = HttpGet, mimetype = "", body: string = "", multipart: MultipartDataClone = nil): LoadResult =
  let curl = curl_easy_init()

  if curl == nil: return # fail

  let surl = url.serialize()
  curl.setopt(CURLOPT_URL, surl)

  var cs = newStringStream()
  curl.setopt(CURLOPT_WRITEDATA, cs)
  curl.setopt(CURLOPT_WRITEFUNCTION, curlWriteBody)

  let headers = newHttpHeaders(true)
  let headerres = HeaderResult(headers: headers)
  curl.setopt(CURLOPT_HEADERDATA, headerres)
  curl.setopt(CURLOPT_HEADERFUNCTION, curlWriteHeader)
  #curl.setopt(CURLOPT_FOLLOWLOCATION, 1)

  var mime: curl_mime = nil

  case smethod
  of HttpGet: curl.setopt(CURLOPT_HTTPGET, 1)
  of HttpPost:
    curl.setopt(CURLOPT_POST, 1)
    if multipart != nil:
      mime = curl_mime_init(curl)
      if mime == nil: return # fail
      for entry in multipart.content:
        let part = curl_mime_addpart(mime)
        if part == nil: return # fail
        curl_mime_name(part, cstring(entry.name))
        if entry.isFile:
          if entry.isStream:
            curl_mime_filedata(part, cstring(entry.filename))
          else:
            let fd = readFile(entry.filename)
            curl_mime_data(part, cstring(fd), csize_t(fd.len))
          # may be overridden by curl_mime_filedata, so set it here
          curl_mime_filename(part, cstring(entry.filename))
        else:
          curl_mime_data(part, cstring(entry.content), csize_t(entry.content.len))
      curl.setopt(CURLOPT_MIMEPOST, mime)
    elif body != "":
      curl.setopt(CURLOPT_POSTFIELDS, cstring(body))
      curl.setopt(CURLOPT_POSTFIELDSIZE, body.len)
  else: discard

  var requestHeaders = newHttpHeaders(true)
  requestHeaders.table = loader.headers.table
  if mimetype != "":
    requestHeaders["Content-Type"] = mimetype
  var slist: curl_slist = nil
  for k, v in requestHeaders:
    let header = k & ": " & v
    slist = curl_slist_append(slist, cstring(header))
  if slist != nil:
    curl.setopt(CURLOPT_HTTPHEADER, slist)

  let res = curl_easy_perform(curl)
  if res == CURLE_OK: # TODO handle errors
    cs.setPosition(0)
    result.s = cs

    let ct = headers.getOrDefault("Content-Type")
    if ct != "":
      result.contenttype = ct.until(';')
    else:
      result.contenttype = guessContentType(url.path.serialize())
    discard curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, addr result.status)
    if result.status in {301, 302, 303}: #TODO 300, 304, 307
      var urlp: cstring
      curl.getinfo(CURLINFO_REDIRECT_URL, addr urlp)
      if urlp != nil:
        let urls = $urlp
        result.redirect = parseUrl(urls, some(url))

  curl_easy_cleanup(curl)
  if mime != nil:
    curl_mime_free(mime)
  if slist != nil:
    curl_slist_free_all(slist)

proc getPage*(loader: FileLoader, url: Url, smethod: HttpMethod = HttpGet, mimetype = "", body: string = "", multipart: MultipartData = nil): LoadResult =
  if url.scheme == "file":
    when defined(windows) or defined(OS2) or defined(DOS):
      let path = url.path.serialize_unicode_dos()
    else:
      let path = url.path.serialize_unicode()
    result.contenttype = guessContentType(path)
    result.s = newFileStream(path, fmRead)
    result.status = 200 # doesn't make much sense...
  elif url.scheme == "http" or url.scheme == "https":
    return getPageLibcurl(loader, url, smethod, mimetype, body, cast[MultipartDataClone](multipart))

proc getPage*(loader: FileLoader, url: string, smethod: HttpMethod = HttpGet, mimetype = "", body: string = "", multipart: MultipartData = nil): LoadResult =
  let url = parseUrl(url)
  if url.isnone:
    raise newException(Exception, "Invalid URL")
  loader.getPage(url.get, smethod, mimetype, body, multipart)