Revert "Merge pull request #239 from manaflow-ai/issue-151-ssh-remote-port-proxying"
This reverts commit78e4bd32ba, reversing changes made tocf75da8f8a.
This commit is contained in:
parent
78e4bd32ba
commit
f7cbbad434
60 changed files with 1250 additions and 17140 deletions
20
tests/fixtures/ssh-remote/Dockerfile
vendored
20
tests/fixtures/ssh-remote/Dockerfile
vendored
|
|
@ -1,20 +0,0 @@
|
|||
FROM alpine:3.20
|
||||
|
||||
RUN apk add --no-cache openssh python3 iproute2 net-tools ncurses
|
||||
|
||||
RUN adduser -D -s /bin/sh dev \
|
||||
&& mkdir -p /home/dev/.ssh /run/sshd /srv/www \
|
||||
&& chown -R dev:dev /home/dev/.ssh \
|
||||
&& chmod 700 /home/dev/.ssh \
|
||||
&& echo "cmux-ssh-forward-ok" > /srv/www/index.html
|
||||
|
||||
RUN ssh-keygen -A
|
||||
|
||||
COPY sshd_config /etc/ssh/sshd_config
|
||||
COPY run.sh /usr/local/bin/run.sh
|
||||
COPY ws_echo.py /usr/local/bin/ws_echo.py
|
||||
RUN chmod +x /usr/local/bin/run.sh
|
||||
|
||||
EXPOSE 22
|
||||
|
||||
CMD ["/usr/local/bin/run.sh"]
|
||||
38
tests/fixtures/ssh-remote/run.sh
vendored
38
tests/fixtures/ssh-remote/run.sh
vendored
|
|
@ -1,38 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ -z "${AUTHORIZED_KEY:-}" ]; then
|
||||
echo "AUTHORIZED_KEY is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REMOTE_HTTP_PORT="${REMOTE_HTTP_PORT:-43173}"
|
||||
REMOTE_WS_PORT="${REMOTE_WS_PORT:-43174}"
|
||||
|
||||
mkdir -p /home/dev/.ssh /root/.ssh /run/sshd
|
||||
printf '%s\n' "$AUTHORIZED_KEY" > /home/dev/.ssh/authorized_keys
|
||||
printf '%s\n' "$AUTHORIZED_KEY" > /root/.ssh/authorized_keys
|
||||
chown -R dev:dev /home/dev/.ssh
|
||||
chmod 700 /home/dev/.ssh
|
||||
chmod 600 /home/dev/.ssh/authorized_keys
|
||||
chmod 700 /root/.ssh
|
||||
chmod 600 /root/.ssh/authorized_keys
|
||||
|
||||
python3 -m http.server "$REMOTE_HTTP_PORT" --bind 127.0.0.1 --directory /srv/www >/tmp/http.log 2>&1 &
|
||||
HTTP_PID=$!
|
||||
python3 /usr/local/bin/ws_echo.py --host 127.0.0.1 --port "$REMOTE_WS_PORT" >/tmp/ws.log 2>&1 &
|
||||
WS_PID=$!
|
||||
|
||||
sleep 0.2
|
||||
if ! kill -0 "$HTTP_PID" 2>/dev/null; then
|
||||
echo "HTTP fixture failed to start (see /tmp/http.log)" >&2
|
||||
cat /tmp/http.log >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
if ! kill -0 "$WS_PID" 2>/dev/null; then
|
||||
echo "WebSocket fixture failed to start (see /tmp/ws.log)" >&2
|
||||
cat /tmp/ws.log >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec /usr/sbin/sshd -D -e
|
||||
31
tests/fixtures/ssh-remote/sshd_config
vendored
31
tests/fixtures/ssh-remote/sshd_config
vendored
|
|
@ -1,31 +0,0 @@
|
|||
Port 22
|
||||
Protocol 2
|
||||
AddressFamily any
|
||||
ListenAddress 0.0.0.0
|
||||
ListenAddress ::
|
||||
|
||||
HostKey /etc/ssh/ssh_host_rsa_key
|
||||
HostKey /etc/ssh/ssh_host_ecdsa_key
|
||||
HostKey /etc/ssh/ssh_host_ed25519_key
|
||||
|
||||
PermitRootLogin yes
|
||||
PubkeyAuthentication yes
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
ChallengeResponseAuthentication no
|
||||
UsePAM no
|
||||
AuthorizedKeysFile .ssh/authorized_keys
|
||||
PermitEmptyPasswords no
|
||||
AcceptEnv TERM_PROGRAM TERM_PROGRAM_VERSION COLORTERM
|
||||
|
||||
X11Forwarding no
|
||||
AllowTcpForwarding yes
|
||||
AllowStreamLocalForwarding yes
|
||||
StreamLocalBindUnlink yes
|
||||
GatewayPorts no
|
||||
PermitTunnel no
|
||||
ClientAliveInterval 30
|
||||
ClientAliveCountMax 2
|
||||
PrintMotd no
|
||||
PidFile /run/sshd.pid
|
||||
Subsystem sftp /usr/lib/ssh/sftp-server
|
||||
132
tests/fixtures/ssh-remote/ws_echo.py
vendored
132
tests/fixtures/ssh-remote/ws_echo.py
vendored
|
|
@ -1,132 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Tiny WebSocket echo server for SSH proxy integration tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
|
||||
|
||||
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
|
||||
|
||||
def _recv_exact(conn: socket.socket, n: int) -> bytes:
|
||||
data = bytearray()
|
||||
while len(data) < n:
|
||||
chunk = conn.recv(n - len(data))
|
||||
if not chunk:
|
||||
raise ConnectionError("unexpected EOF")
|
||||
data.extend(chunk)
|
||||
return bytes(data)
|
||||
|
||||
|
||||
def _recv_until(conn: socket.socket, marker: bytes, limit: int = 8192) -> bytes:
|
||||
data = bytearray()
|
||||
while marker not in data:
|
||||
chunk = conn.recv(1024)
|
||||
if not chunk:
|
||||
raise ConnectionError("unexpected EOF while reading headers")
|
||||
data.extend(chunk)
|
||||
if len(data) > limit:
|
||||
raise ValueError("header too large")
|
||||
return bytes(data)
|
||||
|
||||
|
||||
def _read_frame(conn: socket.socket) -> tuple[int, bytes]:
|
||||
first, second = _recv_exact(conn, 2)
|
||||
opcode = first & 0x0F
|
||||
masked = (second & 0x80) != 0
|
||||
length = second & 0x7F
|
||||
if length == 126:
|
||||
length = struct.unpack("!H", _recv_exact(conn, 2))[0]
|
||||
elif length == 127:
|
||||
length = struct.unpack("!Q", _recv_exact(conn, 8))[0]
|
||||
|
||||
mask_key = _recv_exact(conn, 4) if masked else b""
|
||||
payload = _recv_exact(conn, length) if length else b""
|
||||
if masked and payload:
|
||||
payload = bytes(b ^ mask_key[i % 4] for i, b in enumerate(payload))
|
||||
return opcode, payload
|
||||
|
||||
|
||||
def _send_frame(conn: socket.socket, opcode: int, payload: bytes) -> None:
|
||||
first = 0x80 | (opcode & 0x0F)
|
||||
length = len(payload)
|
||||
if length < 126:
|
||||
header = bytes([first, length])
|
||||
elif length <= 0xFFFF:
|
||||
header = bytes([first, 126]) + struct.pack("!H", length)
|
||||
else:
|
||||
header = bytes([first, 127]) + struct.pack("!Q", length)
|
||||
conn.sendall(header + payload)
|
||||
|
||||
|
||||
def handle_client(conn: socket.socket) -> None:
|
||||
try:
|
||||
request = _recv_until(conn, b"\r\n\r\n")
|
||||
headers_raw = request.decode("utf-8", errors="replace").split("\r\n")
|
||||
header_map: dict[str, str] = {}
|
||||
for line in headers_raw[1:]:
|
||||
if not line or ":" not in line:
|
||||
continue
|
||||
k, v = line.split(":", 1)
|
||||
header_map[k.strip().lower()] = v.strip()
|
||||
|
||||
key = header_map.get("sec-websocket-key", "")
|
||||
upgrade = header_map.get("upgrade", "").lower()
|
||||
connection_hdr = header_map.get("connection", "").lower()
|
||||
if not key or upgrade != "websocket" or "upgrade" not in connection_hdr:
|
||||
conn.sendall(b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n")
|
||||
return
|
||||
|
||||
accept = base64.b64encode(hashlib.sha1((key + GUID).encode("utf-8")).digest()).decode("ascii")
|
||||
response = (
|
||||
"HTTP/1.1 101 Switching Protocols\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
f"Sec-WebSocket-Accept: {accept}\r\n"
|
||||
"\r\n"
|
||||
)
|
||||
conn.sendall(response.encode("utf-8"))
|
||||
|
||||
while True:
|
||||
opcode, payload = _read_frame(conn)
|
||||
if opcode == 0x8: # close
|
||||
_send_frame(conn, 0x8, b"")
|
||||
return
|
||||
if opcode == 0x9: # ping
|
||||
_send_frame(conn, 0xA, payload)
|
||||
continue
|
||||
if opcode == 0x1: # text
|
||||
_send_frame(conn, 0x1, payload)
|
||||
continue
|
||||
# ignore all other opcodes
|
||||
finally:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="WebSocket echo server")
|
||||
parser.add_argument("--host", default="127.0.0.1")
|
||||
parser.add_argument("--port", type=int, default=43174)
|
||||
args = parser.parse_args()
|
||||
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
|
||||
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
server.bind((args.host, args.port))
|
||||
server.listen(16)
|
||||
while True:
|
||||
conn, _ = server.accept()
|
||||
thread = threading.Thread(target=handle_client, args=(conn,), daemon=True)
|
||||
thread.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -32,17 +32,13 @@ def resolve_cmux_cli() -> str:
|
|||
raise RuntimeError("Unable to find cmux CLI binary. Set CMUX_CLI_BIN.")
|
||||
|
||||
|
||||
def run(cli_path: str, *args: str, timeout: float = 5.0) -> tuple[int, str, str]:
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[cli_path, *args],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=timeout,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return 124, "", f"timed out after {timeout:.1f}s"
|
||||
def run(cli_path: str, *args: str) -> tuple[int, str, str]:
|
||||
proc = subprocess.run(
|
||||
[cli_path, *args],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
return proc.returncode, proc.stdout.strip(), proc.stderr.strip()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
OUTPUT_DIR="$(mktemp -d "${TMPDIR:-/tmp}/cmux-remote-assets-test.XXXXXX")"
|
||||
trap 'rm -rf "$OUTPUT_DIR"' EXIT
|
||||
|
||||
"$ROOT_DIR/scripts/build_remote_daemon_release_assets.sh" \
|
||||
--version "0.62.0-test" \
|
||||
--release-tag "v0.62.0-test" \
|
||||
--repo "manaflow-ai/cmux" \
|
||||
--output-dir "$OUTPUT_DIR" >/dev/null
|
||||
|
||||
for asset in \
|
||||
cmuxd-remote-darwin-arm64 \
|
||||
cmuxd-remote-darwin-amd64 \
|
||||
cmuxd-remote-linux-arm64 \
|
||||
cmuxd-remote-linux-amd64 \
|
||||
cmuxd-remote-checksums.txt \
|
||||
cmuxd-remote-manifest.json
|
||||
do
|
||||
if [[ ! -f "$OUTPUT_DIR/$asset" ]]; then
|
||||
echo "FAIL: missing asset $asset" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
python3 - <<'PY' "$OUTPUT_DIR/cmuxd-remote-manifest.json" "$OUTPUT_DIR/cmuxd-remote-checksums.txt"
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
manifest_path = Path(sys.argv[1])
|
||||
checksums_path = Path(sys.argv[2])
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
|
||||
expected_targets = {
|
||||
("darwin", "arm64"),
|
||||
("darwin", "amd64"),
|
||||
("linux", "arm64"),
|
||||
("linux", "amd64"),
|
||||
}
|
||||
actual_targets = {(entry["goOS"], entry["goArch"]) for entry in manifest["entries"]}
|
||||
if actual_targets != expected_targets:
|
||||
raise SystemExit(f"FAIL: manifest targets {sorted(actual_targets)} != {sorted(expected_targets)}")
|
||||
|
||||
if manifest["appVersion"] != "0.62.0-test":
|
||||
raise SystemExit(f"FAIL: unexpected appVersion {manifest['appVersion']}")
|
||||
if manifest["releaseTag"] != "v0.62.0-test":
|
||||
raise SystemExit(f"FAIL: unexpected releaseTag {manifest['releaseTag']}")
|
||||
if not manifest["checksumsURL"].endswith("/cmuxd-remote-checksums.txt"):
|
||||
raise SystemExit(f"FAIL: unexpected checksumsURL {manifest['checksumsURL']}")
|
||||
|
||||
checksum_lines = [line for line in checksums_path.read_text(encoding="utf-8").splitlines() if line.strip()]
|
||||
if len(checksum_lines) != 4:
|
||||
raise SystemExit(f"FAIL: expected 4 checksum lines, got {len(checksum_lines)}")
|
||||
|
||||
for entry in manifest["entries"]:
|
||||
if not entry["downloadURL"].endswith("/" + entry["assetName"]):
|
||||
raise SystemExit(f"FAIL: downloadURL mismatch for {entry['assetName']}")
|
||||
if len(entry["sha256"]) != 64:
|
||||
raise SystemExit(f"FAIL: invalid sha256 for {entry['assetName']}")
|
||||
|
||||
print("PASS: remote daemon release assets include all targets and manifest entries")
|
||||
PY
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Regression test: sidebar context menu shows Copy SSH Error only when an SSH error exists."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def require(content: str, needle: str, message: str, failures: list[str]) -> None:
|
||||
if needle not in content:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = get_repo_root()
|
||||
content_view_path = repo_root / "Sources" / "ContentView.swift"
|
||||
if not content_view_path.exists():
|
||||
print(f"FAIL: missing expected file: {content_view_path}")
|
||||
return 1
|
||||
|
||||
content = content_view_path.read_text(encoding="utf-8")
|
||||
failures: list[str] = []
|
||||
|
||||
require(
|
||||
content,
|
||||
"private var copyableSidebarSSHError: String?",
|
||||
"Missing sidebar SSH error extraction helper",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
'tab.statusEntries["remote.error"]?.value',
|
||||
"Missing remote.error status fallback for copyable SSH error text",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
"if let copyableSidebarSSHError {",
|
||||
"Copy SSH Error menu entry is no longer conditionally gated",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
'Button("Copy SSH Error")',
|
||||
"Missing Copy SSH Error context menu button",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
"copyTextToPasteboard(copyableSidebarSSHError)",
|
||||
"Copy SSH Error button no longer writes the resolved error text",
|
||||
failures,
|
||||
)
|
||||
|
||||
if failures:
|
||||
print("FAIL: sidebar copy SSH error context-menu regression(s) detected")
|
||||
for failure in failures:
|
||||
print(f"- {failure}")
|
||||
return 1
|
||||
|
||||
print("PASS: sidebar Copy SSH Error context menu wiring is intact")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue