Family agostic FTP and expose HTTP/FTP url parsing

This commit is contained in:
Diego Nehab 2016-03-07 01:30:30 -03:00
parent 5b4b915879
commit 916b548240
4 changed files with 96 additions and 32 deletions

View File

@ -51,7 +51,7 @@ end
function metat.__index:pasvconnect() function metat.__index:pasvconnect()
self.data = self.try(socket.tcp()) self.data = self.try(socket.tcp())
self.try(self.data:settimeout(_M.TIMEOUT)) self.try(self.data:settimeout(_M.TIMEOUT))
self.try(self.data:connect(self.pasvt.ip, self.pasvt.port)) self.try(self.data:connect(self.pasvt.address, self.pasvt.port))
end end
function metat.__index:login(user, password) function metat.__index:login(user, password)
@ -71,32 +71,65 @@ function metat.__index:pasv()
local a, b, c, d, p1, p2 = socket.skip(2, string.find(reply, pattern)) 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.try(a and b and c and d and p1 and p2, reply)
self.pasvt = { self.pasvt = {
ip = string.format("%d.%d.%d.%d", a, b, c, d), address = string.format("%d.%d.%d.%d", a, b, c, d),
port = p1*256 + p2 port = p1*256 + p2
} }
if self.server then if self.server then
self.server:close() self.server:close()
self.server = nil self.server = nil
end end
return self.pasvt.ip, self.pasvt.port return self.pasvt.address, self.pasvt.port
end end
function metat.__index:port(ip, port) function metat.__index:epsv()
self.try(self.tp:command("epsv"))
local code, reply = self.try(self.tp:check("229"))
local pattern = "%((.)(.-)%1(.-)%1(.-)%1%)"
local d, prt, address, port = string.match(reply, pattern)
self.try(port, "invalid epsv response")
self.pasvt = {
address = self.tp:getpeername(),
port = port
}
if self.server then
self.server:close()
self.server = nil
end
return self.pasvt.address, self.pasvt.port
end
function metat.__index:port(address, port)
self.pasvt = nil self.pasvt = nil
if not ip then if not address then
ip, port = self.try(self.tp:getcontrol():getsockname()) address, port = self.try(self.tp:getsockname())
self.server = self.try(socket.bind(ip, 0)) self.server = self.try(socket.bind(address, 0))
ip, port = self.try(self.server:getsockname()) address, port = self.try(self.server:getsockname())
self.try(self.server:settimeout(_M.TIMEOUT)) self.try(self.server:settimeout(_M.TIMEOUT))
end end
local pl = math.mod(port, 256) local pl = math.mod(port, 256)
local ph = (port - pl)/256 local ph = (port - pl)/256
local arg = string.gsub(string.format("%s,%d,%d", ip, ph, pl), "%.", ",") local arg = string.gsub(string.format("%s,%d,%d", address, ph, pl), "%.", ",")
self.try(self.tp:command("port", arg)) self.try(self.tp:command("port", arg))
self.try(self.tp:check("2..")) self.try(self.tp:check("2.."))
return 1 return 1
end end
function metat.__index:eprt(family, address, port)
self.pasvt = nil
if not address then
address, port = self.try(self.tp:getsockname())
self.server = self.try(socket.bind(address, 0))
address, port = self.try(self.server:getsockname())
self.try(self.server:settimeout(_M.TIMEOUT))
end
local arg = string.format("|%s|%s|%d|", family, address, port)
self.try(self.tp:command("eprt", arg))
self.try(self.tp:check("2.."))
return 1
end
function metat.__index:send(sendt) function metat.__index:send(sendt)
self.try(self.pasvt or self.server, "need port or pasv first") self.try(self.pasvt or self.server, "need port or pasv first")
-- if there is a pasvt table, we already sent a PASV command -- if there is a pasvt table, we already sent a PASV command
@ -110,12 +143,12 @@ function metat.__index:send(sendt)
-- send the transfer command and check the reply -- send the transfer command and check the reply
self.try(self.tp:command(command, argument)) self.try(self.tp:command(command, argument))
local code, reply = self.try(self.tp:check{"2..", "1.."}) local code, reply = self.try(self.tp:check{"2..", "1.."})
-- if there is not a a pasvt table, then there is a server -- if there is not a pasvt table, then there is a server
-- and we already sent a PORT command -- and we already sent a PORT command
if not self.pasvt then self:portconnect() end if not self.pasvt then self:portconnect() end
-- get the sink, source and step for the transfer -- get the sink, source and step for the transfer
local step = sendt.step or ltn12.pump.step local step = sendt.step or ltn12.pump.step
local readt = {self.tp.c} local readt = { self.tp }
local checkstep = function(src, snk) local checkstep = function(src, snk)
-- check status in control connection while downloading -- check status in control connection while downloading
local readyt = socket.select(readt, nil, 0) local readyt = socket.select(readt, nil, 0)
@ -207,7 +240,7 @@ local function tput(putt)
f:greet() f:greet()
f:login(putt.user, putt.password) f:login(putt.user, putt.password)
if putt.type then f:type(putt.type) end if putt.type then f:type(putt.type) end
f:pasv() f:epsv()
local sent = f:send(putt) local sent = f:send(putt)
f:quit() f:quit()
f:close() f:close()
@ -219,7 +252,7 @@ local default = {
scheme = "ftp" scheme = "ftp"
} }
local function parse(u) local function genericform(u)
local t = socket.try(url.parse(u, default)) local t = socket.try(url.parse(u, default))
socket.try(t.scheme == "ftp", "wrong scheme '" .. t.scheme .. "'") socket.try(t.scheme == "ftp", "wrong scheme '" .. t.scheme .. "'")
socket.try(t.host, "missing hostname") socket.try(t.host, "missing hostname")
@ -232,8 +265,10 @@ local function parse(u)
return t return t
end end
_M.genericform = genericform
local function sput(u, body) local function sput(u, body)
local putt = parse(u) local putt = genericform(u)
putt.source = ltn12.source.string(body) putt.source = ltn12.source.string(body)
return tput(putt) return tput(putt)
end end
@ -250,14 +285,14 @@ local function tget(gett)
f:greet() f:greet()
f:login(gett.user, gett.password) f:login(gett.user, gett.password)
if gett.type then f:type(gett.type) end if gett.type then f:type(gett.type) end
f:pasv() f:epsv()
f:receive(gett) f:receive(gett)
f:quit() f:quit()
return f:close() return f:close()
end end
local function sget(u) local function sget(u)
local gett = parse(u) local gett = genericform(u)
local t = {} local t = {}
gett.sink = ltn12.sink.table(t) gett.sink = ltn12.sink.table(t)
tget(gett) tget(gett)

