about summary refs log blame commit diff stats
path: root/adapter/protocol/ftp.nim
blob: 88ced6be0fe6272c881f1e2b192e7a6cc021d993 (plain) (tree)
1
2
3
4
5
6
7
8
9
                  
             
                   
 
           



                 
                          

                   

                           



                
                  
 

















                                                                           

                              
                         
                                    
                                    
                       

                                                        

                                                          
                                               
                      


                                                  

                                                          




                                



                           

      




                            


                                           
                                                                        
                       
                                    


                        



                                                  
                                          
         
                                                       

              

                            
                                   
                                    
                              























                                                 
                      




                          
                                          

                             
                                            

                  
                                   

                                                





                            
                       




                            
                



                            
                         
        

                                              
 

























































                                                                         
             

                             


                                       
                                 



                                             

                                       

                                          


                                              
                                      
                                              
















                                                 


                                        



                                                                        














                                                                             





                                                                               
                            
                                 
                                     
                                                      
                                    
                                                   
                                                              

                                
                                    
                                       
          

                                                                



                                     



                                                   


                       


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

import curl
import curlerrors
import curlwrap
import dirlist

import loader/connecterror
import utils/twtstr

type FtpHandle = ref object
  curl: CURL
  buffer: string
  dirmode: bool
  base: string
  path: string
  statusline: bool

proc printHeader(op: FtpHandle) =
    if op.dirmode:
      stdout.write("""Content-Type: text/html

<HTML>
<HEAD>
<BASE HREF=""" & op.base & """>
<TITLE>""" & op.path & """</TITLE>
</HEAD>
<BODY>
<H1>Index of """ & htmlEscape(op.path) & """</H1>
<PRE>
""")
    else:
      stdout.write('\n')

proc curlWriteHeader(p: cstring; size, 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
      var status: clong
      op.curl.getinfo(CURLINFO_RESPONSE_CODE, addr status)
      stdout.write("Status: " & $status & "\n")
      op.printHeader()
      return nitems
    elif line.startsWith("530"): # login incorrect
      op.statusline = true
      var status: clong
      op.curl.getinfo(CURLINFO_RESPONSE_CODE, addr status)
      # unauthorized (shim http)
      stdout.write("""
Status: 401
Content-Type: text/html

<HTML>
<HEAD>
<TITLE>Unauthorized</TITLE>
</HEAD>
<BODY>
<PRE>
""" & htmlEscape(line) & """
</PRE>
</BODY>
</HTML>
""")
  return nitems

# From the documentation: size is always 1.
proc curlWriteBody(p: cstring; size, nmemb: csize_t; userdata: pointer):
    csize_t {.cdecl.} =
  let op = cast[FtpHandle](userdata)
  if not op.statusline:
    op.statusline = true
    op.printHeader()
  if nmemb > 0:
    if op.dirmode:
      let i = op.buffer.len
      op.buffer.setLen(op.buffer.len + int(nmemb))
      copyMem(addr op.buffer[i], p, nmemb)
    else:
      return csize_t(stdout.writeBuffer(p, int(nmemb)))
  return nmemb

proc finish(op: FtpHandle) =
  let op = 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 linki = name.find(" -> ")
      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)
      ))
  stdout.write(makeDirlist(items))
  stdout.write("\n</PRE>\n</BODY>\n</HTML>\n")

proc matchesPattern(s, pat: openArray[char]): bool =
  var i = 0
  for j, c in pat:
    if c == '*':
      while i < s.len:
        if s.toOpenArray(i, s.high).matchesPattern(pat.toOpenArray(j + 1,
            pat.high)):
          return true
        inc i
      return false
    if i >= s.len or c != '?' and c != s[i]:
      return false
    inc i
  return true

