Edit Install/Uninstall scripts; Update README.md
Edited Install and Uninstall scripts to handle both at_telnet_daemon and simpleadmin
This commit is contained in:
281
attelnetdaemon/at-telnet/modem-multiclient.py
Normal file
281
attelnetdaemon/at-telnet/modem-multiclient.py
Normal file
@@ -0,0 +1,281 @@
|
||||
#!/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)
|
||||
BIN
attelnetdaemon/at-telnet/picocom
Normal file
BIN
attelnetdaemon/at-telnet/picocom
Normal file
Binary file not shown.
BIN
attelnetdaemon/at-telnet/socat-armel-static
Normal file
BIN
attelnetdaemon/at-telnet/socat-armel-static
Normal file
Binary file not shown.
@@ -0,0 +1,25 @@
|
||||
[Unit]
|
||||
Description=Telnet daemon for AT command
|
||||
|
||||
# Being extra silly with the dependencies for this.
|
||||
# TODO: Update the python code to validate that the serial port
|
||||
# is working on a regular basis, and keep attempting to retry
|
||||
# if not. Then these dependencies won't need to be so strict.
|
||||
After=socat-smd11.service
|
||||
Requires=socat-smd11.service socat-smd11-from-ttyIN.service socat-smd11-to-ttyIN.service
|
||||
ReloadPropagatedFrom=socat-smd11.service socat-smd11-from-ttyIN.service socat-smd11-to-ttyIN.service
|
||||
|
||||
StartLimitIntervalSec=2m
|
||||
StartLimitBurst=100
|
||||
|
||||
[Service]
|
||||
ExecStart=/usrdata/at-telnet/modem-multiclient.py
|
||||
Nice=5
|
||||
Restart=always
|
||||
RestartSec=2s
|
||||
# Increased log rate limits, so we can see what's going on.
|
||||
LogRateLimitIntervalSec=5s
|
||||
LogRateLimitBurst=100
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Read from /dev/ttyIN and write to smd11
|
||||
BindsTo=socat-smd11.service
|
||||
After=socat-smd11.service
|
||||
|
||||
[Service]
|
||||
ExecStart=/bin/bash -c "/bin/cat /dev/ttyIN > /dev/smd11"
|
||||
ExecStartPost=/bin/sleep 2s
|
||||
StandardInput=tty-force
|
||||
Restart=always
|
||||
RestartSec=1s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Read from /dev/smd11 and write to ttyIN
|
||||
BindsTo=socat-smd11.service
|
||||
After=socat-smd11.service
|
||||
|
||||
[Service]
|
||||
ExecStart=/bin/bash -c "/bin/cat /dev/smd11 > /dev/ttyIN"
|
||||
ExecStartPost=/bin/sleep 2s
|
||||
StandardInput=tty-force
|
||||
Restart=always
|
||||
RestartSec=1s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
13
attelnetdaemon/at-telnet/systemd_units/socat-smd11.service
Normal file
13
attelnetdaemon/at-telnet/systemd_units/socat-smd11.service
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Socat Serial Emulation for smd11
|
||||
After=ql-netd.service
|
||||
|
||||
[Service]
|
||||
ExecStart=/usrdata/at-telnet/socat-armel-static -d -d pty,link=/dev/ttyIN,raw,echo=0 pty,link=/dev/ttyOUT,raw,echo=1
|
||||
# Add a delay to prevent the clients from starting too early
|
||||
ExecStartPost=/bin/sleep 2s
|
||||
Restart=always
|
||||
RestartSec=1s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user