View File

@ -346,11 +346,13 @@ end
return 1, code, headers, status return 1, code, headers, status
end end
local function srequest(u, b) -- turns an url and a body into a generic request
local function genericform(u, b)
local t = {} local t = {}
local reqt = { local reqt = {
url = u, url = u,
sink = ltn12.sink.table(t) sink = ltn12.sink.table(t),
target = t
} }
if b then if b then
reqt.source = ltn12.source.string(b) reqt.source = ltn12.source.string(b)
@ -360,8 +362,15 @@ local function srequest(u, b)
} }
reqt.method = "POST" reqt.method = "POST"
end end
local code, headers, status = socket.skip(1, trequest(reqt)) return reqt
return table.concat(t), code, headers, status end
_M.genericform = genericform
local function srequest(u, b)
local reqt = genericform(u, b)
local _, code, headers, status = trequest(reqt)
return table.concat(reqt.target), code, headers, status
end end
_M.request = socket.protect(function(reqt, body) _M.request = socket.protect(function(reqt, body)

View File

@ -46,6 +46,14 @@ end
-- metatable for sock object -- metatable for sock object
local metat = { __index = {} } local metat = { __index = {} }
function metat.__index:getpeername()
return self.c:getpeername()
end
function metat.__index:getsockname()
return self.c:getpeername()
end
function metat.__index:check(ok) function metat.__index:check(ok)
local code, reply = get_reply(self.c) local code, reply = get_reply(self.c)
if not code then return nil, reply end if not code then return nil, reply end
@ -123,4 +131,4 @@ function _M.connect(host, port, timeout, create)
return base.setmetatable({c = c}, metat) return base.setmetatable({c = c}, metat)
end end
return _M return _M

View File

@ -3,19 +3,31 @@ local ftp = require("socket.ftp")
local url = require("socket.url") local url = require("socket.url")
local ltn12 = require("ltn12") local ltn12 = require("ltn12")
-- use dscl to create user "luasocket" with password "password"
-- with home in /Users/diego/luasocket/test/ftp
-- with group com.apple.access_ftp
-- with shell set to /sbin/nologin
-- set /etc/ftpchroot to chroot luasocket
-- must set group com.apple.access_ftp on user _ftp (for anonymous access)
-- copy index.html to /var/empty/pub (home of user ftp)
-- start ftp server with
-- sudo -s launchctl load -w /System/Library/LaunchDaemons/ftp.plist
-- copy index.html to /Users/diego/luasocket/test/ftp
-- stop with
-- sudo -s launchctl unload -w /System/Library/LaunchDaemons/ftp.plist
-- override protection to make sure we see all errors -- override protection to make sure we see all errors
--socket.protect = function(s) return s end --socket.protect = function(s) return s end
dofile("testsupport.lua") dofile("testsupport.lua")
local host, port, index_file, index, back, err, ret local host = host or "localhost"
local port, index_file, index, back, err, ret
local t = socket.gettime() local t = socket.gettime()
host = host or "localhost"
index_file = "index.html" index_file = "index.html"
-- a function that returns a directory listing -- a function that returns a directory listing
local function nlst(u) local function nlst(u)
local t = {} local t = {}
@ -55,27 +67,27 @@ assert(not err and back == index, err)
print("ok") print("ok")
io.write("erasing before upload: ") io.write("erasing before upload: ")
ret, err = dele("ftp://luasocket:pedrovian@" .. host .. "/index.up.html") ret, err = dele("ftp://luasocket:password@" .. host .. "/index.up.html")
if not ret then print(err) if not ret then print(err)
else print("ok") end else print("ok") end
io.write("testing upload: ") io.write("testing upload: ")
ret, err = ftp.put("ftp://luasocket:pedrovian@" .. host .. "/index.up.html;type=i", index) ret, err = ftp.put("ftp://luasocket:password@" .. host .. "/index.up.html;type=i", index)
assert(ret and not err, err) assert(ret and not err, err)
print("ok") print("ok")
io.write("downloading uploaded file: ") io.write("downloading uploaded file: ")
back, err = ftp.get("ftp://luasocket:pedrovian@" .. host .. "/index.up.html;type=i") back, err = ftp.get("ftp://luasocket:password@" .. host .. "/index.up.html;type=i")
assert(ret and not err and index == back, err) assert(ret and not err and index == back, err)
print("ok") print("ok")
io.write("erasing after upload/download: ") io.write("erasing after upload/download: ")
ret, err = dele("ftp://luasocket:pedrovian@" .. host .. "/index.up.html") ret, err = dele("ftp://luasocket:password@" .. host .. "/index.up.html")
assert(ret and not err, err) assert(ret and not err, err)
print("ok") print("ok")
io.write("testing weird-character translation: ") io.write("testing weird-character translation: ")
back, err = ftp.get("ftp://luasocket:pedrovian@" .. host .. "/%23%3f;type=i") back, err = ftp.get("ftp://luasocket:password@" .. host .. "/%23%3f;type=i")
assert(not err and back == index, err) assert(not err and back == index, err)
print("ok") print("ok")
@ -84,7 +96,7 @@ local back = {}
ret, err = ftp.get{ ret, err = ftp.get{
url = "//stupid:mistake@" .. host .. "/index.html", url = "//stupid:mistake@" .. host .. "/index.html",
user = "luasocket", user = "luasocket",
password = "pedrovian", password = "password",
type = "i", type = "i",
sink = ltn12.sink.table(back) sink = ltn12.sink.table(back)
} }