Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create ftpserver.lua #2345

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions lua_modules/ftp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# FTPServer Module

This is a Lua module to access the [SPIFFS file system](../../docs/en/spiffs.md) via FTP protocol.
FTP server uses only one specified user and password to authenticate clients.
All clients need authentication - anonymous access is not supported yet.
All files have RW access.
Directory creation do not supported because SPIFFS do not have that.

## Require
```lua
ftpserver = require("ftpserver")
```
## Release
```lua
ftpserver:closeServer()
ftpserver = nil
package.loaded["ftpserver"]=nil
```

# Methods

## createServer()
Starts listen on 20 and 21 ports for serve FTP clients.

The module requires active network connection.

#### Syntax
`createServer(username, password)`

#### Parameters
- `username` string username for 'USER username' ftp command.
- `password` string password.

#### Returns
nil

#### Example
```lua
require("ftpserver").createServer("test","12345")
```

## closeServer()
Closes all server sockets.

#### Syntax
`closeServer()`

#### Returns
nil

#### Example
```lua
ftpserver = require("ftpserver")
ftpserver:createServer("test","12345")
-------------------------
-- Some program code
-------------------------
if needStopFtp = true then
ftpserver:closeServer()
ftpserver = nil
package.loaded["ftpserver"] = nil
end
```

