akashcli 4.5.0__tar.gz → 4.5.2__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.
Files changed (31) hide show
  1. {akashcli-4.5.0 → akashcli-4.5.2}/PKG-INFO +1 -1
  2. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/relay.py +116 -17
  3. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli.egg-info/PKG-INFO +1 -1
  4. {akashcli-4.5.0 → akashcli-4.5.2}/pyproject.toml +1 -1
  5. {akashcli-4.5.0 → akashcli-4.5.2}/README.md +0 -0
  6. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/__init__.py +0 -0
  7. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/auth.py +0 -0
  8. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/cli.py +0 -0
  9. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/client.py +0 -0
  10. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/config.py +0 -0
  11. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/crypto.py +0 -0
  12. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/env_manager.py +0 -0
  13. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/exceptions.py +0 -0
  14. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/exec.py +0 -0
  15. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/integrity.py +0 -0
  16. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/logger.py +0 -0
  17. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/models.py +0 -0
  18. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/p2p.py +0 -0
  19. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/packages.py +0 -0
  20. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/restore.py +0 -0
  21. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/snapshot_crypto.py +0 -0
  22. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/storage.py +0 -0
  23. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/terminal.py +0 -0
  24. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/uploader.py +0 -0
  25. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli/utils.py +0 -0
  26. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli.egg-info/SOURCES.txt +0 -0
  27. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli.egg-info/dependency_links.txt +0 -0
  28. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli.egg-info/entry_points.txt +0 -0
  29. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli.egg-info/requires.txt +0 -0
  30. {akashcli-4.5.0 → akashcli-4.5.2}/akashcli.egg-info/top_level.txt +0 -0
  31. {akashcli-4.5.0 → akashcli-4.5.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.5.0
3
+ Version: 4.5.2
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
@@ -554,32 +554,49 @@ def guest_serve(base_url: str) -> None:
554
554
  Host a shell session with no API key.
555
555
  Requests a pairing code from the server, prints it, then waits for
556
556
  someone to connect with: akash shell --code <CODE>
557
+
558
+ Security: every incoming connection shows the guest IP + fingerprint
559
+ and asks the host to approve before the shell is bridged.
557
560
  """
558
- import httpx
559
561
  import websockets.sync.client as _ws
560
562
 
561
- # 1. Get a pairing code
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
+
562
579
  try:
563
- r = httpx.post(_guest_code_url(base_url), timeout=10)
564
- r.raise_for_status()
565
- code = r.json()["code"]
566
- ttl = r.json().get("expires_in", 600)
580
+ data = _post_code(_guest_code_url(base_url))
581
+ code = data["code"]
582
+ ttl = data.get("expires_in", 600)
567
583
  except Exception as e:
568
584
  print(f" {_R}✗{_RS} could not get pairing code: {e}", file=sys.stderr)
585
+ print(f" {_DM}Make sure you can reach {base_url}{_RS}", file=sys.stderr)
569
586
  return
570
587
 
571
588
  print()
572
589
  print(f" {_G}◉{_RS} guest relay ready")
573
590
  print()
574
- print(f" Your pairing code: {_C}{_Y}{code}{_RS}")
591
+ print(f" {_DM}Pairing code:{_RS} {_C}{code}{_RS}")
575
592
  print()
576
- print(f" {_DM}On another machine, run:{_RS}")
577
- print(f" {_C}akash shell --code {code}{_RS}")
593
+ print(f" {_DM}On another machine:{_RS} {_C}akash shell --code {code}{_RS}")
578
594
  print()
579
595
  print(f" {_DM}Code expires in {ttl // 60} min · Ctrl-C to stop{_RS}")
596
+ print(f" {_DM}You will be asked to approve each connection before it gets shell access.{_RS}")
580
597
  print()
581
598
 
582
- # 2. Connect to host endpoint and serve shell frames
599
+ # 2. Connect to host endpoint — handle knock/allow/deny then serve shell
583
600
  while True:
584
601
  try:
585
602
  ws_url = _guest_host_url(base_url, code)
@@ -588,20 +605,70 @@ def guest_serve(base_url: str) -> None:
588
605
  if ack.get("type") == "error":
589
606
  print(f" {_R}✗{_RS} {ack.get('message')}", file=sys.stderr)
590
607
  return
591
- print(f" {_DM}waiting for connection…{_RS}")
592
- _run_relay_device(ws, f"guest:{code}")
593
- print(f"\n {_DM}session ended — waiting for next connection…{_RS}\n")
608
+
609
+ print(f" {_DM}waiting for connection…{_RS}", flush=True)
610
+
611
+ # Wait for either a knock (someone wants in) or a ping
612
+ while True:
613
+ try:
614
+ raw = ws.recv(timeout=35)
615
+ except Exception:
616
+ # Timeout or disconnect — break to reconnect loop
617
+ break
618
+ frame = json.loads(raw)
619
+ ftype = frame.get("type")
620
+
621
+ if ftype == "ping":
622
+ ws.send(json.dumps({"type": "pong"}))
623
+ continue
624
+
625
+ if ftype == "knock":
626
+ guest_ip = frame.get("ip", "unknown")
627
+ fp = frame.get("fingerprint", "????????")
628
+ print()
629
+ print(f" {_Y}⚡{_RS} Incoming connection request")
630
+ print(f" {_DM}From:{_RS} {_C}{guest_ip}{_RS}")
631
+ print(f" {_DM}Fingerprint:{_RS} {_C}{fp}{_RS}")
632
+ print()
633
+
634
+ # Prompt host — default is N (deny)
635
+ try:
636
+ sys.stdout.write(f" Allow? [y/N]: ")
637
+ sys.stdout.flush()
638
+ answer = sys.stdin.readline().strip().lower()
639
+ except (EOFError, KeyboardInterrupt):
640
+ answer = "n"
641
+
642
+ if answer == "y":
643
+ ws.send(json.dumps({"type": "allow"}))
644
+ print(f" {_G}✓{_RS} connection allowed — starting shell…")
645
+ print()
646
+ # Now serve shell frames
647
+ _run_relay_device(ws, f"guest:{code}")
648
+ print(f"\n {_DM}session ended — waiting for next connection…{_RS}\n")
649
+ else:
650
+ ws.send(json.dumps({"type": "deny"}))
651
+ print(f" {_R}✗{_RS} connection denied")
652
+ print()
653
+ # Loop back — still waiting on same code
654
+ continue
655
+
656
+ # Any other frame — ignore and keep waiting
657
+ continue
658
+
594
659
  except KeyboardInterrupt:
595
660
  print(f"\n {_DM}guest serve stopped{_RS}")
596
661
  break
597
662
  except Exception as exc:
598
- print(f" {_Y}!{_RS} relay disconnected ({exc}) — reconnecting…")
663
+ print(f" {_Y}!{_RS} relay disconnected ({exc}) — reconnecting in 3s…")
599
664
  time.sleep(3)
600
665
 
601
666
 
602
667
  def guest_connect_shell(base_url: str, code: str) -> int:
603
668
  """
604
669
  Connect to a guest host by pairing code — no API key needed.
670
+ Handles the approval handshake: shows a spinner while the host decides,
671
+ then opens a full PTY shell if approved.
605
672
  Returns the remote exit code.
606
673
  """
607
674
  import websockets.sync.client as _ws
@@ -636,10 +703,42 @@ def guest_connect_shell(base_url: str, code: str) -> int:
636
703
  exit_code = 1
637
704
  try:
638
705
  with _ws.connect(ws_url, open_timeout=15, close_timeout=10) as ws:
639
- ack = json.loads(ws.recv())
640
- if ack.get("type") == "error":
706
+ # First frame: could be error, waiting_approval, or connected
707
+ first = json.loads(ws.recv())
708
+ ftype = first.get("type")
709
+
710
+ if ftype == "error":
641
711
  _restore()
642
- raise RuntimeError(ack.get("message", "relay error"))
712
+ raise RuntimeError(first.get("message", "relay error"))
713
+
714
+ if ftype == "waiting_approval":
715
+ fp = first.get("fingerprint", "")
716
+ _restore() # restore terminal so we can print cleanly
717
+ print(f" {_Y}…{_RS} waiting for host to approve this connection…")
718
+ if fp:
719
+ print(f" {_DM}Your fingerprint: {_C}{fp}{_RS}")
720
+ print(f" {_DM}(host has 30 seconds to respond){_RS}")
721
+ print()
722
+ # Wait for next frame: connected or denied
723
+ try:
724
+ second = json.loads(ws.recv(timeout=35))
725
+ except Exception:
726
+ raise RuntimeError("Timed out waiting for host approval.")
727
+ if second.get("type") == "denied":
728
+ raise RuntimeError("Host declined the connection.")
729
+ if second.get("type") != "connected":
730
+ raise RuntimeError(second.get("message", "unexpected response from host"))
731
+ # Re-enter raw mode for the PTY session
732
+ try:
733
+ import tty as _tty2, termios as _term2
734
+ _old_settings = _term2.tcgetattr(_stdin_fd)
735
+ _tty2.setraw(_stdin_fd)
736
+ _raw_ok = True
737
+ except Exception:
738
+ pass
739
+ elif ftype != "connected":
740
+ _restore()
741
+ raise RuntimeError(first.get("message", "unexpected server response"))
643
742
 
644
743
  ws.send(json.dumps({
645
744
  "type": "pty_run",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.5.0
3
+ Version: 4.5.2
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "akashcli"
3
- version = "4.5.0"
3
+ version = "4.5.2"
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"
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