mirror of https://github.com/arendst/Tasmota.git
565 lines
17 KiB
Plaintext
565 lines
17 KiB
Plaintext
|
#- Simple FTP server in Berry by Christian Baars
|
||
|
# supports active and passive mode - but passive is preferred!
|
||
|
# only light error handling
|
||
|
-#
|
||
|
|
||
|
class PATH # helper class to hold the current directory
|
||
|
var p # path components in a list
|
||
|
|
||
|
def init()
|
||
|
import string
|
||
|
self.p = []
|
||
|
end
|
||
|
|
||
|
def set(p)
|
||
|
import string
|
||
|
import path
|
||
|
|
||
|
if path.isdir(p) != true
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
var new = string.split(p,"/")
|
||
|
self.p = []
|
||
|
for c:new
|
||
|
if c != ""
|
||
|
self.p.push(c)
|
||
|
end
|
||
|
end
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
def dir_up()
|
||
|
if size(self.p) > 0
|
||
|
self.p.pop()
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def get_url()
|
||
|
var url = "/"
|
||
|
for c:self.p
|
||
|
if c != ""
|
||
|
url += f"{c}/"
|
||
|
end
|
||
|
end
|
||
|
return url
|
||
|
end
|
||
|
end
|
||
|
|
||
|
class FTP : Driver
|
||
|
|
||
|
var connection, server, client, data_server, data_client, data_ip
|
||
|
var dir, dir_list, dir_pos
|
||
|
var file, file_size, file_rename, retries, chunk_size
|
||
|
var binary_mode, active_ip, active_port, user_input
|
||
|
var data_buf, data_ptr, fast_loop, data_op
|
||
|
static port = 21
|
||
|
static data_port = 20 # data connection in passive mode
|
||
|
static allow_anonymous = true # allow everything ..
|
||
|
static user = "user"
|
||
|
static password = "pass"
|
||
|
|
||
|
def init()
|
||
|
self.server = tcpserver(self.port) # connection for control data
|
||
|
self.connection = false
|
||
|
self.data_ip = tasmota.wifi()['ip']
|
||
|
self.dir = PATH()
|
||
|
self.readDir()
|
||
|
self.data_ptr = 0
|
||
|
self.active_port = nil
|
||
|
tasmota.add_driver(self)
|
||
|
log(f"FTP: init server on port {self.port}",1)
|
||
|
end
|
||
|
|
||
|
def deinit()
|
||
|
self.server.deinit()
|
||
|
self.data_server.deinit()
|
||
|
tasmota.remove_driver(self)
|
||
|
end
|
||
|
|
||
|
def every_50ms()
|
||
|
if self.connection == true
|
||
|
self.loop()
|
||
|
elif self.server.hasclient()
|
||
|
self.client = self.server.acceptasync()
|
||
|
self.sendResponse("220 Welcome")
|
||
|
self.connection = true
|
||
|
self.pubClientInfo()
|
||
|
else
|
||
|
self.connection = false
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def every_second()
|
||
|
if self.client && self.connection != false
|
||
|
if self.client.connected() == false
|
||
|
self.pubClientInfo()
|
||
|
self.connection = false
|
||
|
self.abortDataOp()
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def pubClientInfo()
|
||
|
import mqtt
|
||
|
var payload = self.client.info().tostring()
|
||
|
mqtt.publish("FTP",format("{'server':%s}", payload))
|
||
|
end
|
||
|
|
||
|
def loop()
|
||
|
if self.connection == true
|
||
|
self.handleConnection()
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def abortDataOp()
|
||
|
if self.data_op == "d"
|
||
|
self.finishDownload(true)
|
||
|
elif self.data_op == "u"
|
||
|
self.finishUpload(true)
|
||
|
elif self.data_op == "dir"
|
||
|
self.finishUpload(false)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def download() # ESP -> client
|
||
|
self.data_buf..self.file.readbytes(self.chunk_size)
|
||
|
if size(self.data_buf) == 0
|
||
|
self.retries -= 1
|
||
|
if self.retries > 0
|
||
|
return
|
||
|
end
|
||
|
else
|
||
|
var written = self.data_client.write(self.data_buf)
|
||
|
self.data_buf.clear()
|
||
|
self.data_ptr += written
|
||
|
if self.data_ptr < self.file_size
|
||
|
self.file.seek(self.data_ptr)
|
||
|
if self.retries > 0
|
||
|
return
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
self.finishDownload()
|
||
|
end
|
||
|
|
||
|
def finishDownload(error)
|
||
|
self.data_client.close()
|
||
|
tasmota.remove_fast_loop(self.fast_loop)
|
||
|
self.file.close()
|
||
|
if error
|
||
|
self.sendResponse(f"426 Connection closed; transfer aborted after {self.data_ptr} bytes.")
|
||
|
else
|
||
|
self.sendResponse(f"250 download done with {self.data_ptr} bytes.")
|
||
|
end
|
||
|
self.data_op = nil
|
||
|
tasmota.gc()
|
||
|
end
|
||
|
|
||
|
def upload() # client -> ESP
|
||
|
self.data_buf..self.data_client.readbytes()
|
||
|
|
||
|
if size(self.data_buf) > 0
|
||
|
self.file.write(self.data_buf)
|
||
|
self.data_ptr += size(self.data_buf)
|
||
|
self.data_buf.clear()
|
||
|
else
|
||
|
log(f"FTP: {self.retries} retries",4)
|
||
|
self.retries -= 1
|
||
|
if self.retries > 0
|
||
|
return
|
||
|
end
|
||
|
self.finishUpload()
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def finishUpload(error)
|
||
|
self.data_client.close()
|
||
|
tasmota.remove_fast_loop(self.fast_loop)
|
||
|
self.file.close()
|
||
|
if error
|
||
|
self.sendResponse(f"426 Connection closed; transfer after {self.data_ptr} bytes")
|
||
|
else
|
||
|
self.sendResponse(f"250 upload done with {self.data_ptr} bytes")
|
||
|
end
|
||
|
self.data_op = nil
|
||
|
tasmota.gc()
|
||
|
end
|
||
|
|
||
|
def transferDir(mode)
|
||
|
import path
|
||
|
var sz, date, isdir
|
||
|
var i = self.dir_list[self.dir_pos]
|
||
|
var url = f"{self.dir.get_url()}{i}"
|
||
|
isdir = path.isdir(url)
|
||
|
if isdir == false
|
||
|
var f = open(url,"r")
|
||
|
sz = f.size()
|
||
|
f.close()
|
||
|
date = path.last_modified(url)
|
||
|
end
|
||
|
if self.data_client.connected()
|
||
|
var dir = ""
|
||
|
if mode == "MLSD"
|
||
|
if isdir
|
||
|
dir = "Type=dir;Perm=edlmp; "
|
||
|
else
|
||
|
date = tasmota.time_dump(date)
|
||
|
var y = str(date['year'])
|
||
|
var m = f"{date['month']:02s}"
|
||
|
var d = f"{date['day']:02s}"
|
||
|
var h = f"{date['hour']:02s}"
|
||
|
var min = f"{date['min']:02s}"
|
||
|
var sec = f"{date['sec']:02s}"
|
||
|
var modif =f"{y}{m}{d}{h}{min}{sec}"
|
||
|
dir = f"Type=file;Perm=rwd;Modify={modif};Size={sz}; "
|
||
|
end
|
||
|
elif mode == "LIST"
|
||
|
var d = "-"
|
||
|
if isdir
|
||
|
d = "d"
|
||
|
date = ""
|
||
|
sz = ""
|
||
|
else
|
||
|
date = tasmota.strftime("%b %d %H:%M", date)
|
||
|
end
|
||
|
dir = f"{d}rw------- 1 all all{sz:14s} {date} "
|
||
|
|
||
|
elif mode == "NLST"
|
||
|
dir=self.dir.get_url()
|
||
|
end
|
||
|
var entry = f"{dir}{i}"
|
||
|
log(entry,4)
|
||
|
self.data_client.write(entry + "\r\n")
|
||
|
self.dir_pos += 1
|
||
|
else
|
||
|
self.finishTransferDir(false)
|
||
|
end
|
||
|
if self.dir_pos < size(self.dir_list)
|
||
|
return
|
||
|
end
|
||
|
self.finishTransferDir(true)
|
||
|
end
|
||
|
|
||
|
def finishTransferDir(success)
|
||
|
self.data_client.close()
|
||
|
if success
|
||
|
var n = size(self.dir_list)
|
||
|
self.sendResponse(f"226 {n} files in {self.dir.get_url()}")
|
||
|
else
|
||
|
self.sendResponse("426 Transfer aborted")
|
||
|
end
|
||
|
self.data_op = nil
|
||
|
tasmota.remove_fast_loop(self.fast_loop)
|
||
|
tasmota.gc()
|
||
|
end
|
||
|
|
||
|
def readDir()
|
||
|
import path
|
||
|
self.dir_list = path.listdir(self.dir.get_url())
|
||
|
end
|
||
|
|
||
|
def openFile(name,mode)
|
||
|
import path
|
||
|
var url = f"{self.dir.get_url()}{name}"
|
||
|
if path.isdir(url) == true
|
||
|
log(f"FTP: {url} is a folder",2)
|
||
|
return false
|
||
|
end
|
||
|
if mode == "r"
|
||
|
if path.exists(url) != true
|
||
|
log(f"FTP: {url} not found",2)
|
||
|
return false
|
||
|
end
|
||
|
end
|
||
|
log(f"FTP: Open file {url} in {mode} mode",3)
|
||
|
self.file = open(f"{url}",mode)
|
||
|
if mode == "a"
|
||
|
if self.data_ptr != 0
|
||
|
log(f"FTP: Appending file {url} at position {self.data_ptr}",3)
|
||
|
if self.data_ptr != self.file.size()
|
||
|
log(f"FTP: !!! resume position of {self.data_ptr} != file size of {self.file.size()} !!!",2)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
def close()
|
||
|
self.sendResponse("221 Closing connection")
|
||
|
self.connection = false
|
||
|
end
|
||
|
|
||
|
|
||
|
def deinitConnectServer()
|
||
|
if self.data_server != nil
|
||
|
self.data_server.close()
|
||
|
self.data_server.deinit()
|
||
|
self.data_server = nil
|
||
|
log("FTP: Delete server for passive data connection",2)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def initConnectServer()
|
||
|
if self.data_server == nil
|
||
|
self.data_server = tcpserver(self.data_port)
|
||
|
log("FTP: Start server for passive data connection",2)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def connectActive()
|
||
|
self.data_client = tcpclientasync()
|
||
|
if self.data_client.connect(self.active_ip,self.active_port) != false
|
||
|
log(f"FTP: Try to connect to {self.active_ip}:{self.active_port}",3)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def connectPassive()
|
||
|
if self.data_server.hasclient()
|
||
|
self.data_client = self.data_server.acceptasync()
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def dataconnect()
|
||
|
if self.data_client != nil
|
||
|
self.data_client.close()
|
||
|
self.data_client.deinit()
|
||
|
end
|
||
|
if self.active_port != nil
|
||
|
self.connectActive()
|
||
|
else
|
||
|
self.connectPassive()
|
||
|
end
|
||
|
if self.data_client == nil
|
||
|
self.sendResponse("425 Data connection failed")
|
||
|
return false
|
||
|
end
|
||
|
self.data_buf = bytes()
|
||
|
self.retries = 10
|
||
|
self.chunk_size = 5760
|
||
|
self.sendResponse("150 Ready for data transfer")
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
def sendResponse(resp)
|
||
|
self.client.write(f"{resp}\r\n")
|
||
|
log(f"FTP: Response: {resp}",3)
|
||
|
end
|
||
|
|
||
|
def handleConnection() # main loop for incoming commands
|
||
|
import string
|
||
|
import mqtt
|
||
|
import path
|
||
|
var d = self.client.read()
|
||
|
if size(d) == 0 return end
|
||
|
var items = string.split(d," ")
|
||
|
var cmd = items[0]
|
||
|
var arg = ""
|
||
|
var response = ""
|
||
|
if size(items) > 1
|
||
|
arg = string.split(items[1],'\r\n')[0]
|
||
|
else
|
||
|
cmd = string.split(cmd,'\r\n')[0]
|
||
|
end
|
||
|
|
||
|
log(f"FTP: Received: {cmd} {arg}",3)
|
||
|
|
||
|
# connect
|
||
|
if cmd == "USER"
|
||
|
if self.allow_anonymous
|
||
|
response = "230 accept any/anonymous user"
|
||
|
else
|
||
|
self.user_input = arg
|
||
|
response = "331 Password required"
|
||
|
end
|
||
|
elif cmd == "PASS"
|
||
|
if self.user_input == self.user && arg == self.password
|
||
|
response = "230 User accepted"
|
||
|
else
|
||
|
response = "530 Wrong login credentials"
|
||
|
mqtt.publish("FTP","{'login':'wrong credentials'}")
|
||
|
end
|
||
|
elif cmd == "AUTH"
|
||
|
response = f"500 Server does not support {arg}"
|
||
|
elif cmd == "ABOR"
|
||
|
self.abortDataOp()
|
||
|
response = f"200 Aborting"
|
||
|
elif cmd == "QUIT"
|
||
|
self.close()
|
||
|
#options
|
||
|
elif cmd == "FEAT"
|
||
|
self.sendResponse("211-Extensions supported:")
|
||
|
self.sendResponse(" MLSD")
|
||
|
self.sendResponse(" EPSV")
|
||
|
self.sendResponse(" SIZE")
|
||
|
# self.sendResponse(" MDTM")
|
||
|
self.sendResponse(" REST STREAM")
|
||
|
response = "211 End"
|
||
|
elif cmd == "OPTS"
|
||
|
if arg == "UTF8"
|
||
|
response = "200 UTF Ok"
|
||
|
else
|
||
|
response = f"500 Server does not support {arg}"
|
||
|
end
|
||
|
elif cmd == "STRU"
|
||
|
if arg == "F"
|
||
|
response = "200 F Ok"
|
||
|
else
|
||
|
response = "504 Only F (ile) is supported"
|
||
|
end
|
||
|
elif cmd == "SYST"
|
||
|
response = "215 UNIX"
|
||
|
elif cmd == "LPRT"
|
||
|
response = f"501 active connection with long address not supported"
|
||
|
elif cmd == "PORT"
|
||
|
var el = string.split(arg,",")
|
||
|
self.active_ip = f"{el[0]}.{el[1]}.{el[2]}.{el[3]}"
|
||
|
self.active_port = int(el[4])*256 + int(el[5])
|
||
|
response = f"200 port received {self.active_ip}:{self.active_port}"
|
||
|
self.deinitConnectServer()
|
||
|
# response = f"501 active connection not supported"
|
||
|
elif cmd == "EPRT"
|
||
|
var el = string.split(arg,"|") # |1|192.168.1.54|65519| -> 1 IPV4, 2 IPV6
|
||
|
self.active_ip = el[2]
|
||
|
self.active_port = int(el[3])
|
||
|
self.deinitConnectServer()
|
||
|
response = f"200 extended port received {self.active_ip}:{self.active_port}"
|
||
|
elif cmd == "TYPE"
|
||
|
if arg == "I"
|
||
|
response = "200 binary mode"
|
||
|
self.binary_mode = true
|
||
|
elif arg == "A"
|
||
|
response = "200 ascii mode"
|
||
|
self.binary_mode = false
|
||
|
end
|
||
|
elif cmd == "EPSV"
|
||
|
self.active_port = nil
|
||
|
self.initConnectServer()
|
||
|
response = f"229 Entering Extended Passive Mode (|||{self.data_port}|)"
|
||
|
elif cmd == "PASV"
|
||
|
self.active_port = nil
|
||
|
var el = string.split(self.data_ip,".")
|
||
|
var hi = self.data_port >> 8
|
||
|
var lo = self.data_port & 0xff
|
||
|
self.initConnectServer()
|
||
|
response = f"227 Entering passive mode ({el[0]},{el[1]},{el[2]},{el[3]},{hi},{lo})"
|
||
|
elif cmd == "DELE"
|
||
|
if path.remove(f"{self.dir.get_url()}{arg}")
|
||
|
response = f"250 {self.dir.get_url()}{arg} deleted"
|
||
|
else
|
||
|
response = f"550 Could not delete file {self.dir.get_url()}{arg}"
|
||
|
end
|
||
|
elif cmd == "RMD"
|
||
|
var url = arg
|
||
|
if arg[0] != "/"
|
||
|
url = f"{self.dir.get_url()}{arg}"
|
||
|
end
|
||
|
if path.rmdir(url)
|
||
|
response = f"250 {url} deleted"
|
||
|
else
|
||
|
response = f"550 Could not delete folder {url}"
|
||
|
end
|
||
|
elif cmd == "STOR"
|
||
|
self.dataconnect()
|
||
|
if self.data_client != nil
|
||
|
response = ""
|
||
|
var mode = "w"
|
||
|
if self.data_ptr > 0
|
||
|
mode = "a"
|
||
|
end
|
||
|
if self.openFile(arg,mode)
|
||
|
self.data_op = "u"
|
||
|
self.fast_loop = /->self.upload()
|
||
|
tasmota.add_fast_loop(self.fast_loop)
|
||
|
else
|
||
|
response = f"550 Could not open file"
|
||
|
end
|
||
|
else
|
||
|
response = f"501 Could not init data connection"
|
||
|
end
|
||
|
elif cmd == "REST"
|
||
|
self.data_ptr = int(arg)
|
||
|
response = f"350 {self.data_ptr}"
|
||
|
elif cmd == "RNFR"
|
||
|
if self.openFile(arg,"r")
|
||
|
self.file_rename = f"{self.dir.get_url()}{arg}"
|
||
|
response = f"350 {arg}"
|
||
|
self.file.close()
|
||
|
else
|
||
|
self.file_rename = nil
|
||
|
response = f"550 Could not open file"
|
||
|
end
|
||
|
elif cmd == "RNTO"
|
||
|
if self.file_rename != nil
|
||
|
tasmota.cmd(f"UfsRename {self.file_rename},{self.dir.get_url()}{arg}")
|
||
|
response = f"250 Renamed {self.file_rename} -> {arg}"
|
||
|
else
|
||
|
response = f"550 Could not rename file"
|
||
|
end
|
||
|
self.file_rename = nil
|
||
|
elif cmd == "SIZE"
|
||
|
if self.openFile(arg,"r")
|
||
|
response = f"213 {self.file.size()}"
|
||
|
self.file.close()
|
||
|
else
|
||
|
response = f"550 Could not open file"
|
||
|
end
|
||
|
elif cmd == "RETR"
|
||
|
self.dataconnect()
|
||
|
if self.data_client != nil
|
||
|
if self.openFile(arg,"r")
|
||
|
self.file_size = self.file.size()
|
||
|
self.data_op = "d"
|
||
|
self.fast_loop = /->self.download()
|
||
|
tasmota.add_fast_loop(self.fast_loop)
|
||
|
else
|
||
|
response = f"550 Could not open file"
|
||
|
end
|
||
|
else
|
||
|
response = f"501 Could not init data connection"
|
||
|
end
|
||
|
# folder
|
||
|
elif cmd == "CDUP"
|
||
|
self.dir.dir_up()
|
||
|
response = "250 okay"
|
||
|
elif cmd == "CWD"
|
||
|
if self.dir.set(arg)
|
||
|
response = "250 okay"
|
||
|
else
|
||
|
response = "550 Failed to change directory."
|
||
|
end
|
||
|
elif cmd == "PWD"
|
||
|
self.readDir()
|
||
|
response = f"250 {self.dir.get_url()}"
|
||
|
elif cmd == "MKD"
|
||
|
path.mkdir(f"{self.dir.get_url()}{arg}")
|
||
|
response = f"250 {self.dir.get_url()}{arg} created"
|
||
|
elif cmd == "LIST" || cmd == "MLSD" || cmd == "NLST"
|
||
|
if arg != ""
|
||
|
self.dir.set(arg)
|
||
|
end
|
||
|
self.readDir()
|
||
|
if self.dataconnect()
|
||
|
if size(self.dir_list) > 0
|
||
|
self.data_op = "dir"
|
||
|
self.dir_pos = 0
|
||
|
self.fast_loop = /->self.transferDir(cmd)
|
||
|
tasmota.add_fast_loop(self.fast_loop)
|
||
|
else
|
||
|
self.finishTransferDir(true)
|
||
|
end
|
||
|
else
|
||
|
response = f"501 Could not init data connection"
|
||
|
end
|
||
|
else # any unknown command
|
||
|
response = "202 Command not implemented in Berry FTP"
|
||
|
end
|
||
|
|
||
|
if response != ""
|
||
|
self.sendResponse(response)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
var ftp = FTP()
|