diff --git a/FTP-Server-for-ESP8266-ESP32-and-PYBD b/FTP-Server-for-ESP8266-ESP32-and-PYBD deleted file mode 160000 index b8d8313..0000000 --- a/FTP-Server-for-ESP8266-ESP32-and-PYBD +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b8d83138f8230c78fc7bb25f38f3de67cb4c3f00 diff --git a/.gitignore b/pixo/.gitignore similarity index 100% rename from .gitignore rename to pixo/.gitignore diff --git a/.gitmodules b/pixo/.gitmodules similarity index 100% rename from .gitmodules rename to pixo/.gitmodules diff --git a/pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/LICENSE b/pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/LICENSE new file mode 100644 index 0000000..a8fc851 --- /dev/null +++ b/pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/LICENSE @@ -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. diff --git a/pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/README.md b/pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/README.md new file mode 100644 index 0000000..20b72c9 --- /dev/null +++ b/pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/README.md @@ -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 diff --git a/pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/ftp.py b/pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/ftp.py new file mode 100644 index 0000000..7e068f0 --- /dev/null +++ b/pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/ftp.py @@ -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() diff --git a/pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/ftp_thread.py b/pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/ftp_thread.py new file mode 100644 index 0000000..c791685 --- /dev/null +++ b/pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/ftp_thread.py @@ -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) diff --git a/pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/uftpd.py b/pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/uftpd.py new file mode 100644 index 0000000..d9e40f9 --- /dev/null +++ b/pixo/FTP-Server-for-ESP8266-ESP32-and-PYBD/uftpd.py @@ -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) diff --git a/Pipfile b/pixo/Pipfile similarity index 100% rename from Pipfile rename to pixo/Pipfile diff --git a/board.py b/pixo/board.py similarity index 100% rename from board.py rename to pixo/board.py diff --git a/boot.py b/pixo/boot.py similarity index 100% rename from boot.py rename to pixo/boot.py diff --git a/helpers.py b/pixo/helpers.py similarity index 100% rename from helpers.py rename to pixo/helpers.py diff --git a/pixels.py b/pixo/pixels.py similarity index 100% rename from pixels.py rename to pixo/pixels.py diff --git a/wifi.py b/pixo/wifi.py similarity index 100% rename from wifi.py rename to pixo/wifi.py