Make the telnet example an Lua module (#3133)

Also update ftp server
This commit is contained in:
Marcel Stör 2020-06-03 14:36:47 +02:00
parent f20591a82e
commit 8d091c476e
7 changed files with 243 additions and 318 deletions

View File

@ -0,0 +1,50 @@
# Telnet Module
| Since | Origin / Contributor | Maintainer | Source |
| :----- | :-------------------- | :---------- | :------ |
| 2018-05-24 | [Terry Ellison](https://github.com/TerryE) | [Terry Ellison](https://github.com/TerryE) | [telnet.lua](../../lua_modules/telnet/telnet.lua) |
The current version of this module exploits the stdin / stdout pipe functionality and
task integration that is now build into the NodeNMCU Lua core.
There are two nice advantages of this core implementation:
- Errors are now written to stdout in a separate task execution.
- The pipes pretty much eliminate UART and telnet overrun.
Both have the same interface if required into the variable `telnet`
## telnet:open()
Open a telnet server based on the provided parameters.
#### Syntax
`telnet:open(ssid, pwd, port)`
#### Parameters
`ssid` and `password`. Strings. SSID and Password for the Wifi network. If these are
`nil` then the wifi is assumed to be configured or auto-configured.
`port`. Integer TCP listening port for the Telnet service. The default is 2323
#### Returns
Nothing returned (this is evaluated as `nil` in a scalar context).
## telnet:close()
Close a telnet server and release all resources. Also set the variable `telnet` to nil to fully reference and GC the resources.
#### Syntax
`telnet:close()`
#### Parameters
None
#### Returns
Nothing returned (this is evaluated as `nil` in a scalar context).

View File

@ -1,56 +0,0 @@
# Telnet Module
| Since | Origin / Contributor | Maintainer | Source |
| :----- | :-------------------- | :---------- | :------ |
| 2014-12-22 | [Zeroday](https://github.com/funshine) | [Terry Ellison](https://github.com/TerryE) | [simple_telnet.lua](./simple_telnet.lua) |
| 2018-05-24 | [Terry Ellison](https://github.com/TerryE) | [Terry Ellison](https://github.com/TerryE) | [telnet.lua](./telnet.lua) |
This README discusses the packet marshalling versions of telnet. The first (fifosock)
version was written for SDK 2 implementations, with all of the marshalling imlemented
in Lua; the second (pipe) version uses the latest features added to the SDK 3 version
that have been added to prepare for the `lua53` implementation. These exploit the
stdin / stdout pipe functionality and task integration that is now build into the
NodeNMCU Lua core.
There are two nice advantages of this core implementation:
- Errors are now written to stdout in a spearate task execution.
- The pipes pretty much eliminate uart and telnet overrun.
Both have the same interface if required into the variable `telnet`
## telnet:open()
Open a telnet server based on the provided parameters.
#### Syntax
`telnet:open(ssid, pwd, port)`
#### Parameters
`ssid` and `password`. Strings. SSID and Password for the Wifi network. If these are
`nil` then the wifi is assumed to be configured or autoconfigured.
`port`. Integer TCP listenting port for the Telnet service. The default is 2323
#### Returns
Nothing returned (this is evaluted as `nil` in a scalar context).
## telnet:close()
Close a telnet server and release all resources. Also set the variable `telnet` to nil to fully reference and GC the resources.
#### Syntax
`telnet:close()`
#### Parameters
None
#### Returns
Nothing returned (this is evaluted as `nil` in a scalar context).

View File

@ -1,84 +0,0 @@
--[[ A telnet server T. Ellison, May 2018
This version is more complex than the simple Lua example previously provided in our
distro. The main reason is that a single Lua command can produce a LOT of output,
and the server has to work within four constraints:
- The SDK rules are that you can only issue one send per task invocation, so any
overflow must be buffered, and the buffer emptied using an on:sent cb
- Since the interpeter invokes a node.output cb per field, you have a double
whammy that these fields are typically small, so using a simple array FIFO
would rapidly exhaust RAM.
- For network efficiency, you want to aggregate any FIFO buffered into sensible
sized packet, say 1024 bytes, but you also need to handle the case when larger
string span multiple packets. However, you must flush the buffer if necessary.
- The overall buffering strategy needs to be reasonably memory efficient and avoid
hitting the GC too hard, so where practical avoid aggregating small strings to
more than 256 chars (as NodeMCU handles <256 using stack buffers), and avoid
serial aggregation such as buf = buf .. str as this hammers the GC.
So this server adopts a simple buffering scheme using a two level FIFO. The node.output
cb adds cb records to the 1st level FIFO until the #recs is > 32 or the total size
would exceed 256 bytes. Once over this threashold, the contents of the FIFO are
concatenated into a 2nd level FIFO entry of upto 256 bytes, and the 1st level FIFO
cleared down to any residue.
]]
--luacheck: no unused args
local node, tmr, wifi, uwrite = node, tmr, wifi, uart.write
local function telnet_listener(socket)
local queueLine = (require "fifosock").wrap(socket)
local function receiveLine(s, line)
node.input(line)
end
local function disconnect(s)
socket:on("disconnection", nil)
socket:on("reconnection", nil)
socket:on("connection", nil)
socket:on("receive", nil)
socket:on("sent", nil)
node.output(nil)
end
socket:on("receive", receiveLine)
socket:on("disconnection", disconnect)
node.output(queueLine, 0)
print(("Welcome to NodeMCU world (%d mem free, %s)"):format(node.heap(), wifi.sta.getip()))
end
local listenerSocket
return {
open = function(this, ssid, pwd, port)
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(("Telnet server started (%d mem free, %s)"):format(node.heap(), wifi.sta.getip()))
net.createServer(net.TCP, 180):listen(port or 23, telnet_listener)
else
uwrite(0,".")
end
end)
end,
close = function(this)
if listenerSocket then
listenerSocket:close()
package.loaded.telnet = nil
listenerSocket = nil
collectgarbage()
end
end,
}

View File

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

View File

@ -0,0 +1,3 @@
# Telnet Module
Documentation for this Lua module is available in the [telnet.md](../../docs/lua-modules/telnet.md) file and in the [Official NodeMCU Documentation](https://nodemcu.readthedocs.io/) in `Lua Modules` section.

View File

@ -1,3 +1,5 @@
--[[SPLIT MODULE telnet]]
--[[ A telnet server T. Ellison, June 2019 --[[ A telnet server T. Ellison, June 2019
This version of the telnet server demonstrates the use of the new stdin and stout This version of the telnet server demonstrates the use of the new stdin and stout
@ -65,5 +67,4 @@ function M.close(this)
end end
return M return M
--[[SPLIT HERE]]

View File

@ -58,6 +58,7 @@ pages:
- 'lm92': 'lua-modules/lm92.md' - 'lm92': 'lua-modules/lm92.md'
- 'mcp23008': 'lua-modules/mcp23008.md' - 'mcp23008': 'lua-modules/mcp23008.md'
- 'redis': 'lua-modules/redis.md' - 'redis': 'lua-modules/redis.md'
- 'telnet': 'lua-modules/telnet.md'
- 'yeelink': 'lua-modules/yeelink.md' - 'yeelink': 'lua-modules/yeelink.md'
- C Modules: - C Modules:
- 'adc': 'modules/adc.md' - 'adc': 'modules/adc.md'