├── LICENSE ├── README.md ├── ftp.py ├── ftp_pycom.py ├── ftp_thread.py ├── package.json └── uftpd.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uftpd: small FTP server for ESP8266, ESP32 and Pyboard D 2 | 3 | **Intro** 4 | 5 | Based on the work of chrisgp - Christopher Popp and pfalcon - Paul Sokolovsky 6 | Christopher made a first uftp server script, which runs in foreground. 7 | Paul made webrepl with the framework for background operations, which then was used 8 | also by Christopher to implement his utelnetsever code. 9 | My task was to put all these pieces together and assemble this uftpd.py script, 10 | which runs in background and acts as ftp server. 11 | Due to its size, for ESP8266 it either has to be integrated into the flash image as frozen 12 | bytecode, by placing it into the esp8266/modules folder and performing a rebuild, 13 | or it must be compiled into bytecode using mpy-cross and loaded as an .mpy file. 14 | The frozen bytecode variant is preferred. 15 | 16 | ## Limitations 17 | 18 | The server has some limitations: 19 | - Binary mode only 20 | - Limited multi-session support. The server accepts multiple sessions, but only 21 | one session command at a time is served while the other sessions receive a 'busy' 22 | response, which still allows interleaved actions. 23 | - No user authentication. Any user may log in without a password. User 24 | authentication may be added easily, if required. 25 | - Not all ftp commands are implemented. 26 | - ESP8266 is **NOT** a multitasking platform and the system calls are NOT re-entrant. 27 | Even when the ftp server sits in background and can serve requests, **no 28 | foreground tasks should run at that time**, especially if they execute system calls. 29 | The effects is hardly to predict, although most of the time the device simply 30 | crashes. Also in turn, when using the SITE command, the code in the payload MUST NOT 31 | be blocking, because that will block the device. 32 | - ESP32 The server is supported from version='v1.9.3-575 on. That is the version 33 | which introduced webrepl. 34 | 35 | 36 | ## Start-up 37 | 38 | You'll start the server with: 39 | 40 | `import uftpd` 41 | 42 | The service will immediately be started at port 21 in silent mode. You may 43 | stop the service then with: 44 | 45 | `uftpd.stop()` 46 | 47 | When stopped or not started yet, start it manually with: 48 | 49 | `uftpd.start([port = 21][, verbose = level])` 50 | or 51 | `uftpd.restart([port = 21][, verbose = level])` 52 | 53 | port is the port number (default 21) 54 | verbose controls the level of printed activity messages, values 0 .. 2 55 | 56 | You may use 57 | `uftd.restart([port = 21][, verbose = level])` 58 | as a shortcut for uftp.stop() and uftpd.start(). 59 | 60 | ## Coverage 61 | The server works well with most dedicated ftp clients, and most browsers and file 62 | managers. These are test results with an arbitrary selected set: 63 | 64 | **Linux** 65 | 66 | - ftp: works for file & directory operations including support for the m* commands 67 | - filezilla, fireftp: work fine, including loading into the editor & saving back. 68 | Take care to limit the number of data session to 1. 69 | - Nautilus: works mostly, including loading into the editor & saving back. 70 | Copying multiple files at once to the esp8266 fails, because nautilus tries 71 | to open multiple sessions for that purpose. 72 | Configure Nautilus with dconf-editor to show directory count for local dirs only. 73 | Once mounted, you can even open a terminal at that spot. 74 | The path is something like: /run/user/1000/gvfs/ftp:host=x.y.y.z. 75 | - Thunar: works fine, including loading & saving of files. 76 | directly into e.g. an editor & saving back. 77 | - Dolphin, Konqueror: work fine most of the time, including loading 78 | directly into e.g. an editor & saving back. But no obvious disconnect. 79 | - Chrome, Firefox: view/navigate directories & and view files 80 | 81 | **Mac OS X, various Versions** 82 | 83 | - ftp: works like on Linux 84 | - Chrome, Firefox: view/navigate directories & and view files 85 | - FileZilla, FireFtp, Cyberduck: Full operation, once proper configured (see above). 86 | Configure Cyberduck to transfer data in the command session. 87 | - Finder: Fails. It connects, but then locks in the attempt to display the 88 | top level directory repeating attempts to open new sessions. Finder needs 89 | full multi-session support, and never closes sessions properly. 90 | - Mountainduck: Works well, including proper disconnect when closing. 91 | 92 | 93 | **Windows 10** (and Windows XP) 94 | 95 | - ftp: supported. Be aware that the Windows variant of ftp differs slightly 96 | from the Linux variant, but the most used commands are the same. 97 | - File explorer: view/navigate directories & and copy files. For editing files you 98 | have to copy them to your PC and back. Windows explorer does not always release the 99 | connection when it is closed, which just results in a silent connection, which 100 | is closed latest when Windows is shut down. 101 | - FileZilla, FireFtp, Cyberduck: Full operation, once proper configured (see above). 102 | Configure Cyberduck to transfer data in the command session. 103 | - WinSCP: works fine 104 | - NppFTP - FTP extension to Notepad++: Works fine and is very convenient. 105 | - Mountainduck: Works to some extent, but sometimes stumbles and takes a long 106 | time to open a file. 107 | 108 | **Android** 109 | 110 | - ftp inside the terminal emulator termux: full operation. 111 | - ftp-express 112 | - Chrome: view/navigate directories & and view files 113 | 114 | **IOS 9.1** 115 | 116 | - FTP Client lite: works flawless 117 | 118 | **Windows 10 mobile** 119 | 120 | - Metro file manager: Works with file/directory view & navigate, file download, 121 | file upload, file delete, file rename. Slow and chaotic sequence of FTP commands. 122 | Many unneeded re-login attempts. 123 | 124 | **Conclusion**: All dedicated ftp clients work fine, and most 125 | of the file managers too. 126 | 127 | ## Trouble shooting 128 | The only trouble observed so far was clients not releasing the connections. You may tell 129 | 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. 130 | In that case you may restart the server with uftpd.restart(). If `uftd.client_busy` 131 | is `True` when no client is connected, then restart the server with with 132 | `uftpd.restart()`. If you want to see what happens at the server, you may set verbose to 2. 133 | Just restart it with `uftpd.restart(verbose = 1)`, or set `uftpd.verbose_l = 1`, and 134 | `uftpd.verbose_l = 0` to stop control messages again. 135 | 136 | ## Files 137 | - uftpd.py: Server source file for ESP8266 and ESP32 from version='v1.9.3-575 on 138 | - ftp.py: Simple version of the ftp server, which works in foreground. This 139 | can be used with all Micorpython versions. It terminates when the client closes the 140 | session. Only a single session is supported by this variant. 141 | - README.md: This one 142 | -------------------------------------------------------------------------------- /ftp.py: -------------------------------------------------------------------------------- 1 | # 2 | # Small ftp server for ESP8266 ans ESP32 Micropython 3 | # 4 | # Based on the work of chrisgp - Christopher Popp and pfalcon - Paul Sokolovsky 5 | # 6 | # The server accepts passive mode only. 7 | # It runs in foreground and quits, when it receives a quit command 8 | # Start the server with: 9 | # 10 | # import ftp 11 | # 12 | # Copyright (c) 2016 Christopher Popp (initial ftp server framework) 13 | # Copyright (c) 2016 Robert Hammelrath (putting the pieces together 14 | # and a few extensions) 15 | # Distributed under MIT License 16 | # 17 | import socket 18 | import network 19 | import uos 20 | import gc 21 | 22 | 23 | def send_list_data(path, dataclient, full): 24 | try: # whether path is a directory name 25 | for fname in sorted(uos.listdir(path), key=str.lower): 26 | dataclient.sendall(make_description(path, fname, full)) 27 | except: # path may be a file name or pattern 28 | pattern = path.split("/")[-1] 29 | path = path[:-(len(pattern) + 1)] 30 | if path == "": 31 | path = "/" 32 | for fname in sorted(uos.listdir(path), key=str.lower): 33 | if fncmp(fname, pattern): 34 | dataclient.sendall(make_description(path, fname, full)) 35 | 36 | 37 | def make_description(path, fname, full): 38 | if full: 39 | stat = uos.stat(get_absolute_path(path, fname)) 40 | file_permissions = ("drwxr-xr-x" 41 | if (stat[0] & 0o170000 == 0o040000) 42 | else "-rw-r--r--") 43 | file_size = stat[6] 44 | description = "{} 1 owner group {:>10} Jan 1 2000 {}\r\n".format( 45 | file_permissions, file_size, fname) 46 | else: 47 | description = fname + "\r\n" 48 | return description 49 | 50 | 51 | def send_file_data(path, dataclient): 52 | with open(path, "rb") as file: 53 | chunk = file.read(512) 54 | while len(chunk) > 0: 55 | dataclient.sendall(chunk) 56 | chunk = file.read(512) 57 | 58 | 59 | def save_file_data(path, dataclient): 60 | with open(path, "wb") as file: 61 | chunk = dataclient.recv(512) 62 | while len(chunk) > 0: 63 | file.write(chunk) 64 | chunk = dataclient.recv(512) 65 | 66 | 67 | def get_absolute_path(cwd, payload): 68 | # Just a few special cases "..", "." and "" 69 | # If payload start's with /, set cwd to / 70 | # and consider the remainder a relative path 71 | if payload.startswith('/'): 72 | cwd = "/" 73 | for token in payload.split("/"): 74 | if token == '..': 75 | if cwd != '/': 76 | cwd = '/'.join(cwd.split('/')[:-1]) 77 | if cwd == '': 78 | cwd = '/' 79 | elif token != '.' and token != '': 80 | if cwd == '/': 81 | cwd += token 82 | else: 83 | cwd = cwd + '/' + token 84 | return cwd 85 | 86 | 87 | # compare fname against pattern. Pattern may contain 88 | # wildcards ? and *. 89 | def fncmp(fname, pattern): 90 | pi = 0 91 | si = 0 92 | while pi < len(pattern) and si < len(fname): 93 | if (fname[si] == pattern[pi]) or (pattern[pi] == '?'): 94 | si += 1 95 | pi += 1 96 | else: 97 | if pattern[pi] == '*': # recurse 98 | if (pi + 1) == len(pattern): 99 | return True 100 | while si < len(fname): 101 | if fncmp(fname[si:], pattern[pi+1:]): 102 | return True 103 | else: 104 | si += 1 105 | return False 106 | else: 107 | return False 108 | if pi == len(pattern.rstrip("*")) and si == len(fname): 109 | return True 110 | else: 111 | return False 112 | 113 | 114 | def ftpserver(port=21, timeout=None): 115 | 116 | DATA_PORT = 13333 117 | 118 | ftpsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 119 | datasocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 120 | 121 | ftpsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 122 | datasocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 123 | 124 | ftpsocket.bind(socket.getaddrinfo("0.0.0.0", port)[0][4]) 125 | datasocket.bind(socket.getaddrinfo("0.0.0.0", DATA_PORT)[0][4]) 126 | 127 | ftpsocket.listen(1) 128 | ftpsocket.settimeout(timeout) 129 | datasocket.listen(1) 130 | datasocket.settimeout(None) 131 | 132 | msg_250_OK = '250 OK\r\n' 133 | msg_550_fail = '550 Failed\r\n' 134 | # check for an active interface, STA first 135 | wlan = network.WLAN(network.STA_IF) 136 | if wlan.active(): 137 | addr = wlan.ifconfig()[0] 138 | else: 139 | wlan = network.WLAN(network.AP_IF) 140 | if wlan.active(): 141 | addr = wlan.ifconfig()[0] 142 | else: 143 | print("No active connection") 144 | return 145 | 146 | print("FTP Server started on ", addr) 147 | try: 148 | dataclient = None 149 | fromname = None 150 | do_run = True 151 | while do_run: 152 | cl, remote_addr = ftpsocket.accept() 153 | cl.settimeout(300) 154 | cwd = '/' 155 | try: 156 | # print("FTP connection from:", remote_addr) 157 | cl.sendall("220 Hello, this is the ESP8266/ESP32.\r\n") 158 | while True: 159 | gc.collect() 160 | data = cl.readline().decode("utf-8").rstrip("\r\n") 161 | if len(data) <= 0: 162 | print("Client disappeared") 163 | do_run = False 164 | break 165 | 166 | command = data.split(" ")[0].upper() 167 | payload = data[len(command):].lstrip() 168 | 169 | path = get_absolute_path(cwd, payload) 170 | 171 | print("Command={}, Payload={}".format(command, payload)) 172 | 173 | if command == "USER": 174 | cl.sendall("230 Logged in.\r\n") 175 | elif command == "SYST": 176 | cl.sendall("215 UNIX Type: L8\r\n") 177 | elif command == "NOOP": 178 | cl.sendall("200 OK\r\n") 179 | elif command == "FEAT": 180 | cl.sendall("211 no-features\r\n") 181 | elif command == "PWD" or command == "XPWD": 182 | cl.sendall('257 "{}"\r\n'.format(cwd)) 183 | elif command == "CWD" or command == "XCWD": 184 | try: 185 | files = uos.listdir(path) 186 | cwd = path 187 | cl.sendall(msg_250_OK) 188 | except: 189 | cl.sendall(msg_550_fail) 190 | elif command == "CDUP": 191 | cwd = get_absolute_path(cwd, "..") 192 | cl.sendall(msg_250_OK) 193 | elif command == "TYPE": 194 | # probably should switch between binary and not 195 | cl.sendall('200 Transfer mode set\r\n') 196 | elif command == "SIZE": 197 | try: 198 | size = uos.stat(path)[6] 199 | cl.sendall('213 {}\r\n'.format(size)) 200 | except: 201 | cl.sendall(msg_550_fail) 202 | elif command == "QUIT": 203 | cl.sendall('221 Bye.\r\n') 204 | do_run = False 205 | break 206 | elif command == "PASV": 207 | cl.sendall('227 Entering Passive Mode ({},{},{}).\r\n'. 208 | format(addr.replace('.', ','), DATA_PORT >> 8, 209 | DATA_PORT % 256)) 210 | dataclient, data_addr = datasocket.accept() 211 | print("FTP Data connection from:", data_addr) 212 | DATA_PORT = 13333 213 | active = False 214 | elif command == "PORT": 215 | items = payload.split(",") 216 | if len(items) >= 6: 217 | data_addr = '.'.join(items[:4]) 218 | # replace by command session addr 219 | if data_addr == "127.0.1.1": 220 | data_addr = remote_addr 221 | DATA_PORT = int(items[4]) * 256 + int(items[5]) 222 | dataclient = socket.socket(socket.AF_INET, 223 | socket.SOCK_STREAM) 224 | dataclient.settimeout(10) 225 | dataclient.connect((data_addr, DATA_PORT)) 226 | print("FTP Data connection with:", data_addr) 227 | cl.sendall('200 OK\r\n') 228 | active = True 229 | else: 230 | cl.sendall('504 Fail\r\n') 231 | elif command == "LIST" or command == "NLST": 232 | if not payload.startswith("-"): 233 | place = path 234 | else: 235 | place = cwd 236 | try: 237 | cl.sendall("150 Here comes the directory listing.\r\n") 238 | send_list_data(place, dataclient, 239 | command == "LIST" or payload == "-l") 240 | cl.sendall("226 Listed.\r\n") 241 | except: 242 | cl.sendall(msg_550_fail) 243 | if dataclient is not None: 244 | dataclient.close() 245 | dataclient = None 246 | elif command == "RETR": 247 | try: 248 | cl.sendall("150 Opening data connection.\r\n") 249 | send_file_data(path, dataclient) 250 | cl.sendall("226 Transfer complete.\r\n") 251 | except: 252 | cl.sendall(msg_550_fail) 253 | if dataclient is not None: 254 | dataclient.close() 255 | dataclient = None 256 | elif command == "STOR": 257 | try: 258 | cl.sendall("150 Ok to send data.\r\n") 259 | save_file_data(path, dataclient) 260 | cl.sendall("226 Transfer complete.\r\n") 261 | except: 262 | cl.sendall(msg_550_fail) 263 | if dataclient is not None: 264 | dataclient.close() 265 | dataclient = None 266 | elif command == "DELE": 267 | try: 268 | uos.remove(path) 269 | cl.sendall(msg_250_OK) 270 | except: 271 | cl.sendall(msg_550_fail) 272 | elif command == "RMD" or command == "XRMD": 273 | try: 274 | uos.rmdir(path) 275 | cl.sendall(msg_250_OK) 276 | except: 277 | cl.sendall(msg_550_fail) 278 | elif command == "MKD" or command == "XMKD": 279 | try: 280 | uos.mkdir(path) 281 | cl.sendall(msg_250_OK) 282 | except: 283 | cl.sendall(msg_550_fail) 284 | elif command == "RNFR": 285 | fromname = path 286 | cl.sendall("350 Rename from\r\n") 287 | elif command == "RNTO": 288 | if fromname is not None: 289 | try: 290 | uos.rename(fromname, path) 291 | cl.sendall(msg_250_OK) 292 | except: 293 | cl.sendall(msg_550_fail) 294 | else: 295 | cl.sendall(msg_550_fail) 296 | fromname = None 297 | elif command == "MDTM": 298 | try: 299 | tm=localtime(uos.stat(path)[8]) 300 | cl.sendall('213 {:04d}{:02d}{:02d}{:02d}{:02d}{:02d}\r\n'.format(*tm[0:6])) 301 | except: 302 | cl.sendall('550 Fail\r\n') 303 | elif command == "STAT": 304 | if payload == "": 305 | cl.sendall("211-Connected to ({})\r\n" 306 | " Data address ({})\r\n" 307 | "211 TYPE: Binary STRU: File MODE:" 308 | " Stream\r\n".format( 309 | remote_addr[0], addr)) 310 | else: 311 | cl.sendall("213-Directory listing:\r\n") 312 | send_list_data(path, cl, True) 313 | cl.sendall("213 Done.\r\n") 314 | else: 315 | cl.sendall("502 Unsupported command.\r\n") 316 | print("Unsupported command {} with payload {}".format( 317 | command, payload)) 318 | except Exception as err: 319 | print(err) 320 | 321 | finally: 322 | cl.close() 323 | cl = None 324 | except Exception as e: 325 | print(e) 326 | finally: 327 | datasocket.close() 328 | ftpsocket.close() 329 | if dataclient is not None: 330 | dataclient.close() 331 | 332 | 333 | # ftpserver() 334 | -------------------------------------------------------------------------------- /ftp_pycom.py: -------------------------------------------------------------------------------- 1 | # 2 | # Small ftp server for ESP8266 ans ESP32 Micropython 3 | # 4 | # Based on the work of chrisgp - Christopher Popp and pfalcon - Paul Sokolovsky 5 | # 6 | # The server accepts passive mode only. 7 | # It runs in foreground and quits, when it receives a quit command 8 | # Start the server with: 9 | # 10 | # import ftp 11 | # 12 | # Copyright (c) 2016 Christopher Popp (initial ftp server framework) 13 | # Copyright (c) 2016 Robert Hammelrath (putting the pieces together 14 | # and a few extensions) 15 | # Distributed under MIT License 16 | # 17 | import socket 18 | import network 19 | import uos 20 | import gc 21 | 22 | 23 | def send_list_data(path, dataclient, full): 24 | try: # whether path is a directory name 25 | for fname in sorted(uos.listdir(path), key=str.lower): 26 | dataclient.sendall(make_description(path, fname, full)) 27 | except: # path may be a file name or pattern 28 | pattern = path.split("/")[-1] 29 | path = path[:-(len(pattern) + 1)] 30 | if path == "": 31 | path = "/" 32 | for fname in sorted(uos.listdir(path), key=str.lower): 33 | if fncmp(fname, pattern): 34 | dataclient.sendall(make_description(path, fname, full)) 35 | 36 | 37 | def make_description(path, fname, full): 38 | if full: 39 | stat = uos.stat(get_absolute_path(path, fname)) 40 | file_permissions = ("drwxr-xr-x" 41 | if (stat[0] & 0o170000 == 0o040000) 42 | else "-rw-r--r--") 43 | file_size = stat[6] 44 | description = "{} 1 owner group {:>10} Jan 1 2000 {}\r\n".format( 45 | file_permissions, file_size, fname) 46 | else: 47 | description = fname + "\r\n" 48 | return description 49 | 50 | 51 | def send_file_data(path, dataclient): 52 | with open(path, "rb") as file: 53 | chunk = file.read(512) 54 | while len(chunk) > 0: 55 | dataclient.sendall(chunk) 56 | chunk = file.read(512) 57 | 58 | 59 | def save_file_data(path, dataclient): 60 | with open(path, "wb") as file: 61 | chunk = dataclient.recv(512) 62 | while len(chunk) > 0: 63 | file.write(chunk) 64 | chunk = dataclient.recv(512) 65 | 66 | 67 | def get_absolute_path(cwd, payload): 68 | # Just a few special cases "..", "." and "" 69 | # If payload start's with /, set cwd to / 70 | # and consider the remainder a relative path 71 | if payload.startswith('/'): 72 | cwd = "/" 73 | for token in payload.split("/"): 74 | if token == '..': 75 | if cwd != '/': 76 | cwd = '/'.join(cwd.split('/')[:-1]) 77 | if cwd == '': 78 | cwd = '/' 79 | elif token != '.' and token != '': 80 | if cwd == '/': 81 | cwd += token 82 | else: 83 | cwd = cwd + '/' + token 84 | return cwd 85 | 86 | 87 | # compare fname against pattern. Pattern may contain 88 | # wildcards ? and *. 89 | def fncmp(fname, pattern): 90 | pi = 0 91 | si = 0 92 | while pi < len(pattern) and si < len(fname): 93 | if (fname[si] == pattern[pi]) or (pattern[pi] == '?'): 94 | si += 1 95 | pi += 1 96 | else: 97 | if pattern[pi] == '*': # recurse 98 | if (pi + 1) == len(pattern): 99 | return True 100 | while si < len(fname): 101 | if fncmp(fname[si:], pattern[pi+1:]): 102 | return True 103 | else: 104 | si += 1 105 | return False 106 | else: 107 | return False 108 | if pi == len(pattern.rstrip("*")) and si == len(fname): 109 | return True 110 | else: 111 | return False 112 | 113 | 114 | def ftpserver(port=21): 115 | 116 | DATA_PORT = 13333 117 | 118 | ftpsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 119 | datasocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 120 | 121 | ftpsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 122 | datasocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 123 | 124 | ftpsocket.bind(socket.getaddrinfo("0.0.0.0", port)[0][4]) 125 | datasocket.bind(socket.getaddrinfo("0.0.0.0", DATA_PORT)[0][4]) 126 | 127 | ftpsocket.listen(1) 128 | ftpsocket.settimeout(None) 129 | datasocket.listen(1) 130 | datasocket.settimeout(None) 131 | 132 | msg_250_OK = '250 OK\r\n' 133 | msg_550_fail = '550 Failed\r\n' 134 | # check for an active interface, STA first 135 | wlan = network.WLAN() 136 | addr = wlan.ifconfig()[0] 137 | 138 | print("FTP Server started on ", addr, "Port", port) 139 | try: 140 | dataclient = None 141 | fromname = None 142 | do_run = True 143 | while do_run: 144 | cl, remote_addr = ftpsocket.accept() 145 | cl.settimeout(300) 146 | cwd = '/' 147 | try: 148 | # print("FTP connection from:", remote_addr) 149 | cl.sendall("220 Hello, this is the ESP8266/ESP32.\r\n") 150 | while True: 151 | gc.collect() 152 | data = cl.readline().decode("utf-8").rstrip("\r\n") 153 | if len(data) <= 0: 154 | print("Client disappeared") 155 | do_run = False 156 | break 157 | 158 | command = data.split(" ")[0].upper() 159 | payload = data[len(command):].lstrip() 160 | 161 | path = get_absolute_path(cwd, payload) 162 | 163 | print("Command={}, Payload={}".format(command, payload)) 164 | 165 | if command == "USER": 166 | cl.sendall("230 Logged in.\r\n") 167 | elif command == "SYST": 168 | cl.sendall("215 UNIX Type: L8\r\n") 169 | elif command == "NOOP": 170 | cl.sendall("200 OK\r\n") 171 | elif command == "FEAT": 172 | cl.sendall("211 no-features\r\n") 173 | elif command == "PWD" or command == "XPWD": 174 | cl.sendall('257 "{}"\r\n'.format(cwd)) 175 | elif command == "CWD" or command == "XCWD": 176 | try: 177 | files = uos.listdir(path) 178 | cwd = path 179 | cl.sendall(msg_250_OK) 180 | except: 181 | cl.sendall(msg_550_fail) 182 | elif command == "CDUP": 183 | cwd = get_absolute_path(cwd, "..") 184 | cl.sendall(msg_250_OK) 185 | elif command == "TYPE": 186 | # probably should switch between binary and not 187 | cl.sendall('200 Transfer mode set\r\n') 188 | elif command == "SIZE": 189 | try: 190 | size = uos.stat(path)[6] 191 | cl.sendall('213 {}\r\n'.format(size)) 192 | except: 193 | cl.sendall(msg_550_fail) 194 | elif command == "QUIT": 195 | cl.sendall('221 Bye.\r\n') 196 | do_run = False 197 | break 198 | elif command == "PASV": 199 | cl.sendall('227 Entering Passive Mode ({},{},{}).\r\n'. 200 | format(addr.replace('.', ','), DATA_PORT >> 8, 201 | DATA_PORT % 256)) 202 | dataclient, data_addr = datasocket.accept() 203 | print("FTP Data connection from:", data_addr) 204 | DATA_PORT = 13333 205 | active = False 206 | elif command == "PORT": 207 | items = payload.split(",") 208 | if len(items) >= 6: 209 | data_addr = '.'.join(items[:4]) 210 | # replace by command session addr 211 | if data_addr == "127.0.1.1": 212 | data_addr = remote_addr 213 | DATA_PORT = int(items[4]) * 256 + int(items[5]) 214 | dataclient = socket.socket(socket.AF_INET, 215 | socket.SOCK_STREAM) 216 | dataclient.settimeout(10) 217 | dataclient.connect((data_addr, DATA_PORT)) 218 | print("FTP Data connection with:", data_addr) 219 | cl.sendall('200 OK\r\n') 220 | active = True 221 | else: 222 | cl.sendall('504 Fail\r\n') 223 | elif command == "LIST" or command == "NLST": 224 | if not payload.startswith("-"): 225 | place = path 226 | else: 227 | place = cwd 228 | try: 229 | cl.sendall("150 Here comes the directory listing.\r\n") 230 | send_list_data(place, dataclient, 231 | command == "LIST" or payload == "-l") 232 | cl.sendall("226 Listed.\r\n") 233 | except: 234 | cl.sendall(msg_550_fail) 235 | if dataclient is not None: 236 | dataclient.close() 237 | dataclient = None 238 | elif command == "RETR": 239 | try: 240 | cl.sendall("150 Opening data connection.\r\n") 241 | send_file_data(path, dataclient) 242 | cl.sendall("226 Transfer complete.\r\n") 243 | except: 244 | cl.sendall(msg_550_fail) 245 | if dataclient is not None: 246 | dataclient.close() 247 | dataclient = None 248 | elif command == "STOR": 249 | try: 250 | cl.sendall("150 Ok to send data.\r\n") 251 | save_file_data(path, dataclient) 252 | cl.sendall("226 Transfer complete.\r\n") 253 | except: 254 | cl.sendall(msg_550_fail) 255 | if dataclient is not None: 256 | dataclient.close() 257 | dataclient = None 258 | elif command == "DELE": 259 | try: 260 | uos.remove(path) 261 | cl.sendall(msg_250_OK) 262 | except: 263 | cl.sendall(msg_550_fail) 264 | elif command == "RMD" or command == "XRMD": 265 | try: 266 | uos.rmdir(path) 267 | cl.sendall(msg_250_OK) 268 | except: 269 | cl.sendall(msg_550_fail) 270 | elif command == "MKD" or command == "XMKD": 271 | try: 272 | uos.mkdir(path) 273 | cl.sendall(msg_250_OK) 274 | except: 275 | cl.sendall(msg_550_fail) 276 | elif command == "RNFR": 277 | fromname = path 278 | cl.sendall("350 Rename from\r\n") 279 | elif command == "RNTO": 280 | if fromname is not None: 281 | try: 282 | uos.rename(fromname, path) 283 | cl.sendall(msg_250_OK) 284 | except: 285 | cl.sendall(msg_550_fail) 286 | else: 287 | cl.sendall(msg_550_fail) 288 | fromname = None 289 | elif command == "MDTM": 290 | try: 291 | tm=localtime(uos.stat(path)[8]) 292 | cl.sendall('213 {:04d}{:02d}{:02d}{:02d}{:02d}{:02d}\r\n'.format(*tm[0:6])) 293 | except: 294 | cl.sendall('550 Fail\r\n') 295 | elif command == "STAT": 296 | if payload == "": 297 | cl.sendall("211-Connected to ({})\r\n" 298 | " Data address ({})\r\n" 299 | "211 TYPE: Binary STRU: File MODE:" 300 | " Stream\r\n".format( 301 | remote_addr[0], addr)) 302 | else: 303 | cl.sendall("213-Directory listing:\r\n") 304 | send_list_data(path, cl, True) 305 | cl.sendall("213 Done.\r\n") 306 | else: 307 | cl.sendall("502 Unsupported command.\r\n") 308 | print("Unsupported command {} with payload {}".format( 309 | command, payload)) 310 | except Exception as err: 311 | print(err) 312 | 313 | finally: 314 | cl.close() 315 | cl = None 316 | finally: 317 | datasocket.close() 318 | ftpsocket.close() 319 | if dataclient is not None: 320 | dataclient.close() 321 | 322 | 323 | ftpserver(port=5024) 324 | -------------------------------------------------------------------------------- /ftp_thread.py: -------------------------------------------------------------------------------- 1 | # 2 | # Small ftp server for ESP8266 ans ESP32 Micropython 3 | # 4 | # Based on the work of chrisgp - Christopher Popp and pfalcon - Paul Sokolovsky 5 | # 6 | # The server accepts passive mode only. 7 | # It runs in foreground and quits, when it receives a quit command 8 | # Start the server with: 9 | # 10 | # import ftp 11 | # 12 | # Copyright (c) 2016 Christopher Popp (initial ftp server framework) 13 | # Copyright (c) 2016 Robert Hammelrath (putting the pieces together 14 | # and a few extensions) 15 | # Distributed under MIT License 16 | # 17 | import socket 18 | import network 19 | import uos 20 | import gc 21 | 22 | 23 | def send_list_data(path, dataclient, full): 24 | try: # whether path is a directory name 25 | for fname in sorted(uos.listdir(path), key=str.lower): 26 | dataclient.sendall(make_description(path, fname, full)) 27 | except: # path may be a file name or pattern 28 | pattern = path.split("/")[-1] 29 | path = path[:-(len(pattern) + 1)] 30 | if path == "": 31 | path = "/" 32 | for fname in sorted(uos.listdir(path), key=str.lower): 33 | if fncmp(fname, pattern): 34 | dataclient.sendall(make_description(path, fname, full)) 35 | 36 | 37 | def make_description(path, fname, full): 38 | if full: 39 | stat = uos.stat(get_absolute_path(path, fname)) 40 | file_permissions = "drwxr-xr-x"\ 41 | if (stat[0] & 0o170000 == 0o040000)\ 42 | else "-rw-r--r--" 43 | file_size = stat[6] 44 | description = "{} 1 owner group {:>10} Jan 1 2000 {}\r\n".format( 45 | file_permissions, file_size, fname) 46 | else: 47 | description = fname + "\r\n" 48 | return description 49 | 50 | 51 | def send_file_data(path, dataclient): 52 | with open(path, "rb") as file: 53 | chunk = file.read(512) 54 | while len(chunk) > 0: 55 | dataclient.sendall(chunk) 56 | chunk = file.read(512) 57 | 58 | 59 | def save_file_data(path, dataclient): 60 | with open(path, "wb") as file: 61 | chunk = dataclient.recv(512) 62 | while len(chunk) > 0: 63 | file.write(chunk) 64 | chunk = dataclient.recv(512) 65 | 66 | 67 | def get_absolute_path(cwd, payload): 68 | # Just a few special cases "..", "." and "" 69 | # If payload start's with /, set cwd to / 70 | # and consider the remainder a relative path 71 | if payload.startswith('/'): 72 | cwd = "/" 73 | for token in payload.split("/"): 74 | if token == '..': 75 | if cwd != '/': 76 | cwd = '/'.join(cwd.split('/')[:-1]) 77 | if cwd == '': 78 | cwd = '/' 79 | elif token != '.' and token != '': 80 | if cwd == '/': 81 | cwd += token 82 | else: 83 | cwd = cwd + '/' + token 84 | return cwd 85 | 86 | 87 | # compare fname against pattern. Pattern may contain 88 | # wildcards ? and *. 89 | def fncmp(fname, pattern): 90 | pi = 0 91 | si = 0 92 | while pi < len(pattern) and si < len(fname): 93 | if (fname[si] == pattern[pi]) or (pattern[pi] == '?'): 94 | si += 1 95 | pi += 1 96 | else: 97 | if pattern[pi] == '*': # recurse 98 | if (pi + 1) == len(pattern): 99 | return True 100 | while si < len(fname): 101 | if fncmp(fname[si:], pattern[pi+1:]): 102 | return True 103 | else: 104 | si += 1 105 | return False 106 | else: 107 | return False 108 | if pi == len(pattern.rstrip("*")) and si == len(fname): 109 | return True 110 | else: 111 | return False 112 | 113 | 114 | def ftpserver(not_stop_on_quit): 115 | 116 | DATA_PORT = 13333 117 | 118 | ftpsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 119 | datasocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 120 | 121 | ftpsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 122 | datasocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 123 | 124 | ftpsocket.bind(socket.getaddrinfo("0.0.0.0", 21)[0][4]) 125 | datasocket.bind(socket.getaddrinfo("0.0.0.0", DATA_PORT)[0][4]) 126 | 127 | ftpsocket.listen(1) 128 | ftpsocket.settimeout(None) 129 | datasocket.listen(1) 130 | datasocket.settimeout(None) 131 | 132 | msg_250_OK = '250 OK\r\n' 133 | msg_550_fail = '550 Failed\r\n' 134 | # check for an active interface, STA first 135 | wlan = network.WLAN(network.STA_IF) 136 | if wlan.active(): 137 | addr = wlan.ifconfig()[0] 138 | else: 139 | wlan = network.WLAN(network.AP_IF) 140 | if wlan.active(): 141 | addr = wlan.ifconfig()[0] 142 | else: 143 | print("No active connection") 144 | return 145 | 146 | print("FTP Server started on ", addr) 147 | try: 148 | dataclient = None 149 | fromname = None 150 | do_run = True 151 | while do_run: 152 | cl, remote_addr = ftpsocket.accept() 153 | cl.settimeout(300) 154 | cwd = '/' 155 | try: 156 | # print("FTP connection from:", remote_addr) 157 | cl.sendall("220 Hello, this is the ESP8266/ESP32.\r\n") 158 | while True: 159 | gc.collect() 160 | data = cl.readline().decode("utf-8").rstrip("\r\n") 161 | if len(data) <= 0: 162 | print("Client disappeared") 163 | do_run = not_stop_on_quit 164 | break 165 | 166 | command = data.split(" ")[0].upper() 167 | payload = data[len(command):].lstrip() 168 | 169 | path = get_absolute_path(cwd, payload) 170 | 171 | print("Command={}, Payload={}".format(command, payload)) 172 | 173 | if command == "USER": 174 | cl.sendall("230 Logged in.\r\n") 175 | elif command == "SYST": 176 | cl.sendall("215 UNIX Type: L8\r\n") 177 | elif command == "NOOP": 178 | cl.sendall("200 OK\r\n") 179 | elif command == "FEAT": 180 | cl.sendall("211 no-features\r\n") 181 | elif command == "PWD" or command == "XPWD": 182 | cl.sendall('257 "{}"\r\n'.format(cwd)) 183 | elif command == "CWD" or command == "XCWD": 184 | try: 185 | files = uos.listdir(path) 186 | cwd = path 187 | cl.sendall(msg_250_OK) 188 | except: 189 | cl.sendall(msg_550_fail) 190 | elif command == "CDUP": 191 | cwd = get_absolute_path(cwd, "..") 192 | cl.sendall(msg_250_OK) 193 | elif command == "TYPE": 194 | # probably should switch between binary and not 195 | cl.sendall('200 Transfer mode set\r\n') 196 | elif command == "SIZE": 197 | try: 198 | size = uos.stat(path)[6] 199 | cl.sendall('213 {}\r\n'.format(size)) 200 | except: 201 | cl.sendall(msg_550_fail) 202 | elif command == "QUIT": 203 | cl.sendall('221 Bye.\r\n') 204 | do_run = not_stop_on_quit 205 | break 206 | elif command == "PASV": 207 | cl.sendall('227 Entering Passive Mode ({},{},{}).\r\n'.format( 208 | addr.replace('.', ','), DATA_PORT >> 8, DATA_PORT % 256)) 209 | dataclient, data_addr = datasocket.accept() 210 | print("FTP Data connection from:", data_addr) 211 | DATA_PORT = 13333 212 | active = False 213 | elif command == "PORT": 214 | items = payload.split(",") 215 | if len(items) >= 6: 216 | data_addr = '.'.join(items[:4]) 217 | if data_addr == "127.0.1.1": 218 | # replace by command session addr 219 | data_addr = remote_addr 220 | DATA_PORT = int(items[4]) * 256 + int(items[5]) 221 | dataclient = socket.socket(socket.AF_INET, 222 | socket.SOCK_STREAM) 223 | dataclient.settimeout(10) 224 | dataclient.connect((data_addr, DATA_PORT)) 225 | print("FTP Data connection with:", data_addr) 226 | cl.sendall('200 OK\r\n') 227 | active = True 228 | else: 229 | cl.sendall('504 Fail\r\n') 230 | elif command == "LIST" or command == "NLST": 231 | if not payload.startswith("-"): 232 | place = path 233 | else: 234 | place = cwd 235 | try: 236 | cl.sendall("150 Here comes the " 237 | "directory listing.\r\n") 238 | send_list_data(place, dataclient, 239 | command == "LIST" or payload == "-l") 240 | cl.sendall("226 Listed.\r\n") 241 | except: 242 | cl.sendall(msg_550_fail) 243 | if dataclient is not None: 244 | dataclient.close() 245 | dataclient = None 246 | elif command == "RETR": 247 | try: 248 | cl.sendall("150 Opening data connection.\r\n") 249 | send_file_data(path, dataclient) 250 | cl.sendall("226 Transfer complete.\r\n") 251 | except: 252 | cl.sendall(msg_550_fail) 253 | if dataclient is not None: 254 | dataclient.close() 255 | dataclient = None 256 | elif command == "STOR": 257 | try: 258 | cl.sendall("150 Ok to send data.\r\n") 259 | save_file_data(path, dataclient) 260 | cl.sendall("226 Transfer complete.\r\n") 261 | except: 262 | cl.sendall(msg_550_fail) 263 | if dataclient is not None: 264 | dataclient.close() 265 | dataclient = None 266 | elif command == "DELE": 267 | try: 268 | uos.remove(path) 269 | cl.sendall(msg_250_OK) 270 | except: 271 | cl.sendall(msg_550_fail) 272 | elif command == "RMD" or command == "XRMD": 273 | try: 274 | uos.rmdir(path) 275 | cl.sendall(msg_250_OK) 276 | except: 277 | cl.sendall(msg_550_fail) 278 | elif command == "MKD" or command == "XMKD": 279 | try: 280 | uos.mkdir(path) 281 | cl.sendall(msg_250_OK) 282 | except: 283 | cl.sendall(msg_550_fail) 284 | elif command == "RNFR": 285 | fromname = path 286 | cl.sendall("350 Rename from\r\n") 287 | elif command == "RNTO": 288 | if fromname is not None: 289 | try: 290 | uos.rename(fromname, path) 291 | cl.sendall(msg_250_OK) 292 | except: 293 | cl.sendall(msg_550_fail) 294 | else: 295 | cl.sendall(msg_550_fail) 296 | fromname = None 297 | elif command == "MDTM": 298 | try: 299 | tm=localtime(uos.stat(path)[8]) 300 | cl.sendall('213 {:04d}{:02d}{:02d}{:02d}{:02d}{:02d}\r\n'.format(*tm[0:6])) 301 | except: 302 | cl.sendall('550 Fail\r\n') 303 | elif command == "STAT": 304 | if payload == "": 305 | cl.sendall("211-Connected to ({})\r\n" 306 | " Data address ({})\r\n" 307 | "211 TYPE: Binary STRU: File " 308 | "MODE: Stream\r\n".format( 309 | remote_addr[0], addr)) 310 | else: 311 | cl.sendall("213-Directory listing:\r\n") 312 | send_list_data(path, cl, True) 313 | cl.sendall("213 Done.\r\n") 314 | else: 315 | cl.sendall("502 Unsupported command.\r\n") 316 | print("Unsupported command {} with payload {}". 317 | format(command, payload)) 318 | except Exception as err: 319 | print(err) 320 | 321 | finally: 322 | cl.close() 323 | cl = None 324 | finally: 325 | datasocket.close() 326 | ftpsocket.close() 327 | if dataclient is not None: 328 | dataclient.close() 329 | 330 | try: 331 | import _thread 332 | _thread.start_new_thread(ftpserver, ((True,))) 333 | except: 334 | ftpserver(False) 335 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "urls": [ 3 | ["ftp.py", "github:robert-hh/FTP-Server-for-ESP8266-ESP32-and-PYBD/ftp.py"], 4 | ["uftpd.py", "github:robert-hh/FTP-Server-for-ESP8266-ESP32-and-PYBD/uftpd.py"] 5 | ], 6 | "version": "1.0.0", 7 | "deps": [] 8 | } -------------------------------------------------------------------------------- /uftpd.py: -------------------------------------------------------------------------------- 1 | # 2 | # Small ftp server for ESP8266 Micropython 3 | # Based on the work of chrisgp - Christopher Popp and pfalcon - Paul Sokolovsky 4 | # 5 | # The server accepts passive mode only. It runs in background. 6 | # Start the server with: 7 | # 8 | # import uftpd 9 | # uftpd.start([port = 21][, verbose = level]) 10 | # 11 | # port is the port number (default 21) 12 | # verbose controls the level of printed activity messages, values 0, 1, 2 13 | # 14 | # Copyright (c) 2016 Christopher Popp (initial ftp server framework) 15 | # Copyright (c) 2016 Paul Sokolovsky (background execution control structure) 16 | # Copyright (c) 2016 Robert Hammelrath (putting the pieces together and a 17 | # few extensions) 18 | # Copyright (c) 2020 Jan Wieck Use separate FTP servers per socket for STA + AP mode 19 | # Copyright (c) 2021 JD Smith Use a preallocated buffer and improve error handling. 20 | # Distributed under MIT License 21 | # 22 | import socket 23 | import network 24 | import uos 25 | import gc 26 | import sys 27 | import errno 28 | from time import sleep_ms, localtime 29 | from micropython import alloc_emergency_exception_buf 30 | 31 | # constant definitions 32 | _CHUNK_SIZE = const(1024) 33 | _SO_REGISTER_HANDLER = const(20) 34 | _COMMAND_TIMEOUT = const(300) 35 | _DATA_TIMEOUT = const(100) 36 | _DATA_PORT = const(13333) 37 | 38 | # Global variables 39 | ftpsockets = [] 40 | datasocket = None 41 | client_list = [] 42 | verbose_l = 0 43 | client_busy = False 44 | # Interfaces: (IP-Address (string), IP-Address (integer), Netmask (integer)) 45 | 46 | _month_name = ("", "Jan", "Feb", "Mar", "Apr", "May", "Jun", 47 | "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") 48 | 49 | 50 | class FTP_client: 51 | 52 | def __init__(self, ftpsocket, local_addr): 53 | self.command_client, self.remote_addr = ftpsocket.accept() 54 | self.remote_addr = self.remote_addr[0] 55 | self.command_client.settimeout(_COMMAND_TIMEOUT) 56 | log_msg(1, "FTP Command connection from:", self.remote_addr) 57 | self.command_client.setsockopt(socket.SOL_SOCKET, 58 | _SO_REGISTER_HANDLER, 59 | self.exec_ftp_command) 60 | self.command_client.sendall("220 Hello, this is the {}.\r\n".format(sys.platform)) 61 | self.cwd = '/' 62 | self.fromname = None 63 | # self.logged_in = False 64 | self.act_data_addr = self.remote_addr 65 | self.DATA_PORT = 20 66 | self.active = True 67 | self.pasv_data_addr = local_addr 68 | 69 | def send_list_data(self, path, data_client, full): 70 | try: 71 | for fname in uos.listdir(path): 72 | data_client.sendall(self.make_description(path, fname, full)) 73 | except Exception as e: # path may be a file name or pattern 74 | path, pattern = self.split_path(path) 75 | try: 76 | for fname in uos.listdir(path): 77 | if self.fncmp(fname, pattern): 78 | data_client.sendall( 79 | self.make_description(path, fname, full)) 80 | except: 81 | pass 82 | 83 | def make_description(self, path, fname, full): 84 | global _month_name 85 | if full: 86 | stat = uos.stat(self.get_absolute_path(path, fname)) 87 | file_permissions = ("drwxr-xr-x" 88 | if (stat[0] & 0o170000 == 0o040000) 89 | else "-rw-r--r--") 90 | file_size = stat[6] 91 | tm = stat[7] & 0xffffffff 92 | tm = localtime(tm if tm < 0x80000000 else tm - 0x100000000) 93 | if tm[0] != localtime()[0]: 94 | description = "{} 1 owner group {:>10} {} {:2} {:>5} {}\r\n".\ 95 | format(file_permissions, file_size, 96 | _month_name[tm[1]], tm[2], tm[0], fname) 97 | else: 98 | description = "{} 1 owner group {:>10} {} {:2} {:02}:{:02} {}\r\n".\ 99 | format(file_permissions, file_size, 100 | _month_name[tm[1]], tm[2], tm[3], tm[4], fname) 101 | else: 102 | description = fname + "\r\n" 103 | return description 104 | 105 | def send_file_data(self, path, data_client): 106 | buffer = bytearray(_CHUNK_SIZE) 107 | mv = memoryview(buffer) 108 | with open(path, "rb") as file: 109 | bytes_read = file.readinto(buffer) 110 | while bytes_read > 0: 111 | data_client.write(mv[0:bytes_read]) 112 | bytes_read = file.readinto(buffer) 113 | data_client.close() 114 | 115 | def save_file_data(self, path, data_client, mode): 116 | buffer = bytearray(_CHUNK_SIZE) 117 | mv = memoryview(buffer) 118 | with open(path, mode) as file: 119 | bytes_read = data_client.readinto(buffer) 120 | while bytes_read > 0: 121 | file.write(mv[0:bytes_read]) 122 | bytes_read = data_client.readinto(buffer) 123 | data_client.close() 124 | 125 | def get_absolute_path(self, cwd, payload): 126 | # Just a few special cases "..", "." and "" 127 | # If payload start's with /, set cwd to / 128 | # and consider the remainder a relative path 129 | if payload.startswith('/'): 130 | cwd = "/" 131 | for token in payload.split("/"): 132 | if token == '..': 133 | cwd = self.split_path(cwd)[0] 134 | elif token != '.' and token != '': 135 | if cwd == '/': 136 | cwd += token 137 | else: 138 | cwd = cwd + '/' + token 139 | return cwd 140 | 141 | def split_path(self, path): # instead of path.rpartition('/') 142 | tail = path.split('/')[-1] 143 | head = path[:-(len(tail) + 1)] 144 | return ('/' if head == '' else head, tail) 145 | 146 | # compare fname against pattern. Pattern may contain 147 | # the wildcards ? and *. 148 | def fncmp(self, fname, pattern): 149 | pi = 0 150 | si = 0 151 | while pi < len(pattern) and si < len(fname): 152 | if (fname[si] == pattern[pi]) or (pattern[pi] == '?'): 153 | si += 1 154 | pi += 1 155 | else: 156 | if pattern[pi] == '*': # recurse 157 | if pi == len(pattern.rstrip("*?")): # only wildcards left 158 | return True 159 | while si < len(fname): 160 | if self.fncmp(fname[si:], pattern[pi + 1:]): 161 | return True 162 | else: 163 | si += 1 164 | return False 165 | else: 166 | return False 167 | if pi == len(pattern.rstrip("*")) and si == len(fname): 168 | return True 169 | else: 170 | return False 171 | 172 | def open_dataclient(self): 173 | if self.active: # active mode 174 | data_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 175 | data_client.settimeout(_DATA_TIMEOUT) 176 | data_client.connect((self.act_data_addr, self.DATA_PORT)) 177 | log_msg(1, "FTP Data connection with:", self.act_data_addr) 178 | else: # passive mode 179 | data_client, data_addr = datasocket.accept() 180 | log_msg(1, "FTP Data connection with:", data_addr[0]) 181 | return data_client 182 | 183 | def exec_ftp_command(self, cl): 184 | global datasocket 185 | global client_busy 186 | global my_ip_addr 187 | 188 | try: 189 | gc.collect() 190 | 191 | try: 192 | data = cl.readline().decode("utf-8").rstrip("\r\n") 193 | except OSError: 194 | # treat an error as QUIT situation. 195 | data = "" 196 | 197 | if len(data) <= 0: 198 | # No data, close 199 | # This part is NOT CLEAN; there is still a chance that a 200 | # closing data connection will be signalled as closing 201 | # command connection 202 | log_msg(1, "*** No data, assume QUIT") 203 | close_client(cl) 204 | return 205 | 206 | if client_busy: # check if another client is busy 207 | cl.sendall("400 Device busy.\r\n") # tell so the remote client 208 | return # and quit 209 | client_busy = True # now it's my turn 210 | 211 | # check for log-in state may done here, like 212 | # if self.logged_in == False and not command in\ 213 | # ("USER", "PASS", "QUIT"): 214 | # cl.sendall("530 Not logged in.\r\n") 215 | # return 216 | 217 | command = data.split()[0].upper() 218 | payload = data[len(command):].lstrip() # partition is missing 219 | path = self.get_absolute_path(self.cwd, payload) 220 | log_msg(1, "Command={}, Payload={}".format(command, payload)) 221 | 222 | if command == "USER": 223 | # self.logged_in = True 224 | cl.sendall("230 Logged in.\r\n") 225 | # If you want to see a password,return 226 | # "331 Need password.\r\n" instead 227 | # If you want to reject an user, return 228 | # "530 Not logged in.\r\n" 229 | elif command == "PASS": 230 | # you may check here for a valid password and return 231 | # "530 Not logged in.\r\n" in case it's wrong 232 | # self.logged_in = True 233 | cl.sendall("230 Logged in.\r\n") 234 | elif command == "SYST": 235 | cl.sendall("215 UNIX Type: L8\r\n") 236 | elif command in ("TYPE", "NOOP", "ABOR"): # just accept & ignore 237 | cl.sendall('200 OK\r\n') 238 | elif command == "QUIT": 239 | cl.sendall('221 Bye.\r\n') 240 | close_client(cl) 241 | elif command == "PWD" or command == "XPWD": 242 | cl.sendall('257 "{}"\r\n'.format(self.cwd)) 243 | elif command == "CWD" or command == "XCWD": 244 | try: 245 | if (uos.stat(path)[0] & 0o170000) == 0o040000: 246 | self.cwd = path 247 | cl.sendall('250 OK\r\n') 248 | else: 249 | cl.sendall('550 Fail\r\n') 250 | except: 251 | cl.sendall('550 Fail\r\n') 252 | elif command == "PASV": 253 | cl.sendall('227 Entering Passive Mode ({},{},{}).\r\n'.format( 254 | self.pasv_data_addr.replace('.', ','), 255 | _DATA_PORT >> 8, _DATA_PORT % 256)) 256 | self.active = False 257 | elif command == "PORT": 258 | items = payload.split(",") 259 | if len(items) >= 6: 260 | self.act_data_addr = '.'.join(items[:4]) 261 | if self.act_data_addr == "127.0.1.1": 262 | # replace by command session addr 263 | self.act_data_addr = self.remote_addr 264 | self.DATA_PORT = int(items[4]) * 256 + int(items[5]) 265 | cl.sendall('200 OK\r\n') 266 | self.active = True 267 | else: 268 | cl.sendall('504 Fail\r\n') 269 | elif command == "LIST" or command == "NLST": 270 | if payload.startswith("-"): 271 | option = payload.split()[0].lower() 272 | path = self.get_absolute_path( 273 | self.cwd, payload[len(option):].lstrip()) 274 | else: 275 | option = "" 276 | try: 277 | data_client = self.open_dataclient() 278 | cl.sendall("150 Directory listing:\r\n") 279 | self.send_list_data(path, data_client, 280 | command == "LIST" or 'l' in option) 281 | cl.sendall("226 Done.\r\n") 282 | data_client.close() 283 | except: 284 | cl.sendall('550 Fail\r\n') 285 | if data_client is not None: 286 | data_client.close() 287 | elif command == "RETR": 288 | try: 289 | data_client = self.open_dataclient() 290 | cl.sendall("150 Opened data connection.\r\n") 291 | self.send_file_data(path, data_client) 292 | # if the next statement is reached, 293 | # the data_client was closed. 294 | data_client = None 295 | cl.sendall("226 Done.\r\n") 296 | except: 297 | cl.sendall('550 Fail\r\n') 298 | if data_client is not None: 299 | data_client.close() 300 | elif command == "STOR" or command == "APPE": 301 | try: 302 | data_client = self.open_dataclient() 303 | cl.sendall("150 Opened data connection.\r\n") 304 | self.save_file_data(path, data_client, 305 | "wb" if command == "STOR" else "ab") 306 | # if the next statement is reached, 307 | # the data_client was closed. 308 | data_client = None 309 | cl.sendall("226 Done.\r\n") 310 | except: 311 | cl.sendall('550 Fail\r\n') 312 | if data_client is not None: 313 | data_client.close() 314 | elif command == "SIZE": 315 | try: 316 | cl.sendall('213 {}\r\n'.format(uos.stat(path)[6])) 317 | except: 318 | cl.sendall('550 Fail\r\n') 319 | elif command == "MDTM": 320 | try: 321 | tm=localtime(uos.stat(path)[8]) 322 | cl.sendall('213 {:04d}{:02d}{:02d}{:02d}{:02d}{:02d}\r\n'.format(*tm[0:6])) 323 | except: 324 | cl.sendall('550 Fail\r\n') 325 | elif command == "STAT": 326 | if payload == "": 327 | cl.sendall("211-Connected to ({})\r\n" 328 | " Data address ({})\r\n" 329 | " TYPE: Binary STRU: File MODE: Stream\r\n" 330 | " Session timeout {}\r\n" 331 | "211 Client count is {}\r\n".format( 332 | self.remote_addr, self.pasv_data_addr, 333 | _COMMAND_TIMEOUT, len(client_list))) 334 | else: 335 | cl.sendall("213-Directory listing:\r\n") 336 | self.send_list_data(path, cl, True) 337 | cl.sendall("213 Done.\r\n") 338 | elif command == "DELE": 339 | try: 340 | uos.remove(path) 341 | cl.sendall('250 OK\r\n') 342 | except: 343 | cl.sendall('550 Fail\r\n') 344 | elif command == "RNFR": 345 | try: 346 | # just test if the name exists, exception if not 347 | uos.stat(path) 348 | self.fromname = path 349 | cl.sendall("350 Rename from\r\n") 350 | except: 351 | cl.sendall('550 Fail\r\n') 352 | elif command == "RNTO": 353 | try: 354 | uos.rename(self.fromname, path) 355 | cl.sendall('250 OK\r\n') 356 | except: 357 | cl.sendall('550 Fail\r\n') 358 | self.fromname = None 359 | elif command == "CDUP" or command == "XCUP": 360 | self.cwd = self.get_absolute_path(self.cwd, "..") 361 | cl.sendall('250 OK\r\n') 362 | elif command == "RMD" or command == "XRMD": 363 | try: 364 | uos.rmdir(path) 365 | cl.sendall('250 OK\r\n') 366 | except: 367 | cl.sendall('550 Fail\r\n') 368 | elif command == "MKD" or command == "XMKD": 369 | try: 370 | uos.mkdir(path) 371 | cl.sendall('250 OK\r\n') 372 | except: 373 | cl.sendall('550 Fail\r\n') 374 | elif command == "SITE": 375 | try: 376 | exec(payload.replace('\0','\n')) 377 | cl.sendall('250 OK\r\n') 378 | except: 379 | cl.sendall('550 Fail\r\n') 380 | else: 381 | cl.sendall("502 Unsupported command.\r\n") 382 | # log_msg(2, 383 | # "Unsupported command {} with payload {}".format(command, 384 | # payload)) 385 | except OSError as err: 386 | if verbose_l > 0: 387 | log_msg(1, "Exception in exec_ftp_command:") 388 | sys.print_exception(err) 389 | if err.errno in (errno.ECONNABORTED, errno.ENOTCONN): 390 | close_client(cl) 391 | # handle unexpected errors 392 | except Exception as err: 393 | log_msg(1, "Exception in exec_ftp_command: {}".format(err)) 394 | # tidy up before leaving 395 | client_busy = False 396 | 397 | 398 | def log_msg(level, *args): 399 | global verbose_l 400 | if verbose_l >= level: 401 | print(*args) 402 | 403 | 404 | # close client and remove it from the list 405 | def close_client(cl): 406 | cl.setsockopt(socket.SOL_SOCKET, _SO_REGISTER_HANDLER, None) 407 | cl.close() 408 | for i, client in enumerate(client_list): 409 | if client.command_client == cl: 410 | del client_list[i] 411 | break 412 | 413 | 414 | def accept_ftp_connect(ftpsocket, local_addr): 415 | # Accept new calls for the server 416 | try: 417 | client_list.append(FTP_client(ftpsocket, local_addr)) 418 | except: 419 | log_msg(1, "Attempt to connect failed") 420 | # try at least to reject 421 | try: 422 | temp_client, temp_addr = ftpsocket.accept() 423 | temp_client.close() 424 | except: 425 | pass 426 | 427 | 428 | def num_ip(ip): 429 | items = ip.split(".") 430 | return (int(items[0]) << 24 | int(items[1]) << 16 | 431 | int(items[2]) << 8 | int(items[3])) 432 | 433 | 434 | def stop(): 435 | global ftpsockets, datasocket 436 | global client_list 437 | global client_busy 438 | 439 | for client in client_list: 440 | client.command_client.setsockopt(socket.SOL_SOCKET, 441 | _SO_REGISTER_HANDLER, None) 442 | client.command_client.close() 443 | del client_list 444 | client_list = [] 445 | client_busy = False 446 | for sock in ftpsockets: 447 | sock.setsockopt(socket.SOL_SOCKET, _SO_REGISTER_HANDLER, None) 448 | sock.close() 449 | ftpsockets = [] 450 | if datasocket is not None: 451 | datasocket.close() 452 | datasocket = None 453 | 454 | 455 | # start listening for ftp connections on port 21 456 | def start(port=21, verbose=0, splash=True): 457 | global ftpsockets, datasocket 458 | global verbose_l 459 | global client_list 460 | global client_busy 461 | 462 | alloc_emergency_exception_buf(100) 463 | verbose_l = verbose 464 | client_list = [] 465 | client_busy = False 466 | 467 | for interface in [network.AP_IF, network.STA_IF]: 468 | wlan = network.WLAN(interface) 469 | if not wlan.active(): 470 | continue 471 | 472 | ifconfig = wlan.ifconfig() 473 | addr = socket.getaddrinfo(ifconfig[0], port) 474 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 475 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 476 | sock.bind(addr[0][4]) 477 | sock.listen(1) 478 | sock.setsockopt(socket.SOL_SOCKET, 479 | _SO_REGISTER_HANDLER, 480 | lambda s : accept_ftp_connect(s, ifconfig[0])) 481 | ftpsockets.append(sock) 482 | if splash: 483 | print("FTP server started on {}:{}".format(ifconfig[0], port)) 484 | 485 | datasocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 486 | datasocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 487 | datasocket.bind(('0.0.0.0', _DATA_PORT)) 488 | datasocket.listen(1) 489 | datasocket.settimeout(10) 490 | 491 | def restart(port=21, verbose=0, splash=True): 492 | stop() 493 | sleep_ms(200) 494 | start(port, verbose, splash) 495 | 496 | 497 | start(splash=True) 498 | --------------------------------------------------------------------------------