|
|
|
@ -1,8 +1,10 @@
|
|
|
|
|
--[[SPLIT MODULE ftp]]
|
|
|
|
|
|
|
|
|
|
--[[ A simple ftp server
|
|
|
|
|
|
|
|
|
|
This is my implementation of a FTP server using Github user Neronix's
|
|
|
|
|
example as inspriration, but as a cleaner Lua implementation that has been
|
|
|
|
|
optimised for use in LFS. The coding style adopted here is more similar to
|
|
|
|
|
example as inspriration, but as a cleaner Lua implementation that is
|
|
|
|
|
suitable for use in LFS. The coding style adopted here is more similar to
|
|
|
|
|
best practice for normal (PC) module implementations, as using LFS enables
|
|
|
|
|
me to bias towards clarity of coding over brevity. It includes extra logic
|
|
|
|
|
to handle some of the edge case issues more robustly. It also uses a
|
|
|
|
@ -13,113 +15,116 @@
|
|
|
|
|
with any multiple calls requected, so FTP is a singleton static object.
|
|
|
|
|
However there is nothing to stop multiple clients connecting to the FTP
|
|
|
|
|
listener at the same time, and indeed some FTP clients do use multiple
|
|
|
|
|
connections, so this server can accept and create multiple CON objects.
|
|
|
|
|
Each CON object can also have a single DATA connection.
|
|
|
|
|
connections, so this server can accept and create multiple cxt objects.
|
|
|
|
|
Each cxt object can also have a single DATA connection.
|
|
|
|
|
|
|
|
|
|
Note that FTP also exposes a number of really private properties (which
|
|
|
|
|
could be stores in local / upvals) as FTP properties for debug purposes.
|
|
|
|
|
|
|
|
|
|
Note that this version has now been updated to allow the main methods to
|
|
|
|
|
be optionally loaded lazily, and SPILT comments allow the source to be
|
|
|
|
|
preprocessed for loading as either components in the "fast" Cmodule or as
|
|
|
|
|
LC files in SPIFFS.
|
|
|
|
|
]]
|
|
|
|
|
local file, net, wifi, node, table, tmr, pairs, print, pcall, tostring =
|
|
|
|
|
file, net, wifi, node, table, tmr, pairs, print, pcall, tostring
|
|
|
|
|
local post = node.task.post
|
|
|
|
|
local FTP, cnt = {client = {}}, 0
|
|
|
|
|
--luacheck: read globals fast file net node tmr uart wifi FAST_ftp SPIFFS_ftp
|
|
|
|
|
|
|
|
|
|
-- Local functions
|
|
|
|
|
local FTP, FTPindex = {client = {}}, nil
|
|
|
|
|
|
|
|
|
|
local processCommand -- function(cxt, sock, data)
|
|
|
|
|
local processBareCmds -- function(cxt, cmd)
|
|
|
|
|
local processSimpleCmds -- function(cxt, cmd, arg)
|
|
|
|
|
local processDataCmds -- function(cxt, cmd, arg)
|
|
|
|
|
local dataServer -- function(cxt, n)
|
|
|
|
|
local ftpDataOpen -- function(dataSocket)
|
|
|
|
|
if FAST_ftp then
|
|
|
|
|
function FTPindex(_, name) return fast.load('ftp-'..name) end
|
|
|
|
|
elseif SPIFFS_ftp then
|
|
|
|
|
function FTPindex(_, name) return loadfile('ftp-'..name..'.lc') end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Note these routines all used hoisted locals such as table and debug as
|
|
|
|
|
-- upvals for performance (ROTable lookup is slow on NodeMCU Lua), but
|
|
|
|
|
-- data upvals (e.g. FTP) are explicitly list is -- "upval:" comments.
|
|
|
|
|
if FTPindex then return setmetatable(FTP,{__index=FTPindex}) end
|
|
|
|
|
|
|
|
|
|
-- Note that the space between debug and the arglist is there for a reason
|
|
|
|
|
-- so that a simple global edit " debug(" -> "-- debug(" or v.v. to
|
|
|
|
|
-- toggle debug compiled into the module.
|
|
|
|
|
function FTP.open(...) --[[SPLIT HERE ftp-open]]
|
|
|
|
|
--------------------------- Set up the FTP object ----------------------------
|
|
|
|
|
-- FTP has three static methods: open, createServer and close
|
|
|
|
|
------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
local function debug (fmt, ...) -- upval: cnt (, print, node, tmr)
|
|
|
|
|
if not FTP.debug then return end
|
|
|
|
|
-- optional wrapper around createServer() which also starts the wifi session
|
|
|
|
|
-- Lua: FTP:open(user, pass, ssid, pwd[, dbgFlag])
|
|
|
|
|
|
|
|
|
|
local this, user, pass, ssid, pwd, dbgFlag = ...
|
|
|
|
|
|
|
|
|
|
if ssid then
|
|
|
|
|
wifi.setmode(wifi.STATION, false)
|
|
|
|
|
wifi.sta.config { ssid = ssid, pwd = pwd, save = false }
|
|
|
|
|
end
|
|
|
|
|
tmr.create():alarm(500, tmr.ALARM_AUTO, function(t) -- this: FTP, user, pass, dbgFlag
|
|
|
|
|
if (wifi.sta.status() == wifi.STA_GOTIP) then
|
|
|
|
|
t:unregister()
|
|
|
|
|
print("Welcome to NodeMCU world", node.heap(), wifi.sta.getip())
|
|
|
|
|
return this:createServer(user, pass, dbgFlag)
|
|
|
|
|
else
|
|
|
|
|
uart.write(0,".")
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
end --[[SPLIT IGNORE]]
|
|
|
|
|
function FTP.createServer(...) --[[SPLIT HERE ftp-createServer]]
|
|
|
|
|
-- Lua: FTP:createServer(user, pass[, dbgFlag])
|
|
|
|
|
local this, user, pass, dbgFlag = ...
|
|
|
|
|
local cnt = 0
|
|
|
|
|
this.user, this.pass, dbgFlag = user, pass, (dbgFlag and true or false)
|
|
|
|
|
|
|
|
|
|
this.debug = (not dbgFlag) and type -- executing type(...) is in effect a NOOP
|
|
|
|
|
or function(fmt, ...) -- upval: cnt
|
|
|
|
|
if (...) then fmt = fmt:format(...) end
|
|
|
|
|
print(node.heap(),fmt)
|
|
|
|
|
cnt = cnt + 1
|
|
|
|
|
if cnt % 10 then tmr.wdclr() end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
--------------------------- Set up the FTP object ----------------------------
|
|
|
|
|
-- FTP has three static methods: open, createServer and close
|
|
|
|
|
------------------------------------------------------------------------------
|
|
|
|
|
this.server = net.createServer(net.TCP, 180)
|
|
|
|
|
_G.FTP = this
|
|
|
|
|
this.debug("Server created: (userdata) %s", tostring(this.server))
|
|
|
|
|
|
|
|
|
|
-- optional wrapper around createServer() which also starts the wifi session
|
|
|
|
|
function FTP.open(user, pass, ssid, pwd, dbgFlag) -- upval: FTP (, wifi, tmr, print)
|
|
|
|
|
if ssid then
|
|
|
|
|
wifi.setmode(wifi.STATION, false)
|
|
|
|
|
wifi.sta.config { ssid = ssid, pwd = pwd, save = false }
|
|
|
|
|
end
|
|
|
|
|
local t = tmr.create()
|
|
|
|
|
t:alarm(500, tmr.ALARM_AUTO, function()
|
|
|
|
|
if (wifi.sta.status() == wifi.STA_GOTIP) then
|
|
|
|
|
t:unregister()
|
|
|
|
|
t=nil
|
|
|
|
|
print("Welcome to NodeMCU world", node.heap(), wifi.sta.getip())
|
|
|
|
|
return FTP.createServer(user, pass, dbgFlag)
|
|
|
|
|
else
|
|
|
|
|
uart.write(0,".")
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function FTP.createServer(user, pass, dbgFlag) -- upval: FTP (, debug, tostring, pcall, type, processCommand)
|
|
|
|
|
FTP.user, FTP.pass, FTP.debug = user, pass, dbgFlag
|
|
|
|
|
FTP.server = net.createServer(net.TCP, 180)
|
|
|
|
|
_G.FTP = FTP
|
|
|
|
|
debug("Server created: (userdata) %s", tostring(FTP.server))
|
|
|
|
|
|
|
|
|
|
FTP.server:listen(21, function(sock) -- upval: FTP (, debug, pcall, type, processCommand)
|
|
|
|
|
this.server:listen(21, function(sock) -- upval: this
|
|
|
|
|
-- since a server can have multiple connections, each connection
|
|
|
|
|
-- has a CNX table to store connection-wide globals.
|
|
|
|
|
local client = FTP.client
|
|
|
|
|
local CNX; CNX = {
|
|
|
|
|
-- has its own CXN object (table) to store connection-wide globals.
|
|
|
|
|
local CXN; CXN = {
|
|
|
|
|
validUser = false,
|
|
|
|
|
cmdSocket = sock,
|
|
|
|
|
send = function(rec, cb) -- upval: CNX (,debug)
|
|
|
|
|
-- debug("Sending: %s", rec)
|
|
|
|
|
return CNX.cmdSocket:send(rec.."\r\n", cb)
|
|
|
|
|
end, --- send()
|
|
|
|
|
close = function(socket) -- upval: client, CNX (,debug, pcall, type)
|
|
|
|
|
-- debug("Closing CNX.socket=%s, sock=%s", tostring(CNX.socket), tostring(sock))
|
|
|
|
|
debug = this.debug,
|
|
|
|
|
FTP = this,
|
|
|
|
|
send = function(rec, cb) -- upval: CXN
|
|
|
|
|
CXN.debug("Sending: %s", rec)
|
|
|
|
|
return CXN.cmdSocket:send(rec.."\r\n", cb)
|
|
|
|
|
end, --- CXN. send()
|
|
|
|
|
close = function(socket) -- upval: CXN
|
|
|
|
|
CXN.debug("Closing CXN.cmdSocket=%s", tostring(CXN.cmdSocket))
|
|
|
|
|
for _,s in ipairs{'cmdSocket', 'dataServer', 'dataSocket'} do
|
|
|
|
|
local sck; sck,CNX[s] = CNX[s], nil
|
|
|
|
|
-- debug("closing CNX.%s=%s", s, tostring(sck))
|
|
|
|
|
if type(sck)=='userdata' then pcall(sck.close, sck) end
|
|
|
|
|
CXN.debug("closing CXN.%s=%s", s, tostring(CXN[s]))
|
|
|
|
|
if type(CXN[s])=='userdata' then
|
|
|
|
|
pcall(socket.close, CXN[s])
|
|
|
|
|
CXN[s]= nil
|
|
|
|
|
end
|
|
|
|
|
client[socket] = nil
|
|
|
|
|
end -- CNX.close()
|
|
|
|
|
end
|
|
|
|
|
CXN.FTP.client[socket] = nil
|
|
|
|
|
end -- CXN.close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
local function validateUser(socket, data) -- upval: CNX, FTP (, debug, processCommand)
|
|
|
|
|
local function validateUser(socket, data) -- upval: CXN
|
|
|
|
|
-- validate the logon and if then switch to processing commands
|
|
|
|
|
|
|
|
|
|
-- debug("Authorising: %s", data)
|
|
|
|
|
CXN.debug("Authorising: %s", data)
|
|
|
|
|
local cmd, arg = data:match('([A-Za-z]+) *([^\r\n]*)')
|
|
|
|
|
local msg = "530 Not logged in, authorization required"
|
|
|
|
|
cmd = cmd:upper()
|
|
|
|
|
|
|
|
|
|
if cmd == 'USER' then
|
|
|
|
|
CNX.validUser = (arg == FTP.user)
|
|
|
|
|
msg = CNX.validUser and
|
|
|
|
|
CXN.validUser = (arg == CXN.FTP.user)
|
|
|
|
|
msg = CXN.validUser and
|
|
|
|
|
"331 OK. Password required" or
|
|
|
|
|
"530 user not found"
|
|
|
|
|
|
|
|
|
|
elseif CNX.validUser and cmd == 'PASS' then
|
|
|
|
|
if arg == FTP.pass then
|
|
|
|
|
CNX.cwd = '/'
|
|
|
|
|
socket:on("receive", function(socketObj, dataObj)
|
|
|
|
|
processCommand(CNX,socketObj, dataObj)
|
|
|
|
|
elseif CXN.validUser and cmd == 'PASS' then
|
|
|
|
|
if arg == CXN.FTP.pass then
|
|
|
|
|
CXN.cwd = '/'
|
|
|
|
|
socket:on("receive", function(soc, rec) -- upval: CXN
|
|
|
|
|
assert(soc==CXN.cmdSocket)
|
|
|
|
|
CXN.FTP.processCommand(CXN, rec)
|
|
|
|
|
end) -- logged on so switch to command mode
|
|
|
|
|
msg = "230 Login successful. Username & password correct; proceed."
|
|
|
|
|
else
|
|
|
|
@ -128,46 +133,45 @@ function FTP.createServer(user, pass, dbgFlag) -- upval: FTP (, debug, tostring
|
|
|
|
|
|
|
|
|
|
elseif cmd == 'AUTH' then
|
|
|
|
|
msg = "500 AUTH not understood"
|
|
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return CNX.send(msg)
|
|
|
|
|
return CXN.send(msg)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local port,ip = sock:getpeer() -- luacheck: no unused
|
|
|
|
|
--debug("Connection accepted: (userdata) %s client %s:%u", tostring(sock), ip, port)
|
|
|
|
|
--cxt.debug("Connection accepted: (userdata) %s client %s:%u", tostring(sock), ip, port)
|
|
|
|
|
sock:on("receive", validateUser)
|
|
|
|
|
sock:on("disconnection", CNX.close)
|
|
|
|
|
FTP.client[sock]=CNX
|
|
|
|
|
sock:on("disconnection", CXN.close)
|
|
|
|
|
this.client[sock]=CXN
|
|
|
|
|
|
|
|
|
|
CNX.send("220 FTP server ready");
|
|
|
|
|
end) -- FTP.server:listen()
|
|
|
|
|
end -- FTP.createServer()
|
|
|
|
|
CXN.send("220 FTP server ready");
|
|
|
|
|
end) -- this.server:listen()
|
|
|
|
|
end --[[SPLIT IGNORE]]
|
|
|
|
|
function FTP.close(...) --[[SPLIT HERE ftp-close]]
|
|
|
|
|
-- Lua: FTP:close()
|
|
|
|
|
|
|
|
|
|
local this = ...
|
|
|
|
|
|
|
|
|
|
function FTP.close() -- upval: FTP (, debug, post, tostring)
|
|
|
|
|
local svr = FTP.server
|
|
|
|
|
|
|
|
|
|
local function rollupClients(client, server) -- upval: FTP (,debug, post, tostring, rollupClients)
|
|
|
|
|
-- this is done recursively so that we only close one client per task
|
|
|
|
|
local skt,cxt = next(client)
|
|
|
|
|
-- this.client is a table of soc = cnx. The first (and usually only connection) is cleared
|
|
|
|
|
-- immediately and next() used to do a post chain so we only close one client per task
|
|
|
|
|
local function rollupClients(skt,cxt) -- upval: this, rollupClients
|
|
|
|
|
if skt then
|
|
|
|
|
-- debug("Client close: %s", tostring(skt))
|
|
|
|
|
this.debug("Client close: %s", tostring(skt))
|
|
|
|
|
cxt.close(skt)
|
|
|
|
|
post(function() return rollupClients(client, server) end) -- upval: rollupClients, client, server
|
|
|
|
|
else
|
|
|
|
|
-- debug("Server close: %s", tostring(server))
|
|
|
|
|
server:close()
|
|
|
|
|
server:__gc()
|
|
|
|
|
FTP,_G.FTP = nil, nil -- the upval FTP can only be zeroed once FTP.client is cleared.
|
|
|
|
|
this.client[skt] = nil
|
|
|
|
|
node.task.post(function() return rollupClients(next(this.client, skt)) end) -- upval: rollupClients, this, skt
|
|
|
|
|
else -- we have emptied the open socket table, so can now shut the server
|
|
|
|
|
this.debug("Server close: %s", tostring(this. server))
|
|
|
|
|
this.server:close()
|
|
|
|
|
this.server:__gc()
|
|
|
|
|
_G.FTP = nil
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if svr then rollupClients(FTP.client, svr) end
|
|
|
|
|
rollupClients(next(this.client))
|
|
|
|
|
package.loaded.ftpserver = nil
|
|
|
|
|
end -- FTP.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
end --[[SPLIT IGNORE]]
|
|
|
|
|
function FTP.processCommand(...) --[[SPLIT HERE ftp-processCommand]]
|
|
|
|
|
----------------------------- Process Command --------------------------------
|
|
|
|
|
-- This splits the valid commands into one of three categories:
|
|
|
|
|
-- * bare commands (which take no arg)
|
|
|
|
@ -177,28 +181,30 @@ end -- FTP.close()
|
|
|
|
|
--
|
|
|
|
|
-- Find strings are used do this lookup and minimise long if chains.
|
|
|
|
|
------------------------------------------------------------------------------
|
|
|
|
|
-- upvals: (, debug, processBareCmds, processSimpleCmds, processDataCmds)
|
|
|
|
|
processCommand = function(cxt, socket, data) -- luacheck: no unused
|
|
|
|
|
debug("Command: %s", data)
|
|
|
|
|
|
|
|
|
|
local cxt, data = ...
|
|
|
|
|
|
|
|
|
|
cxt.debug("Command: %s", data)
|
|
|
|
|
data = data:gsub('[\r\n]+$', '') -- chomp trailing CRLF
|
|
|
|
|
local cmd, arg = data:match('([a-zA-Z]+) *(.*)')
|
|
|
|
|
cmd = cmd:upper()
|
|
|
|
|
local _cmd_ = '_'..cmd..'_'
|
|
|
|
|
|
|
|
|
|
if ('_CDUP_NOOP_PASV_PWD_QUIT_SYST_'):find(_cmd_) then
|
|
|
|
|
processBareCmds(cxt, cmd)
|
|
|
|
|
cxt.FTP.processBareCmds(cxt, cmd)
|
|
|
|
|
elseif ('_CWD_DELE_MODE_PORT_RNFR_RNTO_SIZE_TYPE_'):find(_cmd_) then
|
|
|
|
|
processSimpleCmds(cxt, cmd, arg)
|
|
|
|
|
cxt.FTP.processSimpleCmds(cxt, cmd, arg)
|
|
|
|
|
elseif ('_LIST_NLST_RETR_STOR_'):find(_cmd_) then
|
|
|
|
|
processDataCmds(cxt, cmd, arg)
|
|
|
|
|
cxt.FTP.processDataCmds(cxt, cmd, arg)
|
|
|
|
|
else
|
|
|
|
|
cxt.send("500 Unknown error")
|
|
|
|
|
end
|
|
|
|
|
end -- processCommand(sock, data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
end --[[SPLIT IGNORE]]
|
|
|
|
|
function FTP.processBareCmds(...) --[[SPLIT HERE ftp-processBareCmds]]
|
|
|
|
|
-------------------------- Process Bare Commands -----------------------------
|
|
|
|
|
processBareCmds = function(cxt, cmd) -- upval: (dataServer)
|
|
|
|
|
|
|
|
|
|
local cxt, cmd = ...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
local send = cxt.send
|
|
|
|
|
|
|
|
|
@ -220,7 +226,7 @@ processBareCmds = function(cxt, cmd) -- upval: (dataServer)
|
|
|
|
|
pplo = port % 256
|
|
|
|
|
pphi = (port-pplo)/256
|
|
|
|
|
i1,i2,i3,i4 = ip:match("(%d+).(%d+).(%d+).(%d+)")
|
|
|
|
|
dataServer(cxt, port)
|
|
|
|
|
cxt.FTP.dataServer(cxt, port)
|
|
|
|
|
return send(
|
|
|
|
|
('227 Entering Passive Mode(%d,%d,%d,%d,%d,%d)'):format(
|
|
|
|
|
i1,i2,i3,i4,pphi,pplo))
|
|
|
|
@ -229,7 +235,7 @@ processBareCmds = function(cxt, cmd) -- upval: (dataServer)
|
|
|
|
|
return send('257 "/" is the current directory')
|
|
|
|
|
|
|
|
|
|
elseif cmd == 'QUIT' then
|
|
|
|
|
send("221 Goodbye", function() cxt.close(cxt.cmdSocket) end)
|
|
|
|
|
send("221 Goodbye", function() cxt.close(cxt.cmdSocket) end) -- upval: cxt
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
elseif cmd == 'SYST' then
|
|
|
|
@ -239,11 +245,14 @@ processBareCmds = function(cxt, cmd) -- upval: (dataServer)
|
|
|
|
|
else
|
|
|
|
|
error('Oops. Missed '..cmd)
|
|
|
|
|
end
|
|
|
|
|
end -- processBareCmds(cmd, send)
|
|
|
|
|
|
|
|
|
|
end --[[SPLIT IGNORE]]
|
|
|
|
|
function FTP.processSimpleCmds(...) --[[SPLIT HERE ftp-processSimpleCmds]]
|
|
|
|
|
|
|
|
|
|
------------------------- Process Simple Commands ----------------------------
|
|
|
|
|
local from -- needs to persist between simple commands
|
|
|
|
|
processSimpleCmds = function(cxt, cmd, arg) -- upval: from (, file, tostring, dataServer, debug)
|
|
|
|
|
|
|
|
|
|
local cxt, cmd, arg = ...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
local send = cxt.send
|
|
|
|
|
|
|
|
|
@ -252,7 +261,7 @@ processSimpleCmds = function(cxt, cmd, arg) -- upval: from (, file, tostring, d
|
|
|
|
|
"504 Only S(tream) is suported")
|
|
|
|
|
|
|
|
|
|
elseif cmd == 'PORT' then
|
|
|
|
|
dataServer(cxt,nil) -- clear down any PASV setting
|
|
|
|
|
cxt.FTP.dataServer(cxt,nil) -- clear down any PASV setting
|
|
|
|
|
return send("502 Active mode not supported. PORT not implemented")
|
|
|
|
|
|
|
|
|
|
elseif cmd == 'TYPE' then
|
|
|
|
@ -269,7 +278,7 @@ processSimpleCmds = function(cxt, cmd, arg) -- upval: from (, file, tostring, d
|
|
|
|
|
|
|
|
|
|
-- The remaining commands take a filename as an arg. Strip off leading / and ./
|
|
|
|
|
arg = arg:gsub('^%.?/',''):gsub('^%.?/','')
|
|
|
|
|
debug("Filename is %s",arg)
|
|
|
|
|
cxt.debug("Filename is %s",arg)
|
|
|
|
|
|
|
|
|
|
if cmd == 'CWD' then
|
|
|
|
|
if arg:match('^[%./]*$') then
|
|
|
|
@ -285,14 +294,14 @@ processSimpleCmds = function(cxt, cmd, arg) -- upval: from (, file, tostring, d
|
|
|
|
|
return send("550 Requested action not taken")
|
|
|
|
|
|
|
|
|
|
elseif cmd == 'RNFR' then
|
|
|
|
|
from = arg
|
|
|
|
|
cxt.from = arg
|
|
|
|
|
send("350 RNFR accepted")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
elseif cmd == 'RNTO' then
|
|
|
|
|
local status = from and file.rename(from, arg)
|
|
|
|
|
-- debug("rename('%s','%s')=%s", tostring(from), tostring(arg), tostring(status))
|
|
|
|
|
from = nil
|
|
|
|
|
local status = cxt.from and file.rename(cxt.from, arg)
|
|
|
|
|
cxt.debug("rename('%s','%s')=%s", tostring(cxt.from), tostring(arg), tostring(status))
|
|
|
|
|
cxt.from = nil
|
|
|
|
|
return send(status and "250 File renamed" or
|
|
|
|
|
"550 Requested action not taken")
|
|
|
|
|
elseif cmd == "SIZE" then
|
|
|
|
@ -303,16 +312,19 @@ processSimpleCmds = function(cxt, cmd, arg) -- upval: from (, file, tostring, d
|
|
|
|
|
else
|
|
|
|
|
error('Oops. Missed '..cmd)
|
|
|
|
|
end
|
|
|
|
|
end -- processSimpleCmds(cmd, arg, send)
|
|
|
|
|
|
|
|
|
|
end --[[SPLIT IGNORE]]
|
|
|
|
|
function FTP.processDataCmds(...) --[[SPLIT HERE ftp-processDataCmds]]
|
|
|
|
|
|
|
|
|
|
-------------------------- Process Data Commands -----------------------------
|
|
|
|
|
processDataCmds = function(cxt, cmd, arg) -- upval: FTP (, pairs, file, tostring, debug, post)
|
|
|
|
|
|
|
|
|
|
local send = cxt.send
|
|
|
|
|
local cxt, cmd, arg = ...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
local send, FTP = cxt.send, cxt.FTP -- luacheck: ignore FTP
|
|
|
|
|
|
|
|
|
|
-- The data commands are only accepted if a PORT command is in scope
|
|
|
|
|
if cxt.dataServer == nil and cxt.dataSocket == nil then
|
|
|
|
|
if FTP.dataServer == nil and cxt.dataSocket == nil then
|
|
|
|
|
return send("502 Active mode not supported. "..cmd.." not implemented")
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
@ -340,8 +352,8 @@ processDataCmds = function(cxt, cmd, arg) -- upval: FTP (, pairs, file, tostrin
|
|
|
|
|
end
|
|
|
|
|
table.sort(nameList)
|
|
|
|
|
|
|
|
|
|
function cxt.getData() -- upval: cmd, fileSize, nameList (, table)
|
|
|
|
|
local list, user = {}, FTP.user
|
|
|
|
|
function cxt.getData(c) -- upval: cmd, fileSize, nameList
|
|
|
|
|
local list, user = {}, c.FTP.user
|
|
|
|
|
for i = 1,10 do -- luacheck: no unused
|
|
|
|
|
if #nameList == 0 then break end
|
|
|
|
|
local f = table.remove(nameList, 1)
|
|
|
|
@ -355,7 +367,7 @@ processDataCmds = function(cxt, cmd, arg) -- upval: FTP (, pairs, file, tostrin
|
|
|
|
|
elseif cmd == "RETR" then
|
|
|
|
|
local f = file.open(arg, "r")
|
|
|
|
|
if f then -- define a getter to read the file
|
|
|
|
|
function cxt.getData() -- upval: f
|
|
|
|
|
function cxt.getData(c) -- luacheck: ignore c -- upval: f
|
|
|
|
|
local buf = f:read(1024)
|
|
|
|
|
if not buf then f:close(); f = nil; end
|
|
|
|
|
return buf
|
|
|
|
@ -365,13 +377,13 @@ processDataCmds = function(cxt, cmd, arg) -- upval: FTP (, pairs, file, tostrin
|
|
|
|
|
elseif cmd == "STOR" then
|
|
|
|
|
local f = file.open(arg, "w")
|
|
|
|
|
if f then -- define a setter to write the file
|
|
|
|
|
function cxt.setData(rec) -- upval f, arg (, debug)
|
|
|
|
|
-- debug("writing %u bytes to %s", #rec, arg)
|
|
|
|
|
function cxt.setData(c, rec) -- luacheck: ignore c -- upval: f (, arg)
|
|
|
|
|
cxt.debug("writing %u bytes to %s", #rec, arg)
|
|
|
|
|
return f:write(rec)
|
|
|
|
|
end -- cxt.saveData(rec)
|
|
|
|
|
function cxt.fileClose() -- upval cxt, f, arg (,debug)
|
|
|
|
|
-- debug("closing %s", arg)
|
|
|
|
|
f:close(); cxt.fileClose, f = nil, nil
|
|
|
|
|
function cxt.fileClose(c) -- luacheck: ignore c -- upval: f (,arg)
|
|
|
|
|
cxt.debug("closing %s", arg)
|
|
|
|
|
f:close(); f = nil
|
|
|
|
|
end -- cxt.close()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
@ -380,13 +392,12 @@ processDataCmds = function(cxt, cmd, arg) -- upval: FTP (, pairs, file, tostrin
|
|
|
|
|
send((cxt.getData or cxt.setData) and "150 Accepted data connection" or
|
|
|
|
|
"451 Can't open/create "..arg)
|
|
|
|
|
if cxt.getData and cxt.dataSocket then
|
|
|
|
|
debug ("poking sender to initiate first xfer")
|
|
|
|
|
post(function() cxt.sender(cxt.dataSocket) end)
|
|
|
|
|
cxt.debug ("poking sender to initiate first xfer")
|
|
|
|
|
node.task.post(function() cxt.sender(cxt.dataSocket) end) -- upval: cxt
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
end -- processDataCmds(cmd, arg, send)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
end --[[SPLIT IGNORE]]
|
|
|
|
|
function FTP.dataServer(...) --[[SPLIT HERE ftp-dataServer]]
|
|
|
|
|
----------------------------- Data Port Routines -----------------------------
|
|
|
|
|
-- These are used to manage the data transfer over the data port. This is
|
|
|
|
|
-- set up lazily either by a PASV or by the first LIST NLST RETR or STOR
|
|
|
|
@ -394,51 +405,53 @@ end -- processDataCmds(cmd, arg, send)
|
|
|
|
|
-- handle the actual xfer. Also note that the sending process can be primed in
|
|
|
|
|
--
|
|
|
|
|
---------------- Open a new data server and port ---------------------------
|
|
|
|
|
dataServer = function(cxt, n) -- upval: (pcall, net, ftpDataOpen, debug, tostring)
|
|
|
|
|
local dataSrv = cxt.dataServer
|
|
|
|
|
if dataSrv then -- close any existing listener
|
|
|
|
|
pcall(dataSrv.close, dataSrv)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local cxt, n = ...
|
|
|
|
|
|
|
|
|
|
local dataSvr = cxt.dataServer
|
|
|
|
|
if dataSvr then pcall(dataSvr.close, dataSrv) end -- luacheck: ignore -- close any existing listener
|
|
|
|
|
if n then
|
|
|
|
|
-- Open a new listener if needed. Note that this is only used to establish
|
|
|
|
|
-- a single connection, so ftpDataOpen closes the server socket
|
|
|
|
|
cxt.dataServer = net.createServer(net.TCP, 300)
|
|
|
|
|
cxt.dataServer:listen(n, function(sock) -- upval: cxt, (ftpDataOpen)
|
|
|
|
|
ftpDataOpen(cxt,sock)
|
|
|
|
|
dataSvr = net.createServer(net.TCP, 300)
|
|
|
|
|
cxt.dataServer = dataSvr
|
|
|
|
|
dataSvr:listen(n, function(sock) -- upval: cxt
|
|
|
|
|
cxt.FTP.ftpDataOpen(cxt,sock)
|
|
|
|
|
end)
|
|
|
|
|
-- debug("Listening on Data port %u, server %s",n, tostring(cxt.dataServer))
|
|
|
|
|
cxt.debug("Listening on Data port %u, server %s",n, tostring(cxt.dataServer))
|
|
|
|
|
else
|
|
|
|
|
cxt.dataServer = nil
|
|
|
|
|
-- debug("Stopped listening on Data port",n)
|
|
|
|
|
cxt.debug("Stopped listening on Data port",n)
|
|
|
|
|
end
|
|
|
|
|
end -- dataServer(n)
|
|
|
|
|
|
|
|
|
|
end --[[SPLIT IGNORE]]
|
|
|
|
|
function FTP.ftpDataOpen(...) --[[SPLIT HERE ftp-ftpDataOpen]]
|
|
|
|
|
----------------------- Connection on FTP data port ------------------------
|
|
|
|
|
ftpDataOpen = function(cxt, dataSocket) -- upval: (debug, tostring, post, pcall)
|
|
|
|
|
|
|
|
|
|
local cxt, dataSocket = ...
|
|
|
|
|
|
|
|
|
|
local sport,sip = dataSocket:getaddr()
|
|
|
|
|
local cport,cip = dataSocket:getpeer()
|
|
|
|
|
debug("Opened data socket %s from %s:%u to %s:%u", tostring(dataSocket),sip,sport,cip,cport )
|
|
|
|
|
cxt.debug("Opened data socket %s from %s:%u to %s:%u", tostring(dataSocket),sip,sport,cip,cport )
|
|
|
|
|
cxt.dataSocket = dataSocket
|
|
|
|
|
|
|
|
|
|
cxt.dataServer:close()
|
|
|
|
|
cxt.dataServer = nil
|
|
|
|
|
|
|
|
|
|
local function cleardown(skt,type) -- upval: cxt (, debug, tostring, post, pcall)
|
|
|
|
|
-- luacheck: push no unused
|
|
|
|
|
type = type==1 and "disconnection" or "reconnection"
|
|
|
|
|
function cxt.cleardown(cxt, skt, cdtype) --luacheck: ignore cxt -- shadowing
|
|
|
|
|
-- luacheck: ignore cdtype which
|
|
|
|
|
cdtype = cdtype==1 and "disconnection" or "reconnection"
|
|
|
|
|
local which = cxt.setData and "setData" or (cxt.getData and cxt.getData or "neither")
|
|
|
|
|
--debug("Cleardown entered from %s with %s", type, which)
|
|
|
|
|
-- luacheck: pop
|
|
|
|
|
cxt.debug("Cleardown entered from %s with %s", cdtype, which)
|
|
|
|
|
if cxt.setData then
|
|
|
|
|
cxt.fileClose()
|
|
|
|
|
cxt:fileClose()
|
|
|
|
|
cxt.setData = nil
|
|
|
|
|
cxt.send("226 Transfer complete.")
|
|
|
|
|
else
|
|
|
|
|
cxt.getData, cxt.sender = nil, nil
|
|
|
|
|
end
|
|
|
|
|
-- debug("Clearing down data socket %s", tostring(skt))
|
|
|
|
|
post(function() -- upval: skt, cxt, (, pcall)
|
|
|
|
|
cxt.debug("Clearing down data socket %s", tostring(skt))
|
|
|
|
|
node.task.post(function() -- upval: cxt, skt
|
|
|
|
|
pcall(skt.close, skt); skt=nil
|
|
|
|
|
cxt.dataSocket = nil
|
|
|
|
|
end)
|
|
|
|
@ -446,10 +459,10 @@ ftpDataOpen = function(cxt, dataSocket) -- upval: (debug, tostring, post, pcall)
|
|
|
|
|
|
|
|
|
|
local on_hold = false
|
|
|
|
|
|
|
|
|
|
dataSocket:on("receive", function(skt, rec) --upval: cxt, on_hold (, debug, tstring, post, node, pcall)
|
|
|
|
|
dataSocket:on("receive", function(skt, rec) -- upval: cxt, on_hold
|
|
|
|
|
|
|
|
|
|
local which = cxt.setData and "setData" or (cxt.getData and cxt.getData or "neither")-- luacheck: no unused
|
|
|
|
|
--debug("Received %u data bytes with %s", #rec, which)
|
|
|
|
|
local rectype = cxt.setData and "setData" or (cxt.getData and cxt.getData or "neither")
|
|
|
|
|
cxt.debug("Received %u data bytes with %s", #rec, rectype)
|
|
|
|
|
|
|
|
|
|
if not cxt.setData then return end
|
|
|
|
|
|
|
|
|
@ -458,33 +471,33 @@ ftpDataOpen = function(cxt, dataSocket) -- upval: (debug, tostring, post, pcall)
|
|
|
|
|
-- large file. As soon as a record arrives assert a flow control hold.
|
|
|
|
|
-- This can take up to 5 packets to come into effect at which point the
|
|
|
|
|
-- low priority unhold task is executed releasing the flow again.
|
|
|
|
|
-- debug("Issuing hold on data socket %s", tostring(skt))
|
|
|
|
|
cxt.debug("Issuing hold on data socket %s", tostring(skt))
|
|
|
|
|
skt:hold(); on_hold = true
|
|
|
|
|
post(node.task.LOW_PRIORITY,
|
|
|
|
|
function() -- upval: skt, on_hold (, debug, tostring))
|
|
|
|
|
-- debug("Issuing unhold on data socket %s", tostring(skt))
|
|
|
|
|
node.task.post(node.task.LOW_PRIORITY,
|
|
|
|
|
function() -- upval: skt, on_hold
|
|
|
|
|
cxt.debug("Issuing unhold on data socket %s", tostring(skt))
|
|
|
|
|
pcall(skt.unhold, skt); on_hold = false
|
|
|
|
|
end)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if not cxt.setData(rec) then
|
|
|
|
|
-- debug("Error writing to SPIFFS")
|
|
|
|
|
cxt.fileClose()
|
|
|
|
|
if not cxt:setData(rec) then
|
|
|
|
|
cxt.debug("Error writing to SPIFFS")
|
|
|
|
|
cxt:fileClose()
|
|
|
|
|
cxt.setData = nil
|
|
|
|
|
cxt.send("552 Upload aborted. Exceeded storage allocation")
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
function cxt.sender(skt) -- upval: cxt (, debug)
|
|
|
|
|
debug ("entering sender")
|
|
|
|
|
function cxt.sender(skt) -- upval: cxt
|
|
|
|
|
cxt.debug ("entering sender")
|
|
|
|
|
if not cxt.getData then return end
|
|
|
|
|
skt = skt or cxt.dataSocket
|
|
|
|
|
local rec = cxt.getData()
|
|
|
|
|
local rec = cxt:getData()
|
|
|
|
|
if rec and #rec > 0 then
|
|
|
|
|
-- debug("Sending %u data bytes", #rec)
|
|
|
|
|
cxt.debug("Sending %u data bytes", #rec)
|
|
|
|
|
skt:send(rec)
|
|
|
|
|
else
|
|
|
|
|
-- debug("Send of data completed")
|
|
|
|
|
cxt.debug("Send of data completed")
|
|
|
|
|
skt:close()
|
|
|
|
|
cxt.send("226 Transfer complete.")
|
|
|
|
|
cxt.getData, cxt.dataSocket = nil, nil
|
|
|
|
@ -492,14 +505,11 @@ ftpDataOpen = function(cxt, dataSocket) -- upval: (debug, tostring, post, pcall)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
dataSocket:on("sent", cxt.sender)
|
|
|
|
|
dataSocket:on("disconnection", function(skt) return cleardown(skt,1) end)
|
|
|
|
|
dataSocket:on("reconnection", function(skt) return cleardown(skt,2) end)
|
|
|
|
|
dataSocket:on("disconnection", function(skt) return cxt:cleardown(skt,1) end) -- upval: cxt
|
|
|
|
|
dataSocket:on("reconnection", function(skt) return cxt:cleardown(skt,2) end) -- upval: cxt
|
|
|
|
|
|
|
|
|
|
-- if we are sending to client then kick off the first send
|
|
|
|
|
if cxt.getData then cxt.sender(cxt.dataSocket) end
|
|
|
|
|
|
|
|
|
|
end -- ftpDataOpen(socket)
|
|
|
|
|
|
|
|
|
|
------------------------------------------------ -----------------------------
|
|
|
|
|
|
|
|
|
|
return FTP
|
|
|
|
|
end --[[SPLIT HERE]]
|
|
|
|
|
return FTP --[[SPLIT IGNORE]]
|
|
|
|
|