282 lines
13 KiB
Python
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)
|