diff --git a/scripts/libraries/uftpd.py b/scripts/libraries/uftpd.py new file mode 100644 index 000000000..549ea5699 --- /dev/null +++ b/scripts/libraries/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) +# Copyright (c) 2020 Jan Wieck Use separate FTP servers per socket for STA + AP mode +# Copyright (c) 2021 JD Smith Use a preallocated buffer and improve error handling. +# Distributed under MIT License +# +import socket +import network +import uos +import gc +import sys +import errno +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 +ftpsockets = [] +datasocket = None +client_list = [] +verbose_l = 0 +client_busy = False +# Interfaces: (IP-Address (string), IP-Address (integer), Netmask (integer)) + +_month_name = ("", "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") + + +class FTP_client: + + def __init__(self, ftpsocket, local_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 {}.\r\n".format(sys.platform)) + self.cwd = '/' + self.fromname = None +# self.logged_in = False + self.act_data_addr = self.remote_addr + self.DATA_PORT = 20 + self.active = True + self.pasv_data_addr = local_addr + + 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 Exception as e: # 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 = stat[7] & 0xffffffff + tm = localtime(tm if tm < 0x80000000 else tm - 0x100000000) + 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): + buffer = bytearray(_CHUNK_SIZE) + mv = memoryview(buffer) + with open(path, "rb") as file: + bytes_read = file.readinto(buffer) + while bytes_read > 0: + data_client.write(mv[0:bytes_read]) + bytes_read = file.readinto(buffer) + data_client.close() + + def save_file_data(self, path, data_client, mode): + buffer = bytearray(_CHUNK_SIZE) + mv = memoryview(buffer) + with open(path, mode) as file: + bytes_read = data_client.readinto(buffer) + while bytes_read > 0: + file.write(mv[0:bytes_read]) + bytes_read = data_client.readinto(buffer) + 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, + "wb" if command == "STOR" else "ab") + # 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') + elif command == "SITE": + try: + exec(payload.replace('\0','\n')) + 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)) + except OSError as err: + if verbose_l > 0: + log_msg(1, "Exception in exec_ftp_command:") + sys.print_exception(err) + if err.errno in (errno.ECONNABORTED, errno.ENOTCONN): + close_client(cl) + # 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, local_addr): + # Accept new calls for the server + try: + client_list.append(FTP_client(ftpsocket, local_addr)) + 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 ftpsockets, 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 + for sock in ftpsockets: + sock.setsockopt(socket.SOL_SOCKET, _SO_REGISTER_HANDLER, None) + sock.close() + ftpsockets = [] + if datasocket is not None: + datasocket.close() + datasocket = None + + +# start listening for ftp connections on port 21 +def start(port=21, verbose=0, splash=True): + global ftpsockets, datasocket + global verbose_l + global client_list + global client_busy + + alloc_emergency_exception_buf(100) + verbose_l = verbose + client_list = [] + client_busy = False + + for interface in [network.AP_IF, network.STA_IF]: + wlan = network.WLAN(interface) + if not wlan.active(): + continue + + ifconfig = wlan.ifconfig() + addr = socket.getaddrinfo(ifconfig[0], port) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(addr[0][4]) + sock.listen(1) + sock.setsockopt(socket.SOL_SOCKET, + _SO_REGISTER_HANDLER, + lambda s : accept_ftp_connect(s, ifconfig[0])) + ftpsockets.append(sock) + if splash: + print("FTP server started on {}:{}".format(ifconfig[0], port)) + + datasocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + datasocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + datasocket.bind(('0.0.0.0', _DATA_PORT)) + datasocket.listen(1) + datasocket.settimeout(10) + +def restart(port=21, verbose=0, splash=True): + stop() + sleep_ms(200) + start(port, verbose, splash) + + +start(splash=True) diff --git a/scripts/libraries/uping.py b/scripts/libraries/uping.py new file mode 100644 index 000000000..b4806fc5a --- /dev/null +++ b/scripts/libraries/uping.py @@ -0,0 +1,107 @@ +# µPing (MicroPing) for MicroPython +# copyright (c) 2018 Shawwwn +# License: MIT + +# Internet Checksum Algorithm +# Author: Olav Morken +# https://github.com/olavmrk/python-ping/blob/master/ping.py +# @data: bytes +def checksum(data): + if len(data) & 0x1: # Odd number of bytes + data += b'\0' + cs = 0 + for pos in range(0, len(data), 2): + b1 = data[pos] + b2 = data[pos + 1] + cs += (b1 << 8) + b2 + while cs >= 0x10000: + cs = (cs & 0xffff) + (cs >> 16) + cs = ~cs & 0xffff + return cs + +def ping(host, count=4, timeout=5000, interval=10, quiet=False, size=64): + import utime + import uselect + import uctypes + import usocket + import ustruct + import urandom + + # prepare packet + assert size >= 16, "pkt size too small" + pkt = b'Q'*size + pkt_desc = { + "type": uctypes.UINT8 | 0, + "code": uctypes.UINT8 | 1, + "checksum": uctypes.UINT16 | 2, + "id": uctypes.UINT16 | 4, + "seq": uctypes.INT16 | 6, + "timestamp": uctypes.UINT64 | 8, + } # packet header descriptor + h = uctypes.struct(uctypes.addressof(pkt), pkt_desc, uctypes.BIG_ENDIAN) + h.type = 8 # ICMP_ECHO_REQUEST + h.code = 0 + h.checksum = 0 + h.id = urandom.randint(0, 65535) + h.seq = 1 + + # init socket + sock = usocket.socket(usocket.AF_INET, usocket.SOCK_RAW, 1) + sock.setblocking(0) + sock.settimeout(timeout/1000) + addr = usocket.getaddrinfo(host, 1)[0][-1][0] # ip address + sock.connect((addr, 1)) + not quiet and print("PING %s (%s): %u data bytes" % (host, addr, len(pkt))) + + seqs = list(range(1, count+1)) # [1,2,...,count] + c = 1 + t = 0 + n_trans = 0 + n_recv = 0 + finish = False + while t < timeout: + if t==interval and c<=count: + # send packet + h.checksum = 0 + h.seq = c + h.timestamp = utime.ticks_us() + h.checksum = checksum(pkt) + if sock.send(pkt) == size: + n_trans += 1 + t = 0 # reset timeout + else: + seqs.remove(c) + c += 1 + + # recv packet + while 1: + socks, _, _ = uselect.select([sock], [], [], 0) + if socks: + resp = socks[0].recv(4096) + resp_mv = memoryview(resp) + h2 = uctypes.struct(uctypes.addressof(resp_mv[20:]), pkt_desc, uctypes.BIG_ENDIAN) + # TODO: validate checksum (optional) + seq = h2.seq + if h2.type==0 and h2.id==h.id and (seq in seqs): # 0: ICMP_ECHO_REPLY + t_elasped = (utime.ticks_us()-h2.timestamp) / 1000 + ttl = ustruct.unpack('!B', resp_mv[8:9])[0] # time-to-live + n_recv += 1 + not quiet and print("%u bytes from %s: icmp_seq=%u, ttl=%u, time=%f ms" % (len(resp), addr, seq, ttl, t_elasped)) + seqs.remove(seq) + if len(seqs) == 0: + finish = True + break + else: + break + + if finish: + break + + utime.sleep_ms(1) + t += 1 + + # close + sock.close() + ret = (n_trans, n_recv) + not quiet and print("%u packets transmitted, %u packets received" % (n_trans, n_recv)) + return (n_trans, n_recv) diff --git a/scripts/libraries/utelnet.py b/scripts/libraries/utelnet.py new file mode 100644 index 000000000..7f4306e53 --- /dev/null +++ b/scripts/libraries/utelnet.py @@ -0,0 +1,113 @@ +# Public domain code. +# Source https://github.com/cpopp/MicroTelnetServer + +import socket +import network +import uos +import errno +from uio import IOBase + +last_client_socket = None +server_socket = None + +# Provide necessary functions for dupterm and replace telnet control characters that come in. +class TelnetWrapper(IOBase): + def __init__(self, socket): + self.socket = socket + self.discard_count = 0 + + def readinto(self, b): + readbytes = 0 + for i in range(len(b)): + try: + byte = 0 + # discard telnet control characters and + # null bytes + while(byte == 0): + byte = self.socket.recv(1)[0] + if byte == 0xFF: + self.discard_count = 2 + byte = 0 + elif self.discard_count > 0: + self.discard_count -= 1 + byte = 0 + + b[i] = byte + + readbytes += 1 + except (IndexError, OSError) as e: + if type(e) == IndexError or len(e.args) > 0 and e.args[0] == errno.EAGAIN: + if readbytes == 0: + return None + else: + return readbytes + else: + raise + return readbytes + + def write(self, data): + # we need to write all the data but it's a non-blocking socket + # so loop until it's all written eating EAGAIN exceptions + while len(data) > 0: + try: + written_bytes = self.socket.write(data) + data = data[written_bytes:] + except OSError as e: + if len(e.args) > 0 and e.args[0] == errno.EAGAIN: + # can't write yet, try again + pass + else: + # something else...propagate the exception + raise + + def close(self): + self.socket.close() + +# Attach new clients to dupterm and +# send telnet control characters to disable line mode +# and stop local echoing +def accept_telnet_connect(telnet_server): + global last_client_socket + + if last_client_socket: + # close any previous clients + uos.dupterm(None) + last_client_socket.close() + + last_client_socket, remote_addr = telnet_server.accept() + print("Telnet connection from:", remote_addr) + last_client_socket.setblocking(False) + # dupterm_notify() not available under MicroPython v1.1 + # last_client_socket.setsockopt(socket.SOL_SOCKET, 20, uos.dupterm_notify) + + last_client_socket.sendall(bytes([255, 252, 34])) # dont allow line mode + last_client_socket.sendall(bytes([255, 251, 1])) # turn off local echo + + uos.dupterm(TelnetWrapper(last_client_socket)) + +def stop(): + global server_socket, last_client_socket + uos.dupterm(None) + if server_socket: + server_socket.close() + if last_client_socket: + last_client_socket.close() + +# start listening for telnet connections on port 23 +def start(port=23): + stop() + global server_socket + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + ai = socket.getaddrinfo("0.0.0.0", port) + addr = ai[0][4] + + server_socket.bind(addr) + server_socket.listen(1) + server_socket.setsockopt(socket.SOL_SOCKET, 20, accept_telnet_connect) + + for i in (network.AP_IF, network.STA_IF): + wlan = network.WLAN(i) + if wlan.active(): + print("Telnet server started on {}:{}".format(wlan.ifconfig()[0], port))