3 changes: 3 additions & 0 deletions lua_modules/ftp/ftpserver-example.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Start a simple ftp server
-- createServer("user", "password")
require("ftpserver").createServer("test","12345")
182 changes: 182 additions & 0 deletions lua_modules/ftp/ftpserver.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
-- a simple ftp server
local file,net,pairs,print,string,table = file,net,pairs,print,string,table
local ftp, ftp_data, ftp_srv
do
local createServer = function (user, pass)
local data_fnc, data_sock = nil, nil
ftp_data = net.createServer(net.TCP, 180)
ftp_data:listen(1024, function (s) if data_fnc then data_fnc(s) else data_sock = s end end)
ftp_srv = net.createServer(net.TCP, 180)
ftp_srv:listen(21, function(socket)
local s = 0
local cwd = "/"
local buf = ""
local t = 0
socket:on("receive", function(c, d)
local a = {}
for i in string.gmatch(d, "([^ \r\n]+)") do
table.insert(a,i)
end
local a1,a2 = unpack(a)
if a1 == nil or a1 == "" then return end
if s == 0 and a1 == "USER" then
if a2 ~= user then
return c:send("530 user not found\r\n")
end
s = 1
return c:send("331 OK. Password required\r\n")
end
if s == 1 and a1 == "PASS" then
if a2 ~= pass then
return c:send("530 Try again\r\n")
end
s = 2
return c:send("230 OK.\r\n")
end
if s ~= 2 then
return c:send("530 Not logged in, authorization required\r\n")
end
if a1 == "SYST" then
return c:send("250 UNIX Type: L8\r\n")
end
if a1 == "CDUP" then
return c:send("250 OK. Current directory is "..cwd.."\r\n")
end
if a1 == "CWD" then
if a2 == "." then
return c:send("257 \""..cwd.."\" is your current directory\r\n")
end
if a2 == "/" then
cwd = a2
return c:send("250 OK. Current directory is "..cwd.."\r\n")
end
return c:send("550 Failed to change directory.\r\n")
end
if a1 == "PWD" then
return c:send("257 \""..cwd.."\" is your current directory\r\n")
end
if a1 == "TYPE" then
if a2 == "A" then
t = 0
return c:send("200 TYPE is now ASII\r\n")
end
if a2 == "I" then
t = 1
return c:send("200 TYPE is now 8-bit binary\r\n")
end
return c:send("504 Unknown TYPE")
end
if a1 == "MODE" then
if a2 ~= "S" then
return c:send("504 Only S(tream) is suported\r\n")
end
return c:send("200 S OK\r\n")
end
if a1 == "PASV" then
local _,ip = c:getaddr()
local _,_,i1,i2,i3,i4 = string.find(ip,"(%d+).(%d+).(%d+).(%d+)")
return c:send("227 Entering Passive Mode ("..i1..","..i2..","..i3..","..i4..",4,0).\r\n")
end
if a1 == "LIST" or a1 == "NLST" then
c:send("150 Accepted data connection\r\n")
data_fnc = function(sd)
local l = file.list();
for k,v in pairs(l) do
sd:send( a1 == "NLST" and k.."\r\n" or "-rwxrwxrwx 1 esp esp "..v.." Jan 1 2018 "..k.."\r\n")
end
sd:close()
data_fnc = nil
c:send("226 Transfer complete.\r\n")
end
if data_sock then
node.task.post(function() data_fnc(data_sock);data_sock=nil end)
end
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make the list commands work use this data_fnc. tested with 70+ files

      data_fnc = function(sd)
        sd:on("sent", function(cd)
          if #files > 0 then
            local file = table.remove(files,1)
            local size = l and l[file]
            cd:send( a1 == "NLST" and file.."\r\n" or "-rwxrwxrwx 1 esp esp "..size.." Jan  1  2018 "..file.."\r\n")
          else
            l = nil
            files = nil
            cd:close()
            data_fnc = nil
            c:send("226 Transfer complete.\r\n")
            collectgarbage()
          end
        end)
        l = file.list();
        files = {}
        for file in pairs(l) do
          table.insert(files, file)
        end
        if a1 == "NLST" then l = nil end
        local file = table.remove(files,1)
        local size = l and l[file]
        sd:send( a1 == "NLST" and file.."\r\n" or "-rwxrwxrwx 1 esp esp "..size.." Jan  1  2018 "..file.."\r\n")
      end

end
if a1 == "RETR" then
local f = file.open(a2:gsub("%/",""),"r")
if f == nil then
return c:send("550 File "..a2.." not found\r\n")
end
c:send("150 Accepted data connection\r\n")
data_fnc = function(sd)
sd:on("sent", function(cd)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if b should be declared local before the sent callback. Just to avoid that b in the lines below refers to a global variable.

b=f:read(1024)
if b then
cd:send(b)
b=nil
else
cd:close()
f:close()
data_fnc = nil
c:send("226 Transfer complete.\r\n")
end
end)
local b=f:read(1024)
sd:send(b)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check for b not being nil, to prevent crashing reading an empty file.

b=nil
end
if data_sock then
node.task.post(function() data_fnc(data_sock);data_sock=nil end)
end
return
end
if a1 == "STOR" then
local f = file.open(a2:gsub("%/",""),"w")
if f == nil then
return c:send("451 Can't open/create "..a2.."\r\n")
end
c:send("150 Accepted data connection\r\n")
data_fnc = function(sd)
sd:on("receive", function(cd, dd)
f:write(dd)
end)
sd:on("disconnection", function(c)
f:close()
data_fnc = nil
end)
c:send("226 Transfer complete.\r\n")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The c:send("226 Transfer complete.\r\n") has to be executed after disconnect only. Else it stops sending the file after a couple of blocks which results in cut off files.

The on disconnection should look loke this: (also note the rename of the function param from c to cd)

        sd:on("disconnection", function(cd)
          f:close()
          data_fnc = nil
          c:send("226 Transfer complete.\r\n")
        end)

end
if data_sock then
node.task.post(function() data_fnc(data_sock);data_sock=nil end)
end
return
end
if a1 == "RNFR" then
buf = a2
return c:send("350 RNFR accepted\r\n")
end
if a1 == "RNTO" and buf ~= "" then
file.rename(buf, a2)
buf = ""
return c:send("250 File renamed\r\n")
end
if a1 == "DELE" then
if a2 == nil or a2 == "" then
return c:send("501 No file name\r\n")
end
file.remove(a2:gsub("%/",""))
return c:send("250 Deleted "..a2.."\r\n")
end
if a1 == "SIZE" then
local f = file.stat(a2)
return c:send("213 "..(f and f.size or 1).."\r\n")
end
if a1 == "NOOP" then
return c:send("200 OK\r\n")
end
if a1 == "QUIT" then
return c:send("221 Goodbye\r\n", function (s) s:close() end)
end
c:send("500 Unknown error\r\n")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add support for SIZE command: this is generally used by web-browsers.

    if a1 == "SIZE" then
      local st,size
      st = file.stat(string.sub(a2, 2))
      if st then
        return c:send("213 "..st.size.."\r\n")
      else
        return c:send("550 Could not get file size.\r\n")
      end
    end

end)
socket:send("220--- Welcome to FTP for ESP8266/ESP32 ---\r\n220--- By NeiroN ---\r\n220 -- Version 1.8 --\r\n");
end)
end
local closeServer = function()
ftp_data:close()
ftp_srv:close()
end
ftp = { createServer = createServer, closeServer = closeServer }
end
return ftp