mirror of
https://github.com/Gathin23/cleanyourtv.git
synced 2026-11-07 10:46:07 -10:00
The relay read the address as the TV's host, so http://<tv ip> split into the host
"http" and the port "//<tv ip>" ("not a port number: '//<tv ip>'"), while
http://<tv ip>:5555 and <tv ip>/6/system became hosts named for the URL and the page:
the relay bound its loopback port, printed where it forwards to, and only a client
that connected was told "cannot reach ... (is the TV on, and network debugging still
on?)" - the TV blamed for an argument the script cannot use. The word is named before
the TV's port is read out of it and before anything is bound, exit 2 as the script's
other refusals do.
170 lines
8.0 KiB
Python
170 lines
8.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Forward a port on this computer to the TV's ADB port, for assistants whose sandbox can't reach the network.
|
|
|
|
Some coding assistants run their commands in a sandbox that may talk to this computer but not to other devices
|
|
on the local network (Claude Code on macOS, where Local Network privacy blocks it). Run this in an ordinary
|
|
terminal, then point the tools at it:
|
|
|
|
tools/venv/bin/python -I tools/relay.py 192.168.1.50 # leave it running
|
|
TV_HOST=127.0.0.1 TV_PORT=15555 tools/venv/bin/python -I tools/tv.py --probe
|
|
|
|
Usage: relay.py [TV_HOST[:PORT]] [--tv-port 5555] [--port 15555]
|
|
The TV's port is --tv-port, else the one in the address, else TV_PORT, else 5555.
|
|
A URL or a page opened at the address is refused by name: give the IP (or IP:port) alone.
|
|
It listens on 127.0.0.1 only, never on the network. Read tools/relay.md first.
|
|
"""
|
|
import argparse
|
|
import os
|
|
import socket
|
|
import sys
|
|
import threading
|
|
|
|
LISTEN = "127.0.0.1" # loopback only: the relay must never put the TV's ADB port on the network
|
|
|
|
|
|
def split_host_port(endpoint):
|
|
"""Split host:port or [ipv6]:port into (host, port_or_none)."""
|
|
if endpoint.startswith("["):
|
|
end = endpoint.find("]")
|
|
if end != -1:
|
|
host = endpoint[1:end]
|
|
rest = endpoint[end + 1:]
|
|
if rest.startswith(":"):
|
|
return host, rest[1:]
|
|
return host, None
|
|
elif endpoint.count(":") == 1:
|
|
host, port = endpoint.split(":", 1)
|
|
return host, port
|
|
return endpoint, None
|
|
|
|
|
|
def pipe(src, dst):
|
|
"""Copy one direction until either side closes, then close both directions."""
|
|
try:
|
|
while True:
|
|
data = src.recv(65536)
|
|
if not data:
|
|
break
|
|
dst.sendall(data)
|
|
except OSError:
|
|
pass
|
|
finally:
|
|
for s in (src, dst):
|
|
try:
|
|
s.shutdown(socket.SHUT_RDWR)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def serve(srv, tv_host, tv_port):
|
|
while True:
|
|
client, _ = srv.accept()
|
|
try:
|
|
tv = socket.create_connection((tv_host, tv_port), timeout=10)
|
|
tv.settimeout(None)
|
|
except OSError as e:
|
|
print(f"cannot reach {tv_host}:{tv_port}: {e} (is the TV on, and network debugging still on?)",
|
|
flush=True)
|
|
client.close()
|
|
continue
|
|
for a, b in ((client, tv), (tv, client)):
|
|
threading.Thread(target=pipe, args=(a, b), daemon=True).start()
|
|
|
|
|
|
def address_problem(word):
|
|
"""Why this word cannot be the TV's address, as the sentence to print, or None.
|
|
|
|
The address is read out of a browser or copied off a page often enough that the two shapes it is
|
|
never in are worth naming. `http://<tv ip>` split into the host "http" and the port "//<tv ip>", so
|
|
the run answered "not a port number: '//<tv ip>'" - a sentence about a port fragment for a word that
|
|
is an address of another kind. `http://<tv ip>:5555` and `<tv ip>/6/system` became hosts literally
|
|
named for the URL and the page, and the relay started and printed where it forwards to, so the owner
|
|
was told it was up; only a client that connected was told "cannot reach ... (is the TV on, and network
|
|
debugging still on?)" - the TV, its standby and the network blamed for an argument this script cannot
|
|
use. Neither a scheme nor a path is ever part of an address, in brackets for IPv6 or not.
|
|
"""
|
|
word = word.strip()
|
|
if "://" in word:
|
|
return ("that is a URL, not the TV's address: give the IP (or IP:port) alone, e.g. "
|
|
"tools/venv/bin/python -I tools/relay.py <tv ip>")
|
|
if "/" in word:
|
|
return ("that is a path, not the TV's address: give the IP (or IP:port) alone, without the / or "
|
|
"the page it opens, e.g. tools/venv/bin/python -I tools/relay.py <tv ip>")
|
|
return None
|
|
|
|
|
|
def checked_port(p, flag, port, lowest):
|
|
"""A port this script can bind or connect to, or the parser's refusal: --port may be 0, the TV's port may not."""
|
|
if port is not None and not lowest <= port <= 65535:
|
|
hint = " (0 picks one)" if lowest == 0 else ""
|
|
p.error(f"{flag}: not a port number: {port!r}{hint}")
|
|
return port
|
|
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser(description="Forward 127.0.0.1:PORT to the TV's ADB port. See tools/relay.md.")
|
|
p.add_argument("tv_host", nargs="?", default=os.environ.get("TV_HOST"),
|
|
help="the TV's IP address, or IP:port (default: $TV_HOST)")
|
|
p.add_argument("--tv-port", type=int,
|
|
help="the TV's ADB port (default: the port in the address, else $TV_PORT, else 5555)")
|
|
p.add_argument("--port", type=int, default=15555, help="local port to listen on (default 15555; 0 picks one)")
|
|
args = p.parse_args()
|
|
if not args.tv_host:
|
|
p.error("give the TV's IP address, e.g. tools/venv/bin/python -I tools/relay.py 192.168.1.50")
|
|
# Before the port is read out of it, so a URL's "//<tv ip>" is not answered as a port fragment and a
|
|
# page's "/6/system" does not become the host the relay prints as where it forwards to. 2, like the
|
|
# other refusals here: nothing was bound and nothing was asked of the TV.
|
|
problem = address_problem(args.tv_host)
|
|
if problem:
|
|
p.error(problem)
|
|
# Both options are ports, and neither was checked: --port went to bind() as typed, and bind() refuses a
|
|
# port above 65535 (or a negative one) with an OverflowError - not an OSError, so the "cannot listen"
|
|
# message below never saw it - while --tv-port skipped the 1..65535 check the address and TV_PORT get a
|
|
# few lines further down. socket.create_connection cuts a servname to 16 bits, so "--tv-port 70000" printed
|
|
# "-> <tv>:70000" and forwarded to <tv>:4464, whatever service is there.
|
|
args.port = checked_port(p, "--port", args.port, 0)
|
|
args.tv_port = checked_port(p, "--tv-port", args.tv_port, 1)
|
|
# The same order as tv.py and adb-closed.py (#997). With 5555 fixed, a TV whose adbd has a port of its
|
|
# own (5877 on the Wanbo T6R Max) was relayed to 5555 whatever TV_HOST and TV_PORT said.
|
|
args.tv_host, endpoint_port = split_host_port(args.tv_host)
|
|
port_from_env = args.tv_port is None and not endpoint_port and bool(os.environ.get("TV_PORT"))
|
|
if args.tv_port is None:
|
|
port_str = endpoint_port or os.environ.get("TV_PORT") or "5555"
|
|
try:
|
|
args.tv_port = int(port_str)
|
|
if not (1 <= args.tv_port <= 65535):
|
|
raise ValueError
|
|
except ValueError:
|
|
p.error(f"not a port number: {port_str!r}")
|
|
# TV_HOST=127.0.0.1 TV_PORT=15555 exported for the tools, then the relay started in that same terminal: that
|
|
# TV_PORT is a relay's (this one's, or the one TV_HOST points at), not the TV's, and with the TV's IP given
|
|
# the relay would forward to IP:15555.
|
|
loopback = ("127.0.0.1", "localhost")
|
|
env_host = split_host_port(os.environ.get("TV_HOST", ""))[0]
|
|
if port_from_env and (args.tv_port == args.port or (env_host in loopback and args.tv_host not in loopback)):
|
|
p.error(f"TV_PORT={args.tv_port} is a relay's port, set for the tools: unset TV_HOST TV_PORT in this "
|
|
"terminal, or give the TV's port, e.g. tools/venv/bin/python -I tools/relay.py 192.168.1.50:5555")
|
|
if args.tv_host in loopback and args.tv_port == args.port:
|
|
p.error("that is the relay itself: give the TV's IP address")
|
|
|
|
srv = socket.socket()
|
|
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
try:
|
|
srv.bind((LISTEN, args.port))
|
|
except OSError as e:
|
|
sys.exit(f"cannot listen on {LISTEN}:{args.port}: {e}. Is a relay already running? Pick another --port.")
|
|
srv.listen(8)
|
|
port = srv.getsockname()[1]
|
|
print(f"relay {LISTEN}:{port} -> {args.tv_host}:{args.tv_port}", flush=True)
|
|
print(f"use TV_HOST={LISTEN} TV_PORT={port} with tools/tv.py and the other tools; Ctrl-C stops it", flush=True)
|
|
try:
|
|
serve(srv, args.tv_host, args.tv_port)
|
|
except KeyboardInterrupt:
|
|
print("relay stopped", flush=True)
|
|
finally:
|
|
srv.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|