7 Commits

5 changed files with 446 additions and 341 deletions

312
HTTPInterface.py Executable file
View File

@@ -0,0 +1,312 @@
import os
import threading
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
from queue import Empty, Queue
from socketserver import ThreadingMixIn
from threading import Event, Thread
import requests
import RNS
from RNS.Interfaces.Interface import Interface
class HTTPTunnelInterface(Interface):
"""HTTP Tunnel Interface for Reticulum.
This interface implements bidirectional communication over HTTP POST requests,
allowing Reticulum to traverse firewalls and proxies that allow HTTP/HTTPS traffic.
Configuration:
mode: "client" or "server"
listen_host: IP address to bind server to (server mode)
listen_port: Port to bind server to (server mode)
server_url: URL of the HTTP server (client mode)
poll_interval: Polling interval in seconds for client (default: 0.1)
check_user_agent: Enable User-Agent validation (default: True)
serve_html_page: Serve HTML page on GET requests (default: False)
html_file_path: Path to HTML file to serve (optional)
Config example for server:
[[HTTP Tunnel Server]]
type = HTTPTunnelInterface
interface_enabled = True
mode = server
listen_host = 0.0.0.0
listen_port = 8080
Config example for server with HTML:
[[HTTP Tunnel Server]]
type = HTTPTunnelInterface
interface_enabled = True
mode = server
listen_host = 0.0.0.0
listen_port = 8080
serve_html_page = True
html_file_path = index.html
Config example for client:
[[HTTP Tunnel Client]]
type = HTTPTunnelInterface
interface_enabled = True
mode = client
server_url = http://example.com:8080/
poll_interval = 0.1
"""
DEFAULT_IFAC_SIZE = 16
BITRATE_GUESS = 10_000_000
AUTOCONFIGURE_MTU = True
DEFAULT_MTU = 4096
TUNNEL_USER_AGENT = "RNS-HTTP-Tunnel/1.0"
DEFAULT_POLL_INTERVAL = 0.1
def __init__(self, owner, configuration):
super().__init__()
ifconf = Interface.get_config_obj(configuration)
self.name = ifconf["name"]
mode = ifconf["mode"] if "mode" in ifconf else "client"
listen_host = ifconf["listen_host"] if "listen_host" in ifconf else "0.0.0.0"
listen_port = int(ifconf["listen_port"]) if "listen_port" in ifconf else 8080
server_url = ifconf["server_url"] if "server_url" in ifconf else None
poll_interval = float(ifconf["poll_interval"]) if "poll_interval" in ifconf else self.DEFAULT_POLL_INTERVAL
check_user_agent = ifconf.as_bool("check_user_agent") if "check_user_agent" in ifconf else True
mtu = int(ifconf["mtu"]) if "mtu" in ifconf else self.DEFAULT_MTU
serve_html_page = ifconf.as_bool("serve_html_page") if "serve_html_page" in ifconf else False
html_file_path = ifconf["html_file_path"] if "html_file_path" in ifconf else None
if mode not in ["client", "server"]:
raise ValueError(f"Invalid mode '{mode}' for {self}. Must be 'client' or 'server'")
if mode == "client" and server_url is None:
raise ValueError(f"No server_url specified for client mode in {self}")
self.owner = owner
self.mode = mode
self.mtu = mtu
self.check_user_agent = check_user_agent
self.serve_html_page = serve_html_page
self.html_file_path = html_file_path
self.html_content = None
if self.serve_html_page and self.html_file_path:
self._load_html_content()
self._recv_queue = Queue()
self._send_queue = Queue()
self._stop_event = Event()
self.HW_MTU = mtu
self.online = False
self.bitrate = HTTPTunnelInterface.BITRATE_GUESS
if mode == "server":
self.listen_host = listen_host
self.listen_port = listen_port
else:
self.server_url = server_url
self.poll_interval = poll_interval
self.optimise_mtu()
if mode == "server":
self.setup_server()
else:
self.setup_client()
def _load_html_content(self):
try:
if os.path.isfile(self.html_file_path):
with open(self.html_file_path, encoding="utf-8") as f:
self.html_content = f.read()
RNS.log(f"Loaded HTML content from {self.html_file_path}", RNS.LOG_INFO)
else:
RNS.log(f"HTML file not found: {self.html_file_path}", RNS.LOG_WARNING)
self.html_content = None
except Exception as e:
RNS.log(f"Error loading HTML file {self.html_file_path}: {e}", RNS.LOG_ERROR)
self.html_content = None
def setup_server(self):
interface_instance = self
class TunnelRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/" and interface_instance.serve_html_page and interface_instance.html_content:
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(interface_instance.html_content)))
self.end_headers()
self.wfile.write(interface_instance.html_content.encode("utf-8"))
else:
self.send_response(404)
self.end_headers()
def do_POST(self):
if self.path == "/":
if interface_instance.check_user_agent:
user_agent = self.headers.get("User-Agent", "")
if user_agent != HTTPTunnelInterface.TUNNEL_USER_AGENT:
RNS.log(f"Rejected request with invalid User-Agent: {user_agent}", RNS.LOG_WARNING)
self.send_response(403)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"Forbidden")
return
content_length = int(self.headers.get("Content-Length", 0))
client_data = self.rfile.read(content_length) if content_length > 0 else b""
if client_data:
RNS.log(f"Received {len(client_data)} bytes from client", RNS.LOG_EXTREME)
interface_instance._recv_queue.put(client_data)
server_data_parts = []
while not interface_instance._send_queue.empty():
try:
server_data_parts.append(interface_instance._send_queue.get_nowait())
except Empty:
break
server_data = b"".join(server_data_parts)
if server_data:
RNS.log(f"Sending {len(server_data)} bytes ({len(server_data_parts)} chunks) to client", RNS.LOG_EXTREME)
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Length", str(len(server_data)))
self.end_headers()
self.wfile.write(server_data)
else:
self.send_response(404)
self.end_headers()
def log_message(self, fmt, *args):
pass
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
pass
def run_server():
try:
self._http_server = ThreadedHTTPServer((self.listen_host, self.listen_port), TunnelRequestHandler)
self._http_server.serve_forever()
except Exception as e:
if not self._stop_event.is_set():
RNS.log(f"HTTP server error for {self}: {e}", RNS.LOG_ERROR)
if RNS.Reticulum.panic_on_interface_error:
RNS.panic()
self._server_thread = Thread(target=run_server, daemon=True)
self._server_thread.start()
thread = threading.Thread(target=self.receive_loop)
thread.daemon = True
thread.start()
self.online = True
RNS.log(f"HTTP server started on http://{self.listen_host}:{self.listen_port}", RNS.LOG_NOTICE)
def setup_client(self):
self.session = requests.Session()
self.session.headers.update({"User-Agent": HTTPTunnelInterface.TUNNEL_USER_AGENT})
self._consecutive_failures = 0
self._max_backoff = 30.0
thread = threading.Thread(target=self.client_loop)
thread.daemon = True
thread.start()
self.online = True
RNS.log(f"HTTP client started, connecting to {self.server_url}", RNS.LOG_NOTICE)
def receive_loop(self):
while not self._stop_event.is_set():
try:
received_data = self._recv_queue.get(timeout=1)
if received_data:
self.process_incoming(received_data)
except Empty:
continue
except Exception as e:
if not self._stop_event.is_set():
RNS.log(f"Error in receive loop for {self}: {e}", RNS.LOG_ERROR)
def client_loop(self):
while not self._stop_event.is_set():
data_to_send = b""
if not self._send_queue.empty():
try:
data_to_send = self._send_queue.get_nowait()
except Empty:
pass
try:
RNS.log(f"Sending {len(data_to_send)} bytes to server", RNS.LOG_EXTREME)
response = self.session.post(self.server_url, data=data_to_send, timeout=5)
response.raise_for_status()
if response.content:
RNS.log(f"Received {len(response.content)} bytes from server", RNS.LOG_EXTREME)
self.process_incoming(response.content)
if self._consecutive_failures > 0:
RNS.log(f"Reconnected to server for {self}", RNS.LOG_INFO)
self._consecutive_failures = 0
except requests.exceptions.RequestException as e:
self._consecutive_failures += 1
if self._consecutive_failures % 10 == 1:
RNS.log(f"Error communicating with server for {self} (attempt {self._consecutive_failures}): {e}", RNS.LOG_WARNING)
if self._consecutive_failures > 0:
delay = min(self.poll_interval * (2 ** min(self._consecutive_failures - 1, 5)), self._max_backoff)
else:
delay = self.poll_interval
time.sleep(delay)
def process_incoming(self, data):
if len(data) > 0 and self.online:
self.rxb += len(data)
self.owner.inbound(data, self)
def process_outgoing(self, data):
if self.online:
if len(data) > self.mtu:
RNS.log(f"Payload too large ({len(data)} > {self.mtu}) for {self}", RNS.LOG_ERROR)
return
self._send_queue.put(data)
self.txb += len(data)
def detach(self):
RNS.log(f"Detaching {self}", RNS.LOG_DEBUG)
self._stop_event.set()
self.online = False
if self.mode == "server":
if hasattr(self, "_http_server") and self._http_server:
try:
self._http_server.shutdown()
self._http_server.server_close()
except Exception as e:
RNS.log(f"Error while shutting down HTTP server for {self}: {e}", RNS.LOG_ERROR)
if hasattr(self, "_server_thread") and self._server_thread:
self._server_thread.join(timeout=2)
def should_ingress_limit(self):
return False
def __str__(self):
if self.mode == "server":
return f"HTTPTunnelInterface[{self.name}/server/{self.listen_host}:{self.listen_port}]"
return f"HTTPTunnelInterface[{self.name}/client/{self.server_url}]"
interface_class = HTTPTunnelInterface

View File

@@ -2,7 +2,7 @@
[Русский](README-RU.md) [Русский](README-RU.md)
A Reticulum interface that tunnels traffic over standard HTTP/S POST requests. This allows Reticulum to operate on networks where only web traffic is permitted, effectively bypassing firewalls, DPI, and other restrictions. A custom Reticulum interface that tunnels traffic over standard HTTP/S POST requests. This allows Reticulum to operate on networks where only web traffic is permitted, effectively bypassing firewalls, DPI, and other restrictions.
[Non-GitHub Mirror](https://lavaforge.org/Ivan/RNS-over-HTTP). Also available on the network `RNS-over-HTTP` node. [Non-GitHub Mirror](https://lavaforge.org/Ivan/RNS-over-HTTP). Also available on the network `RNS-over-HTTP` node.
@@ -40,35 +40,37 @@ This continuous cycle creates a reliable, albeit higher-latency, communication c
### Requirements ### Requirements
- Python 3.9 or later - Python 3.9 or later
- `pip` for installing packages - `rns`
- `requests`
### Installation ### Installation
1. **Install the `requests` library if not already installed:** 1. **Install Reticulum and dependencies:**
```bash ```bash
pip install requests pip install rns requests
``` ```
2. **Download the interface script:** 2. **Install the custom interface:**
Place `http_interface.py` in a known location on both your client and server machines, for example, `~/.reticulum/interfaces/`. Place `HTTPInterface.py` in your Reticulum interfaces directory: `~/.reticulum/interfaces/`.
## Configuration ## Configuration
Set up a `PipeInterface` in your `~/.reticulum/config` file on both the server and client machines. Add an interface entry to your Reticulum configuration file (`~/.reticulum/config`) on both the server and client machines.
### Server Configuration ### Server Configuration
The server listens for incoming connections from clients. The server listens for incoming connections from clients.
```ini ```ini
[[HTTP Interface]] [[HTTP Server Interface]]
type = PipeInterface type = HTTPInterface
enabled = True enabled = true
# The command to run the server script. Listens on all interfaces by default. mode = server
command = python3 /path/to/your/http_interface.py server --host 0.0.0.0 --port 8080 listen_host = 0.0.0.0
# Optional: delay before respawning the interface if it crashes. listen_port = 8080
respawn_delay = 5 mtu = 4096
name = HTTP Interface Server check_user_agent = true
user_agent = RNS-HTTP-Tunnel/1.0
``` ```
### Client Configuration ### Client Configuration
@@ -76,26 +78,36 @@ The server listens for incoming connections from clients.
The client connects to the server's public URL. The client connects to the server's public URL.
```ini ```ini
[[HTTP Interface]] [[HTTP Client Interface]]
type = PipeInterface type = HTTPInterface
enabled = True enabled = true
# The command to run the client script. Point --url to your server. mode = client
command = python3 /path/to/your/http_interface.py client --url http://<your-server-ip-or-domain> server_url = http://your-server-ip-or-domain:8080
# Optional: delay before respawning the interface if it crashes. poll_interval = 1.0
respawn_delay = 5 mtu = 4096
name = HTTP Interface Client user_agent = RNS-HTTP-Tunnel/1.0
``` ```
## Command-Line Options ## Configuration Options
You can customize the behavior of the script with these arguments: ### Common Options
- `--mtu`: Maximum Transmission Unit in bytes (default: `4096`). - `mtu`: Maximum Transmission Unit in bytes (default: `4096`).
- `--poll-interval`: Client polling interval in seconds (default: `0.1`). - `name`: Interface name for logging and identification.
- `--verbose` or `-v`: Enable verbose debug logging. - `user_agent`: User-Agent string to use for HTTP requests (default: `"RNS-HTTP-Tunnel/1.0"`).
- `--host`: Server listen host (default: `0.0.0.0`).
- `--port`: Server listen port (default: `8080`). ### Server Mode Options
- `--disable-user-agent-check`: Disable User-Agent validation on the server.
- `mode`: Must be set to `server`.
- `listen_host`: Host to bind the HTTP server to (default: `0.0.0.0`).
- `listen_port`: Port to listen on (default: `8080`).
- `check_user_agent`: Whether to validate User-Agent headers (default: `true`).
### Client Mode Options
- `mode`: Must be set to `client`.
- `server_url`: Full URL of the server to connect to (required for client mode).
- `poll_interval`: Polling interval in seconds (default: `1.0`).
## Reverse Proxy Setup (Caddy Example) ## Reverse Proxy Setup (Caddy Example)

67
config_example Normal file
View File

@@ -0,0 +1,67 @@
# RNS HTTP Tunnel Interface Configuration Examples
# Server Mode Configuration
# This creates an HTTP server that accepts connections from HTTP tunnel clients
[[HTTP Tunnel Server]]
type = HTTPTunnelInterface
interface_enabled = True
mode = server
listen_host = 0.0.0.0
listen_port = 8080
check_user_agent = True
mtu = 4096
# Server Mode with HTML Page
# This creates an HTTP server that serves an HTML page on GET requests
[[HTTP Tunnel Server with HTML]]
type = HTTPTunnelInterface
interface_enabled = True
mode = server
listen_host = 0.0.0.0
listen_port = 8080
check_user_agent = True
mtu = 4096
serve_html_page = True
html_file_path = index.html
# Client Mode Configuration
# This connects to an HTTP tunnel server
[[HTTP Tunnel Client]]
type = HTTPTunnelInterface
interface_enabled = False
mode = client
server_url = http://example.com:8080/
poll_interval = 0.1
mtu = 4096
# HTTPS Example (Client)
# Use this to connect to an HTTPS-enabled server
[[HTTPS Tunnel Client]]
type = HTTPTunnelInterface
interface_enabled = False
mode = client
server_url = https://secure.example.com:443/
poll_interval = 0.2
check_user_agent = True
# Configuration Options:
#
# mode (required):
# - "server": Run as HTTP server accepting incoming connections
# - "client": Run as HTTP client connecting to a server
#
# Server Mode Options:
# listen_host: IP address to bind to (default: 0.0.0.0)
# listen_port: TCP port to bind to (default: 8080)
#
# Client Mode Options:
# server_url: Full URL of the HTTP server (required for client mode)
# poll_interval: Seconds between polls (default: 0.1)
#
# Common Options:
# check_user_agent: Validate User-Agent header (default: True)
# mtu: Maximum transmission unit in bytes (default: 4096)
# serve_html_page: Serve HTML page on GET requests (default: False)
# html_file_path: Path to HTML file to serve (required if serve_html_page is True)
# interface_enabled: Enable/disable the interface (default: False)

View File

@@ -1,310 +0,0 @@
"""
RNS-over-HTTP Interface
HTTP interface for Reticulum that implements bidirectional communication using HTTP POST requests.
"""
import logging
import logging.handlers
import sys
import threading
from abc import ABC, abstractmethod
from queue import Queue, Empty
from threading import Thread, Event
from time import sleep
from typing import Iterable
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
import socket
import requests
MTU = 4096
TUNNEL_USER_AGENT = "RNS-HTTP-Tunnel/1.0"
def setup_logging():
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
console_handler = logging.StreamHandler(sys.stderr)
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
setup_logging()
class AbstractTunnel(ABC):
def __init__(self, mtu: int):
self.mtu = mtu
self._recv_queue: Queue[bytes] = Queue()
self._send_queue: Queue[bytes] = Queue()
self._stop_event = Event()
self.logger = logging.getLogger(self.__class__.__name__)
def send(self, pkt: bytes) -> None:
if len(pkt) > self.mtu:
raise ValueError(f"payload too large ({len(pkt)} > {self.mtu})")
self._send_queue.put(pkt)
def recv(self) -> Iterable[bytes]:
while True:
yield self._recv_queue.get(block=True)
@abstractmethod
def start(self) -> None:
pass
@abstractmethod
def stop(self) -> None:
pass
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
pass
class Server(AbstractTunnel):
def __init__(self, listen_host: str, listen_port: int, mtu: int, check_user_agent: bool = True):
super().__init__(mtu)
self.listen_host = listen_host
self.listen_port = listen_port
self.check_user_agent = check_user_agent
self._server_thread: Thread | None = None
self._http_server: HTTPServer | None = None
class TunnelRequestHandler(BaseHTTPRequestHandler):
def __init__(self, request, client_address, server, tunnel_instance=self):
self.tunnel = tunnel_instance
super().__init__(request, client_address, server)
def do_POST(self):
if self.path == "/":
if self.tunnel.check_user_agent:
user_agent = self.headers.get('User-Agent', '')
if user_agent != TUNNEL_USER_AGENT:
self.tunnel.logger.warning(f"Rejected request with invalid User-Agent: {user_agent}")
self.send_response(403)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(b'Forbidden')
return
content_length = int(self.headers.get('Content-Length', 0))
client_data = self.rfile.read(content_length) if content_length > 0 else b""
if client_data:
self.tunnel.logger.debug(f"Received {len(client_data)} bytes from client")
self.tunnel._recv_queue.put(client_data)
server_data_parts = []
while not self.tunnel._send_queue.empty():
try:
server_data_parts.append(self.tunnel._send_queue.get_nowait())
except Empty:
break
server_data = b"".join(server_data_parts)
if server_data:
self.tunnel.logger.debug(f"Sending {len(server_data)} bytes ({len(server_data_parts)} chunks) to client")
self.send_response(200)
self.send_header('Content-Type', 'application/octet-stream')
self.send_header('Content-Length', str(len(server_data)))
self.end_headers()
self.wfile.write(server_data)
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
pass
self._request_handler_class = TunnelRequestHandler
def start(self) -> None:
def run_server():
try:
self._http_server = ThreadedHTTPServer((self.listen_host, self.listen_port), self._request_handler_class)
self._http_server.serve_forever()
except Exception as e:
if not self._stop_event.is_set():
self.logger.error(f"Server error: {e}")
self._server_thread = Thread(target=run_server, daemon=True)
self._server_thread.start()
self.logger.info(f"HTTP server started on http://{self.listen_host}:{self.listen_port}")
def stop(self) -> None:
self.logger.info("Stopping HTTP server...")
self._stop_event.set()
if self._http_server:
self._http_server.shutdown()
self._http_server.server_close()
if self._server_thread:
self._server_thread.join(timeout=2)
class Client(AbstractTunnel):
def __init__(self, server_url: str, mtu: int, poll_interval: float = 1.0):
super().__init__(mtu)
self.server_url = server_url
self.poll_interval = poll_interval
self._client_thread: Thread | None = None
self.session = requests.Session()
self.session.headers.update({'User-Agent': TUNNEL_USER_AGENT})
self._consecutive_failures = 0
self._max_backoff = 30.0 # Maximum backoff time in seconds
def start(self) -> None:
self._client_thread = Thread(target=self._run, daemon=True)
self._client_thread.start()
self.logger.info(f"HTTP client started, connecting to {self.server_url}")
def stop(self) -> None:
self.logger.info("Stopping HTTP client...")
self._stop_event.set()
if self._client_thread:
self._client_thread.join(timeout=2)
def _run(self):
while not self._stop_event.is_set():
data_to_send = b""
if not self._send_queue.empty():
try:
data_to_send = self._send_queue.get_nowait()
except Empty:
pass
try:
self.logger.debug(f"Sending {len(data_to_send)} bytes to server")
response = self.session.post(self.server_url, data=data_to_send, timeout=5)
response.raise_for_status()
if response.content:
self.logger.debug(f"Received {len(response.content)} bytes from server")
self._recv_queue.put(response.content)
if self._consecutive_failures > 0:
self.logger.info("Reconnected to server")
self._consecutive_failures = 0
except requests.exceptions.RequestException as e:
self._consecutive_failures += 1
if self._consecutive_failures % 10 == 1:
self.logger.error(f"Error communicating with server (attempt {self._consecutive_failures}): {e}")
if self._consecutive_failures > 0:
delay = min(self.poll_interval * (2 ** min(self._consecutive_failures - 1, 5)), self._max_backoff)
else:
delay = self.poll_interval
sleep(delay)
if __name__ == "__main__":
import argparse
import os
import errno
logger = logging.getLogger(__name__)
parser = argparse.ArgumentParser(description="HTTP Tunnel - Server/Client")
parser.add_argument("mode", choices=["server", "client"], help="Run mode: server or client")
parser.add_argument("--mtu", type=int, default=MTU, help=f"MTU size (default: {MTU})")
parser.add_argument("--host", type=str, default="0.0.0.0", help="Listen host (for server mode)")
parser.add_argument("--port", type=int, default=8080, help="Listen port (for server mode)")
parser.add_argument("--url", type=str, help="Server URL (required for client mode)")
parser.add_argument("--poll-interval", type=float, default=0.1, help="Client poll interval in seconds")
parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose logging")
parser.add_argument("--disable-user-agent-check", action="store_true", help="Disable User-Agent validation (server mode only)")
args = parser.parse_args()
if args.verbose:
for handler in logging.getLogger().handlers:
if isinstance(handler, logging.StreamHandler):
handler.setLevel(logging.DEBUG)
def receive_messages(tunnel, stop_event):
stdout_fd = sys.stdout.fileno()
try:
for received_data in tunnel.recv():
if stop_event.is_set():
break
if received_data:
try:
os.write(stdout_fd, received_data)
except OSError as e:
if e.errno == errno.EPIPE:
stop_event.set()
return
else:
logger.error("Error writing to stdout: %s", e)
stop_event.set()
return
except Exception:
if not stop_event.is_set():
logger.error("Error in receive thread", exc_info=True)
return
def read_stdin_bytes():
try:
return os.read(0, 4096)
except (IOError, OSError):
return None
try:
if args.mode == "server":
server = Server(listen_host=args.host, listen_port=args.port, mtu=args.mtu, check_user_agent=not args.disable_user_agent_check)
server.start()
stop_event = threading.Event()
receive_thread = threading.Thread(target=receive_messages, args=(server, stop_event), daemon=True)
receive_thread.start()
try:
while not stop_event.is_set():
message = read_stdin_bytes()
if message:
server.send(message)
else:
sleep(0.01)
except KeyboardInterrupt:
logger.info("Stopping server...")
finally:
stop_event.set()
server.stop()
receive_thread.join(timeout=1)
elif args.mode == "client":
if not args.url:
parser.error("--url is required for client mode")
client = Client(server_url=args.url, mtu=args.mtu, poll_interval=args.poll_interval)
client.start()
stop_event = threading.Event()
receive_thread = threading.Thread(target=receive_messages, args=(client, stop_event), daemon=True)
receive_thread.start()
try:
while not stop_event.is_set():
message = read_stdin_bytes()
if message:
client.send(message)
else:
sleep(0.01)
except KeyboardInterrupt:
logger.info("Stopping client...")
finally:
stop_event.set()
client.stop()
receive_thread.join(timeout=1)
except Exception as e:
logger.error(f"A critical error occurred: {e}", exc_info=True)
sys.exit(1)

24
index.html Normal file
View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Service Status</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; background-color: #f5f5f5; }
.container { max-width: 800px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.status { color: #28a745; font-weight: bold; }
.header { text-align: center; margin-bottom: 30px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Service Status</h1>
<p class="status">✓ All systems operational</p>
</div>
<p>This service is currently running normally. If you're seeing this page, the service is functioning as expected.</p>
<p>For technical inquiries, please contact the system administrator.</p>
</div>
</body>
</html>