mirror of
https://github.com/c0de-archive/pixoPython.git
synced 2024-12-22 05:12:41 +00:00
move to own directory
This commit is contained in:
parent
0297799295
commit
8832e85727
@ -1 +0,0 @@
|
|||||||
Subproject commit b8d83138f8230c78fc7bb25f38f3de67cb4c3f00
|
|
0
.gitignore → pixo/.gitignore
vendored
0
.gitignore → pixo/.gitignore
vendored
0
.gitmodules → pixo/.gitmodules
vendored
0
.gitmodules → pixo/.gitmodules
vendored
21
pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/LICENSE
Normal file
21
pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2016
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
138
pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/README.md
Normal file
138
pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/README.md
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# uftpd: small FTP server for ESP8266, ESP32 and Pyboard D
|
||||||
|
|
||||||
|
**Intro**
|
||||||
|
|
||||||
|
Based on the work of chrisgp - Christopher Popp and pfalcon - Paul Sokolovsky
|
||||||
|
Christopher made a first uftp server script, which runs in foreground.
|
||||||
|
Paul made webrepl with the framework for background operations, which then was used
|
||||||
|
also by Christopher to implement his utelnetsever code.
|
||||||
|
My task was to put all these pieces together and assemble this uftpd.py script,
|
||||||
|
which runs in background and acts as ftp server.
|
||||||
|
Due to its size, for ESP8266 it either has to be integrated into the flash image as frozen
|
||||||
|
bytecode, by placing it into the esp8266/modules folder and performing a rebuild,
|
||||||
|
or it must be compiled into bytecode using mpy-cross and loaded as an .mpy file.
|
||||||
|
The frozen bytecode variant is preferred.
|
||||||
|
|
||||||
|
The server has some limitations:
|
||||||
|
- Binary mode only
|
||||||
|
- Limited multi-session support. The server accepts multiple sessions, but only
|
||||||
|
one session command at a time is served while the other sessions receive a 'busy'
|
||||||
|
response, which still allows interleaved actions.
|
||||||
|
- No user authentication. Any user may log in without a password. User
|
||||||
|
authentication may be added easily, if required.
|
||||||
|
- Not all ftp commands are implemented.
|
||||||
|
- ESP8266 is **NOT** a multitasking platform and the system calls are NOT re-entrant.
|
||||||
|
Even when the ftp server sits in background and can serve requests, **no
|
||||||
|
foreground tasks should run at that time**, especially if they execute system calls.
|
||||||
|
The effects is hardly to predict, although most of the time the device simply
|
||||||
|
crashes.
|
||||||
|
- ESP32 The server is supported from version='v1.9.3-575 on. That is the version
|
||||||
|
which introduced webrepl.
|
||||||
|
|
||||||
|
|
||||||
|
## Start-up
|
||||||
|
|
||||||
|
You'll start the server with:
|
||||||
|
|
||||||
|
`import uftpd`
|
||||||
|
|
||||||
|
The service will immediately be started at port 21 in silent mode. You may
|
||||||
|
stop the service then with:
|
||||||
|
|
||||||
|
`utfpd.stop()`
|
||||||
|
|
||||||
|
When stopped or not started yet, start it manually with:
|
||||||
|
|
||||||
|
`uftpd.start([port = 21][, verbose = level])`
|
||||||
|
or
|
||||||
|
`uftpd.restart([port = 21][, verbose = level])`
|
||||||
|
|
||||||
|
port is the port number (default 21)
|
||||||
|
verbose controls the level of printed activity messages, values 0 .. 2
|
||||||
|
|
||||||
|
You may use
|
||||||
|
`uftd.restart([port = 21][, verbose = level])`
|
||||||
|
as a shortcut for uftp.stop() and uftpd.start().
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
The server works well with most dedicated ftp clients, and most browsers and file
|
||||||
|
managers. These are test results with an arbitrary selected set:
|
||||||
|
|
||||||
|
**Linux**
|
||||||
|
|
||||||
|
- ftp: works for file & directory operations including support for the m* commands
|
||||||
|
- filezilla, fireftp: work fine, including loading into the editor & saving back.
|
||||||
|
Take care to limit the number of data session to 1.
|
||||||
|
- Nautilus: works mostly, including loading into the editor & saving back.
|
||||||
|
Copying multiple files at once to the esp8266 fails, because nautilus tries
|
||||||
|
to open multiple sessions for that purpose.
|
||||||
|
Configure Nautilus with dconf-editor to show directory count for local dirs only.
|
||||||
|
Once mounted, you can even open a terminal at that spot.
|
||||||
|
The path is something like: /run/user/1000/gvfs/ftp:host=x.y.y.z.
|
||||||
|
- Thunar: works fine, including loading & saving of files.
|
||||||
|
directly into e.g. an editor & saving back.
|
||||||
|
- Dolphin, Konqueror: work fine most of the time, including loading
|
||||||
|
directly into e.g. an editor & saving back. But no obvious disconnect.
|
||||||
|
- Chrome, Firefox: view/navigate directories & and view files
|
||||||
|
|
||||||
|
**Mac OS X, various Versions**
|
||||||
|
|
||||||
|
- ftp: works like on Linux
|
||||||
|
- Chrome, Firefox: view/navigate directories & and view files
|
||||||
|
- FileZilla, FireFtp, Cyberduck: Full operation, once proper configured (see above).
|
||||||
|
Configure Cyberduck to transfer data in the command session.
|
||||||
|
- Finder: Fails. It connects, but then locks in the attempt to display the
|
||||||
|
top level directory repeating attempts to open new sessions. Finder needs
|
||||||
|
full multi-session support, and never closes sessions properly.
|
||||||
|
- Mountainduck: Works well, including proper disconnect when closing.
|
||||||
|
|
||||||
|
|
||||||
|
**Windows 10** (and Windows XP)
|
||||||
|
|
||||||
|
- ftp: supported. Be aware that the Windows variant of ftp differs slightly
|
||||||
|
from the Linux variant, but the most used commands are the same.
|
||||||
|
- File explorer: view/navigate directories & and copy files. For editing files you
|
||||||
|
have to copy them to your PC and back. Windows explorer does not always release the
|
||||||
|
connection when it is closed, which just results in a silent connection, which
|
||||||
|
is closed latest when Windows is shut down.
|
||||||
|
- FileZilla, FireFtp, Cyberduck: Full operation, once proper configured (see above).
|
||||||
|
Configure Cyberduck to transfer data in the command session.
|
||||||
|
- WinSCP: works fine
|
||||||
|
- NppFTP - FTP extension to Notepad++: Works fine and is very convenient.
|
||||||
|
- Mountainduck: Works to some extent, but sometimes stumbles and takes a long
|
||||||
|
time to open a file.
|
||||||
|
|
||||||
|
**Android**
|
||||||
|
|
||||||
|
- ftp inside the terminal emulator termux: full operation.
|
||||||
|
- ftp-express
|
||||||
|
- Chrome: view/navigate directories & and view files
|
||||||
|
|
||||||
|
**IOS 9.1**
|
||||||
|
|
||||||
|
- FTP Client lite: works flawless
|
||||||
|
|
||||||
|
**Windows 10 mobile**
|
||||||
|
|
||||||
|
- Metro file manager: Works with file/directory view & navigate, file download,
|
||||||
|
file upload, file delete, file rename. Slow and chaotic sequence of FTP commands.
|
||||||
|
Many unneeded re-login attempts.
|
||||||
|
|
||||||
|
**Conclusion**: All dedicated ftp clients work fine, and most
|
||||||
|
of the file managers too.
|
||||||
|
|
||||||
|
## Trouble shooting
|
||||||
|
The only trouble observed so far was clients not releasing the connections. You may tell
|
||||||
|
by the value of `uftp.client_list`, which should be empty if no client is connected, or by issuing the command rstat in ftp, which shows the number of connected clients.
|
||||||
|
In that case you may restart the server with uftpd.restart(). If `uftd.client_busy`
|
||||||
|
is `True` when no client is connected, then restart the server with with
|
||||||
|
`uftpd.restart()`. If you want to see what happens at the server, you may set verbose to 2.
|
||||||
|
Just restart it with `uftpd.restart(verbose = 1)`, or set `uftpd.verbose_l = 1`, and
|
||||||
|
`uftpd.verbose_l = 0` to stop control messages again.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
- uftpd.py: Server source file for ESP8266 and ESP32 from version='v1.9.3-575 on
|
||||||
|
- ftp.py: Simple version of the ftp server, which works in foreground. This
|
||||||
|
can be used with all Micorpython versions. It terminates when the client closes the
|
||||||
|
session. Only a single session is supported by this variant.
|
||||||
|
- README.md: This one
|
331
pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/ftp.py
Normal file
331
pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/ftp.py
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
#
|
||||||
|
# Small ftp server for ESP8266 ans ESP32 Micropython
|
||||||
|
#
|
||||||
|
# Based on the work of chrisgp - Christopher Popp and pfalcon - Paul Sokolovsky
|
||||||
|
#
|
||||||
|
# The server accepts passive mode only.
|
||||||
|
# It runs in foreground and quits, when it receives a quit command
|
||||||
|
# Start the server with:
|
||||||
|
#
|
||||||
|
# import ftp
|
||||||
|
#
|
||||||
|
# Copyright (c) 2016 Christopher Popp (initial ftp server framework)
|
||||||
|
# Copyright (c) 2016 Robert Hammelrath (putting the pieces together
|
||||||
|
# and a few extensions)
|
||||||
|
# Distributed under MIT License
|
||||||
|
#
|
||||||
|
import socket
|
||||||
|
import network
|
||||||
|
import uos
|
||||||
|
import gc
|
||||||
|
|
||||||
|
|
||||||
|
def send_list_data(path, dataclient, full):
|
||||||
|
try: # whether path is a directory name
|
||||||
|
for fname in sorted(uos.listdir(path), key=str.lower):
|
||||||
|
dataclient.sendall(make_description(path, fname, full))
|
||||||
|
except: # path may be a file name or pattern
|
||||||
|
pattern = path.split("/")[-1]
|
||||||
|
path = path[:-(len(pattern) + 1)]
|
||||||
|
if path == "":
|
||||||
|
path = "/"
|
||||||
|
for fname in sorted(uos.listdir(path), key=str.lower):
|
||||||
|
if fncmp(fname, pattern):
|
||||||
|
dataclient.sendall(make_description(path, fname, full))
|
||||||
|
|
||||||
|
|
||||||
|
def make_description(path, fname, full):
|
||||||
|
if full:
|
||||||
|
stat = uos.stat(get_absolute_path(path, fname))
|
||||||
|
file_permissions = ("drwxr-xr-x"
|
||||||
|
if (stat[0] & 0o170000 == 0o040000)
|
||||||
|
else "-rw-r--r--")
|
||||||
|
file_size = stat[6]
|
||||||
|
description = "{} 1 owner group {:>10} Jan 1 2000 {}\r\n".format(
|
||||||
|
file_permissions, file_size, fname)
|
||||||
|
else:
|
||||||
|
description = fname + "\r\n"
|
||||||
|
return description
|
||||||
|
|
||||||
|
|
||||||
|
def send_file_data(path, dataclient):
|
||||||
|
with open(path, "r") as file:
|
||||||
|
chunk = file.read(512)
|
||||||
|
while len(chunk) > 0:
|
||||||
|
dataclient.sendall(chunk)
|
||||||
|
chunk = file.read(512)
|
||||||
|
|
||||||
|
|
||||||
|
def save_file_data(path, dataclient):
|
||||||
|
with open(path, "w") as file:
|
||||||
|
chunk = dataclient.recv(512)
|
||||||
|
while len(chunk) > 0:
|
||||||
|
file.write(chunk)
|
||||||
|
chunk = dataclient.recv(512)
|
||||||
|
|
||||||
|
|
||||||
|
def get_absolute_path(cwd, payload):
|
||||||
|
# Just a few special cases "..", "." and ""
|
||||||
|
# If payload start's with /, set cwd to /
|
||||||
|
# and consider the remainder a relative path
|
||||||
|
if payload.startswith('/'):
|
||||||
|
cwd = "/"
|
||||||
|
for token in payload.split("/"):
|
||||||
|
if token == '..':
|
||||||
|
if cwd != '/':
|
||||||
|
cwd = '/'.join(cwd.split('/')[:-1])
|
||||||
|
if cwd == '':
|
||||||
|
cwd = '/'
|
||||||
|
elif token != '.' and token != '':
|
||||||
|
if cwd == '/':
|
||||||
|
cwd += token
|
||||||
|
else:
|
||||||
|
cwd = cwd + '/' + token
|
||||||
|
return cwd
|
||||||
|
|
||||||
|
|
||||||
|
# compare fname against pattern. Pattern may contain
|
||||||
|
# wildcards ? and *.
|
||||||
|
def fncmp(fname, pattern):
|
||||||
|
pi = 0
|
||||||
|
si = 0
|
||||||
|
while pi < len(pattern) and si < len(fname):
|
||||||
|
if (fname[si] == pattern[pi]) or (pattern[pi] == '?'):
|
||||||
|
si += 1
|
||||||
|
pi += 1
|
||||||
|
else:
|
||||||
|
if pattern[pi] == '*': # recurse
|
||||||
|
if (pi + 1) == len(pattern):
|
||||||
|
return True
|
||||||
|
while si < len(fname):
|
||||||
|
if fncmp(fname[si:], pattern[pi+1:]):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
si += 1
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
if pi == len(pattern.rstrip("*")) and si == len(fname):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def ftpserver():
|
||||||
|
|
||||||
|
DATA_PORT = 13333
|
||||||
|
|
||||||
|
ftpsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
datasocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
|
||||||
|
ftpsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
datasocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
|
||||||
|
ftpsocket.bind(socket.getaddrinfo("0.0.0.0", 21)[0][4])
|
||||||
|
datasocket.bind(socket.getaddrinfo("0.0.0.0", DATA_PORT)[0][4])
|
||||||
|
|
||||||
|
ftpsocket.listen(1)
|
||||||
|
ftpsocket.settimeout(None)
|
||||||
|
datasocket.listen(1)
|
||||||
|
datasocket.settimeout(None)
|
||||||
|
|
||||||
|
msg_250_OK = '250 OK\r\n'
|
||||||
|
msg_550_fail = '550 Failed\r\n'
|
||||||
|
# check for an active interface, STA first
|
||||||
|
wlan = network.WLAN(network.STA_IF)
|
||||||
|
if wlan.active():
|
||||||
|
addr = wlan.ifconfig()[0]
|
||||||
|
else:
|
||||||
|
wlan = network.WLAN(network.AP_IF)
|
||||||
|
if wlan.active():
|
||||||
|
addr = wlan.ifconfig()[0]
|
||||||
|
else:
|
||||||
|
print("No active connection")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("FTP Server started on ", addr)
|
||||||
|
try:
|
||||||
|
dataclient = None
|
||||||
|
fromname = None
|
||||||
|
do_run = True
|
||||||
|
while do_run:
|
||||||
|
cl, remote_addr = ftpsocket.accept()
|
||||||
|
cl.settimeout(300)
|
||||||
|
cwd = '/'
|
||||||
|
try:
|
||||||
|
# print("FTP connection from:", remote_addr)
|
||||||
|
cl.sendall("220 Hello, this is the ESP8266/ESP32.\r\n")
|
||||||
|
while True:
|
||||||
|
gc.collect()
|
||||||
|
data = cl.readline().decode("utf-8").rstrip("\r\n")
|
||||||
|
if len(data) <= 0:
|
||||||
|
print("Client disappeared")
|
||||||
|
do_run = False
|
||||||
|
break
|
||||||
|
|
||||||
|
command = data.split(" ")[0].upper()
|
||||||
|
payload = data[len(command):].lstrip()
|
||||||
|
|
||||||
|
path = get_absolute_path(cwd, payload)
|
||||||
|
|
||||||
|
print("Command={}, Payload={}".format(command, payload))
|
||||||
|
|
||||||
|
if command == "USER":
|
||||||
|
cl.sendall("230 Logged in.\r\n")
|
||||||
|
elif command == "SYST":
|
||||||
|
cl.sendall("215 UNIX Type: L8\r\n")
|
||||||
|
elif command == "NOOP":
|
||||||
|
cl.sendall("200 OK\r\n")
|
||||||
|
elif command == "FEAT":
|
||||||
|
cl.sendall("211 no-features\r\n")
|
||||||
|
elif command == "PWD" or command == "XPWD":
|
||||||
|
cl.sendall('257 "{}"\r\n'.format(cwd))
|
||||||
|
elif command == "CWD" or command == "XCWD":
|
||||||
|
try:
|
||||||
|
files = uos.listdir(path)
|
||||||
|
cwd = path
|
||||||
|
cl.sendall(msg_250_OK)
|
||||||
|
except:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
elif command == "CDUP":
|
||||||
|
cwd = get_absolute_path(cwd, "..")
|
||||||
|
cl.sendall(msg_250_OK)
|
||||||
|
elif command == "TYPE":
|
||||||
|
# probably should switch between binary and not
|
||||||
|
cl.sendall('200 Transfer mode set\r\n')
|
||||||
|
elif command == "SIZE":
|
||||||
|
try:
|
||||||
|
size = uos.stat(path)[6]
|
||||||
|
cl.sendall('213 {}\r\n'.format(size))
|
||||||
|
except:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
elif command == "QUIT":
|
||||||
|
cl.sendall('221 Bye.\r\n')
|
||||||
|
do_run = False
|
||||||
|
break
|
||||||
|
elif command == "PASV":
|
||||||
|
cl.sendall('227 Entering Passive Mode ({},{},{}).\r\n'.
|
||||||
|
format(addr.replace('.', ','), DATA_PORT >> 8,
|
||||||
|
DATA_PORT % 256))
|
||||||
|
dataclient, data_addr = datasocket.accept()
|
||||||
|
print("FTP Data connection from:", data_addr)
|
||||||
|
DATA_PORT = 13333
|
||||||
|
active = False
|
||||||
|
elif command == "PORT":
|
||||||
|
items = payload.split(",")
|
||||||
|
if len(items) >= 6:
|
||||||
|
data_addr = '.'.join(items[:4])
|
||||||
|
# replace by command session addr
|
||||||
|
if data_addr == "127.0.1.1":
|
||||||
|
data_addr = remote_addr
|
||||||
|
DATA_PORT = int(items[4]) * 256 + int(items[5])
|
||||||
|
dataclient = socket.socket(socket.AF_INET,
|
||||||
|
socket.SOCK_STREAM)
|
||||||
|
dataclient.settimeout(10)
|
||||||
|
dataclient.connect((data_addr, DATA_PORT))
|
||||||
|
print("FTP Data connection with:", data_addr)
|
||||||
|
cl.sendall('200 OK\r\n')
|
||||||
|
active = True
|
||||||
|
else:
|
||||||
|
cl.sendall('504 Fail\r\n')
|
||||||
|
elif command == "LIST" or command == "NLST":
|
||||||
|
if not payload.startswith("-"):
|
||||||
|
place = path
|
||||||
|
else:
|
||||||
|
place = cwd
|
||||||
|
try:
|
||||||
|
cl.sendall("150 Here comes the directory listing.\r\n")
|
||||||
|
send_list_data(place, dataclient,
|
||||||
|
command == "LIST" or payload == "-l")
|
||||||
|
cl.sendall("226 Listed.\r\n")
|
||||||
|
except:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
if dataclient is not None:
|
||||||
|
dataclient.close()
|
||||||
|
dataclient = None
|
||||||
|
elif command == "RETR":
|
||||||
|
try:
|
||||||
|
cl.sendall("150 Opening data connection.\r\n")
|
||||||
|
send_file_data(path, dataclient)
|
||||||
|
cl.sendall("226 Transfer complete.\r\n")
|
||||||
|
except:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
if dataclient is not None:
|
||||||
|
dataclient.close()
|
||||||
|
dataclient = None
|
||||||
|
elif command == "STOR":
|
||||||
|
try:
|
||||||
|
cl.sendall("150 Ok to send data.\r\n")
|
||||||
|
save_file_data(path, dataclient)
|
||||||
|
cl.sendall("226 Transfer complete.\r\n")
|
||||||
|
except:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
if dataclient is not None:
|
||||||
|
dataclient.close()
|
||||||
|
dataclient = None
|
||||||
|
elif command == "DELE":
|
||||||
|
try:
|
||||||
|
uos.remove(path)
|
||||||
|
cl.sendall(msg_250_OK)
|
||||||
|
except:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
elif command == "RMD" or command == "XRMD":
|
||||||
|
try:
|
||||||
|
uos.rmdir(path)
|
||||||
|
cl.sendall(msg_250_OK)
|
||||||
|
except:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
elif command == "MKD" or command == "XMKD":
|
||||||
|
try:
|
||||||
|
uos.mkdir(path)
|
||||||
|
cl.sendall(msg_250_OK)
|
||||||
|
except:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
elif command == "RNFR":
|
||||||
|
fromname = path
|
||||||
|
cl.sendall("350 Rename from\r\n")
|
||||||
|
elif command == "RNTO":
|
||||||
|
if fromname is not None:
|
||||||
|
try:
|
||||||
|
uos.rename(fromname, path)
|
||||||
|
cl.sendall(msg_250_OK)
|
||||||
|
except:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
else:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
fromname = None
|
||||||
|
elif command == "MDTM":
|
||||||
|
try:
|
||||||
|
tm=localtime(uos.stat(path)[8])
|
||||||
|
cl.sendall('213 {:04d}{:02d}{:02d}{:02d}{:02d}{:02d}\r\n'.format(*tm[0:6]))
|
||||||
|
except:
|
||||||
|
cl.sendall('550 Fail\r\n')
|
||||||
|
elif command == "STAT":
|
||||||
|
if payload == "":
|
||||||
|
cl.sendall("211-Connected to ({})\r\n"
|
||||||
|
" Data address ({})\r\n"
|
||||||
|
"211 TYPE: Binary STRU: File MODE:"
|
||||||
|
" Stream\r\n".format(
|
||||||
|
remote_addr[0], addr))
|
||||||
|
else:
|
||||||
|
cl.sendall("213-Directory listing:\r\n")
|
||||||
|
send_list_data(path, cl, True)
|
||||||
|
cl.sendall("213 Done.\r\n")
|
||||||
|
else:
|
||||||
|
cl.sendall("502 Unsupported command.\r\n")
|
||||||
|
print("Unsupported command {} with payload {}".format(
|
||||||
|
command, payload))
|
||||||
|
except Exception as err:
|
||||||
|
print(err)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
cl.close()
|
||||||
|
cl = None
|
||||||
|
finally:
|
||||||
|
datasocket.close()
|
||||||
|
ftpsocket.close()
|
||||||
|
if dataclient is not None:
|
||||||
|
dataclient.close()
|
||||||
|
|
||||||
|
|
||||||
|
ftpserver()
|
334
pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/ftp_thread.py
Normal file
334
pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/ftp_thread.py
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
#
|
||||||
|
# Small ftp server for ESP8266 ans ESP32 Micropython
|
||||||
|
#
|
||||||
|
# Based on the work of chrisgp - Christopher Popp and pfalcon - Paul Sokolovsky
|
||||||
|
#
|
||||||
|
# The server accepts passive mode only.
|
||||||
|
# It runs in foreground and quits, when it receives a quit command
|
||||||
|
# Start the server with:
|
||||||
|
#
|
||||||
|
# import ftp
|
||||||
|
#
|
||||||
|
# Copyright (c) 2016 Christopher Popp (initial ftp server framework)
|
||||||
|
# Copyright (c) 2016 Robert Hammelrath (putting the pieces together
|
||||||
|
# and a few extensions)
|
||||||
|
# Distributed under MIT License
|
||||||
|
#
|
||||||
|
import socket
|
||||||
|
import network
|
||||||
|
import uos
|
||||||
|
import gc
|
||||||
|
|
||||||
|
|
||||||
|
def send_list_data(path, dataclient, full):
|
||||||
|
try: # whether path is a directory name
|
||||||
|
for fname in sorted(uos.listdir(path), key=str.lower):
|
||||||
|
dataclient.sendall(make_description(path, fname, full))
|
||||||
|
except: # path may be a file name or pattern
|
||||||
|
pattern = path.split("/")[-1]
|
||||||
|
path = path[:-(len(pattern) + 1)]
|
||||||
|
if path == "":
|
||||||
|
path = "/"
|
||||||
|
for fname in sorted(uos.listdir(path), key=str.lower):
|
||||||
|
if fncmp(fname, pattern):
|
||||||
|
dataclient.sendall(make_description(path, fname, full))
|
||||||
|
|
||||||
|
|
||||||
|
def make_description(path, fname, full):
|
||||||
|
if full:
|
||||||
|
stat = uos.stat(get_absolute_path(path, fname))
|
||||||
|
file_permissions = "drwxr-xr-x"\
|
||||||
|
if (stat[0] & 0o170000 == 0o040000)\
|
||||||
|
else "-rw-r--r--"
|
||||||
|
file_size = stat[6]
|
||||||
|
description = "{} 1 owner group {:>10} Jan 1 2000 {}\r\n".format(
|
||||||
|
file_permissions, file_size, fname)
|
||||||
|
else:
|
||||||
|
description = fname + "\r\n"
|
||||||
|
return description
|
||||||
|
|
||||||
|
|
||||||
|
def send_file_data(path, dataclient):
|
||||||
|
with open(path, "r") as file:
|
||||||
|
chunk = file.read(512)
|
||||||
|
while len(chunk) > 0:
|
||||||
|
dataclient.sendall(chunk)
|
||||||
|
chunk = file.read(512)
|
||||||
|
|
||||||
|
|
||||||
|
def save_file_data(path, dataclient):
|
||||||
|
with open(path, "w") as file:
|
||||||
|
chunk = dataclient.recv(512)
|
||||||
|
while len(chunk) > 0:
|
||||||
|
file.write(chunk)
|
||||||
|
chunk = dataclient.recv(512)
|
||||||
|
|
||||||
|
|
||||||
|
def get_absolute_path(cwd, payload):
|
||||||
|
# Just a few special cases "..", "." and ""
|
||||||
|
# If payload start's with /, set cwd to /
|
||||||
|
# and consider the remainder a relative path
|
||||||
|
if payload.startswith('/'):
|
||||||
|
cwd = "/"
|
||||||
|
for token in payload.split("/"):
|
||||||
|
if token == '..':
|
||||||
|
if cwd != '/':
|
||||||
|
cwd = '/'.join(cwd.split('/')[:-1])
|
||||||
|
if cwd == '':
|
||||||
|
cwd = '/'
|
||||||
|
elif token != '.' and token != '':
|
||||||
|
if cwd == '/':
|
||||||
|
cwd += token
|
||||||
|
else:
|
||||||
|
cwd = cwd + '/' + token
|
||||||
|
return cwd
|
||||||
|
|
||||||
|
|
||||||
|
# compare fname against pattern. Pattern may contain
|
||||||
|
# wildcards ? and *.
|
||||||
|
def fncmp(fname, pattern):
|
||||||
|
pi = 0
|
||||||
|
si = 0
|
||||||
|
while pi < len(pattern) and si < len(fname):
|
||||||
|
if (fname[si] == pattern[pi]) or (pattern[pi] == '?'):
|
||||||
|
si += 1
|
||||||
|
pi += 1
|
||||||
|
else:
|
||||||
|
if pattern[pi] == '*': # recurse
|
||||||
|
if (pi + 1) == len(pattern):
|
||||||
|
return True
|
||||||
|
while si < len(fname):
|
||||||
|
if fncmp(fname[si:], pattern[pi+1:]):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
si += 1
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
if pi == len(pattern.rstrip("*")) and si == len(fname):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def ftpserver(not_stop_on_quit):
|
||||||
|
|
||||||
|
DATA_PORT = 13333
|
||||||
|
|
||||||
|
ftpsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
datasocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
|
||||||
|
ftpsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
datasocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
|
||||||
|
ftpsocket.bind(socket.getaddrinfo("0.0.0.0", 21)[0][4])
|
||||||
|
datasocket.bind(socket.getaddrinfo("0.0.0.0", DATA_PORT)[0][4])
|
||||||
|
|
||||||
|
ftpsocket.listen(1)
|
||||||
|
ftpsocket.settimeout(None)
|
||||||
|
datasocket.listen(1)
|
||||||
|
datasocket.settimeout(None)
|
||||||
|
|
||||||
|
msg_250_OK = '250 OK\r\n'
|
||||||
|
msg_550_fail = '550 Failed\r\n'
|
||||||
|
# check for an active interface, STA first
|
||||||
|
wlan = network.WLAN(network.STA_IF)
|
||||||
|
if wlan.active():
|
||||||
|
addr = wlan.ifconfig()[0]
|
||||||
|
else:
|
||||||
|
wlan = network.WLAN(network.AP_IF)
|
||||||
|
if wlan.active():
|
||||||
|
addr = wlan.ifconfig()[0]
|
||||||
|
else:
|
||||||
|
print("No active connection")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("FTP Server started on ", addr)
|
||||||
|
try:
|
||||||
|
dataclient = None
|
||||||
|
fromname = None
|
||||||
|
do_run = True
|
||||||
|
while do_run:
|
||||||
|
cl, remote_addr = ftpsocket.accept()
|
||||||
|
cl.settimeout(300)
|
||||||
|
cwd = '/'
|
||||||
|
try:
|
||||||
|
# print("FTP connection from:", remote_addr)
|
||||||
|
cl.sendall("220 Hello, this is the ESP8266/ESP32.\r\n")
|
||||||
|
while True:
|
||||||
|
gc.collect()
|
||||||
|
data = cl.readline().decode("utf-8").rstrip("\r\n")
|
||||||
|
if len(data) <= 0:
|
||||||
|
print("Client disappeared")
|
||||||
|
do_run = not_stop_on_quit
|
||||||
|
break
|
||||||
|
|
||||||
|
command = data.split(" ")[0].upper()
|
||||||
|
payload = data[len(command):].lstrip()
|
||||||
|
|
||||||
|
path = get_absolute_path(cwd, payload)
|
||||||
|
|
||||||
|
print("Command={}, Payload={}".format(command, payload))
|
||||||
|
|
||||||
|
if command == "USER":
|
||||||
|
cl.sendall("230 Logged in.\r\n")
|
||||||
|
elif command == "SYST":
|
||||||
|
cl.sendall("215 UNIX Type: L8\r\n")
|
||||||
|
elif command == "NOOP":
|
||||||
|
cl.sendall("200 OK\r\n")
|
||||||
|
elif command == "FEAT":
|
||||||
|
cl.sendall("211 no-features\r\n")
|
||||||
|
elif command == "PWD" or command == "XPWD":
|
||||||
|
cl.sendall('257 "{}"\r\n'.format(cwd))
|
||||||
|
elif command == "CWD" or command == "XCWD":
|
||||||
|
try:
|
||||||
|
files = uos.listdir(path)
|
||||||
|
cwd = path
|
||||||
|
cl.sendall(msg_250_OK)
|
||||||
|
except:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
elif command == "CDUP":
|
||||||
|
cwd = get_absolute_path(cwd, "..")
|
||||||
|
cl.sendall(msg_250_OK)
|
||||||
|
elif command == "TYPE":
|
||||||
|
# probably should switch between binary and not
|
||||||
|
cl.sendall('200 Transfer mode set\r\n')
|
||||||
|
elif command == "SIZE":
|
||||||
|
try:
|
||||||
|
size = uos.stat(path)[6]
|
||||||
|
cl.sendall('213 {}\r\n'.format(size))
|
||||||
|
except:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
elif command == "QUIT":
|
||||||
|
cl.sendall('221 Bye.\r\n')
|
||||||
|
do_run = not_stop_on_quit
|
||||||
|
break
|
||||||
|
elif command == "PASV":
|
||||||
|
cl.sendall('227 Entering Passive Mode ({},{},{}).\r\n'.format(
|
||||||
|
addr.replace('.', ','), DATA_PORT >> 8, DATA_PORT % 256))
|
||||||
|
dataclient, data_addr = datasocket.accept()
|
||||||
|
print("FTP Data connection from:", data_addr)
|
||||||
|
DATA_PORT = 13333
|
||||||
|
active = False
|
||||||
|
elif command == "PORT":
|
||||||
|
items = payload.split(",")
|
||||||
|
if len(items) >= 6:
|
||||||
|
data_addr = '.'.join(items[:4])
|
||||||
|
if data_addr == "127.0.1.1":
|
||||||
|
# replace by command session addr
|
||||||
|
data_addr = remote_addr
|
||||||
|
DATA_PORT = int(items[4]) * 256 + int(items[5])
|
||||||
|
dataclient = socket.socket(socket.AF_INET,
|
||||||
|
socket.SOCK_STREAM)
|
||||||
|
dataclient.settimeout(10)
|
||||||
|
dataclient.connect((data_addr, DATA_PORT))
|
||||||
|
print("FTP Data connection with:", data_addr)
|
||||||
|
cl.sendall('200 OK\r\n')
|
||||||
|
active = True
|
||||||
|
else:
|
||||||
|
cl.sendall('504 Fail\r\n')
|
||||||
|
elif command == "LIST" or command == "NLST":
|
||||||
|
if not payload.startswith("-"):
|
||||||
|
place = path
|
||||||
|
else:
|
||||||
|
place = cwd
|
||||||
|
try:
|
||||||
|
cl.sendall("150 Here comes the "
|
||||||
|
"directory listing.\r\n")
|
||||||
|
send_list_data(place, dataclient,
|
||||||
|
command == "LIST" or payload == "-l")
|
||||||
|
cl.sendall("226 Listed.\r\n")
|
||||||
|
except:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
if dataclient is not None:
|
||||||
|
dataclient.close()
|
||||||
|
dataclient = None
|
||||||
|
elif command == "RETR":
|
||||||
|
try:
|
||||||
|
cl.sendall("150 Opening data connection.\r\n")
|
||||||
|
send_file_data(path, dataclient)
|
||||||
|
cl.sendall("226 Transfer complete.\r\n")
|
||||||
|
except:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
if dataclient is not None:
|
||||||
|
dataclient.close()
|
||||||
|
dataclient = None
|
||||||
|
elif command == "STOR":
|
||||||
|
try:
|
||||||
|
cl.sendall("150 Ok to send data.\r\n")
|
||||||
|
save_file_data(path, dataclient)
|
||||||
|
cl.sendall("226 Transfer complete.\r\n")
|
||||||
|
except:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
if dataclient is not None:
|
||||||
|
dataclient.close()
|
||||||
|
dataclient = None
|
||||||
|
elif command == "DELE":
|
||||||
|
try:
|
||||||
|
uos.remove(path)
|
||||||
|
cl.sendall(msg_250_OK)
|
||||||
|
except:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
elif command == "RMD" or command == "XRMD":
|
||||||
|
try:
|
||||||
|
uos.rmdir(path)
|
||||||
|
cl.sendall(msg_250_OK)
|
||||||
|
except:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
elif command == "MKD" or command == "XMKD":
|
||||||
|
try:
|
||||||
|
uos.mkdir(path)
|
||||||
|
cl.sendall(msg_250_OK)
|
||||||
|
except:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
elif command == "RNFR":
|
||||||
|
fromname = path
|
||||||
|
cl.sendall("350 Rename from\r\n")
|
||||||
|
elif command == "RNTO":
|
||||||
|
if fromname is not None:
|
||||||
|
try:
|
||||||
|
uos.rename(fromname, path)
|
||||||
|
cl.sendall(msg_250_OK)
|
||||||
|
except:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
else:
|
||||||
|
cl.sendall(msg_550_fail)
|
||||||
|
fromname = None
|
||||||
|
elif command == "MDTM":
|
||||||
|
try:
|
||||||
|
tm=localtime(uos.stat(path)[8])
|
||||||
|
cl.sendall('213 {:04d}{:02d}{:02d}{:02d}{:02d}{:02d}\r\n'.format(*tm[0:6]))
|
||||||
|
except:
|
||||||
|
cl.sendall('550 Fail\r\n')
|
||||||
|
elif command == "STAT":
|
||||||
|
if payload == "":
|
||||||
|
cl.sendall("211-Connected to ({})\r\n"
|
||||||
|
" Data address ({})\r\n"
|
||||||
|
"211 TYPE: Binary STRU: File "
|
||||||
|
"MODE: Stream\r\n".format(
|
||||||
|
remote_addr[0], addr))
|
||||||
|
else:
|
||||||
|
cl.sendall("213-Directory listing:\r\n")
|
||||||
|
send_list_data(path, cl, True)
|
||||||
|
cl.sendall("213 Done.\r\n")
|
||||||
|
else:
|
||||||
|
cl.sendall("502 Unsupported command.\r\n")
|
||||||
|
print("Unsupported command {} with payload {}".
|
||||||
|
format(command, payload))
|
||||||
|
except Exception as err:
|
||||||
|
print(err)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
cl.close()
|
||||||
|
cl = None
|
||||||
|
finally:
|
||||||
|
datasocket.close()
|
||||||
|
ftpsocket.close()
|
||||||
|
if dataclient is not None:
|
||||||
|
dataclient.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
import _thread
|
||||||
|
_thread.start_new_thread(ftpserver, ((True,)))
|
||||||
|
except:
|
||||||
|
ftpserver(False)
|
492
pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/uftpd.py
Normal file
492
pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/uftpd.py
Normal file
@ -0,0 +1,492 @@
|
|||||||
|
#
|
||||||
|
# Small ftp server for ESP8266 Micropython
|
||||||
|
# Based on the work of chrisgp - Christopher Popp and pfalcon - Paul Sokolovsky
|
||||||
|
#
|
||||||
|
# The server accepts passive mode only. It runs in background.
|
||||||
|
# Start the server with:
|
||||||
|
#
|
||||||
|
# import uftpd
|
||||||
|
# uftpd.start([port = 21][, verbose = level])
|
||||||
|
#
|
||||||
|
# port is the port number (default 21)
|
||||||
|
# verbose controls the level of printed activity messages, values 0, 1, 2
|
||||||
|
#
|
||||||
|
# Copyright (c) 2016 Christopher Popp (initial ftp server framework)
|
||||||
|
# Copyright (c) 2016 Paul Sokolovsky (background execution control structure)
|
||||||
|
# Copyright (c) 2016 Robert Hammelrath (putting the pieces together and a
|
||||||
|
# few extensions)
|
||||||
|
# Distributed under MIT License
|
||||||
|
#
|
||||||
|
import socket
|
||||||
|
import network
|
||||||
|
import uos
|
||||||
|
import gc
|
||||||
|
from time import sleep_ms, localtime
|
||||||
|
from micropython import alloc_emergency_exception_buf
|
||||||
|
|
||||||
|
# constant definitions
|
||||||
|
_CHUNK_SIZE = const(1024)
|
||||||
|
_SO_REGISTER_HANDLER = const(20)
|
||||||
|
_COMMAND_TIMEOUT = const(300)
|
||||||
|
_DATA_TIMEOUT = const(100)
|
||||||
|
_DATA_PORT = const(13333)
|
||||||
|
|
||||||
|
# Global variables
|
||||||
|
ftpsocket = None
|
||||||
|
datasocket = None
|
||||||
|
client_list = []
|
||||||
|
verbose_l = 0
|
||||||
|
client_busy = False
|
||||||
|
# Interfaces: (IP-Address (string), IP-Address (integer), Netmask (integer))
|
||||||
|
AP_addr = ("0.0.0.0", 0, 0xffffff00)
|
||||||
|
STA_addr = ("0.0.0.0", 0, 0xffffff00)
|
||||||
|
|
||||||
|
_month_name = ("", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||||
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
|
||||||
|
|
||||||
|
|
||||||
|
class FTP_client:
|
||||||
|
|
||||||
|
def __init__(self, ftpsocket):
|
||||||
|
global AP_addr, STA_addr
|
||||||
|
self.command_client, self.remote_addr = ftpsocket.accept()
|
||||||
|
self.remote_addr = self.remote_addr[0]
|
||||||
|
self.command_client.settimeout(_COMMAND_TIMEOUT)
|
||||||
|
log_msg(1, "FTP Command connection from:", self.remote_addr)
|
||||||
|
self.command_client.setsockopt(socket.SOL_SOCKET,
|
||||||
|
_SO_REGISTER_HANDLER,
|
||||||
|
self.exec_ftp_command)
|
||||||
|
self.command_client.sendall("220 Hello, this is the ESP8266.\r\n")
|
||||||
|
self.cwd = '/'
|
||||||
|
self.fromname = None
|
||||||
|
# self.logged_in = False
|
||||||
|
self.act_data_addr = self.remote_addr
|
||||||
|
self.DATA_PORT = 20
|
||||||
|
self.active = True
|
||||||
|
# check which interface was used by comparing the caller's ip
|
||||||
|
# adress with the ip adresses of STA and AP; consider netmask;
|
||||||
|
# select IP address for passive mode
|
||||||
|
if ((AP_addr[1] & AP_addr[2]) ==
|
||||||
|
(num_ip(self.remote_addr) & AP_addr[2])):
|
||||||
|
self.pasv_data_addr = AP_addr[0]
|
||||||
|
elif ((STA_addr[1] & STA_addr[2]) ==
|
||||||
|
(num_ip(self.remote_addr) & STA_addr[2])):
|
||||||
|
self.pasv_data_addr = STA_addr[0]
|
||||||
|
else:
|
||||||
|
self.pasv_data_addr = "0.0.0.0" # Ivalid value
|
||||||
|
|
||||||
|
def send_list_data(self, path, data_client, full):
|
||||||
|
try:
|
||||||
|
for fname in uos.listdir(path):
|
||||||
|
data_client.sendall(self.make_description(path, fname, full))
|
||||||
|
except: # path may be a file name or pattern
|
||||||
|
path, pattern = self.split_path(path)
|
||||||
|
try:
|
||||||
|
for fname in uos.listdir(path):
|
||||||
|
if self.fncmp(fname, pattern):
|
||||||
|
data_client.sendall(
|
||||||
|
self.make_description(path, fname, full))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def make_description(self, path, fname, full):
|
||||||
|
global _month_name
|
||||||
|
if full:
|
||||||
|
stat = uos.stat(self.get_absolute_path(path, fname))
|
||||||
|
file_permissions = ("drwxr-xr-x"
|
||||||
|
if (stat[0] & 0o170000 == 0o040000)
|
||||||
|
else "-rw-r--r--")
|
||||||
|
file_size = stat[6]
|
||||||
|
tm = localtime(stat[7])
|
||||||
|
if tm[0] != localtime()[0]:
|
||||||
|
description = "{} 1 owner group {:>10} {} {:2} {:>5} {}\r\n".\
|
||||||
|
format(file_permissions, file_size,
|
||||||
|
_month_name[tm[1]], tm[2], tm[0], fname)
|
||||||
|
else:
|
||||||
|
description = "{} 1 owner group {:>10} {} {:2} {:02}:{:02} {}\r\n".\
|
||||||
|
format(file_permissions, file_size,
|
||||||
|
_month_name[tm[1]], tm[2], tm[3], tm[4], fname)
|
||||||
|
else:
|
||||||
|
description = fname + "\r\n"
|
||||||
|
return description
|
||||||
|
|
||||||
|
def send_file_data(self, path, data_client):
|
||||||
|
with open(path, "r") as file:
|
||||||
|
chunk = file.read(_CHUNK_SIZE)
|
||||||
|
while len(chunk) > 0:
|
||||||
|
data_client.sendall(chunk)
|
||||||
|
chunk = file.read(_CHUNK_SIZE)
|
||||||
|
data_client.close()
|
||||||
|
|
||||||
|
def save_file_data(self, path, data_client, mode):
|
||||||
|
with open(path, mode) as file:
|
||||||
|
chunk = data_client.recv(_CHUNK_SIZE)
|
||||||
|
while len(chunk) > 0:
|
||||||
|
file.write(chunk)
|
||||||
|
chunk = data_client.recv(_CHUNK_SIZE)
|
||||||
|
data_client.close()
|
||||||
|
|
||||||
|
def get_absolute_path(self, cwd, payload):
|
||||||
|
# Just a few special cases "..", "." and ""
|
||||||
|
# If payload start's with /, set cwd to /
|
||||||
|
# and consider the remainder a relative path
|
||||||
|
if payload.startswith('/'):
|
||||||
|
cwd = "/"
|
||||||
|
for token in payload.split("/"):
|
||||||
|
if token == '..':
|
||||||
|
cwd = self.split_path(cwd)[0]
|
||||||
|
elif token != '.' and token != '':
|
||||||
|
if cwd == '/':
|
||||||
|
cwd += token
|
||||||
|
else:
|
||||||
|
cwd = cwd + '/' + token
|
||||||
|
return cwd
|
||||||
|
|
||||||
|
def split_path(self, path): # instead of path.rpartition('/')
|
||||||
|
tail = path.split('/')[-1]
|
||||||
|
head = path[:-(len(tail) + 1)]
|
||||||
|
return ('/' if head == '' else head, tail)
|
||||||
|
|
||||||
|
# compare fname against pattern. Pattern may contain
|
||||||
|
# the wildcards ? and *.
|
||||||
|
def fncmp(self, fname, pattern):
|
||||||
|
pi = 0
|
||||||
|
si = 0
|
||||||
|
while pi < len(pattern) and si < len(fname):
|
||||||
|
if (fname[si] == pattern[pi]) or (pattern[pi] == '?'):
|
||||||
|
si += 1
|
||||||
|
pi += 1
|
||||||
|
else:
|
||||||
|
if pattern[pi] == '*': # recurse
|
||||||
|
if pi == len(pattern.rstrip("*?")): # only wildcards left
|
||||||
|
return True
|
||||||
|
while si < len(fname):
|
||||||
|
if self.fncmp(fname[si:], pattern[pi + 1:]):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
si += 1
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
if pi == len(pattern.rstrip("*")) and si == len(fname):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def open_dataclient(self):
|
||||||
|
if self.active: # active mode
|
||||||
|
data_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
data_client.settimeout(_DATA_TIMEOUT)
|
||||||
|
data_client.connect((self.act_data_addr, self.DATA_PORT))
|
||||||
|
log_msg(1, "FTP Data connection with:", self.act_data_addr)
|
||||||
|
else: # passive mode
|
||||||
|
data_client, data_addr = datasocket.accept()
|
||||||
|
log_msg(1, "FTP Data connection with:", data_addr[0])
|
||||||
|
return data_client
|
||||||
|
|
||||||
|
def exec_ftp_command(self, cl):
|
||||||
|
global datasocket
|
||||||
|
global client_busy
|
||||||
|
global my_ip_addr
|
||||||
|
|
||||||
|
try:
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
data = cl.readline().decode("utf-8").rstrip("\r\n")
|
||||||
|
|
||||||
|
if len(data) <= 0:
|
||||||
|
# No data, close
|
||||||
|
# This part is NOT CLEAN; there is still a chance that a
|
||||||
|
# closing data connection will be signalled as closing
|
||||||
|
# command connection
|
||||||
|
log_msg(1, "*** No data, assume QUIT")
|
||||||
|
close_client(cl)
|
||||||
|
return
|
||||||
|
|
||||||
|
if client_busy: # check if another client is busy
|
||||||
|
cl.sendall("400 Device busy.\r\n") # tell so the remote client
|
||||||
|
return # and quit
|
||||||
|
client_busy = True # now it's my turn
|
||||||
|
|
||||||
|
# check for log-in state may done here, like
|
||||||
|
# if self.logged_in == False and not command in\
|
||||||
|
# ("USER", "PASS", "QUIT"):
|
||||||
|
# cl.sendall("530 Not logged in.\r\n")
|
||||||
|
# return
|
||||||
|
|
||||||
|
command = data.split()[0].upper()
|
||||||
|
payload = data[len(command):].lstrip() # partition is missing
|
||||||
|
path = self.get_absolute_path(self.cwd, payload)
|
||||||
|
log_msg(1, "Command={}, Payload={}".format(command, payload))
|
||||||
|
|
||||||
|
if command == "USER":
|
||||||
|
# self.logged_in = True
|
||||||
|
cl.sendall("230 Logged in.\r\n")
|
||||||
|
# If you want to see a password,return
|
||||||
|
# "331 Need password.\r\n" instead
|
||||||
|
# If you want to reject an user, return
|
||||||
|
# "530 Not logged in.\r\n"
|
||||||
|
elif command == "PASS":
|
||||||
|
# you may check here for a valid password and return
|
||||||
|
# "530 Not logged in.\r\n" in case it's wrong
|
||||||
|
# self.logged_in = True
|
||||||
|
cl.sendall("230 Logged in.\r\n")
|
||||||
|
elif command == "SYST":
|
||||||
|
cl.sendall("215 UNIX Type: L8\r\n")
|
||||||
|
elif command in ("TYPE", "NOOP", "ABOR"): # just accept & ignore
|
||||||
|
cl.sendall('200 OK\r\n')
|
||||||
|
elif command == "QUIT":
|
||||||
|
cl.sendall('221 Bye.\r\n')
|
||||||
|
close_client(cl)
|
||||||
|
elif command == "PWD" or command == "XPWD":
|
||||||
|
cl.sendall('257 "{}"\r\n'.format(self.cwd))
|
||||||
|
elif command == "CWD" or command == "XCWD":
|
||||||
|
try:
|
||||||
|
if (uos.stat(path)[0] & 0o170000) == 0o040000:
|
||||||
|
self.cwd = path
|
||||||
|
cl.sendall('250 OK\r\n')
|
||||||
|
else:
|
||||||
|
cl.sendall('550 Fail\r\n')
|
||||||
|
except:
|
||||||
|
cl.sendall('550 Fail\r\n')
|
||||||
|
elif command == "PASV":
|
||||||
|
cl.sendall('227 Entering Passive Mode ({},{},{}).\r\n'.format(
|
||||||
|
self.pasv_data_addr.replace('.', ','),
|
||||||
|
_DATA_PORT >> 8, _DATA_PORT % 256))
|
||||||
|
self.active = False
|
||||||
|
elif command == "PORT":
|
||||||
|
items = payload.split(",")
|
||||||
|
if len(items) >= 6:
|
||||||
|
self.act_data_addr = '.'.join(items[:4])
|
||||||
|
if self.act_data_addr == "127.0.1.1":
|
||||||
|
# replace by command session addr
|
||||||
|
self.act_data_addr = self.remote_addr
|
||||||
|
self.DATA_PORT = int(items[4]) * 256 + int(items[5])
|
||||||
|
cl.sendall('200 OK\r\n')
|
||||||
|
self.active = True
|
||||||
|
else:
|
||||||
|
cl.sendall('504 Fail\r\n')
|
||||||
|
elif command == "LIST" or command == "NLST":
|
||||||
|
if payload.startswith("-"):
|
||||||
|
option = payload.split()[0].lower()
|
||||||
|
path = self.get_absolute_path(
|
||||||
|
self.cwd, payload[len(option):].lstrip())
|
||||||
|
else:
|
||||||
|
option = ""
|
||||||
|
try:
|
||||||
|
data_client = self.open_dataclient()
|
||||||
|
cl.sendall("150 Directory listing:\r\n")
|
||||||
|
self.send_list_data(path, data_client,
|
||||||
|
command == "LIST" or 'l' in option)
|
||||||
|
cl.sendall("226 Done.\r\n")
|
||||||
|
data_client.close()
|
||||||
|
except:
|
||||||
|
cl.sendall('550 Fail\r\n')
|
||||||
|
if data_client is not None:
|
||||||
|
data_client.close()
|
||||||
|
elif command == "RETR":
|
||||||
|
try:
|
||||||
|
data_client = self.open_dataclient()
|
||||||
|
cl.sendall("150 Opened data connection.\r\n")
|
||||||
|
self.send_file_data(path, data_client)
|
||||||
|
# if the next statement is reached,
|
||||||
|
# the data_client was closed.
|
||||||
|
data_client = None
|
||||||
|
cl.sendall("226 Done.\r\n")
|
||||||
|
except:
|
||||||
|
cl.sendall('550 Fail\r\n')
|
||||||
|
if data_client is not None:
|
||||||
|
data_client.close()
|
||||||
|
elif command == "STOR" or command == "APPE":
|
||||||
|
try:
|
||||||
|
data_client = self.open_dataclient()
|
||||||
|
cl.sendall("150 Opened data connection.\r\n")
|
||||||
|
self.save_file_data(path, data_client,
|
||||||
|
"w" if command == "STOR" else "a")
|
||||||
|
# if the next statement is reached,
|
||||||
|
# the data_client was closed.
|
||||||
|
data_client = None
|
||||||
|
cl.sendall("226 Done.\r\n")
|
||||||
|
except:
|
||||||
|
cl.sendall('550 Fail\r\n')
|
||||||
|
if data_client is not None:
|
||||||
|
data_client.close()
|
||||||
|
elif command == "SIZE":
|
||||||
|
try:
|
||||||
|
cl.sendall('213 {}\r\n'.format(uos.stat(path)[6]))
|
||||||
|
except:
|
||||||
|
cl.sendall('550 Fail\r\n')
|
||||||
|
elif command == "MDTM":
|
||||||
|
try:
|
||||||
|
tm=localtime(uos.stat(path)[8])
|
||||||
|
cl.sendall('213 {:04d}{:02d}{:02d}{:02d}{:02d}{:02d}\r\n'.format(*tm[0:6]))
|
||||||
|
except:
|
||||||
|
cl.sendall('550 Fail\r\n')
|
||||||
|
elif command == "STAT":
|
||||||
|
if payload == "":
|
||||||
|
cl.sendall("211-Connected to ({})\r\n"
|
||||||
|
" Data address ({})\r\n"
|
||||||
|
" TYPE: Binary STRU: File MODE: Stream\r\n"
|
||||||
|
" Session timeout {}\r\n"
|
||||||
|
"211 Client count is {}\r\n".format(
|
||||||
|
self.remote_addr, self.pasv_data_addr,
|
||||||
|
_COMMAND_TIMEOUT, len(client_list)))
|
||||||
|
else:
|
||||||
|
cl.sendall("213-Directory listing:\r\n")
|
||||||
|
self.send_list_data(path, cl, True)
|
||||||
|
cl.sendall("213 Done.\r\n")
|
||||||
|
elif command == "DELE":
|
||||||
|
try:
|
||||||
|
uos.remove(path)
|
||||||
|
cl.sendall('250 OK\r\n')
|
||||||
|
except:
|
||||||
|
cl.sendall('550 Fail\r\n')
|
||||||
|
elif command == "RNFR":
|
||||||
|
try:
|
||||||
|
# just test if the name exists, exception if not
|
||||||
|
uos.stat(path)
|
||||||
|
self.fromname = path
|
||||||
|
cl.sendall("350 Rename from\r\n")
|
||||||
|
except:
|
||||||
|
cl.sendall('550 Fail\r\n')
|
||||||
|
elif command == "RNTO":
|
||||||
|
try:
|
||||||
|
uos.rename(self.fromname, path)
|
||||||
|
cl.sendall('250 OK\r\n')
|
||||||
|
except:
|
||||||
|
cl.sendall('550 Fail\r\n')
|
||||||
|
self.fromname = None
|
||||||
|
elif command == "CDUP" or command == "XCUP":
|
||||||
|
self.cwd = self.get_absolute_path(self.cwd, "..")
|
||||||
|
cl.sendall('250 OK\r\n')
|
||||||
|
elif command == "RMD" or command == "XRMD":
|
||||||
|
try:
|
||||||
|
uos.rmdir(path)
|
||||||
|
cl.sendall('250 OK\r\n')
|
||||||
|
except:
|
||||||
|
cl.sendall('550 Fail\r\n')
|
||||||
|
elif command == "MKD" or command == "XMKD":
|
||||||
|
try:
|
||||||
|
uos.mkdir(path)
|
||||||
|
cl.sendall('250 OK\r\n')
|
||||||
|
except:
|
||||||
|
cl.sendall('550 Fail\r\n')
|
||||||
|
else:
|
||||||
|
cl.sendall("502 Unsupported command.\r\n")
|
||||||
|
# log_msg(2,
|
||||||
|
# "Unsupported command {} with payload {}".format(command,
|
||||||
|
# payload))
|
||||||
|
# handle unexpected errors
|
||||||
|
except Exception as err:
|
||||||
|
log_msg(1, "Exception in exec_ftp_command: {}".format(err))
|
||||||
|
# tidy up before leaving
|
||||||
|
client_busy = False
|
||||||
|
|
||||||
|
|
||||||
|
def log_msg(level, *args):
|
||||||
|
global verbose_l
|
||||||
|
if verbose_l >= level:
|
||||||
|
print(*args)
|
||||||
|
|
||||||
|
|
||||||
|
# close client and remove it from the list
|
||||||
|
def close_client(cl):
|
||||||
|
cl.setsockopt(socket.SOL_SOCKET, _SO_REGISTER_HANDLER, None)
|
||||||
|
cl.close()
|
||||||
|
for i, client in enumerate(client_list):
|
||||||
|
if client.command_client == cl:
|
||||||
|
del client_list[i]
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def accept_ftp_connect(ftpsocket):
|
||||||
|
# Accept new calls for the server
|
||||||
|
try:
|
||||||
|
client_list.append(FTP_client(ftpsocket))
|
||||||
|
except:
|
||||||
|
log_msg(1, "Attempt to connect failed")
|
||||||
|
# try at least to reject
|
||||||
|
try:
|
||||||
|
temp_client, temp_addr = ftpsocket.accept()
|
||||||
|
temp_client.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def num_ip(ip):
|
||||||
|
items = ip.split(".")
|
||||||
|
return (int(items[0]) << 24 | int(items[1]) << 16 |
|
||||||
|
int(items[2]) << 8 | int(items[3]))
|
||||||
|
|
||||||
|
|
||||||
|
def stop():
|
||||||
|
global ftpsocket, datasocket
|
||||||
|
global client_list
|
||||||
|
global client_busy
|
||||||
|
|
||||||
|
for client in client_list:
|
||||||
|
client.command_client.setsockopt(socket.SOL_SOCKET,
|
||||||
|
_SO_REGISTER_HANDLER, None)
|
||||||
|
client.command_client.close()
|
||||||
|
del client_list
|
||||||
|
client_list = []
|
||||||
|
client_busy = False
|
||||||
|
if ftpsocket is not None:
|
||||||
|
ftpsocket.setsockopt(socket.SOL_SOCKET, _SO_REGISTER_HANDLER, None)
|
||||||
|
ftpsocket.close()
|
||||||
|
if datasocket is not None:
|
||||||
|
datasocket.close()
|
||||||
|
|
||||||
|
|
||||||
|
# start listening for ftp connections on port 21
|
||||||
|
def start(port=21, verbose=0, splash=True):
|
||||||
|
global ftpsocket, datasocket
|
||||||
|
global verbose_l
|
||||||
|
global client_list
|
||||||
|
global client_busy
|
||||||
|
global AP_addr, STA_addr
|
||||||
|
|
||||||
|
alloc_emergency_exception_buf(100)
|
||||||
|
verbose_l = verbose
|
||||||
|
client_list = []
|
||||||
|
client_busy = False
|
||||||
|
|
||||||
|
ftpsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
datasocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
|
||||||
|
ftpsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
datasocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
|
||||||
|
ftpsocket.bind(('0.0.0.0', port))
|
||||||
|
datasocket.bind(('0.0.0.0', _DATA_PORT))
|
||||||
|
|
||||||
|
ftpsocket.listen(0)
|
||||||
|
datasocket.listen(0)
|
||||||
|
|
||||||
|
datasocket.settimeout(10)
|
||||||
|
ftpsocket.setsockopt(socket.SOL_SOCKET,
|
||||||
|
_SO_REGISTER_HANDLER, accept_ftp_connect)
|
||||||
|
|
||||||
|
wlan = network.WLAN(network.AP_IF)
|
||||||
|
if wlan.active():
|
||||||
|
ifconfig = wlan.ifconfig()
|
||||||
|
# save IP address string and numerical values of IP adress and netmask
|
||||||
|
AP_addr = (ifconfig[0], num_ip(ifconfig[0]), num_ip(ifconfig[1]))
|
||||||
|
if splash:
|
||||||
|
print("FTP server started on {}:{}".format(ifconfig[0], port))
|
||||||
|
wlan = network.WLAN(network.STA_IF)
|
||||||
|
if wlan.active():
|
||||||
|
ifconfig = wlan.ifconfig()
|
||||||
|
# save IP address string and numerical values of IP adress and netmask
|
||||||
|
STA_addr = (ifconfig[0], num_ip(ifconfig[0]), num_ip(ifconfig[1]))
|
||||||
|
if splash:
|
||||||
|
print("FTP server started on {}:{}".format(ifconfig[0], port))
|
||||||
|
|
||||||
|
|
||||||
|
def restart(port=21, verbose=0, splash=True):
|
||||||
|
stop()
|
||||||
|
sleep_ms(200)
|
||||||
|
start(port, verbose, splash)
|
||||||
|
|
||||||
|
|
||||||
|
start(splash=True)
|
Loading…
Reference in New Issue
Block a user