proc parseSSHConfig(f: File; curl: CURL; host: string; idSet: var bool) =
  var skipTillNext = false
  var line: string
  var certificateFile = ""
  var identityFile = ""
  while f.readLine(line):
    var i = line.skipBlanks(0)
    if i == line.len or line[i] == '#':
      continue
    let k = line.until(AsciiWhitespace, i)
    i = line.skipBlanks(i + k.len)
    if i < line.len and line[i] == '=':
      i = line.skipBlanks(i + 1)
    if i == line.len or line[i] == '#':
      continue
    var arg = ""
    let isStr = line[i] in {'"', '\''}
    if isStr:
      inc i
    var quot = false
    while i < line.len and (quot or line[i] != '"' or not isStr):
      if not quot and line[i] == '\\':
        quot = true
      else:
        arg &= line[i]
      inc i
    if k == "Match": #TODO support this
      skipTillNext = true
    elif k == "Host":
      skipTillNext = not host.matchesPattern(arg)
    elif skipTillNext:
      continue
    elif k == "IdentityFile":
      identityFile = expandTilde(arg)
    elif k == "CertificateFile":
      certificateFile = expandTilde(arg)
  if identityFile != "":
    curl.setopt(CURLOPT_SSH_PRIVATE_KEYFILE, identityFile)
    idSet = true
  if certificateFile != "":
    curl.setopt(CURLOPT_SSH_PUBLIC_KEYFILE, certificateFile)
  f.close()

proc main() =
  let curl = curl_easy_init()
  doAssert curl != nil
  var opath = getEnv("MAPPED_URI_PATH")
  if opath == "":
    opath = "/"
  let path = percentDecode(opath)
  let op = FtpHandle(
    curl: curl,
    dirmode: path.len > 0 and path[^1] == '/'
  )
  let url = curl_url()
  const flags = cuint(CURLU_PATH_AS_IS)
  let scheme = getEnv("MAPPED_URI_SCHEME")
  url.set(CURLUPART_SCHEME, scheme, flags)
  let username = getEnv("MAPPED_URI_USERNAME")
  if username != "":
    url.set(CURLUPART_USER, username, flags)
  let host = getEnv("MAPPED_URI_HOST")
  let password = getEnv("MAPPED_URI_PASSWORD")
  var idSet = false
  # Parse SSH config for sftp.
  if scheme == "sftp":
    let systemConfig = "/etc/ssh/ssh_config"
    if fileExists(systemConfig):
      var f: File
      if f.open(systemConfig):
        parseSSHConfig(f, curl, host, idSet)
    let userConfig = expandTilde("~/.ssh/config")
    if fileExists(userConfig):
      var f: File
      if f.open(userConfig):
        parseSSHConfig(f, curl, host, idSet)
    if idSet:
      curl.setopt(CURLOPT_KEYPASSWD, password)
  url.set(CURLUPART_PASSWORD, password, flags)
  url.set(CURLUPART_HOST, host, flags)
  let port = getEnv("MAPPED_URI_PORT")
  if port != "":
    url.set(CURLUPART_PORT, port, flags)
  # 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.
  #
  # But before that, we take the serialized URL without the path for
  # setting the base URL:
  url.set(CURLUPART_PATH, opath, flags)
  if op.dirmode:
    let surl = url.get(CURLUPART_URL, cuint(CURLU_PUNY2IDN))
    if surl == nil:
      stdout.write("Cha-Control: ConnectionError " & $int(ERROR_INVALID_URL))
      curl_url_cleanup(url)
      curl_easy_cleanup(curl)
      return
    op.base = $surl
    op.path = path
    curl_free(surl)
  # Now for the workaround:
  if scheme != "sftp" and (opath.len <= 1 or opath[1] != '~'):
    url.set(CURLUPART_PATH, '/' & opath, flags)
  # Another hack: if password was set for the identity file, then clear it from
  # the URL.
  if idSet:
    url.set(CURLUPART_PASSWORD, nil, flags)
  # Set opts for the request
  curl.setopt(CURLOPT_CURLU, url)
  curl.setopt(CURLOPT_HEADERDATA, op)
  curl.setopt(CURLOPT_HEADERFUNCTION, curlWriteHeader)
  curl.setopt(CURLOPT_WRITEDATA, op)
  curl.setopt(CURLOPT_WRITEFUNCTION, curlWriteBody)
  curl.setopt(CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_SINGLECWD)
  let purl = getEnv("ALL_PROXY")
  if purl != "":
    curl.setopt(CURLOPT_PROXY, purl)
  if getEnv("REQUEST_METHOD") != "GET":
    # fail
    let code = $int(ERROR_INVALID_METHOD)
    stdout.write("Cha-Control: ConnectionError " & $code & "\n")
  else:
    let res = curl_easy_perform(curl)
    if res != CURLE_OK:
      if not op.statusline:
        if res == CURLE_LOGIN_DENIED:
          stdout.write("Status: 401\n")
        else:
          stdout.write(getCurlConnectionError(res))
    elif op.dirmode:
      op.finish()
  curl_url_cleanup(url)
  curl_easy_cleanup(curl)

main()