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





                          
                     


























                                                                     
                         
                                    



                                    













                                                          





                                  
                                                 
     
     




                                                  
                









                                                                  


                       
              










                                                             
                                
                                          






                                               
                             
                                    
                              























                                                 
                      




                          
                                          

                             
                                            





                                                





                            
                       




                            
                



                            
                         


                                                            





                                                 
                                                 







                                                                        





                                                              
                                                              














                                                        
import strutils

import bindings/curl
import loader/connecterror
import loader/curlhandle
import loader/curlwrap
import loader/dirlist
import loader/headers
import loader/loaderhandle
import loader/request
import types/opt
import types/url
import utils/twtstr

type FtpHandle = ref object of CurlHandle
  buffer: string
  dirmode: bool
  base: string
  path: string

func newFtpHandle(curl: CURL, request: Request, handle: LoaderHandle,
    dirmode: bool): FtpHandle =
  return FtpHandle(
    headers: newHeaders(),
    curl: curl,
    handle: handle,
    request: request,
    dirmode: dirmode
  )

proc curlWriteHeader(p: cstring, size: csize_t, nitems: csize_t,
    userdata: pointer): csize_t {.cdecl.} =
  var line = newString(nitems)
  if nitems > 0:
    prepareMutation(line)
    copyMem(addr line[0], p, nitems)

  let op = cast[FtpHandle](userdata)

  if not op.statusline:
    if line.startsWith("150") or line.startsWith("125"):
      op.statusline = true
      if not op.handle.sendResult(int(CURLE_OK)):
        return 0
      var status: clong
      op.curl.getinfo(CURLINFO_RESPONSE_CODE, addr status)
      if not op.handle.sendStatus(cast[int](status)):
        return 0
      if op.dirmode:
        op.headers.add("Content-Type", "text/html")
      if not op.handle.sendHeaders(op.headers):
        return 0
      if op.dirmode:
        if not op.handle.sendData("""
<HTML>
<HEAD>
<BASE HREF=""" & op.base & """>
<TITLE>""" & op.path & """</TITLE>
</HEAD>
<BODY>
<H1>Index of """ & htmlEscape(op.path) & """</H1>
<PRE>
"""):
          return 0
      return nitems
    elif line.startsWith("530"): # login incorrect
      op.statusline = true
      if not op.handle.sendResult(int(CURLE_OK)):
        return 0
      var status: clong
      op.curl.getinfo(CURLINFO_RESPONSE_CODE, addr status)
      discard op.handle.sendStatus(401) # unauthorized (shim http)
      op.headers.add("Content-Type", "text/html")
      discard op.handle.sendHeaders(op.headers)
      discard op.handle.sendData("""
<HTML>
<HEAD>
<TITLE>Unauthorized</TITLE>
</HEAD>
<BODY>
<PRE>
""" & htmlEscape(line))
      return 0
  return nitems

# From the documentation: size is always 1.
proc curlWriteBody(p: cstring, size: csize_t, nmemb: csize_t,
    userdata: pointer): csize_t {.cdecl.} =
  let op = cast[FtpHandle](userdata)

  if nmemb > 0:
    if op.dirmode:
      let i = op.buffer.len
      op.buffer.setLen(op.buffer.len + int(nmemb))
      prepareMutation(op.buffer)
      copyMem(addr op.buffer[i], p, nmemb)
    else:
      if not op.handle.sendData(p, int(nmemb)):
        return 0
  return nmemb

proc finish(op: CurlHandle) =
  let op = cast[FtpHandle](op)
  var items: seq[DirlistItem]
  for line in op.buffer.split('\n'):
    if line.len == 0: continue
    var i = 10 # permission
    template skip_till_space =
      while i < line.len and line[i] != ' ':
        inc i
    # link count
    i = line.skipBlanks(i)
    while i < line.len and line[i] in AsciiDigit:
      inc i
    # owner
    i = line.skipBlanks(i)
    skip_till_space
    # group
    i = line.skipBlanks(i)
    while i < line.len and line[i] != ' ':
      inc i
    # size
    i = line.skipBlanks(i)
    var sizes = ""
    while i < line.len and line[i] in AsciiDigit:
      sizes &= line[i]
      inc i
    let nsize = parseInt64(sizes).get(-1)
    # date
    i = line.skipBlanks(i)
    let datestarti = i
    skip_till_space # m
    i = line.skipBlanks(i)
    skip_till_space # d
    i = line.skipBlanks(i)
    skip_till_space # y
    let dates = line.substr(datestarti, i)
    inc i
    let name = line.substr(i)
    if name == "." or name == "..": continue
    case line[0]
    of 'l': # link
      let x = " -> "
      let linki = name.find(x)
      let linkfrom = name.substr(0, linki - 1)
      let linkto = name.substr(linki + 4) # you?
      items.add(DirlistItem(
        t: ITEM_LINK,
        name: linkfrom,
        modified: dates,
        linkto: linkto
      ))
    of 'd': # directory
      items.add(DirlistItem(
        t: ITEM_DIR,
        name: name,
        modified: dates
      ))
    else: # file
      items.add(DirlistItem(
        t: ITEM_FILE,
        name: name,
        modified: dates,
        nsize: int(nsize)
      ))
  discard op.handle.sendData(makeDirlist(items))
  discard op.handle.sendData("\n</PRE>\n</BODY>\n</HTML>\n")

proc loadFtp*(handle: LoaderHandle, curlm: CURLM,
    request: Request): CurlHandle =
  let curl = curl_easy_init()
  doAssert curl != nil
  let surl = request.url.serialize()
  let path = request.url.path.serialize_unicode()
  # By default, cURL CWD's into relative paths, and an extra slash is
  # necessary to specify absolute paths.
  # This is incredibly confusing, and probably not what the user wanted.
  # So we work around it by adding the extra slash ourselves.
  let hackurl = newURL(request.url)
  hackurl.setPathname('/' & request.url.pathname)
  let csurl = hackurl.serialize()
  curl.setopt(CURLOPT_URL, csurl)
  let dirmode = path.len > 0 and path[^1] == '/'
  let handleData = curl.newFtpHandle(request, handle, dirmode)
  curl.setopt(CURLOPT_HEADERDATA, handleData)
  curl.setopt(CURLOPT_HEADERFUNCTION, curlWriteHeader)
  curl.setopt(CURLOPT_WRITEDATA, handleData)
  curl.setopt(CURLOPT_WRITEFUNCTION, curlWriteBody)
  curl.setopt(CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_SINGLECWD)
  if dirmode:
    handleData.finish = finish
    handleData.base = surl
    handleData.path = path
  if request.proxy != nil:
    let purl = request.proxy.serialize()
    curl.setopt(CURLOPT_PROXY, purl)
  if request.httpmethod != HTTP_GET:
    discard handle.sendResult(int(ERROR_INVALID_METHOD))
    return nil
  let res = curl_multi_add_handle(curlm, curl)
  if res != CURLM_OK:
    discard handle.sendResult(int(res))
    return nil
  return handleData