evo-cli 0.1.11__tar.gz → 0.3.0__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.
- {evo_cli-0.1.11 → evo_cli-0.3.0}/PKG-INFO +1 -1
- evo_cli-0.3.0/evo_cli/VERSION +1 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/cli.py +4 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/commands/cloudflare.py +238 -52
- evo_cli-0.3.0/evo_cli/commands/gdrive.py +528 -0
- evo_cli-0.3.0/evo_cli/commands/site2s.py +289 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli.egg-info/PKG-INFO +1 -1
- {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli.egg-info/SOURCES.txt +2 -0
- evo_cli-0.1.11/evo_cli/VERSION +0 -1
- {evo_cli-0.1.11 → evo_cli-0.3.0}/Containerfile +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/HISTORY.md +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/LICENSE +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/MANIFEST.in +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/README.md +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/__init__.py +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/__main__.py +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/base.py +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/commands/__init__.py +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/commands/fix_claude.py +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/commands/miniconda.py +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/commands/ssh.py +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/console.py +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli.egg-info/dependency_links.txt +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli.egg-info/entry_points.txt +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli.egg-info/requires.txt +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli.egg-info/top_level.txt +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/pyproject.toml +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/setup.cfg +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/tests/__init__.py +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/tests/test_cli.py +0 -0
- {evo_cli-0.1.11 → evo_cli-0.3.0}/tests/test_fix_claude.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.3.0
|
|
@@ -3,7 +3,9 @@ import rich_click as click
|
|
|
3
3
|
from evo_cli import __version__
|
|
4
4
|
from evo_cli.commands.cloudflare import cfssh
|
|
5
5
|
from evo_cli.commands.fix_claude import f_claude
|
|
6
|
+
from evo_cli.commands.gdrive import gdrive
|
|
6
7
|
from evo_cli.commands.miniconda import miniconda
|
|
8
|
+
from evo_cli.commands.site2s import site2s
|
|
7
9
|
from evo_cli.commands.ssh import setupssh
|
|
8
10
|
|
|
9
11
|
click.rich_click.USE_MARKDOWN = True
|
|
@@ -31,6 +33,8 @@ cli.add_command(setupssh)
|
|
|
31
33
|
cli.add_command(miniconda)
|
|
32
34
|
cli.add_command(cfssh)
|
|
33
35
|
cli.add_command(f_claude)
|
|
36
|
+
cli.add_command(gdrive)
|
|
37
|
+
cli.add_command(site2s)
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
def main():
|
|
@@ -3,6 +3,7 @@ import json
|
|
|
3
3
|
import os
|
|
4
4
|
import platform
|
|
5
5
|
import shutil
|
|
6
|
+
import socket
|
|
6
7
|
import subprocess
|
|
7
8
|
import tempfile
|
|
8
9
|
from pathlib import Path
|
|
@@ -27,15 +28,26 @@ from evo_cli.console import (
|
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
CLOUDFLARED_RELEASE = "https://github.com/cloudflare/cloudflared/releases/latest/download"
|
|
31
|
+
|
|
32
|
+
# Linux (systemd) layout - config and credentials live under /etc, owned by root.
|
|
30
33
|
ETC_DIR = Path("/etc/cloudflared")
|
|
31
34
|
SERVICE_UNIT = Path("/etc/systemd/system/cloudflared.service")
|
|
32
35
|
ROOT_PATH_DIRS = ("/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin")
|
|
33
36
|
|
|
37
|
+
# macOS (launchd) layout - cloudflared installs a per-user LaunchAgent that reads
|
|
38
|
+
# the config from the user's ~/.cloudflared. No root/sudo involved.
|
|
39
|
+
MAC_LABEL = "com.cloudflare.cloudflared"
|
|
40
|
+
MAC_PLIST = Path.home() / "Library" / "LaunchAgents" / f"{MAC_LABEL}.plist"
|
|
41
|
+
|
|
42
|
+
IS_MACOS = platform.system() == "Darwin"
|
|
43
|
+
IS_LINUX = platform.system() == "Linux"
|
|
44
|
+
|
|
34
45
|
EPILOG = Text.from_markup(
|
|
35
46
|
"[bold]Examples[/bold]\n\n"
|
|
36
|
-
" [cyan]evo cfssh -H dev.example.com[/cyan]\n"
|
|
47
|
+
" [cyan]evo cfssh -H dev.example.com[/cyan] # SSH into this machine\n"
|
|
37
48
|
" [cyan]evo cfssh -H box.example.com -n my-box -P 2222[/cyan]\n"
|
|
38
|
-
" [cyan]evo cfssh -H
|
|
49
|
+
" [cyan]evo cfssh -H app.example.com --http 3000[/cyan] # expose a local web app\n"
|
|
50
|
+
" [cyan]evo cfssh -H dev.example.com --no-service[/cyan] # configure only, run manually"
|
|
39
51
|
)
|
|
40
52
|
|
|
41
53
|
|
|
@@ -64,14 +76,30 @@ def cloudflared_dir():
|
|
|
64
76
|
return Path.home() / ".cloudflared"
|
|
65
77
|
|
|
66
78
|
|
|
67
|
-
def
|
|
79
|
+
def server_config_dir():
|
|
80
|
+
"""Where the running service reads config.yml from on this OS."""
|
|
81
|
+
return cloudflared_dir() if IS_MACOS else ETC_DIR
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def server_config_file():
|
|
85
|
+
return server_config_dir() / "config.yml"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _port_listening(port):
|
|
89
|
+
"""True if something is accepting TCP connections on localhost:port."""
|
|
90
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
91
|
+
sock.settimeout(0.5)
|
|
92
|
+
return sock.connect_ex(("127.0.0.1", int(port))) == 0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_config_yaml(tunnel_id, hostname, service, credentials_path):
|
|
68
96
|
return (
|
|
69
97
|
f"tunnel: {tunnel_id}\n"
|
|
70
98
|
f"credentials-file: {credentials_path}\n"
|
|
71
99
|
f"\n"
|
|
72
100
|
f"ingress:\n"
|
|
73
101
|
f" - hostname: {hostname}\n"
|
|
74
|
-
f" service:
|
|
102
|
+
f" service: {service}\n"
|
|
75
103
|
f" - service: http_status:404\n"
|
|
76
104
|
)
|
|
77
105
|
|
|
@@ -82,6 +110,20 @@ def install_cloudflared():
|
|
|
82
110
|
info(f"cloudflared already installed: [accent]{version}[/accent]")
|
|
83
111
|
return True
|
|
84
112
|
|
|
113
|
+
if IS_MACOS:
|
|
114
|
+
if not _has("brew"):
|
|
115
|
+
error("cloudflared not found and Homebrew is unavailable.")
|
|
116
|
+
info(
|
|
117
|
+
"Install it manually: https://github.com/cloudflare/cloudflared/releases (or `brew install cloudflared`)."
|
|
118
|
+
)
|
|
119
|
+
return False
|
|
120
|
+
run_command(["brew", "install", "cloudflared"], check=False, status="Installing cloudflared (brew)")
|
|
121
|
+
if not _has("cloudflared"):
|
|
122
|
+
error("cloudflared installation failed.")
|
|
123
|
+
return False
|
|
124
|
+
success("cloudflared installed.")
|
|
125
|
+
return True
|
|
126
|
+
|
|
85
127
|
arch = _deb_arch()
|
|
86
128
|
url = f"{CLOUDFLARED_RELEASE}/cloudflared-linux-{arch}.deb"
|
|
87
129
|
with tempfile.TemporaryDirectory() as tmp:
|
|
@@ -142,7 +184,7 @@ def find_tunnel(name):
|
|
|
142
184
|
|
|
143
185
|
|
|
144
186
|
def read_local_config():
|
|
145
|
-
config_file =
|
|
187
|
+
config_file = server_config_file()
|
|
146
188
|
if not config_file.exists():
|
|
147
189
|
return None
|
|
148
190
|
try:
|
|
@@ -150,7 +192,7 @@ def read_local_config():
|
|
|
150
192
|
except OSError:
|
|
151
193
|
return None
|
|
152
194
|
tunnel_id = None
|
|
153
|
-
|
|
195
|
+
routes = [] # list of (hostname, service)
|
|
154
196
|
current_host = None
|
|
155
197
|
for raw in text.splitlines():
|
|
156
198
|
line = raw.strip()
|
|
@@ -159,15 +201,17 @@ def read_local_config():
|
|
|
159
201
|
elif line.startswith("- hostname:") or line.startswith("hostname:"):
|
|
160
202
|
current_host = line.split(":", 1)[1].strip()
|
|
161
203
|
elif line.startswith("service:"):
|
|
162
|
-
if current_host
|
|
163
|
-
|
|
204
|
+
if current_host:
|
|
205
|
+
routes.append((current_host, line.split(":", 1)[1].strip()))
|
|
164
206
|
current_host = None
|
|
165
207
|
if not tunnel_id:
|
|
166
208
|
return None
|
|
167
|
-
return {"config_file": config_file, "tunnel_id": tunnel_id, "
|
|
209
|
+
return {"config_file": config_file, "tunnel_id": tunnel_id, "routes": routes}
|
|
168
210
|
|
|
169
211
|
|
|
170
212
|
def cloudflared_service_state():
|
|
213
|
+
if IS_MACOS:
|
|
214
|
+
return mac_service_state() if MAC_PLIST.exists() else None
|
|
171
215
|
if not SERVICE_UNIT.exists():
|
|
172
216
|
return None
|
|
173
217
|
try:
|
|
@@ -177,6 +221,19 @@ def cloudflared_service_state():
|
|
|
177
221
|
return result.stdout.strip() or "unknown"
|
|
178
222
|
|
|
179
223
|
|
|
224
|
+
def mac_service_state():
|
|
225
|
+
"""Read the launchd state of the cloudflared user agent."""
|
|
226
|
+
if not MAC_PLIST.exists():
|
|
227
|
+
return None
|
|
228
|
+
try:
|
|
229
|
+
result = subprocess.run(["launchctl", "list", MAC_LABEL], capture_output=True, text=True)
|
|
230
|
+
except OSError:
|
|
231
|
+
return "unknown"
|
|
232
|
+
if result.returncode != 0:
|
|
233
|
+
return "stopped"
|
|
234
|
+
return "running" if '"PID"' in result.stdout else "loaded"
|
|
235
|
+
|
|
236
|
+
|
|
180
237
|
def detect_local_tunnel():
|
|
181
238
|
config = read_local_config()
|
|
182
239
|
if not config:
|
|
@@ -197,11 +254,10 @@ def show_local_tunnel(existing):
|
|
|
197
254
|
table.add_row("config", str(existing["config_file"]))
|
|
198
255
|
name = existing["name"] or "[dim]not found in account[/dim]"
|
|
199
256
|
table.add_row("tunnel", f"[accent]{name}[/accent] ({existing['tunnel_id']})")
|
|
200
|
-
|
|
257
|
+
routes = ", ".join(f"{host} -> {svc}" for host, svc in existing["routes"]) or "[dim]none[/dim]"
|
|
258
|
+
table.add_row("routes", routes)
|
|
201
259
|
table.add_row("service", existing["service"] or "not installed")
|
|
202
|
-
console.print(
|
|
203
|
-
Panel(table, title="SSH tunnel already configured on this machine", border_style="warning", expand=False)
|
|
204
|
-
)
|
|
260
|
+
console.print(Panel(table, title="Tunnel already configured on this machine", border_style="warning", expand=False))
|
|
205
261
|
|
|
206
262
|
|
|
207
263
|
def ensure_tunnel(name):
|
|
@@ -218,7 +274,19 @@ def ensure_tunnel(name):
|
|
|
218
274
|
return tunnel_id
|
|
219
275
|
|
|
220
276
|
|
|
221
|
-
def
|
|
277
|
+
def _print_config_panel(config_file, config_text):
|
|
278
|
+
success(f"Wrote {config_file}")
|
|
279
|
+
console.print(
|
|
280
|
+
Panel(
|
|
281
|
+
Syntax(config_text.rstrip(), "yaml", theme="ansi_dark", background_color="default"),
|
|
282
|
+
title=str(config_file),
|
|
283
|
+
border_style="step",
|
|
284
|
+
expand=False,
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def write_server_config(tunnel_id, hostname, service):
|
|
222
290
|
local_cred = cloudflared_dir() / f"{tunnel_id}.json"
|
|
223
291
|
if not local_cred.exists():
|
|
224
292
|
error(f"Credentials file {local_cred} not found.")
|
|
@@ -227,9 +295,28 @@ def write_server_config(tunnel_id, hostname, ssh_port):
|
|
|
227
295
|
info("or run this command on the machine that created the tunnel.")
|
|
228
296
|
return None
|
|
229
297
|
|
|
298
|
+
if IS_MACOS:
|
|
299
|
+
return _write_server_config_macos(tunnel_id, hostname, service, local_cred)
|
|
300
|
+
return _write_server_config_linux(tunnel_id, hostname, service, local_cred)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _write_server_config_macos(tunnel_id, hostname, service, local_cred):
|
|
304
|
+
# The LaunchAgent runs as this user, so it reads ~/.cloudflared directly -
|
|
305
|
+
# no copy into /etc and no sudo needed.
|
|
306
|
+
config_file = cloudflared_dir() / "config.yml"
|
|
307
|
+
config_text = build_config_yaml(tunnel_id, hostname, service, str(local_cred))
|
|
308
|
+
if config_file.exists():
|
|
309
|
+
shutil.copy2(config_file, str(config_file) + ".bak")
|
|
310
|
+
info(f"Backed up existing config to {config_file}.bak")
|
|
311
|
+
config_file.write_text(config_text)
|
|
312
|
+
_print_config_panel(config_file, config_text)
|
|
313
|
+
return config_file
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _write_server_config_linux(tunnel_id, hostname, service, local_cred):
|
|
230
317
|
etc_cred = ETC_DIR / f"{tunnel_id}.json"
|
|
231
318
|
config_file = ETC_DIR / "config.yml"
|
|
232
|
-
config_text = build_config_yaml(tunnel_id, hostname,
|
|
319
|
+
config_text = build_config_yaml(tunnel_id, hostname, service, str(etc_cred))
|
|
233
320
|
|
|
234
321
|
run_command(sudo_prefix() + ["mkdir", "-p", str(ETC_DIR)])
|
|
235
322
|
if config_file.exists():
|
|
@@ -237,15 +324,7 @@ def write_server_config(tunnel_id, hostname, ssh_port):
|
|
|
237
324
|
info(f"Backed up existing config to {config_file}.bak")
|
|
238
325
|
run_command(sudo_prefix() + ["cp", str(local_cred), str(etc_cred)])
|
|
239
326
|
run_command(sudo_prefix() + ["tee", str(config_file)], capture=True, input_text=config_text)
|
|
240
|
-
|
|
241
|
-
console.print(
|
|
242
|
-
Panel(
|
|
243
|
-
Syntax(config_text.rstrip(), "yaml", theme="ansi_dark", background_color="default"),
|
|
244
|
-
title=str(config_file),
|
|
245
|
-
border_style="step",
|
|
246
|
-
expand=False,
|
|
247
|
-
)
|
|
248
|
-
)
|
|
327
|
+
_print_config_panel(config_file, config_text)
|
|
249
328
|
return config_file
|
|
250
329
|
|
|
251
330
|
|
|
@@ -277,6 +356,29 @@ def route_dns(name, hostname):
|
|
|
277
356
|
|
|
278
357
|
|
|
279
358
|
def check_sshd(ssh_port):
|
|
359
|
+
if IS_MACOS:
|
|
360
|
+
return _check_sshd_macos(ssh_port)
|
|
361
|
+
return _check_sshd_linux(ssh_port)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _check_sshd_macos(ssh_port):
|
|
365
|
+
if _port_listening(ssh_port):
|
|
366
|
+
info(f"SSH server is listening on localhost:{ssh_port}.")
|
|
367
|
+
return True
|
|
368
|
+
warning(f"No SSH server is listening on localhost:{ssh_port}.")
|
|
369
|
+
info("On macOS this is the 'Remote Login' service.")
|
|
370
|
+
info("Enable it in System Settings > General > Sharing > Remote Login, or run:")
|
|
371
|
+
console.print(" [cmd]sudo systemsetup -setremotelogin on[/cmd]")
|
|
372
|
+
if Confirm.ask("[accent]Enable Remote Login now (needs sudo)?[/accent]", default=True):
|
|
373
|
+
run_command(sudo_prefix() + ["systemsetup", "-setremotelogin", "on"], check=False)
|
|
374
|
+
if _port_listening(ssh_port):
|
|
375
|
+
success("Remote Login enabled.")
|
|
376
|
+
return True
|
|
377
|
+
warning("Continuing without sshd. The tunnel will not be usable for SSH until it runs.")
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _check_sshd_linux(ssh_port):
|
|
280
382
|
if Path("/usr/sbin/sshd").exists() or _has("sshd"):
|
|
281
383
|
return True
|
|
282
384
|
warning(f"No SSH server (sshd) found. The tunnel forwards to ssh://localhost:{ssh_port}")
|
|
@@ -307,6 +409,37 @@ def cloudflared_bin_for_root():
|
|
|
307
409
|
|
|
308
410
|
|
|
309
411
|
def install_service(config_file):
|
|
412
|
+
if IS_MACOS:
|
|
413
|
+
return _install_service_macos()
|
|
414
|
+
return _install_service_linux(config_file)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _install_service_macos():
|
|
418
|
+
uid = os.getuid()
|
|
419
|
+
if MAC_PLIST.exists():
|
|
420
|
+
info("cloudflared launch agent already installed. Restarting to apply config.")
|
|
421
|
+
run_command(
|
|
422
|
+
["launchctl", "kickstart", "-k", f"gui/{uid}/{MAC_LABEL}"],
|
|
423
|
+
check=False,
|
|
424
|
+
status="Restarting launch agent",
|
|
425
|
+
)
|
|
426
|
+
else:
|
|
427
|
+
run_command(
|
|
428
|
+
["cloudflared", "service", "install"],
|
|
429
|
+
check=False,
|
|
430
|
+
status="Installing cloudflared launch agent",
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
state = mac_service_state()
|
|
434
|
+
if state == "running":
|
|
435
|
+
success("cloudflared launch agent: running")
|
|
436
|
+
else:
|
|
437
|
+
warning(f"cloudflared launch agent: {state or 'unknown'}")
|
|
438
|
+
info("Check logs with: log show --predicate 'process == \"cloudflared\"' --last 5m")
|
|
439
|
+
return state == "running"
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _install_service_linux(config_file):
|
|
310
443
|
if not Path("/run/systemd/system").exists():
|
|
311
444
|
warning("systemd not detected. Skipping service install.")
|
|
312
445
|
info(f"Run the tunnel manually: sudo cloudflared --config {config_file} tunnel run")
|
|
@@ -339,7 +472,19 @@ def install_service(config_file):
|
|
|
339
472
|
return state == "active"
|
|
340
473
|
|
|
341
474
|
|
|
342
|
-
def
|
|
475
|
+
def manage_hint():
|
|
476
|
+
if IS_MACOS:
|
|
477
|
+
return f"Manage the tunnel here: [cmd]launchctl list {MAC_LABEL}[/cmd] / [cmd]cloudflared tunnel list[/cmd]"
|
|
478
|
+
return (
|
|
479
|
+
"Manage the tunnel here: [cmd]sudo systemctl status cloudflared[/cmd] / [cmd]cloudflared tunnel list[/cmd]"
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def run_manually_hint(config_file):
|
|
484
|
+
return f"Run it manually: cloudflared --config {config_file} tunnel run"
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def print_ssh_instructions(hostname, ssh_port, config_file, service_installed):
|
|
343
488
|
user = getpass.getuser()
|
|
344
489
|
ssh_config = f"Host {hostname}\n User {user}\n ProxyCommand cloudflared access ssh --hostname %h"
|
|
345
490
|
console.print()
|
|
@@ -356,24 +501,51 @@ def print_client_instructions(hostname, ssh_port):
|
|
|
356
501
|
)
|
|
357
502
|
)
|
|
358
503
|
console.print(Panel(ssh_config, title="client ~/.ssh/config", border_style="step", expand=False))
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
)
|
|
504
|
+
if not service_installed:
|
|
505
|
+
console.print(run_manually_hint(config_file))
|
|
506
|
+
console.print(manage_hint())
|
|
362
507
|
console.print(
|
|
363
508
|
"Optional hardening: create a self-hosted Access application for "
|
|
364
509
|
f"[accent]{hostname}[/accent] in the Cloudflare Zero Trust dashboard."
|
|
365
510
|
)
|
|
366
511
|
|
|
367
512
|
|
|
368
|
-
def
|
|
369
|
-
|
|
370
|
-
|
|
513
|
+
def print_http_instructions(hostname, http_port, config_file, service_installed):
|
|
514
|
+
console.print()
|
|
515
|
+
console.print(
|
|
516
|
+
Panel(
|
|
517
|
+
"Server side is ready.\n\n"
|
|
518
|
+
f"Local service: [accent]http://localhost:{http_port}[/accent]\n"
|
|
519
|
+
f"Public URL: [accent]https://{hostname}[/accent]\n\n"
|
|
520
|
+
"Open the public URL in a browser (DNS may take a few seconds to propagate).",
|
|
521
|
+
title="cfssh complete (http)",
|
|
522
|
+
border_style="success",
|
|
523
|
+
expand=False,
|
|
524
|
+
)
|
|
525
|
+
)
|
|
526
|
+
if not service_installed:
|
|
527
|
+
console.print(run_manually_hint(config_file))
|
|
528
|
+
console.print(manage_hint())
|
|
529
|
+
console.print(
|
|
530
|
+
"Anything reachable at the public URL is public. Make sure the service has its "
|
|
531
|
+
"own auth, or protect it with a Cloudflare Access application."
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def run_setup_cloudflare_tunnel(hostname, name, ssh_port, http_port, no_service):
|
|
536
|
+
if not (IS_MACOS or IS_LINUX):
|
|
537
|
+
error("evo cfssh supports macOS and Linux only.")
|
|
371
538
|
return
|
|
372
539
|
|
|
540
|
+
is_http = http_port is not None
|
|
541
|
+
service_kind = "http" if is_http else "ssh"
|
|
542
|
+
target_port = http_port if is_http else ssh_port
|
|
543
|
+
service = f"http://localhost:{http_port}" if is_http else f"ssh://localhost:{ssh_port}"
|
|
544
|
+
|
|
373
545
|
existing = detect_local_tunnel()
|
|
374
546
|
if existing:
|
|
375
547
|
show_local_tunnel(existing)
|
|
376
|
-
existing_host = existing["
|
|
548
|
+
existing_host = existing["routes"][0][0] if existing["routes"] else None
|
|
377
549
|
if existing_host:
|
|
378
550
|
if not hostname:
|
|
379
551
|
if Confirm.ask(f"[accent]Reuse this tunnel for {existing_host}?[/accent]", default=True):
|
|
@@ -390,30 +562,31 @@ def run_setup_cloudflare_ssh(hostname, name, ssh_port, no_service):
|
|
|
390
562
|
info("Keeping the existing tunnel. Nothing changed.")
|
|
391
563
|
return
|
|
392
564
|
|
|
393
|
-
hostname = hostname or Prompt.ask("[accent]Public hostname
|
|
565
|
+
hostname = hostname or Prompt.ask("[accent]Public hostname (e.g. dev.example.com)[/accent]")
|
|
394
566
|
if not hostname:
|
|
395
567
|
error("Hostname is required.")
|
|
396
568
|
return
|
|
397
569
|
|
|
398
570
|
name = name or hostname.split(".")[0]
|
|
399
|
-
ssh_port = ssh_port or 22
|
|
400
571
|
|
|
401
572
|
summary = Table(show_header=False, box=None, padding=(0, 2, 0, 0))
|
|
402
573
|
summary.add_row("hostname", f"[accent]{hostname}[/accent]")
|
|
403
574
|
summary.add_row("tunnel", f"[accent]{name}[/accent]")
|
|
404
|
-
summary.add_row("
|
|
405
|
-
|
|
575
|
+
summary.add_row("service", f"[accent]{service}[/accent]")
|
|
576
|
+
summary.add_row("platform", f"[accent]{platform.system()}[/accent]")
|
|
577
|
+
console.print(Panel(summary, title="Cloudflare tunnel", border_style="step", expand=False))
|
|
406
578
|
|
|
407
579
|
try:
|
|
408
580
|
if not install_cloudflared():
|
|
409
581
|
return
|
|
410
|
-
|
|
582
|
+
if service_kind == "ssh":
|
|
583
|
+
check_sshd(target_port)
|
|
411
584
|
if not ensure_login():
|
|
412
585
|
return
|
|
413
586
|
tunnel_id = ensure_tunnel(name)
|
|
414
587
|
if not tunnel_id:
|
|
415
588
|
return
|
|
416
|
-
config_file = write_server_config(tunnel_id, hostname,
|
|
589
|
+
config_file = write_server_config(tunnel_id, hostname, service)
|
|
417
590
|
if not config_file:
|
|
418
591
|
return
|
|
419
592
|
run_command(
|
|
@@ -422,26 +595,39 @@ def run_setup_cloudflare_ssh(hostname, name, ssh_port, no_service):
|
|
|
422
595
|
status="Validating ingress rules",
|
|
423
596
|
)
|
|
424
597
|
route_dns(name, hostname)
|
|
598
|
+
service_installed = False
|
|
425
599
|
if not no_service:
|
|
426
|
-
install_service(config_file)
|
|
427
|
-
|
|
600
|
+
service_installed = install_service(config_file)
|
|
601
|
+
if service_kind == "http":
|
|
602
|
+
print_http_instructions(hostname, target_port, config_file, service_installed)
|
|
603
|
+
else:
|
|
604
|
+
print_ssh_instructions(hostname, target_port, config_file, service_installed)
|
|
428
605
|
except CommandError as exc:
|
|
429
606
|
error(str(exc))
|
|
430
|
-
error("Cloudflare
|
|
607
|
+
error("Cloudflare tunnel setup did not finish.")
|
|
431
608
|
|
|
432
609
|
|
|
433
610
|
@click.command("cfssh", epilog=EPILOG)
|
|
434
|
-
@click.option("-H", "--hostname", help="Public hostname for
|
|
611
|
+
@click.option("-H", "--hostname", help="Public hostname for the tunnel, e.g. `dev.example.com`.")
|
|
435
612
|
@click.option("-n", "--name", help="Tunnel name. Defaults to the first label of the hostname.")
|
|
436
|
-
@click.option("-P", "--ssh-port", type=int, default=22, show_default=True, help="Local SSH port to forward.")
|
|
437
|
-
@click.option(
|
|
438
|
-
|
|
439
|
-
""
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
613
|
+
@click.option("-P", "--ssh-port", type=int, default=22, show_default=True, help="Local SSH port to forward (ssh mode).")
|
|
614
|
+
@click.option(
|
|
615
|
+
"--http",
|
|
616
|
+
"http_port",
|
|
617
|
+
type=int,
|
|
618
|
+
default=None,
|
|
619
|
+
metavar="PORT",
|
|
620
|
+
help="Expose a local HTTP service on this port instead of SSH.",
|
|
621
|
+
)
|
|
622
|
+
@click.option("--no-service", is_flag=True, help="Configure only; do not install the launchd/systemd service.")
|
|
623
|
+
def cfssh(hostname, name, ssh_port, http_port, no_service):
|
|
624
|
+
"""Expose this machine through a Cloudflare named tunnel.
|
|
625
|
+
|
|
626
|
+
Installs `cloudflared`, creates a named tunnel, writes a `config.yml` with an
|
|
627
|
+
ingress rule, routes a proxied DNS record, and installs the background
|
|
628
|
+
service (launchd on macOS, systemd on Linux). Defaults to forwarding SSH
|
|
629
|
+
(`ssh://localhost:22`); pass `--http PORT` to expose a local web service
|
|
630
|
+
instead. A Cloudflare-managed domain is required.
|
|
445
631
|
"""
|
|
446
632
|
step("evo cfssh")
|
|
447
|
-
|
|
633
|
+
run_setup_cloudflare_tunnel(hostname, name, ssh_port, http_port, no_service)
|