1----------------------------------------------------------------------------- 2-- FTP support for the Lua language 3-- LuaSocket toolkit. 4-- Author: Diego Nehab 5----------------------------------------------------------------------------- 6 7----------------------------------------------------------------------------- 8-- Declare module and import dependencies 9----------------------------------------------------------------------------- 10local base = _G 11local table = require("table") 12local string = require("string") 13local math = require("math") 14local socket = require("socket") 15local url = require("socket.url") 16local tp = require("socket.tp") 17local ltn12 = require("ltn12") 18socket.ftp = {} 19local _M = socket.ftp 20----------------------------------------------------------------------------- 21-- Program constants 22----------------------------------------------------------------------------- 23-- timeout in seconds before the program gives up on a connection 24_M.TIMEOUT = 60 25-- default port for ftp service 26_M.PORT = 21 27-- this is the default anonymous password. used when no password is 28-- provided in url. should be changed to your e-mail. 29_M.USER = "ftp" 30_M.PASSWORD = "anonymous@anonymous.org" 31 32----------------------------------------------------------------------------- 33-- Low level FTP API 34----------------------------------------------------------------------------- 35local metat = { __index = {} } 36 37function _M.open(server, port, create) 38 local tp = socket.try(tp.connect(server, port or _M.PORT, _M.TIMEOUT, create)) 39 local f = base.setmetatable({ tp = tp }, metat) 40 -- make sure everything gets closed in an exception 41 f.try = socket.newtry(function() f:close() end) 42 return f 43end 44 45function metat.__index:portconnect() 46 self.try(self.server:settimeout(_M.TIMEOUT)) 47 self.data = self.try(self.server:accept()) 48 self.try(self.data:settimeout(_M.TIMEOUT)) 49end 50 51function metat.__index:pasvconnect() 52 self.data = self.try(socket.tcp()) 53 self.try(self.data:settimeout(_M.TIMEOUT)) 54 self.try(self.data:connect(self.pasvt.ip, self.pasvt.port)) 55end 56 57function metat.__index:login(user, password) 58 self.try(self.tp:command("user", user or _M.USER)) 59 local code, reply = self.try(self.tp:check{"2..", 331}) 60 if code == 331 then 61 self.try(self.tp:command("pass", password or _M.PASSWORD)) 62 self.try(self.tp:check("2..")) 63 end 64 return 1 65end 66 67function metat.__index:pasv() 68 self.try(self.tp:command("pasv")) 69 local code, reply = self.try(self.tp:check("2..")) 70 local pattern = "(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)" 71 local a, b, c, d, p1, p2 = socket.skip(2, string.find(reply, pattern)) 72 self.try(a and b and c and d and p1 and p2, reply) 73 self.pasvt = { 74 ip = string.format("%d.%d.%d.%d", a, b, c, d), 75 port = p1*256 + p2 76 } 77 if self.server then 78 self.server:close() 79 self.server = nil 80 end 81 return self.pasvt.ip, self.pasvt.port 82end 83 84function metat.__index:port(ip, port) 85 self.pasvt = nil 86 if not ip then 87 ip, port = self.try(self.tp:getcontrol():getsockname()) 88 self.server = self.try(socket.bind(ip, 0)) 89 ip, port = self.try(self.server:getsockname()) 90 self.try(self.server:settimeout(_M.TIMEOUT)) 91 end 92 local pl = math.mod(port, 256) 93 local ph = (port - pl)/256 94 local arg = string.gsub(string.format("%s,%d,%d", ip, ph, pl), "%.", ",") 95 self.try(self.tp:command("port", arg)) 96 self.try(self.tp:check("2..")) 97 return 1 98end 99 100function metat.__index:send(sendt) 101 self.try(self.pasvt or self.server, "need port or pasv first") 102 -- if there is a pasvt table, we already sent a PASV command 103 -- we just get the data connection into self.data 104 if self.pasvt then self:pasvconnect() end 105 -- get the transfer argument and command 106 local argument = sendt.argument or 107 url.unescape(string.gsub(sendt.path or "", "^[/\\]", "")) 108 if argument == "" then argument = nil end 109 local command = sendt.command or "stor" 110 -- send the transfer command and check the reply 111 self.try(self.tp:command(command, argument)) 112 local code, reply = self.try(self.tp:check{"2..", "1.."}) 113 -- if there is not a a pasvt table, then there is a server 114 -- and we already sent a PORT command 115 if not self.pasvt then self:portconnect() end 116 -- get the sink, source and step for the transfer 117 local step = sendt.step or ltn12.pump.step 118 local readt = {self.tp.c} 119 local checkstep = function(src, snk) 120 -- check status in control connection while downloading 121 local readyt = socket.select(readt, nil, 0) 122 if readyt[tp] then code = self.try(self.tp:check("2..")) end 123 return step(src, snk) 124 end 125 local sink = socket.sink("close-when-done", self.data) 126 -- transfer all data and check error 127 self.try(ltn12.pump.all(sendt.source, sink, checkstep)) 128 if string.find(code, "1..") then self.try(self.tp:check("2..")) end 129 -- done with data connection 130 self.data:close() 131 -- find out how many bytes were sent 132 local sent = socket.skip(1, self.data:getstats()) 133 self.data = nil 134 return sent 135end 136 137function metat.__index:receive(recvt) 138 self.try(self.pasvt or self.server, "need port or pasv first") 139 if self.pasvt then self:pasvconnect() end 140 local argument = recvt.argument or 141 url.unescape(string.gsub(recvt.path or "", "^[/\\]", "")) 142 if argument == "" then argument = nil end 143 local command = recvt.command or "retr" 144 self.try(self.tp:command(command, argument)) 145 local code,reply = self.try(self.tp:check{"1..", "2.."}) 146 if (code >= 200) and (code <= 299) then 147 recvt.sink(reply) 148 return 1 149 end 150 if not self.pasvt then self:portconnect() end 151 local source = socket.source("until-closed", self.data) 152 local step = recvt.step or ltn12.pump.step 153 self.try(ltn12.pump.all(source, recvt.sink, step)) 154 if string.find(code, "1..") then self.try(self.tp:check("2..")) end 155 self.data:close() 156 self.data = nil 157 return 1 158end 159 160function metat.__index:cwd(dir) 161 self.try(self.tp:command("cwd", dir)) 162 self.try(self.tp:check(250)) 163 return 1 164end 165 166function metat.__index:type(type) 167 self.try(self.tp:command("type", type)) 168 self.try(self.tp:check(200)) 169 return 1 170end 171 172function metat.__index:greet() 173 local code = self.try(self.tp:check{"1..", "2.."}) 174 if string.find(code, "1..") then self.try(self.tp:check("2..")) end 175 return 1 176end 177 178function metat.__index:quit() 179 self.try(self.tp:command("quit")) 180 self.try(self.tp:check("2..")) 181 return 1 182end 183 184function metat.__index:close() 185 if self.data then self.data:close() end 186 if self.server then self.server:close() end 187 return self.tp:close() 188end 189 190----------------------------------------------------------------------------- 191-- High level FTP API 192----------------------------------------------------------------------------- 193local function override(t) 194 if t.url then 195 local u = url.parse(t.url) 196 for i,v in base.pairs(t) do 197 u[i] = v 198 end 199 return u 200 else return t end 201end 202 203local function tput(putt) 204 putt = override(putt) 205 socket.try(putt.host, "missing hostname") 206 local f = _M.open(putt.host, putt.port, putt.create) 207 f:greet() 208 f:login(putt.user, putt.password) 209 if putt.type then f:type(putt.type) end 210 f:pasv() 211 local sent = f:send(putt) 212 f:quit() 213 f:close() 214 return sent 215end 216 217local default = { 218 path = "/", 219 scheme = "ftp" 220} 221 222local function parse(u) 223 local t = socket.try(url.parse(u, default)) 224 socket.try(t.scheme == "ftp", "wrong scheme '" .. t.scheme .. "'") 225 socket.try(t.host, "missing hostname") 226 local pat = "^type=(.)$" 227 if t.params then 228 t.type = socket.skip(2, string.find(t.params, pat)) 229 socket.try(t.type == "a" or t.type == "i", 230 "invalid type '" .. t.type .. "'") 231 end 232 return t 233end 234 235local function sput(u, body) 236 local putt = parse(u) 237 putt.source = ltn12.source.string(body) 238 return tput(putt) 239end 240 241_M.put = socket.protect(function(putt, body) 242 if base.type(putt) == "string" then return sput(putt, body) 243 else return tput(putt) end 244end) 245 246local function tget(gett) 247 gett = override(gett) 248 socket.try(gett.host, "missing hostname") 249 local f = _M.open(gett.host, gett.port, gett.create) 250 f:greet() 251 f:login(gett.user, gett.password) 252 if gett.type then f:type(gett.type) end 253 f:pasv() 254 f:receive(gett) 255 f:quit() 256 return f:close() 257end 258 259local function sget(u) 260 local gett = parse(u) 261 local t = {} 262 gett.sink = ltn12.sink.table(t) 263 tget(gett) 264 return table.concat(t) 265end 266 267_M.command = socket.protect(function(cmdt) 268 cmdt = override(cmdt) 269 socket.try(cmdt.host, "missing hostname") 270 socket.try(cmdt.command, "missing command") 271 local f = open(cmdt.host, cmdt.port, cmdt.create) 272 f:greet() 273 f:login(cmdt.user, cmdt.password) 274 f.try(f.tp:command(cmdt.command, cmdt.argument)) 275 if cmdt.check then f.try(f.tp:check(cmdt.check)) end 276 f:quit() 277 return f:close() 278end) 279 280_M.get = socket.protect(function(gett) 281 if base.type(gett) == "string" then return sget(gett) 282 else return tget(gett) end 283end) 284 285return _M