evo-cli 0.33.0__tar.gz → 0.34.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.33.0 → evo_cli-0.34.0}/PKG-INFO +1 -1
- evo_cli-0.34.0/evo_cli/VERSION +1 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/openvpn.py +6 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/openvpn.py +42 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli.egg-info/PKG-INFO +1 -1
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_openvpn.py +62 -5
- evo_cli-0.33.0/evo_cli/VERSION +0 -1
- {evo_cli-0.33.0 → evo_cli-0.34.0}/Containerfile +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/HISTORY.md +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/LICENSE +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/MANIFEST.in +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/README.md +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/__init__.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/__main__.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/base.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/cli.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/__init__.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/agent_toy.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/claude_code.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/cloudflare.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/cred.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/download.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/fix_claude.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/gdrive.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/gh.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/__init__.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/_dag.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/_git.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/_hub.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/_mirror.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/_model.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/_mutate.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/_paths.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/_render.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/_server.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/check.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/clone.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/edit.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/pull.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/serve.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/show.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/views.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/web/assets/index-Cs7hXRPJ.css +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/web/assets/index-CuNOlaH7.js +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/harness/web/index.html +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/hwid.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/hwid_reset.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/image.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/localproxy.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/mcp.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/miniconda.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/netcheck.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/ngrok.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/opencode.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/plantuml.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/route/__init__.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/route/_dns.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/route/_dpi.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/route/_hosts.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/route/_page.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/route/_probe.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/route/_spoofdpi.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/route/check.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/route/fix.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/route/probe.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/route/status.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/serp.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/site2s.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/ssh.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/storage.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/sysmon.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/tts.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/update.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/commands/wifi.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/console.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/credentials/__init__.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/credentials/doctor.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/credentials/google_oauth.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/credentials/migrate.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/credentials/oauth_flow.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/credentials/registry.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/credentials/store.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/credentials/sync.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/imaging/__init__.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/imaging/align.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/imaging/core.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/imaging/creds.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/imaging/errors.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/imaging/gemini.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/imaging/ncnn.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/mcp_registry.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/serp/__init__.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/serp/client.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/serp/creds.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/serp/errors.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/serp/install.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/serp/render.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/tts/__init__.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/tts/chunking.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/tts/core.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/tts/creds.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/tts/errors.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/tts/gemini.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/tts/http.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/tts/openai.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/tts/player.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli/tts/vbee.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli.egg-info/SOURCES.txt +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli.egg-info/dependency_links.txt +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli.egg-info/entry_points.txt +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli.egg-info/requires.txt +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/evo_cli.egg-info/top_level.txt +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/pyproject.toml +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/setup.cfg +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/__init__.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/fake_evo_agents.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/fixtures/hub/contract-0.2.0.json +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/fixtures/hub/mirror-demo.yaml +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_agent_toy.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_claude_code.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_cli.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_console.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_cred.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_download.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_fix_claude.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_gdrive.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_gh.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_harness.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_harness_clone.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_harness_dag.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_harness_hub.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_harness_show.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_image.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_mcp.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_ngrok.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_opencode.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_plantuml.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_route.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_serp.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_storage.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_sysmon.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_tts.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_update.py +0 -0
- {evo_cli-0.33.0 → evo_cli-0.34.0}/tests/test_wifi.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.34.0
|
|
@@ -392,9 +392,15 @@ def connect(name, timeout, sudo_password_stdin, watch):
|
|
|
392
392
|
password = ""
|
|
393
393
|
click.echo(f"Connecting {name}; disconnect any external GUI VPN first. System DNS is not modified.")
|
|
394
394
|
deadline = time.monotonic() + timeout + 20
|
|
395
|
+
shown = None
|
|
395
396
|
try:
|
|
396
397
|
while time.monotonic() < deadline:
|
|
397
398
|
state = _ask(name)
|
|
399
|
+
if state.get("auth_url") and state["auth_url"] != shown:
|
|
400
|
+
shown = state["auth_url"]
|
|
401
|
+
click.echo(f"Sign in to finish connecting (opened in your browser): {shown}")
|
|
402
|
+
if state.get("auth_expires"):
|
|
403
|
+
deadline = max(deadline, time.monotonic() + state["auth_expires"] - time.time() + 20)
|
|
398
404
|
if state["state"] == "CONNECTED":
|
|
399
405
|
click.echo(f"{name}: connected, tunnel IP {state.get('vpn_ip', 'unknown')}")
|
|
400
406
|
break
|
|
@@ -45,6 +45,7 @@ OPTIONS = {
|
|
|
45
45
|
"connect-retry",
|
|
46
46
|
"connect-retry-max",
|
|
47
47
|
"connect-timeout",
|
|
48
|
+
"server-poll-timeout",
|
|
48
49
|
"float",
|
|
49
50
|
"tun-mtu",
|
|
50
51
|
"mssfix",
|
|
@@ -522,6 +523,30 @@ def auth_commands(line, profile):
|
|
|
522
523
|
return f'username "Auth" {quote(username)}\npassword "Auth" {quote(password)}\n'
|
|
523
524
|
|
|
524
525
|
|
|
526
|
+
def web_auth_url(message):
|
|
527
|
+
"""The sign-in URL of a pending-auth INFOMSG (WEB_AUTH:flags:url or the older OPEN_URL:url), else None."""
|
|
528
|
+
if message.startswith("WEB_AUTH:"):
|
|
529
|
+
# No proxy is announced, so flags hold no colon.
|
|
530
|
+
url = message[9:].partition(":")[2]
|
|
531
|
+
elif message.startswith("OPEN_URL:"):
|
|
532
|
+
url = message[9:]
|
|
533
|
+
else:
|
|
534
|
+
return None
|
|
535
|
+
if urlparse(url).scheme != "https" or any(c.isspace() or not c.isprintable() for c in url):
|
|
536
|
+
raise CredentialError("VPN server sent an unusable sign-in URL.")
|
|
537
|
+
return url
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def open_url(url):
|
|
541
|
+
"""Open the sign-in page in the user's browser; the worker runs as the user, never as root."""
|
|
542
|
+
opener = shutil.which("open" if sys.platform == "darwin" else "xdg-open")
|
|
543
|
+
if opener:
|
|
544
|
+
try:
|
|
545
|
+
subprocess.Popen([opener, url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
546
|
+
except OSError:
|
|
547
|
+
pass
|
|
548
|
+
|
|
549
|
+
|
|
525
550
|
def worker(name, timeout):
|
|
526
551
|
"""Keep the management channel alive for reauthentication; never persist raw VPN logs."""
|
|
527
552
|
import fcntl
|
|
@@ -592,6 +617,10 @@ def worker(name, timeout):
|
|
|
592
617
|
"--verb",
|
|
593
618
|
"3",
|
|
594
619
|
]
|
|
620
|
+
if not uses_credentials(profile["config"]):
|
|
621
|
+
# With no password to ask for, a server can add a sign-in step only through the browser (Access
|
|
622
|
+
# Server SAML); announce it, or such a server refuses the client.
|
|
623
|
+
command += ["--setenv", "IV_SSO", "openurl,webauth"]
|
|
595
624
|
pinned = [] if takes_default_route(profile["config"]) else remotes
|
|
596
625
|
if profile.get("otp_url"):
|
|
597
626
|
pinned = [*pinned, urlparse(profile["otp_url"]).hostname]
|
|
@@ -662,6 +691,12 @@ def worker(name, timeout):
|
|
|
662
691
|
if stamp.isdigit():
|
|
663
692
|
clock = time.strftime("%H:%M:%S", time.localtime(int(stamp)))
|
|
664
693
|
log.append(f"{clock} {rest.partition(',')[2]}")
|
|
694
|
+
elif line.startswith(">INFOMSG:"):
|
|
695
|
+
url = web_auth_url(line[9:])
|
|
696
|
+
if url:
|
|
697
|
+
state["auth_url"] = url
|
|
698
|
+
publish()
|
|
699
|
+
open_url(url)
|
|
665
700
|
elif line.startswith(">FATAL:"):
|
|
666
701
|
raise CredentialError("OpenVPN reported a fatal error; check config, TLS and permissions.")
|
|
667
702
|
elif line.startswith(">STATE:"):
|
|
@@ -671,11 +706,18 @@ def worker(name, timeout):
|
|
|
671
706
|
if fields[1] == "CONNECTED" and fields[2] == "SUCCESS":
|
|
672
707
|
state["vpn_ip"] = fields[3]
|
|
673
708
|
state["since"] = time.time()
|
|
709
|
+
state.pop("auth_url", None)
|
|
710
|
+
state.pop("auth_expires", None)
|
|
674
711
|
# Once up, let OpenVPN retry for as long as the network is gone.
|
|
675
712
|
deadline = float("inf")
|
|
676
713
|
elif fields[1] != "CONNECTED":
|
|
677
714
|
state.pop("vpn_ip", None)
|
|
678
715
|
state.pop("since", None)
|
|
716
|
+
if fields[1] == "AUTH_PENDING":
|
|
717
|
+
# Signing in happens in the browser: allow the time the server gives it.
|
|
718
|
+
wait = int(fields[2]) if fields[2].isdigit() else 300
|
|
719
|
+
deadline = max(deadline, time.monotonic() + wait)
|
|
720
|
+
state["auth_expires"] = time.time() + wait
|
|
679
721
|
if fields[1] == "RECONNECTING":
|
|
680
722
|
state["reconnects"] = state.get("reconnects", 0) + 1
|
|
681
723
|
if fields[1] in ("RECONNECTING", "EXITING"):
|
|
@@ -104,6 +104,11 @@ def test_rejects_unsafe_or_external_config(directive):
|
|
|
104
104
|
vpn.validate_config(CONFIG + directive + "\n")
|
|
105
105
|
|
|
106
106
|
|
|
107
|
+
def test_config_accepts_access_server_timing():
|
|
108
|
+
# OpenVPN Access Server profiles carry server-poll-timeout, an alias of connect-timeout.
|
|
109
|
+
assert vpn.validate_config(CONFIG + "server-poll-timeout 4\n") == ["vpn.example 1194 udp"]
|
|
110
|
+
|
|
111
|
+
|
|
107
112
|
def test_config_accepts_inline_key_without_parsing_it():
|
|
108
113
|
assert vpn.validate_config(CONFIG + "lport 0\n<key>\nPRIVATE SECRET\n</key>\n") == ["vpn.example 1194 udp"]
|
|
109
114
|
|
|
@@ -349,6 +354,26 @@ def test_worker_pins_the_otp_host_even_for_a_full_tunnel(store, monkeypatch):
|
|
|
349
354
|
assert pinned == [["otp.example"]]
|
|
350
355
|
|
|
351
356
|
|
|
357
|
+
@pytest.mark.parametrize(
|
|
358
|
+
"message,expected",
|
|
359
|
+
[
|
|
360
|
+
("WEB_AUTH::https://vpn.example/a?b=1", "https://vpn.example/a?b=1"),
|
|
361
|
+
("WEB_AUTH:external:https://vpn.example/a", "https://vpn.example/a"),
|
|
362
|
+
("OPEN_URL:https://vpn.example/a", "https://vpn.example/a"),
|
|
363
|
+
("CR_TEXT:E,R:Enter code", None),
|
|
364
|
+
("some other notice", None),
|
|
365
|
+
],
|
|
366
|
+
)
|
|
367
|
+
def test_web_auth_url(message, expected):
|
|
368
|
+
assert vpn.web_auth_url(message) == expected
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@pytest.mark.parametrize("url", ["http://vpn.example/a", "file:///etc/passwd", "https://vpn.example/a b", "-a"])
|
|
372
|
+
def test_web_auth_url_rejects_anything_but_plain_https(url):
|
|
373
|
+
with pytest.raises(CredentialError):
|
|
374
|
+
vpn.web_auth_url("WEB_AUTH::" + url)
|
|
375
|
+
|
|
376
|
+
|
|
352
377
|
def test_plain_password_escaping_and_no_unrequested_otp():
|
|
353
378
|
text = vpn.auth_commands("Need 'Auth' username/password", {"username": "u", "password": 'p"\\'})
|
|
354
379
|
assert text == 'username "Auth" "u"\npassword "Auth" "p\\"\\\\"\n'
|
|
@@ -462,7 +487,12 @@ with socket.socket(socket.AF_UNIX) as s:
|
|
|
462
487
|
seen = hold()
|
|
463
488
|
assert {b'state on\\n', b'log on\\n', b'bytecount 2\\n'} <= seen
|
|
464
489
|
held = b'hold off\\n' not in seen
|
|
465
|
-
|
|
490
|
+
if sys.argv[2] == 'webauth':
|
|
491
|
+
# Access Server SAML: no password prompt, a browser sign-in instead.
|
|
492
|
+
s.sendall(b'>STATE:1,AUTH_PENDING,120,,,,,\\n')
|
|
493
|
+
s.sendall(b'>INFOMSG:WEB_AUTH::https://vpn.example/auth/1\\n')
|
|
494
|
+
else:
|
|
495
|
+
auth()
|
|
466
496
|
if sys.argv[2] == 'fail':
|
|
467
497
|
s.sendall(b\">PASSWORD:Verification Failed: 'Auth'\\n\")
|
|
468
498
|
else:
|
|
@@ -480,12 +510,15 @@ with socket.socket(socket.AF_UNIX) as s:
|
|
|
480
510
|
"""
|
|
481
511
|
|
|
482
512
|
|
|
483
|
-
@pytest.mark.parametrize("mode", ["ok", "fail", "reconnect"])
|
|
513
|
+
@pytest.mark.parametrize("mode", ["ok", "fail", "reconnect", "webauth"])
|
|
484
514
|
def test_worker_real_sockets_auth_status_shutdown_without_root(store, monkeypatch, mode):
|
|
485
|
-
fail = mode == "fail"
|
|
515
|
+
fail, webauth = mode == "fail", mode == "webauth"
|
|
516
|
+
config = CERT_ONLY if webauth else CONFIG
|
|
486
517
|
vpn.save_profile(
|
|
487
|
-
"m1", {"config":
|
|
518
|
+
"m1", {"config": config, "username": "test", "password": "password-secret", "totp": vpn.parse_totp(SECRET)}
|
|
488
519
|
)
|
|
520
|
+
opened = []
|
|
521
|
+
monkeypatch.setattr(vpn, "open_url", opened.append)
|
|
489
522
|
monkeypatch.setattr(sys, "stdin", io.StringIO("not-a-real-sudo-password\n"))
|
|
490
523
|
monkeypatch.setattr(vpn.os, "geteuid", lambda: 0)
|
|
491
524
|
monkeypatch.setattr(vpn.shutil, "which", lambda _: "/fake/openvpn")
|
|
@@ -497,6 +530,8 @@ def test_worker_real_sockets_auth_status_shutdown_without_root(store, monkeypatc
|
|
|
497
530
|
# A soft restart (ping-restart, connection-reset) must reconnect, not exit.
|
|
498
531
|
assert "--remap-usr1" not in command and "--connect-retry-max" not in command
|
|
499
532
|
assert command[command.index("--ping") + 1] == "10"
|
|
533
|
+
# Only a profile without a password announces browser sign-in.
|
|
534
|
+
assert ("IV_SSO" in command) == webauth
|
|
500
535
|
endpoint = command[command.index("--management") + 1]
|
|
501
536
|
return popen([sys.executable, "-c", FAKE_OPENVPN, endpoint, mode], **kwargs)
|
|
502
537
|
|
|
@@ -511,7 +546,7 @@ def test_worker_real_sockets_auth_status_shutdown_without_root(store, monkeypatc
|
|
|
511
546
|
if state["state"] == "CONNECTED" and (mode != "reconnect" or state.get("reconnects")):
|
|
512
547
|
assert state["vpn_ip"] == "10.8.0.2"
|
|
513
548
|
assert state["since"] and (state["bytes_in"], state["bytes_out"]) == (1024, 2048)
|
|
514
|
-
assert not fail
|
|
549
|
+
assert not fail and "auth_url" not in state and "auth_expires" not in state
|
|
515
550
|
if mode == "reconnect":
|
|
516
551
|
assert state["reconnects"] == 1 and state["last_reason"] == "connection-reset"
|
|
517
552
|
# Only the newest lines are kept, and a reply larger than one read arrives whole.
|
|
@@ -522,6 +557,7 @@ def test_worker_real_sockets_auth_status_shutdown_without_root(store, monkeypatc
|
|
|
522
557
|
time.sleep(0.05)
|
|
523
558
|
thread.join(5)
|
|
524
559
|
assert not thread.is_alive()
|
|
560
|
+
assert opened == (["https://vpn.example/auth/1"] if webauth else [])
|
|
525
561
|
assert vpn.request("m1")["state"] == "STOPPED"
|
|
526
562
|
base = vpn.session_path("m1")
|
|
527
563
|
state = json.loads(Path(str(base) + ".json").read_text())
|
|
@@ -609,6 +645,27 @@ def test_cert_only_profile_connects_and_lists_without_credentials(store, monkeyp
|
|
|
609
645
|
assert rows == {"cert": "cert", "login": "no"}
|
|
610
646
|
|
|
611
647
|
|
|
648
|
+
def test_connect_shows_the_sign_in_url_once_and_waits_for_it(store, monkeypatch):
|
|
649
|
+
vpn.save_profile("sso", {"config": CERT_ONLY})
|
|
650
|
+
pending = {"state": "AUTH_PENDING", "auth_url": "https://vpn.example/auth/1", "auth_expires": time.time() + 120}
|
|
651
|
+
states = iter([{"state": "STOPPED"}, pending, pending])
|
|
652
|
+
monkeypatch.setattr(vpn, "request", lambda name, command="status": next(states, {"state": "CONNECTED"}))
|
|
653
|
+
monkeypatch.setattr("evo_cli.commands.openvpn.shutil.which", lambda _: "/fake/openvpn")
|
|
654
|
+
monkeypatch.setattr("evo_cli.commands.openvpn.os.geteuid", lambda: 0)
|
|
655
|
+
|
|
656
|
+
class Running:
|
|
657
|
+
def __init__(self):
|
|
658
|
+
self.stdin = io.BytesIO()
|
|
659
|
+
|
|
660
|
+
def poll(self):
|
|
661
|
+
return None
|
|
662
|
+
|
|
663
|
+
monkeypatch.setattr("evo_cli.commands.openvpn.subprocess.Popen", lambda command, **_: Running())
|
|
664
|
+
result = CliRunner().invoke(cli, ["openvpn", "connect", "sso", "--no-watch", "--timeout", "10"])
|
|
665
|
+
assert result.exit_code == 0, result.output
|
|
666
|
+
assert result.output.count("https://vpn.example/auth/1") == 1 and "connected" in result.output
|
|
667
|
+
|
|
668
|
+
|
|
612
669
|
def test_pick_profile_by_number_single_or_prompt(store, monkeypatch):
|
|
613
670
|
runner = CliRunner()
|
|
614
671
|
totp = vpn.parse_totp(SECRET)
|
evo_cli-0.33.0/evo_cli/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.33.0
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|