Documented headers.lua Update copyright date everywhere Remove RCSID from files Move version back to 2.1 rather than 2.1.1 Fixed url package to support ipv6 hosts Changed "domain" to "family" in tcp and udp structures Implemented getfamily methods
280 lines
8.9 KiB
Lua
280 lines
8.9 KiB
Lua
-----------------------------------------------------------------------------
|
|
-- FTP support for the Lua language
|
|
-- LuaSocket toolkit.
|
|
-- Author: Diego Nehab
|
|
-----------------------------------------------------------------------------
|
|
|
|
-----------------------------------------------------------------------------
|
|
-- Declare module and import dependencies
|
|
-----------------------------------------------------------------------------
|
|
local base = _G
|
|
local table = require("table")
|
|
local string = require("string")
|
|
local math = require("math")
|
|
local socket = require("socket")
|
|
local url = require("socket.url")
|
|
local tp = require("socket.tp")
|
|
local ltn12 = require("ltn12")
|
|
module("socket.ftp")
|
|
|
|
-----------------------------------------------------------------------------
|
|
-- Program constants
|
|
-----------------------------------------------------------------------------
|
|
-- timeout in seconds before the program gives up on a connection
|
|
TIMEOUT = 60
|
|
-- default port for ftp service
|
|
PORT = 21
|
|
-- this is the default anonymous password. used when no password is
|
|
-- provided in url. should be changed to your e-mail.
|
|
USER = "ftp"
|
|
PASSWORD = "anonymous@anonymous.org"
|
|
|
|
-----------------------------------------------------------------------------
|
|
-- Low level FTP API
|
|
-----------------------------------------------------------------------------
|
|
local metat = { __index = {} }
|
|
|
|
function open(server, port, create)
|
|
local tp = socket.try(tp.connect(server, port or PORT, TIMEOUT, create))
|
|
local f = base.setmetatable({ tp = tp }, metat)
|
|
-- make sure everything gets closed in an exception
|
|
f.try = socket.newtry(function() f:close() end)
|
|
return f
|
|
end
|
|
|
|
function metat.__index:portconnect()
|
|
self.try(self.server:settimeout(TIMEOUT))
|
|
self.data = self.try(self.server:accept())
|
|
self.try(self.data:settimeout(TIMEOUT))
|
|
end
|
|
|
|
function metat.__index:pasvconnect()
|
|
self.data = self.try(socket.tcp())
|
|
self.try(self.data:settimeout(TIMEOUT))
|
|
self.try(self.data:connect(self.pasvt.ip, self.pasvt.port))
|
|
end
|
|
|
|
function metat.__index:login(user, password)
|
|
self.try(self.tp:command("user", user or USER))
|
|
local code, reply = self.try(self.tp:check{"2..", 331})
|
|
if code == 331 then
|
|
self.try(self.tp:command("pass", password or PASSWORD))
|
|
self.try(self.tp:check("2.."))
|
|
end
|
|
return 1
|
|
end
|
|
|
|
function metat.__index:pasv()
|
|
self.try(self.tp:command("pasv"))
|
|
local code, reply = self.try(self.tp:check("2.."))
|
|
local pattern = "(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)"
|
|
local a, b, c, d, p1, p2 = socket.skip(2, string.find(reply, pattern))
|
|
self.try(a and b and c and d and p1 and p2, reply)
|
|
self.pasvt = {
|
|
ip = string.format("%d.%d.%d.%d", a, b, c, d),
|
|
port = p1*256 + p2
|
|
}
|
|
if self.server then
|
|
self.server:close()
|
|
self.server = nil
|
|
end
|
|
return self.pasvt.ip, self.pasvt.port
|
|
end
|
|
|
|
function metat.__index:port(ip, port)
|
|
self.pasvt = nil
|
|
if not ip then
|
|
ip, port = self.try(self.tp:getcontrol():getsockname())
|
|
self.server = self.try(socket.bind(ip, 0))
|
|
ip, port = self.try(self.server:getsockname())
|
|
self.try(self.server:settimeout(TIMEOUT))
|
|
end
|
|
local pl = math.mod(port, 256)
|
|
local ph = (port - pl)/256
|
|
local arg = string.gsub(string.format("%s,%d,%d", ip, ph, pl), "%.", ",")
|
|
self.try(self.tp:command("port", arg))
|
|
self.try(self.tp:check("2.."))
|
|
return 1
|
|
end
|
|
|
|
function metat.__index:send(sendt)
|
|
self.try(self.pasvt or self.server, "need port or pasv first")
|
|
-- if there is a pasvt table, we already sent a PASV command
|
|
-- we just get the data connection into self.data
|
|
if self.pasvt then self:pasvconnect() end
|
|
-- get the transfer argument and command
|
|
local argument = sendt.argument or
|
|
url.unescape(string.gsub(sendt.path or "", "^[/\\]", ""))
|
|
if argument == "" then argument = nil end
|
|
local command = sendt.command or "stor"
|
|
-- send the transfer command and check the reply
|
|
self.try(self.tp:command(command, argument))
|
|
local code, reply = self.try(self.tp:check{"2..", "1.."})
|
|
-- if there is not a a pasvt table, then there is a server
|
|
-- and we already sent a PORT command
|
|
if not self.pasvt then self:portconnect() end
|
|
-- get the sink, source and step for the transfer
|
|
local step = sendt.step or ltn12.pump.step
|
|
local readt = {self.tp.c}
|
|
local checkstep = function(src, snk)
|
|
-- check status in control connection while downloading
|
|
local readyt = socket.select(readt, nil, 0)
|
|
if readyt[tp] then code = self.try(self.tp:check("2..")) end
|
|
return step(src, snk)
|
|
end
|
|
local sink = socket.sink("close-when-done", self.data)
|
|
-- transfer all data and check error
|
|
self.try(ltn12.pump.all(sendt.source, sink, checkstep))
|
|
if string.find(code, "1..") then self.try(self.tp:check("2..")) end
|
|
-- done with data connection
|
|
self.data:close()
|
|
-- find out how many bytes were sent
|
|
local sent = socket.skip(1, self.data:getstats())
|
|
self.data = nil
|
|
return sent
|
|
end
|
|
|
|
function metat.__index:receive(recvt)
|
|
self.try(self.pasvt or self.server, "need port or pasv first")
|
|
if self.pasvt then self:pasvconnect() end
|
|
local argument = recvt.argument or
|
|
url.unescape(string.gsub(recvt.path or "", "^[/\\]", ""))
|
|
if argument == "" then argument = nil end
|
|
local command = recvt.command or "retr"
|
|
self.try(self.tp:command(command, argument))
|
|
local code = self.try(self.tp:check{"1..", "2.."})
|
|
if not self.pasvt then self:portconnect() end
|
|
local source = socket.source("until-closed", self.data)
|
|
local step = recvt.step or ltn12.pump.step
|
|
self.try(ltn12.pump.all(source, recvt.sink, step))
|
|
if string.find(code, "1..") then self.try(self.tp:check("2..")) end
|
|
self.data:close()
|
|
self.data = nil
|
|
return 1
|
|
end
|
|
|
|
function metat.__index:cwd(dir)
|
|
self.try(self.tp:command("cwd", dir))
|
|
self.try(self.tp:check(250))
|
|
return 1
|
|
end
|
|
|
|
function metat.__index:type(type)
|
|
self.try(self.tp:command("type", type))
|
|
self.try(self.tp:check(200))
|
|
return 1
|
|
end
|
|
|
|
function metat.__index:greet()
|
|
local code = self.try(self.tp:check{"1..", "2.."})
|
|
if string.find(code, "1..") then self.try(self.tp:check("2..")) end
|
|
return 1
|
|
end
|
|
|
|
function metat.__index:quit()
|
|
self.try(self.tp:command("quit"))
|
|
self.try(self.tp:check("2.."))
|
|
return 1
|
|
end
|
|
|
|
function metat.__index:close()
|
|
if self.data then self.data:close() end
|
|
if self.server then self.server:close() end
|
|
return self.tp:close()
|
|
end
|
|
|
|
-----------------------------------------------------------------------------
|
|
-- High level FTP API
|
|
-----------------------------------------------------------------------------
|
|
local function override(t)
|
|
if t.url then
|
|
local u = url.parse(t.url)
|
|
for i,v in base.pairs(t) do
|
|
u[i] = v
|
|
end
|
|
return u
|
|
else return t end
|
|
end
|
|
|
|
local function tput(putt)
|
|
putt = override(putt)
|
|
socket.try(putt.host, "missing hostname")
|
|
local f = open(putt.host, putt.port, putt.create)
|
|
f:greet()
|
|
f:login(putt.user, putt.password)
|
|
if putt.type then f:type(putt.type) end
|
|
f:pasv()
|
|
local sent = f:send(putt)
|
|
f:quit()
|
|
f:close()
|
|
return sent
|
|
end
|
|
|
|
local default = {
|
|
path = "/",
|
|
scheme = "ftp"
|
|
}
|
|
|
|
local function parse(u)
|
|
local t = socket.try(url.parse(u, default))
|
|
socket.try(t.scheme == "ftp", "wrong scheme '" .. t.scheme .. "'")
|
|
socket.try(t.host, "missing hostname")
|
|
local pat = "^type=(.)$"
|
|
if t.params then
|
|
t.type = socket.skip(2, string.find(t.params, pat))
|
|
socket.try(t.type == "a" or t.type == "i",
|
|
"invalid type '" .. t.type .. "'")
|
|
end
|
|
return t
|
|
end
|
|
|
|
local function sput(u, body)
|
|
local putt = parse(u)
|
|
putt.source = ltn12.source.string(body)
|
|
return tput(putt)
|
|
end
|
|
|
|
put = socket.protect(function(putt, body)
|
|
if base.type(putt) == "string" then return sput(putt, body)
|
|
else return tput(putt) end
|
|
end)
|
|
|
|
local function tget(gett)
|
|
gett = override(gett)
|
|
socket.try(gett.host, "missing hostname")
|
|
local f = open(gett.host, gett.port, gett.create)
|
|
f:greet()
|
|
f:login(gett.user, gett.password)
|
|
if gett.type then f:type(gett.type) end
|
|
f:pasv()
|
|
f:receive(gett)
|
|
f:quit()
|
|
return f:close()
|
|
end
|
|
|
|
local function sget(u)
|
|
local gett = parse(u)
|
|
local t = {}
|
|
gett.sink = ltn12.sink.table(t)
|
|
tget(gett)
|
|
return table.concat(t)
|
|
end
|
|
|
|
command = socket.protect(function(cmdt)
|
|
cmdt = override(cmdt)
|
|
socket.try(cmdt.host, "missing hostname")
|
|
socket.try(cmdt.command, "missing command")
|
|
local f = open(cmdt.host, cmdt.port, cmdt.create)
|
|
f:greet()
|
|
f:login(cmdt.user, cmdt.password)
|
|
f.try(f.tp:command(cmdt.command, cmdt.argument))
|
|
if cmdt.check then f.try(f.tp:check(cmdt.check)) end
|
|
f:quit()
|
|
return f:close()
|
|
end)
|
|
|
|
get = socket.protect(function(gett)
|
|
if base.type(gett) == "string" then return sget(gett)
|
|
else return tget(gett) end
|
|
end)
|
|
|