akashcli 4.5.2__tar.gz → 4.5.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {akashcli-4.5.2 → akashcli-4.5.4}/PKG-INFO +2 -1
- akashcli-4.5.4/akashcli/_net.py +177 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/client.py +4 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/exec.py +19 -14
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/relay.py +17 -29
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli.egg-info/PKG-INFO +2 -1
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli.egg-info/SOURCES.txt +1 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli.egg-info/requires.txt +1 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/pyproject.toml +2 -1
- {akashcli-4.5.2 → akashcli-4.5.4}/README.md +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/__init__.py +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/auth.py +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/cli.py +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/config.py +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/crypto.py +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/env_manager.py +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/exceptions.py +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/integrity.py +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/logger.py +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/models.py +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/p2p.py +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/packages.py +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/restore.py +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/snapshot_crypto.py +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/storage.py +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/terminal.py +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/uploader.py +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/utils.py +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli.egg-info/dependency_links.txt +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli.egg-info/entry_points.txt +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/akashcli.egg-info/top_level.txt +0 -0
- {akashcli-4.5.2 → akashcli-4.5.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: akashcli
|
|
3
|
-
Version: 4.5.
|
|
3
|
+
Version: 4.5.4
|
|
4
4
|
Summary: Akash CLI — secure remote shell relay, file vault, and device management
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: akash,cli,relay,remote-shell,vault
|
|
@@ -12,6 +12,7 @@ Classifier: Topic :: Utilities
|
|
|
12
12
|
Requires-Python: >=3.9
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
Requires-Dist: httpx
|
|
15
|
+
Requires-Dist: certifi
|
|
15
16
|
Requires-Dist: typer
|
|
16
17
|
Requires-Dist: cryptography
|
|
17
18
|
Requires-Dist: pydantic
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
_net.py — Network helpers for akashcli.
|
|
3
|
+
|
|
4
|
+
On Android/Termux the system DNS daemon (netd) may cache stale IP records
|
|
5
|
+
for the server long after they have changed. curl uses Android's native
|
|
6
|
+
networking APIs and sees fresh records; Python's socket.getaddrinfo() goes
|
|
7
|
+
through netd and gets the old IP, causing all connections to time out.
|
|
8
|
+
|
|
9
|
+
This module provides:
|
|
10
|
+
• doh_resolve(host) — resolve a hostname via Google DoH using curl subprocess
|
|
11
|
+
• patch_dns(host) — globally patch socket.create_connection for one host
|
|
12
|
+
• ws_connect(url) — websockets.sync.client.connect() with DoH pre-resolution
|
|
13
|
+
• ssl_context() — SSLContext using certifi CA bundle
|
|
14
|
+
|
|
15
|
+
All functions are safe to call on non-Android systems (they simply do nothing
|
|
16
|
+
extra).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json as _json
|
|
22
|
+
import shutil as _sh
|
|
23
|
+
import socket as _socket
|
|
24
|
+
import ssl as _ssl
|
|
25
|
+
import subprocess as _sp
|
|
26
|
+
from typing import Optional
|
|
27
|
+
from urllib.parse import urlparse as _urlparse
|
|
28
|
+
|
|
29
|
+
# ── per-host DoH cache (lives for the process lifetime) ──────────────────────
|
|
30
|
+
_DOH_CACHE: dict[str, str] = {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def doh_resolve(host: str) -> Optional[str]:
|
|
34
|
+
"""
|
|
35
|
+
Resolve *host* via DNS-over-HTTPS (Google) using a curl subprocess.
|
|
36
|
+
Returns the first A-record IP string, or None on any failure.
|
|
37
|
+
|
|
38
|
+
Uses curl because it goes through Android's native networking stack and
|
|
39
|
+
therefore sees up-to-date DNS records, unlike Python's socket.getaddrinfo
|
|
40
|
+
which may hit Android's netd stale cache.
|
|
41
|
+
"""
|
|
42
|
+
if host in _DOH_CACHE:
|
|
43
|
+
return _DOH_CACHE[host]
|
|
44
|
+
|
|
45
|
+
curl = _sh.which("curl")
|
|
46
|
+
if not curl:
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
r = _sp.run(
|
|
50
|
+
[curl, "-sk", "--max-time", "8",
|
|
51
|
+
f"https://dns.google/resolve?name={host}&type=A"],
|
|
52
|
+
capture_output=True, timeout=10,
|
|
53
|
+
)
|
|
54
|
+
data = _json.loads(r.stdout)
|
|
55
|
+
answers = [a["data"] for a in data.get("Answer", [])
|
|
56
|
+
if a.get("type") == 1] # type 1 == A record
|
|
57
|
+
if answers:
|
|
58
|
+
_DOH_CACHE[host] = answers[0]
|
|
59
|
+
return answers[0]
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def ssl_context() -> _ssl.SSLContext:
|
|
66
|
+
"""Return an SSLContext backed by certifi's CA bundle (if installed)."""
|
|
67
|
+
try:
|
|
68
|
+
import certifi as _cf
|
|
69
|
+
return _ssl.create_default_context(cafile=_cf.where())
|
|
70
|
+
except ImportError:
|
|
71
|
+
return _ssl.create_default_context()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def make_socket(host: str, port: int, timeout: int = 15) -> _socket.socket:
|
|
75
|
+
"""
|
|
76
|
+
Open a TCP socket to host:port.
|
|
77
|
+
If DoH returns a different IP than what getaddrinfo would return, use the
|
|
78
|
+
DoH IP with the host as SNI (caller must wrap TLS themselves).
|
|
79
|
+
Falls back to normal getaddrinfo on any failure.
|
|
80
|
+
"""
|
|
81
|
+
ip = doh_resolve(host)
|
|
82
|
+
if ip:
|
|
83
|
+
try:
|
|
84
|
+
return _socket.create_connection((ip, port), timeout=timeout)
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
return _socket.create_connection((host, port), timeout=timeout)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def ws_connect(url: str, **kwargs):
|
|
91
|
+
"""
|
|
92
|
+
websockets.sync.client.connect() with DoH-resolved IP.
|
|
93
|
+
Pass open_timeout / close_timeout etc. as keyword arguments as normal.
|
|
94
|
+
The special kwarg _sock_timeout (int, default 15) controls the TCP connect
|
|
95
|
+
timeout before the WebSocket handshake.
|
|
96
|
+
"""
|
|
97
|
+
import websockets.sync.client as _wsc
|
|
98
|
+
|
|
99
|
+
parsed = _urlparse(url)
|
|
100
|
+
host = parsed.hostname or ""
|
|
101
|
+
port = parsed.port or (443 if parsed.scheme == "wss" else 80)
|
|
102
|
+
use_tls = parsed.scheme == "wss"
|
|
103
|
+
|
|
104
|
+
sock_timeout = kwargs.pop("_sock_timeout", 15)
|
|
105
|
+
sock = make_socket(host, port, timeout=sock_timeout)
|
|
106
|
+
|
|
107
|
+
if use_tls:
|
|
108
|
+
return _wsc.connect(url, sock=sock, ssl=ssl_context(), **kwargs)
|
|
109
|
+
return _wsc.connect(url, sock=sock, **kwargs)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def http_post(url: str, *, timeout: int = 10, **kwargs) -> dict:
|
|
113
|
+
"""
|
|
114
|
+
POST *url* and return the parsed JSON response dict.
|
|
115
|
+
Uses curl subprocess (bypasses Android stale DNS) and falls back to
|
|
116
|
+
urllib with a DoH-patched socket on failure.
|
|
117
|
+
"""
|
|
118
|
+
parsed = _urlparse(url)
|
|
119
|
+
host = parsed.hostname or ""
|
|
120
|
+
real_ip = doh_resolve(host)
|
|
121
|
+
|
|
122
|
+
# ── 1. curl subprocess ────────────────────────────────────────────────────
|
|
123
|
+
curl = _sh.which("curl")
|
|
124
|
+
if curl:
|
|
125
|
+
cmd = [curl, "-sk", "--max-time", str(timeout), "-X", "POST"]
|
|
126
|
+
if real_ip:
|
|
127
|
+
cmd += ["--resolve", f"{host}:443:{real_ip}"]
|
|
128
|
+
cmd.append(url)
|
|
129
|
+
try:
|
|
130
|
+
r = _sp.run(cmd, capture_output=True, timeout=timeout + 5)
|
|
131
|
+
if r.returncode == 0 and r.stdout:
|
|
132
|
+
return _json.loads(r.stdout)
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
# ── 2. urllib with monkey-patched socket ──────────────────────────────────
|
|
137
|
+
import urllib.request as _ur
|
|
138
|
+
from urllib.request import HTTPSHandler as _HH
|
|
139
|
+
|
|
140
|
+
ctx = ssl_context()
|
|
141
|
+
|
|
142
|
+
if real_ip:
|
|
143
|
+
_orig = _socket.create_connection
|
|
144
|
+
def _patched(addr, *a, **kw):
|
|
145
|
+
h, p = addr
|
|
146
|
+
return _orig((real_ip, p) if h == host else addr, *a, **kw)
|
|
147
|
+
_socket.create_connection = _patched
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
opener = _ur.build_opener(_HH(context=ctx))
|
|
151
|
+
req = _ur.Request(url, data=b"", method="POST")
|
|
152
|
+
with opener.open(req, timeout=timeout) as resp:
|
|
153
|
+
return _json.loads(resp.read())
|
|
154
|
+
finally:
|
|
155
|
+
if real_ip:
|
|
156
|
+
_socket.create_connection = _orig # type: ignore[possibly-undefined]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def patch_dns_globally(host: str) -> None:
|
|
160
|
+
"""
|
|
161
|
+
Permanently patch socket.create_connection so that connections to *host*
|
|
162
|
+
use the DoH-resolved IP. Safe to call multiple times.
|
|
163
|
+
Intended to be called once at CLI startup for the configured server host.
|
|
164
|
+
"""
|
|
165
|
+
ip = doh_resolve(host)
|
|
166
|
+
if not ip:
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
_orig = getattr(_socket, "_akash_orig_create", _socket.create_connection)
|
|
170
|
+
_socket._akash_orig_create = _orig # type: ignore[attr-defined]
|
|
171
|
+
|
|
172
|
+
def _patched(addr, *args, **kwargs):
|
|
173
|
+
h, p = addr
|
|
174
|
+
real = ip if h == host else None
|
|
175
|
+
return _orig((real or h, p), *args, **kwargs)
|
|
176
|
+
|
|
177
|
+
_socket.create_connection = _patched
|
|
@@ -4,16 +4,20 @@ from typing import List, Optional
|
|
|
4
4
|
from .models import UploadResult, FileInfo, UserInfo
|
|
5
5
|
from .exceptions import *
|
|
6
6
|
from .config import ConfigManager
|
|
7
|
+
from ._net import patch_dns_globally as _patch_dns
|
|
7
8
|
|
|
8
9
|
class AkashClient:
|
|
9
10
|
def __init__(self, api_key: Optional[str] = None, api_url: Optional[str] = None):
|
|
10
11
|
import os as _os
|
|
12
|
+
from urllib.parse import urlparse as _up
|
|
11
13
|
config = ConfigManager.load()
|
|
12
14
|
# Env vars override everything — useful when hardware-ID encryption differs across su sessions
|
|
13
15
|
self.api_key = api_key or _os.getenv("AKASH_API_KEY") or config.get("api_key")
|
|
14
16
|
self.base_url = (
|
|
15
17
|
api_url or _os.getenv("AKASH_API_URL") or config.get("api_url", "https://jstore.2bd.net")
|
|
16
18
|
).rstrip("/")
|
|
19
|
+
# Patch socket DNS to bypass Android stale DNS cache
|
|
20
|
+
_patch_dns(_up(self.base_url).hostname or "")
|
|
17
21
|
|
|
18
22
|
if not self.api_key:
|
|
19
23
|
raise AuthenticationError("No API Key found. Run 'akash login' or set AKASH_API_KEY env var.")
|
|
@@ -48,6 +48,7 @@ from pathlib import Path
|
|
|
48
48
|
from typing import Optional
|
|
49
49
|
|
|
50
50
|
import httpx
|
|
51
|
+
from ._net import patch_dns_globally as _patch_dns, ws_connect as _ws_connect_net
|
|
51
52
|
|
|
52
53
|
# ── Paths & constants ────────────────────────────────────────────────────────
|
|
53
54
|
|
|
@@ -241,6 +242,10 @@ def acquire_exec_token(api_key: str, base_url: str, controller: bool = False) ->
|
|
|
241
242
|
connect / run / shell). Just authenticates as owner.
|
|
242
243
|
controller=False — full challenge/sign/enroll flow (used by akash serve).
|
|
243
244
|
"""
|
|
245
|
+
# Patch socket DNS for this host — bypasses Android stale DNS cache
|
|
246
|
+
from urllib.parse import urlparse as _up
|
|
247
|
+
_patch_dns(_up(base_url).hostname or "")
|
|
248
|
+
|
|
244
249
|
token = _load_cached_token(base_url)
|
|
245
250
|
if token:
|
|
246
251
|
return token
|
|
@@ -327,11 +332,11 @@ def kill_session(token: str, base_url: str, session_id: str) -> None:
|
|
|
327
332
|
|
|
328
333
|
def run_command(token: str, base_url: str, command: str,
|
|
329
334
|
env: Optional[dict] = None) -> int:
|
|
330
|
-
|
|
335
|
+
|
|
331
336
|
ws_url = _build_ws_url(base_url, token)
|
|
332
337
|
exit_code = 1
|
|
333
338
|
try:
|
|
334
|
-
with
|
|
339
|
+
with _ws_connect_net(ws_url, open_timeout=15, close_timeout=10) as ws:
|
|
335
340
|
ws.send(json.dumps({"type": "run", "command": command, "env": env or {}}))
|
|
336
341
|
while True:
|
|
337
342
|
try:
|
|
@@ -367,7 +372,7 @@ def run_shell(token: str, base_url: str) -> int:
|
|
|
367
372
|
Commands run in the same shell process — cd, export, and variables persist.
|
|
368
373
|
Output streams live with ANSI colours passed through.
|
|
369
374
|
"""
|
|
370
|
-
|
|
375
|
+
|
|
371
376
|
|
|
372
377
|
_load_shell_history()
|
|
373
378
|
|
|
@@ -557,7 +562,7 @@ def _run_tui(command: str, token: str, base_url: str,
|
|
|
557
562
|
"""
|
|
558
563
|
import select as _sel
|
|
559
564
|
import signal
|
|
560
|
-
|
|
565
|
+
|
|
561
566
|
|
|
562
567
|
ws_url = _build_ws_url(base_url, token)
|
|
563
568
|
rows, cols = _get_terminal_size()
|
|
@@ -586,7 +591,7 @@ def _run_tui(command: str, token: str, base_url: str,
|
|
|
586
591
|
exit_code = 1
|
|
587
592
|
|
|
588
593
|
try:
|
|
589
|
-
with
|
|
594
|
+
with _ws_connect_net(ws_url, open_timeout=15, close_timeout=10) as ws:
|
|
590
595
|
# Send PTY handshake
|
|
591
596
|
ws.send(json.dumps({
|
|
592
597
|
"type": "pty_run",
|
|
@@ -702,7 +707,7 @@ def _exec_remote(command: str, token: str, base_url: str,
|
|
|
702
707
|
first_output = True
|
|
703
708
|
|
|
704
709
|
try:
|
|
705
|
-
with
|
|
710
|
+
with _ws_connect_net(ws_url, open_timeout=15, close_timeout=15) as ws:
|
|
706
711
|
ws.send(json.dumps({
|
|
707
712
|
"type": "run",
|
|
708
713
|
"command": command,
|
|
@@ -754,9 +759,9 @@ def _exec_remote(command: str, token: str, base_url: str,
|
|
|
754
759
|
def _fetch_cwd(token: str, base_url: str, session_id: Optional[str]) -> str:
|
|
755
760
|
"""Ask the server for the current working directory."""
|
|
756
761
|
try:
|
|
757
|
-
|
|
762
|
+
|
|
758
763
|
ws_url = _build_ws_url(base_url, token)
|
|
759
|
-
with
|
|
764
|
+
with _ws_connect_net(ws_url, open_timeout=8, close_timeout=5) as ws:
|
|
760
765
|
ws.send(json.dumps({
|
|
761
766
|
"type": "run", "command": "pwd",
|
|
762
767
|
"session_id": session_id, "track_cwd": True,
|
|
@@ -836,7 +841,7 @@ def _cmd_session(token: str, base_url: str, session_id: Optional[str],
|
|
|
836
841
|
|
|
837
842
|
def _cmd_status(token: str, base_url: str, session_id: Optional[str]) -> int:
|
|
838
843
|
# Run a compound command to fetch server stats
|
|
839
|
-
|
|
844
|
+
|
|
840
845
|
cmd = (
|
|
841
846
|
r'printf "cwd=%s\npid=%s\nload=%s\nuptime=%s\nshell=%s\nuser=%s\nhostname=%s\n" '
|
|
842
847
|
r'"$(pwd)" "$$" "$(cut -d" " -f1-3 /proc/loadavg 2>/dev/null || uptime | grep -oE "[0-9.]+, [0-9.]+, [0-9.]+")" '
|
|
@@ -846,7 +851,7 @@ def _cmd_status(token: str, base_url: str, session_id: Optional[str]) -> int:
|
|
|
846
851
|
ws_url = _build_ws_url(base_url, token)
|
|
847
852
|
output = b""
|
|
848
853
|
try:
|
|
849
|
-
with
|
|
854
|
+
with _ws_connect_net(ws_url, open_timeout=8, close_timeout=5) as ws:
|
|
850
855
|
ws.send(json.dumps({"type": "run", "command": cmd, "session_id": session_id}))
|
|
851
856
|
while True:
|
|
852
857
|
raw = ws.recv()
|
|
@@ -883,12 +888,12 @@ def _cmd_status(token: str, base_url: str, session_id: Optional[str]) -> int:
|
|
|
883
888
|
|
|
884
889
|
|
|
885
890
|
def _cmd_env(token: str, base_url: str, session_id: Optional[str]) -> int:
|
|
886
|
-
|
|
891
|
+
|
|
887
892
|
ws_url = _build_ws_url(base_url, token)
|
|
888
893
|
output = b""
|
|
889
894
|
ec = 0
|
|
890
895
|
try:
|
|
891
|
-
with
|
|
896
|
+
with _ws_connect_net(ws_url, open_timeout=8, close_timeout=5) as ws:
|
|
892
897
|
ws.send(json.dumps({"type": "run", "command": "env | sort", "session_id": session_id}))
|
|
893
898
|
while True:
|
|
894
899
|
raw = ws.recv()
|
|
@@ -917,12 +922,12 @@ def _cmd_export(kv: str, token: str, base_url: str, session_id: Optional[str]) -
|
|
|
917
922
|
if not kv or "=" not in kv:
|
|
918
923
|
_print_err("Usage: :export KEY=VALUE")
|
|
919
924
|
return 1
|
|
920
|
-
|
|
925
|
+
|
|
921
926
|
ws_url = _build_ws_url(base_url, token)
|
|
922
927
|
cmd = f"export {kv}"
|
|
923
928
|
ec = 0
|
|
924
929
|
try:
|
|
925
|
-
with
|
|
930
|
+
with _ws_connect_net(ws_url, open_timeout=8, close_timeout=5) as ws:
|
|
926
931
|
ws.send(json.dumps({"type": "run", "command": cmd, "session_id": session_id}))
|
|
927
932
|
while True:
|
|
928
933
|
raw = ws.recv()
|
|
@@ -48,6 +48,16 @@ _SERVE_PID = _AKASH_DIR / "serve.pid"
|
|
|
48
48
|
_SERVE_LOG = _AKASH_DIR / "serve.log"
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
# ── Network helpers (bypass Android stale DNS) ────────────────────────────────
|
|
52
|
+
from ._net import (
|
|
53
|
+
doh_resolve as _doh_resolve,
|
|
54
|
+
make_socket as _make_socket,
|
|
55
|
+
ssl_context as _ssl_context,
|
|
56
|
+
ws_connect as _ws_connect,
|
|
57
|
+
http_post as _http_post,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
51
61
|
# ── WS URL builder ────────────────────────────────────────────────────────────
|
|
52
62
|
|
|
53
63
|
def _ws_base(http_url: str) -> str:
|
|
@@ -85,14 +95,12 @@ def serve(api_key: str, base_url: str, label: str, daemon: bool = False) -> None
|
|
|
85
95
|
print(f" {_G}◉{_RS} akash serve {_DM}label={label}{_RS}")
|
|
86
96
|
print(f" {_DM}connecting to relay…{_RS}")
|
|
87
97
|
|
|
88
|
-
import websockets.sync.client as _ws
|
|
89
|
-
|
|
90
98
|
while True: # reconnect loop
|
|
91
99
|
try:
|
|
92
100
|
token = acquire_exec_token(api_key, base_url)
|
|
93
101
|
ws_url = _listen_url(base_url, token)
|
|
94
102
|
|
|
95
|
-
with
|
|
103
|
+
with _ws_connect(ws_url, open_timeout=15, close_timeout=5) as ws:
|
|
96
104
|
# Send listen handshake
|
|
97
105
|
ws.send(json.dumps({"type": "listen", "label": label}))
|
|
98
106
|
# Wait for server ack
|
|
@@ -295,13 +303,12 @@ def connect_run(api_key: str, base_url: str, command: str,
|
|
|
295
303
|
Returns the remote exit code.
|
|
296
304
|
"""
|
|
297
305
|
from .exec import acquire_exec_token
|
|
298
|
-
import websockets.sync.client as _ws
|
|
299
306
|
|
|
300
307
|
token = acquire_exec_token(api_key, base_url, controller=True)
|
|
301
308
|
ws_url = _connect_url(base_url, token, target)
|
|
302
309
|
exit_code = 1
|
|
303
310
|
|
|
304
|
-
with
|
|
311
|
+
with _ws_connect(ws_url, open_timeout=15, close_timeout=10) as ws:
|
|
305
312
|
# Wait for tunnel confirmation
|
|
306
313
|
ack = json.loads(ws.recv())
|
|
307
314
|
if ack.get("type") == "error":
|
|
@@ -345,7 +352,6 @@ def connect_shell(api_key: str, base_url: str,
|
|
|
345
352
|
Returns the remote exit code.
|
|
346
353
|
"""
|
|
347
354
|
from .exec import acquire_exec_token, _get_terminal_size
|
|
348
|
-
import websockets.sync.client as _ws
|
|
349
355
|
import select as _sel, signal
|
|
350
356
|
|
|
351
357
|
token = acquire_exec_token(api_key, base_url, controller=True)
|
|
@@ -376,7 +382,7 @@ def connect_shell(api_key: str, base_url: str,
|
|
|
376
382
|
exit_code = 1
|
|
377
383
|
|
|
378
384
|
try:
|
|
379
|
-
with
|
|
385
|
+
with _ws_connect(ws_url, open_timeout=15, close_timeout=10) as ws:
|
|
380
386
|
# Wait for tunnel open
|
|
381
387
|
ack = json.loads(ws.recv())
|
|
382
388
|
if ack.get("type") == "error":
|
|
@@ -558,26 +564,9 @@ def guest_serve(base_url: str) -> None:
|
|
|
558
564
|
Security: every incoming connection shows the guest IP + fingerprint
|
|
559
565
|
and asks the host to approve before the shell is bridged.
|
|
560
566
|
"""
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
# 1. Get a pairing code — try httpx first, fall back to urllib (system certs)
|
|
564
|
-
def _post_code(url: str) -> dict:
|
|
565
|
-
try:
|
|
566
|
-
import httpx as _hx
|
|
567
|
-
r = _hx.post(url, timeout=10)
|
|
568
|
-
r.raise_for_status()
|
|
569
|
-
return r.json()
|
|
570
|
-
except Exception:
|
|
571
|
-
pass
|
|
572
|
-
# urllib fallback — uses system CA bundle, works on Android/Termux
|
|
573
|
-
import urllib.request as _ur, urllib.error as _ue
|
|
574
|
-
req = _ur.Request(url, data=b"", method="POST")
|
|
575
|
-
with _ur.urlopen(req, timeout=10) as resp:
|
|
576
|
-
import json as _j
|
|
577
|
-
return _j.loads(resp.read())
|
|
578
|
-
|
|
567
|
+
# 1. Get a pairing code using DoH-aware HTTP POST (bypasses Android stale DNS)
|
|
579
568
|
try:
|
|
580
|
-
data =
|
|
569
|
+
data = _http_post(_guest_code_url(base_url))
|
|
581
570
|
code = data["code"]
|
|
582
571
|
ttl = data.get("expires_in", 600)
|
|
583
572
|
except Exception as e:
|
|
@@ -600,7 +589,7 @@ def guest_serve(base_url: str) -> None:
|
|
|
600
589
|
while True:
|
|
601
590
|
try:
|
|
602
591
|
ws_url = _guest_host_url(base_url, code)
|
|
603
|
-
with
|
|
592
|
+
with _ws_connect(ws_url, open_timeout=15, close_timeout=5) as ws:
|
|
604
593
|
ack = json.loads(ws.recv())
|
|
605
594
|
if ack.get("type") == "error":
|
|
606
595
|
print(f" {_R}✗{_RS} {ack.get('message')}", file=sys.stderr)
|
|
@@ -671,7 +660,6 @@ def guest_connect_shell(base_url: str, code: str) -> int:
|
|
|
671
660
|
then opens a full PTY shell if approved.
|
|
672
661
|
Returns the remote exit code.
|
|
673
662
|
"""
|
|
674
|
-
import websockets.sync.client as _ws
|
|
675
663
|
import select as _sel, signal
|
|
676
664
|
|
|
677
665
|
code = code.strip().upper()
|
|
@@ -702,7 +690,7 @@ def guest_connect_shell(base_url: str, code: str) -> int:
|
|
|
702
690
|
|
|
703
691
|
exit_code = 1
|
|
704
692
|
try:
|
|
705
|
-
with
|
|
693
|
+
with _ws_connect(ws_url, open_timeout=15, close_timeout=10) as ws:
|
|
706
694
|
# First frame: could be error, waiting_approval, or connected
|
|
707
695
|
first = json.loads(ws.recv())
|
|
708
696
|
ftype = first.get("type")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: akashcli
|
|
3
|
-
Version: 4.5.
|
|
3
|
+
Version: 4.5.4
|
|
4
4
|
Summary: Akash CLI — secure remote shell relay, file vault, and device management
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: akash,cli,relay,remote-shell,vault
|
|
@@ -12,6 +12,7 @@ Classifier: Topic :: Utilities
|
|
|
12
12
|
Requires-Python: >=3.9
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
Requires-Dist: httpx
|
|
15
|
+
Requires-Dist: certifi
|
|
15
16
|
Requires-Dist: typer
|
|
16
17
|
Requires-Dist: cryptography
|
|
17
18
|
Requires-Dist: pydantic
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "akashcli"
|
|
3
|
-
version = "4.5.
|
|
3
|
+
version = "4.5.4"
|
|
4
4
|
description = "Akash CLI — secure remote shell relay, file vault, and device management"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.9"
|
|
@@ -15,6 +15,7 @@ classifiers = [
|
|
|
15
15
|
]
|
|
16
16
|
dependencies = [
|
|
17
17
|
"httpx",
|
|
18
|
+
"certifi",
|
|
18
19
|
"typer",
|
|
19
20
|
"cryptography",
|
|
20
21
|
"pydantic",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|