New FTP module
This commit is contained in:
parent
8dc6eb62df
commit
8dcc3c8a55
|
@ -0,0 +1,107 @@
|
||||||
|
# FTPServer Module
|
||||||
|
|
||||||
|
This Lua module implementation provides a basic FTP server for the ESP8266.
|
||||||
|
It has been tested against a number of Table, Windows and Linux FTP clients
|
||||||
|
and browsers.
|
||||||
|
|
||||||
|
It provides a limited subset of FTP commands that enable such clients to
|
||||||
|
tranfer files to and from the ESP's file system. Only one server can be
|
||||||
|
started at any one time, but this server can support multiple connected
|
||||||
|
sessions (some FTP clients use multiple sessions and so require this
|
||||||
|
feature).
|
||||||
|
|
||||||
|
### Limitations
|
||||||
|
|
||||||
|
- FTP over SSH or TLS is not currently supported so transfer is unencrypted.
|
||||||
|
- The client session , must, authentical against a single user/password.
|
||||||
|
- Only the SPIFFS filesystem is currently supported, so changing directories is treated as a NO-OP.
|
||||||
|
- This implementation has been optimised for running in LFS.
|
||||||
|
- Only PASV mode is supported as the `net` module does not allow static allocation of outbound sockets.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
The coding style adopted here is more similar to best practice for normal (PC)
|
||||||
|
module implementations, as using LFS permits a 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 standard forward reference coding pattern to allow the
|
||||||
|
code to be laid out in main routine, subroutine order.
|
||||||
|
|
||||||
|
Most FTP clients are capable of higher transfer rates than the ESP SPIFFS write
|
||||||
|
throughput, so the server uses TCP flow control to limit upload rates to the
|
||||||
|
ESP.
|
||||||
|
|
||||||
|
aThe following FTP commands are supported:
|
||||||
|
|
||||||
|
- with no parameter: CDUP, NOOP, PASV, PWD, QUIT, SYST
|
||||||
|
- with one parameter: CWD, DELE, MODE, PASS, PORT, RNFR, RNTO, SIZE, TYPE, USER
|
||||||
|
- xfer commands: LIST, NLST, RETR, STOR
|
||||||
|
|
||||||
|
This implementation is by Terry Ellison, but I wish to acknowledge the inspration
|
||||||
|
and hard work by [Neronix](https://github.com/NeiroNx) that made this possible.
|
||||||
|
|
||||||
|
## createServer()
|
||||||
|
|
||||||
|
Create the FTP server on the standard ports 20 and 21. The global variable `FTP`
|
||||||
|
is set to the server object.
|
||||||
|
|
||||||
|
#### Syntax
|
||||||
|
`FTP.createServer(user, pass[, dbgFlag])`
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
- `user` - Username for access to the server
|
||||||
|
- `pass` - Password for access to the server
|
||||||
|
- `dbgFlag` - optional flag. If set true then internal debug output is printed
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```Lua
|
||||||
|
require("ftpserver").createServer('user', 'password')
|
||||||
|
```
|
||||||
|
|
||||||
|
## open()
|
||||||
|
|
||||||
|
Wrapper to createServer() which also connects to the WiFi channel.
|
||||||
|
|
||||||
|
#### Syntax
|
||||||
|
|
||||||
|
`FTP.open(user, pass, ssid, wifipwd, dbgFlag)`
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
- `user` - Username for access to the server
|
||||||
|
- `pass` - Password for access to the server
|
||||||
|
- `ssid` - SSID for Wifi service
|
||||||
|
- `wifipwd` - password for Wifi service
|
||||||
|
- `dbgFlag` - optional flag. If set true then internal debug output is printed
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```Lua
|
||||||
|
require("ftpserver").open('myWifi', 'wifiPassword', 'user', 'password')
|
||||||
|
```
|
||||||
|
|
||||||
|
## close()
|
||||||
|
|
||||||
|
Close down server including any sockets and return all resouces to Lua. Note that
|
||||||
|
this include removing the FTP global variable and package references.
|
||||||
|
|
||||||
|
#### Syntax
|
||||||
|
`FTP.close()`
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
- none
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
- nil
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```Lua
|
||||||
|
FTP.close()
|
||||||
|
```
|
|
@ -0,0 +1,500 @@
|
||||||
|
o--[[ 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
|
||||||
|
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
|
||||||
|
standard forward reference coding pattern to allow the code to be laid out
|
||||||
|
in main routine, subroutine order.
|
||||||
|
|
||||||
|
The app will only call one FTP.open() or FTP.createServer() at any time,
|
||||||
|
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.
|
||||||
|
|
||||||
|
Note that FTP also exposes a number of really private properties (which
|
||||||
|
could be stores in local / upvals) as FTP properties for debug purposes.
|
||||||
|
]]
|
||||||
|
local file,net,wifi,node,string,table,tmr,pairs,print,pcall, tostring =
|
||||||
|
file,net,wifi,node,string,table,tmr,pairs,print,pcall, tostring
|
||||||
|
local post = node.task.post
|
||||||
|
local FTP, cnt = {client = {}}, 0
|
||||||
|
|
||||||
|
-- Local functions
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
|
||||||
|
local function debug (fmt, ...) -- upval: cnt (, print, node, tmr)
|
||||||
|
if not FTP.debug then return end
|
||||||
|
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
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
tmr.alarm(0, 500, tmr.ALARM_AUTO, function()
|
||||||
|
if (wifi.sta.status() == wifi.STA_GOTIP) then
|
||||||
|
tmr.unregister(0)
|
||||||
|
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)
|
||||||
|
-- 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 = {
|
||||||
|
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(sock) -- upval: client, CNX (,debug, pcall, type)
|
||||||
|
-- debug("Closing CNX.socket=%s, sock=%s", tostring(CNX.socket), tostring(sock))
|
||||||
|
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
|
||||||
|
end
|
||||||
|
client[sock] = nil
|
||||||
|
end -- CNX.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
local function validateUser(sock, data) -- upval: CNX, FTP (, debug, processCommand)
|
||||||
|
-- validate the logon and if then switch to processing commands
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
"331 OK. Password required" or
|
||||||
|
"530 user not found"
|
||||||
|
|
||||||
|
elseif CNX.validUser and cmd == 'PASS' then
|
||||||
|
if arg == FTP.pass then
|
||||||
|
CNX.cwd = '/'
|
||||||
|
sock:on("receive", function(sock,data)
|
||||||
|
processCommand(CNX,sock,data)
|
||||||
|
end) -- logged on so switch to command mode
|
||||||
|
msg = "230 Login successful. Username & password correct; proceed."
|
||||||
|
else
|
||||||
|
msg = "530 Try again"
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif cmd == 'AUTH' then
|
||||||
|
msg = "500 AUTH not understood"
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
return CNX.send(msg)
|
||||||
|
end
|
||||||
|
|
||||||
|
local port,ip = sock:getpeer()
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
CNX.send("220 FTP server ready");
|
||||||
|
end) -- FTP.server:listen()
|
||||||
|
end -- FTP.createServer()
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
if skt then
|
||||||
|
-- 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.
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if svr then rollupClients(FTP.client, svr) end
|
||||||
|
package.loaded.ftpserver=nil
|
||||||
|
end -- FTP.close()
|
||||||
|
|
||||||
|
|
||||||
|
----------------------------- Process Command --------------------------------
|
||||||
|
-- This splits the valid commands into one of three categories:
|
||||||
|
-- * bare commands (which take no arg)
|
||||||
|
-- * simple commands (which take) a single arg; and
|
||||||
|
-- * data commands which initiate data transfer to or from the client and
|
||||||
|
-- hence need to use CBs.
|
||||||
|
--
|
||||||
|
-- Find strings are used do this lookup and minimise long if chains.
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
processCommand = function(cxt, sock, data) -- upvals: (, debug, processBareCmds, processSimpleCmds, processDataCmds)
|
||||||
|
|
||||||
|
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)
|
||||||
|
elseif ('_CWD_DELE_MODE_PORT_RNFR_RNTO_SIZE_TYPE_'):find(_cmd_) then
|
||||||
|
processSimpleCmds(cxt, cmd, arg)
|
||||||
|
elseif ('_LIST_NLST_RETR_STOR_'):find(_cmd_) then
|
||||||
|
processDataCmds(cxt, cmd, arg)
|
||||||
|
else
|
||||||
|
cxt.send("500 Unknown error")
|
||||||
|
end
|
||||||
|
end -- processCommand(sock, data)
|
||||||
|
|
||||||
|
|
||||||
|
-------------------------- Process Bare Commands -----------------------------
|
||||||
|
processBareCmds = function(cxt, cmd) -- upval: (dataServer)
|
||||||
|
|
||||||
|
local send = cxt.send
|
||||||
|
|
||||||
|
if cmd == 'CDUP' then
|
||||||
|
return send("250 OK. Current directory is "..cxt.cwd)
|
||||||
|
|
||||||
|
elseif cmd == 'NOOP' then
|
||||||
|
return send("200 OK")
|
||||||
|
|
||||||
|
elseif cmd == 'PASV' then
|
||||||
|
-- This FTP implementation ONLY supports PASV mode, and the passive port
|
||||||
|
-- listener is opened on receipt of the PASV command. If any data xfer
|
||||||
|
-- commands return an error if the PASV command hasn't been received.
|
||||||
|
-- Note the listener service is closed on receipt of the next PASV or
|
||||||
|
-- quit.
|
||||||
|
local ip, port, pphi, pplo, i1, i2, i3, i4, _
|
||||||
|
_,ip = cxt.cmdSocket:getaddr()
|
||||||
|
port = 2121
|
||||||
|
pplo = port % 256
|
||||||
|
pphi = (port-pplo)/256
|
||||||
|
i1,i2,i3,i4 = ip:match("(%d+).(%d+).(%d+).(%d+)")
|
||||||
|
dataServer(cxt, port)
|
||||||
|
return send(
|
||||||
|
('227 Entering Passive Mode(%d,%d,%d,%d,%d,%d)'):format(
|
||||||
|
i1,i2,i3,i4,pphi,pplo))
|
||||||
|
|
||||||
|
elseif cmd == 'PWD' then
|
||||||
|
return send('257 "/" is the current directory')
|
||||||
|
|
||||||
|
elseif cmd == 'QUIT' then
|
||||||
|
send("221 Goodbye", function() cxt.close(cxt.cmdSocket) end)
|
||||||
|
return
|
||||||
|
|
||||||
|
elseif cmd == 'SYST' then
|
||||||
|
-- return send("215 UNKNOWN")
|
||||||
|
return send("215 UNIX Type: L8") -- must be Unix so ls is parsed correctly
|
||||||
|
|
||||||
|
else
|
||||||
|
error('Oops. Missed '..cmd)
|
||||||
|
end
|
||||||
|
end -- processBareCmds(cmd, send)
|
||||||
|
|
||||||
|
------------------------- Process Simple Commands ----------------------------
|
||||||
|
local from -- needs to persist between simple commands
|
||||||
|
processSimpleCmds = function(cxt, cmd, arg) -- upval: from (, file, tostring, dataServer, debug)
|
||||||
|
|
||||||
|
local send = cxt.send
|
||||||
|
|
||||||
|
if cmd == 'MODE' then
|
||||||
|
return send(arg == "S" and "200 S OK" or
|
||||||
|
"504 Only S(tream) is suported")
|
||||||
|
|
||||||
|
elseif cmd == 'PORT' then
|
||||||
|
dataServer(cxt,nil) -- clear down any PASV setting
|
||||||
|
return send("502 Active mode not supported. PORT not implemented")
|
||||||
|
|
||||||
|
elseif cmd == 'TYPE' then
|
||||||
|
if arg == "A" then
|
||||||
|
cxt.xferType = 0
|
||||||
|
return send("200 TYPE is now ASII")
|
||||||
|
elseif arg == "I" then
|
||||||
|
cxt.xferType = 1
|
||||||
|
return send("200 TYPE is now 8-bit binary")
|
||||||
|
else
|
||||||
|
return send("504 Unknown TYPE")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The remaining commands take a filename as an arg. Strip off leading / and ./
|
||||||
|
arg = arg:gsub('^%.?/',''):gsub('^%.?/','')
|
||||||
|
debug("Filename is %s",arg)
|
||||||
|
|
||||||
|
if cmd == 'CWD' then
|
||||||
|
if arg:match('^[%./]*$') then
|
||||||
|
return send("250 CWD command successful")
|
||||||
|
end
|
||||||
|
return send("550 "..arg..": No such file or directory")
|
||||||
|
|
||||||
|
elseif cmd == 'DELE' then
|
||||||
|
if file.exists(arg) then
|
||||||
|
file.remove(arg)
|
||||||
|
if not file.exists(arg) then return send("250 Deleted "..arg) end
|
||||||
|
end
|
||||||
|
return send("550 Requested action not taken")
|
||||||
|
|
||||||
|
elseif cmd == 'RNFR' then
|
||||||
|
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
|
||||||
|
return send(status and "250 File renamed" or
|
||||||
|
"550 Requested action not taken")
|
||||||
|
elseif cmd == "SIZE" then
|
||||||
|
local st = file.stat(arg)
|
||||||
|
return send(st and ("213 "..st.size) or
|
||||||
|
"550 Could not get file size.")
|
||||||
|
|
||||||
|
else
|
||||||
|
error('Oops. Missed '..cmd)
|
||||||
|
end
|
||||||
|
end -- processSimpleCmds(cmd, arg, send)
|
||||||
|
|
||||||
|
|
||||||
|
-------------------------- Process Data Commands -----------------------------
|
||||||
|
processDataCmds = function(cxt, cmd, arg) -- upval: FTP (, pairs, file, tostring, debug, post)
|
||||||
|
|
||||||
|
local send = cxt.send
|
||||||
|
|
||||||
|
-- The data commands are only accepted if a PORT command is in scope
|
||||||
|
if cxt.dataServer == nil and cxt.dataSocket == nil then
|
||||||
|
return send("502 Active mode not supported. "..cmd.." not implemented")
|
||||||
|
end
|
||||||
|
|
||||||
|
cxt.getData, cxt.setData = nil, nil
|
||||||
|
|
||||||
|
arg = arg:gsub('^%.?/',''):gsub('^%.?/','')
|
||||||
|
|
||||||
|
if cmd == "LIST" or cmd == "NLST" then
|
||||||
|
-- There are
|
||||||
|
local fileSize, nameList, pattern = file.list(), {}, '.'
|
||||||
|
|
||||||
|
arg = arg:gsub('^-[a-z]* *', '') -- ignore any Unix style command parameters
|
||||||
|
arg = arg:gsub('^/','') -- ignore any leading /
|
||||||
|
|
||||||
|
if #arg > 0 and arg ~= '.' then -- replace "*" by [^/%.]* that is any string not including / or .
|
||||||
|
pattern = arg:gsub('*','[^/%%.]*')
|
||||||
|
end
|
||||||
|
|
||||||
|
for k,v in pairs(fileSize) do
|
||||||
|
if k:match(pattern) then
|
||||||
|
nameList[#nameList+1] = k
|
||||||
|
else
|
||||||
|
fileSize[k] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(nameList)
|
||||||
|
|
||||||
|
function cxt.getData() -- upval: cmd, fileSize, nameList (, table)
|
||||||
|
local list, user, v = {}, FTP.user
|
||||||
|
for i = 1,10 do
|
||||||
|
if #nameList == 0 then break end
|
||||||
|
local f = table.remove(nameList, 1)
|
||||||
|
list[#list+1] = (cmd == "LIST") and
|
||||||
|
("-rw-r--r-- 1 %s %s %6u Jan 1 00:00 %s\r\n"):format(user, user, fileSize[f], f) or
|
||||||
|
(f.."\r\n")
|
||||||
|
end
|
||||||
|
return table.concat(list)
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
local buf = f:read(1024)
|
||||||
|
if not buf then f:close(); f = nil; end
|
||||||
|
return buf
|
||||||
|
end -- cxt.getData()
|
||||||
|
end
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
end -- cxt.close()
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
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)
|
||||||
|
end
|
||||||
|
|
||||||
|
end -- processDataCmds(cmd, arg, send)
|
||||||
|
|
||||||
|
|
||||||
|
----------------------------- 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
|
||||||
|
-- command that uses it. These also provide a sendData / receiveData cb to
|
||||||
|
-- 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 dataServer = cxt.dataServer
|
||||||
|
if dataServer then -- close any existing listener
|
||||||
|
pcall(dataServer.close, dataServer)
|
||||||
|
end
|
||||||
|
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)
|
||||||
|
end)
|
||||||
|
-- debug("Listening on Data port %u, server %s",n, tostring(cxt.dataServer))
|
||||||
|
else
|
||||||
|
cxt.dataServer = nil
|
||||||
|
-- debug("Stopped listening on Data port",n)
|
||||||
|
end
|
||||||
|
end -- dataServer(n)
|
||||||
|
|
||||||
|
----------------------- Connection on FTP data port ------------------------
|
||||||
|
ftpDataOpen = function(cxt, dataSocket) -- upval: (debug, tostring, post, pcall)
|
||||||
|
|
||||||
|
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.dataSocket = dataSocket
|
||||||
|
|
||||||
|
cxt.dataServer:close()
|
||||||
|
cxt.dataServer = nil
|
||||||
|
|
||||||
|
local function cleardown(skt,type) -- upval: cxt (, debug, tostring, post, pcall)
|
||||||
|
type = type==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)
|
||||||
|
|
||||||
|
if cxt.setData then
|
||||||
|
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)
|
||||||
|
pcall(skt.close, skt); skt=nil
|
||||||
|
cxt.dataSocket = nil
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local on_hold = false
|
||||||
|
|
||||||
|
dataSocket:on("receive", function(skt, rec) --upval: cxt, on_hold (, debug, tstring, post, node, pcall)
|
||||||
|
local which = cxt.setData and "setData" or (cxt.getData and cxt.getData or "neither")
|
||||||
|
-- debug("Received %u data bytes with %s", #rec, which)
|
||||||
|
|
||||||
|
if not cxt.setData then return end
|
||||||
|
|
||||||
|
if not on_hold then
|
||||||
|
-- Cludge to stop the client flooding the ESP SPIFFS on an upload of a
|
||||||
|
-- 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))
|
||||||
|
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))
|
||||||
|
pcall(skt.unhold, skt); on_hold = false
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not cxt.setData(rec) then
|
||||||
|
-- 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")
|
||||||
|
if not cxt.getData then return end
|
||||||
|
local rec, skt = cxt.getData(), cxt.dataSocket
|
||||||
|
if rec and #rec > 0 then
|
||||||
|
-- debug("Sending %u data bytes", #rec)
|
||||||
|
skt:send(rec)
|
||||||
|
else
|
||||||
|
-- debug("Send of data completed")
|
||||||
|
skt:close()
|
||||||
|
cxt.send("226 Transfer complete.")
|
||||||
|
cxt.getData, cxt.dataSocket = nil, nil
|
||||||
|
end
|
||||||
|
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)
|
||||||
|
|
||||||
|
-- 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
|
Loading…
Reference in New Issue