Files
quectel-rgmii-toolkit/attelnetdaemon/at-telnet/modem-multiclient.py
iamromulan 7f80364ac0 Edit Install/Uninstall scripts; Update README.md
Edited Install and Uninstall scripts to handle both at_telnet_daemon and simpleadmin
2023-09-20 00:35:38 -04:00

282 lines
13 KiB
Python

#!/usrdata/micropython/micropython
# Add the /usrdata/micropython directory to sys.path so we can find the external modules.
# TODO: Move external modules to lib?
# TODO: Recompile Micropython with a syspath set up for our use case.
import sys
# Remove the home directory from sys.path.
if "~/.micropython/lib" in sys.path:
sys.path.remove("~/.micropython/lib")
sys.path.append("/usrdata/micropython/lib")
sys.path.append("/usrdata/micropython")
import uos
import usocket as socket
import _thread as thread
import serial
import select
import traceback
import logging
import re
import time
# Set up logging
logging.basicConfig(level=logging.INFO, format='[%(asctime)s: %(levelname)s/%(msecs)ims] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
# Globally define client_sockets and serialport. That way, we can access them from handle_output and make it a separate thread, so responses (and unsolicited responses) can come in while we're waiting for input.
global client_sockets, serialport
client_sockets = []
# We are referencing one of the two ports exposed by our socat command. The other one is /dev/ttyIN, and two running "cat" commands are keeping it sync'd with /dev/smd11.
serialport = serial.Serial("/dev/ttyOUT", baudrate=115200)
# These will be set in the main routine.
global firewall_is_setup, fwpublicinterface, port
firewall_is_setup = 0
# Make these configurable via /etc/default or similar
port = 5000
fwpublicinterface = "rmnet+"
# Block access to port 5000 via ipv4 and ipv6 on public-facing interfaces.
def add_firewll_rules(port=port, fwpublicinterface=fwpublicinterface):
if not port or not fwpublicinterface:
logging.error(f"Port or fwpublicinterface not set. Values: fwpublicinterface: {fwpublicinterface} port: {port}")
exit(1)
logging.info(f"Adding firewall rules for port {port} on interface {fwpublicinterface}.")
# Check if the rule already exists in iptables
iptables_check_cmd = f"iptables -C INPUT -i {fwpublicinterface} -p tcp --dport {port} -j REJECT &> /dev/null"
iptables_check_result = uos.system(iptables_check_cmd)
if iptables_check_result != 0:
# Rule doesn't exist, add it to iptables
iptables_add_cmd = f"iptables -A INPUT -i {fwpublicinterface} -p tcp --dport {port} -j REJECT"
iptables_add_result = uos.system(iptables_add_cmd)
if iptables_add_result:
logging.error(f"ERROR: Failed to add iptables rule - input interface {fwpublicinterface} port {port}")
# Treat this as fatal.
sys.exit(1)
else:
logging.debug(f"Added iptables rule - input interface {fwpublicinterface} port {port}")
# Check if the rule already exists in ip6tables
ip6tables_check_cmd = f"ip6tables -C INPUT -i {fwpublicinterface} -p tcp --dport {port} -j REJECT &> /dev/null"
ip6tables_check_result = uos.system(ip6tables_check_cmd)
if ip6tables_check_result != 0:
# Rule doesn't exist, add it to ip6tables
ip6tables_add_cmd = f"ip6tables -A INPUT -i {fwpublicinterface} -p tcp --dport {port} -j REJECT"
ip6tables_add_result = uos.system(ip6tables_add_cmd)
if ip6tables_add_result:
logging.error(f"ERROR: Failed to add ip6tables rule - input interface {fwpublicinterface} port {port}")
# Treat this as fatal.
sys.exit(1)
else:
logging.debug(f"Added ip6tables rule - input interface {fwpublicinterface} port {port}")
global firewall_is_setup
firewall_is_setup = 1
logging.info(f"Successfully firewall rules for port {port} on interface {fwpublicinterface}.")
def remove_firewall_rules(port=port, fwpublicinterface=fwpublicinterface):
if firewall_is_setup:
iptables_del_cmd = f"iptables -D INPUT -i {fwpublicinterface} -p tcp --dport {port} -j REJECT"
ip6tables_del_cmd = f"ip6tables -D INPUT -i {fwpublicinterface} -p tcp --dport {port} -j REJECT"
iptables_del_result = uos.system(iptables_del_cmd)
ip6tables_del_result = uos.system(ip6tables_del_cmd)
if iptables_del_result or ip6tables_del_result:
logging.error(f"ERROR: Failed to remove iptables or ip6tables rule - input interface {fwpublicinterface} port {port}")
else:
logging.info(f"Removed iptables and ip6tables rule - input interface {fwpublicinterface} port {port}")
else:
logging.info(f"Firewall rules not set up; not removing.")
# This routine pulls data from the serial port and sends it to all connected clients.
def handle_output():
while True:
# Make data an empty bytes list
data = b''
try:
while serialport.in_waiting > 0:
data += serialport.read(1)
except Exception as e:
# This will keep trying.
print(f"Exception reading data from serialport: {e}")
traceback.print_exc()
if data:
logging.info(f"Got data from modem: {data}")
for client_socket in client_sockets:
client_socket.send(data)
# Start the server on the specified port, listen for clients, etc.
def start_at_server(port):
# Server initialization stuff
# NOTE: This now supports IPv6. And means that on many connections it'll be directly exposed
# to the internet. So we're adding firewall rules to block access to it via rmnet+.
try:
server_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
addr_info = socket.getaddrinfo("::", port)
addr = addr_info[0][4]
server_socket.bind(addr)
server_socket.listen(1)
logging.info(f"AT Server listening on TCP port {port}")
# Disable echo so user doesn't see a second copy of all their commands.
serialport.write("ATE0\r\n")
# time.sleep() segfaults?! ugh.
uos.system("sleep 0.025s")
# wait for an OK
out=b''
while serialport.in_waiting > 0:
out += serialport.read(1)
if "OK" not in str(out):
logging.warning(f"Did not get expected OK when running ATE0. Result: {str(out)}")
except Exception as e:
logging.error(f"Error initializing server: {e}")
traceback.print_exc()
raise
# Start the output handler in its own thread
try:
thread.start_new_thread(handle_output, ())
except Exception as e:
print("Error with output handler:", e)
traceback.print_exc()
raise
# Set up a select.poll object to listen for input from the server socket and all client sockets.
# Logic mostly from https://pymotw.com/2/select/
try:
poll_obj = select.poll()
poll_obj.register(server_socket, select.POLLIN)
# Register the server socket in the fd_to_socket dict; this will also be used to register the rest of the clients.
fd_to_socket = { server_socket.fileno(): server_socket,
}
while True:
events = poll_obj.poll()
for fd, flag in events:
logging.debug(f"Pool loop event. fd: {fd} flag: {flag} fd_to_socket.keys(): {fd_to_socket.keys()}")
# Check if the client already exists in the fd_to_socket dict.
if fd.fileno() in fd_to_socket.keys():
s = fd_to_socket[fd.fileno()]
logging.debug("Event matches existing socket.")
else:
s = fd
logging.debug(f"Event doesn't match existing socket. fd: {fd} fd_to_socket: {fd_to_socket}")
# If the flag is POLLIN, then we have data to process.
if flag & (select.POLLIN):
# If the server socket is ready to read, then we have a new client connection.
if s is server_socket:
# Accept the connection.
client_socket, client_address = s.accept()
# TODO: This gives a garbled IP. Figure it out.
#client_address_translated = socket.inet_ntop(socket.AF_INET, client_address)
logging.info(f"New connection")
# Set the client socket to non-blocking, and add it to the list of client sockets.
# TODO: trim down to just storing one copy of the client sockets..
client_socket.setblocking(0)
fd_to_socket[ client_socket.fileno() ] = client_socket
client_sockets.append(client_socket)
poll_obj.register(client_socket, select.POLLIN)
# Send a good 'ol hello message to the client.
client_socket.send("** Welcome to the AT server!\r\n".encode())
client_socket.send("** Note that your commands are interleaved with any other connected clients,\r\n** so responses may appear out of order.\r\n".encode())
client_socket.send("** \r\n".encode())
client_socket.send("** You may also receive unsolicited responses (URC's) depending on the\r\n** modem configuration.\r\n".encode())
client_socket.send("** \r\n".encode())
client_socket.send("** Echo is off (ATE0); if you change it you'll see what you've typed both\r\n** locally and echo'd back.\r\n".encode())
client_socket.send("** \r\n".encode())
client_socket.send("** I have tested this with telnet.netkit and netcat on Linux. If your client\r\n** doesn't work,\r\n** please open an issue at:\r\n** https://github.com/natecarlson/quectel-rgmii-at-command-client/ **\r\n".encode())
client_socket.send("**\r\n".encode())
client_socket.send("** If you would like to support further development, you can at:\r\n** https://www.buymeacoffee.com/natecarlson **\r\n".encode())
client_socket.send("\r\n".encode())
# Otherwise, we have data from a client socket.
else:
data = s.recv(1024)
logging.info(f"Got data from client: {data}")
if data:
# Ensure it ends with \r\n
if not data.endswith("\r\n"):
# Just stripping \n for now; add others in the future if needed.
data = re.sub(b"\n$", "", data) + "\r\n"
logging.info(f"Modified client data to end with \\r\\n: {data}")
# Good client data; write out to the serial port.
serialport.write(data)
# Write out out to the rest of the clients too
for fd in fd_to_socket.keys():
if fd != server_socket.fileno() and fd != s.fileno():
logging.debug(f"Writing data to other connected client: {data}")
try:
fd_to_socket[fd].send(data)
except Exception as e:
logging.info(f"Failed to write data to an additional client. Ignorning. Result: {e}")
pass
else:
# Client disconnected
print("Client disconnected")
client_sockets.remove(s)
poll_obj.unregister(s)
del fd_to_socket[s.fileno()]
s.close()
# Not sure if this can happen. But , if it does, we should close the socket.
elif flag & select.POLLERR:
logging.warn(f"Strange connection issue with a client; closing.")
# Stop listening for input on the connection
poll_obj.unregister(s)
client_sockets.remove(s)
del fd_to_socket[s.fileno()]
s.close()
# TODO: I don't believe we need this here, since the output is now handled in its own thread.
#uos.system("sleep 0.025s")
except Exception as e:
print("Error after server initialization:", e)
serialport.write("ATE1\r\n")
traceback.print_exc()
# I believe this will drop out of the while loop, so we'll close the sockets and exit.
# Close client sockets and server socket
for client_socket in client_sockets:
client_socket.close()
server_socket.close()
# TODO: By using the dict, we shouldn't need this code. Clean it up.
#def fd_to_socket(fd, client_sockets):
# for client_socket in client_sockets:
# if client_socket.fileno() == fd:
# return client_socket
# return None
# App startup. TODO: Make the port configurable.
if __name__ == "__main__":
# Register an atexit handler to remove the firewall rules.
sys.atexit(remove_firewall_rules)
# Add the firewall rules before starting anything
add_firewll_rules(port=port, fwpublicinterface=fwpublicinterface)
# Light 'er up!
start_at_server(port)