evo-cli 0.31.3__tar.gz → 0.32.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.31.3 → evo_cli-0.32.0}/PKG-INFO +3 -1
- {evo_cli-0.31.3 → evo_cli-0.32.0}/README.md +2 -0
- evo_cli-0.32.0/evo_cli/VERSION +1 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/openvpn.py +39 -14
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/openvpn.py +76 -24
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli.egg-info/PKG-INFO +3 -1
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_openvpn.py +120 -5
- evo_cli-0.31.3/evo_cli/VERSION +0 -1
- {evo_cli-0.31.3 → evo_cli-0.32.0}/Containerfile +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/HISTORY.md +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/LICENSE +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/MANIFEST.in +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/__init__.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/__main__.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/base.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/cli.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/__init__.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/agent_toy.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/claude_code.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/cloudflare.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/cred.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/download.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/fix_claude.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/gdrive.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/gh.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/__init__.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/_dag.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/_git.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/_hub.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/_mirror.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/_model.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/_mutate.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/_paths.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/_render.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/_server.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/check.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/clone.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/edit.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/pull.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/serve.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/show.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/views.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/web/assets/index-Cs7hXRPJ.css +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/web/assets/index-CuNOlaH7.js +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/harness/web/index.html +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/hwid.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/hwid_reset.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/image.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/localproxy.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/mcp.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/miniconda.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/netcheck.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/ngrok.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/opencode.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/plantuml.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/route/__init__.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/route/_dns.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/route/_dpi.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/route/_hosts.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/route/_probe.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/route/_spoofdpi.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/route/check.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/route/fix.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/route/probe.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/route/status.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/serp.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/site2s.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/ssh.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/storage.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/sysmon.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/tts.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/update.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/commands/wifi.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/console.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/credentials/__init__.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/credentials/doctor.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/credentials/google_oauth.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/credentials/migrate.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/credentials/oauth_flow.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/credentials/registry.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/credentials/store.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/credentials/sync.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/imaging/__init__.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/imaging/align.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/imaging/core.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/imaging/creds.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/imaging/errors.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/imaging/gemini.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/imaging/ncnn.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/mcp_registry.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/serp/__init__.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/serp/client.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/serp/creds.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/serp/errors.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/serp/install.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/serp/render.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/tts/__init__.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/tts/chunking.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/tts/core.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/tts/creds.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/tts/errors.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/tts/gemini.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/tts/http.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/tts/openai.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/tts/player.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli/tts/vbee.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli.egg-info/SOURCES.txt +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli.egg-info/dependency_links.txt +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli.egg-info/entry_points.txt +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli.egg-info/requires.txt +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/evo_cli.egg-info/top_level.txt +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/pyproject.toml +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/setup.cfg +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/__init__.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/fake_evo_agents.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/fixtures/hub/contract-0.2.0.json +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/fixtures/hub/mirror-demo.yaml +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_agent_toy.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_claude_code.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_cli.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_console.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_cred.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_download.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_fix_claude.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_gdrive.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_gh.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_harness.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_harness_clone.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_harness_dag.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_harness_hub.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_harness_show.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_image.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_mcp.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_ngrok.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_opencode.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_plantuml.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_route.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_serp.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_storage.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_sysmon.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_tts.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_update.py +0 -0
- {evo_cli-0.31.3 → evo_cli-0.32.0}/tests/test_wifi.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evo_cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.32.0
|
|
4
4
|
Summary: Evolution CLI - a developer toolbox for setting up dev machines
|
|
5
5
|
Author: maycuatroi
|
|
6
6
|
Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
|
|
@@ -159,6 +159,8 @@ permissions and private-repository access, not an encrypted vault. Do not sync i
|
|
|
159
159
|
a public repository or treat storing the password and OTP together as independent factors.
|
|
160
160
|
|
|
161
161
|
Use `credentials NAME --otp` for hidden manual enrollment input instead of a QR image.
|
|
162
|
+
For a PIN+OTP server whose codes come from a link, `credentials NAME --otp-url` saves the
|
|
163
|
+
https link (hidden input; it is a secret) and each login sends the PIN plus a fresh code.
|
|
162
164
|
`evo openvpn otp NAME` explicitly prints the current code; list and status never print
|
|
163
165
|
secrets. Sudo passwords are not stored. Automation can pass the sudo password on stdin
|
|
164
166
|
with `connect NAME --sudo-password-stdin`, not in an argument or environment variable.
|
|
@@ -126,6 +126,8 @@ permissions and private-repository access, not an encrypted vault. Do not sync i
|
|
|
126
126
|
a public repository or treat storing the password and OTP together as independent factors.
|
|
127
127
|
|
|
128
128
|
Use `credentials NAME --otp` for hidden manual enrollment input instead of a QR image.
|
|
129
|
+
For a PIN+OTP server whose codes come from a link, `credentials NAME --otp-url` saves the
|
|
130
|
+
https link (hidden input; it is a secret) and each login sends the PIN plus a fresh code.
|
|
129
131
|
`evo openvpn otp NAME` explicitly prints the current code; list and status never print
|
|
130
132
|
secrets. Sudo passwords are not stored. Automation can pass the sudo password on stdin
|
|
131
133
|
with `connect NAME --sudo-password-stdin`, not in an argument or environment variable.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.32.0
|
|
@@ -43,7 +43,7 @@ def _table(rows):
|
|
|
43
43
|
state.get("vpn_ip", ""),
|
|
44
44
|
(profile.get("remotes") or [""])[0],
|
|
45
45
|
_creds(profile),
|
|
46
|
-
"yes" if profile.get("totp") else "no",
|
|
46
|
+
"url" if profile.get("otp_url") else "yes" if profile.get("totp") else "no",
|
|
47
47
|
)
|
|
48
48
|
return table
|
|
49
49
|
|
|
@@ -58,6 +58,17 @@ def _creds(profile):
|
|
|
58
58
|
return "no"
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
def _ask(name, command="status"):
|
|
62
|
+
"""vpn.request for polling loops, where a missed 2s reply is no verdict.
|
|
63
|
+
|
|
64
|
+
A worker stops answering for a moment while it fetches an OTP or waits for OpenVPN to exit.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
return _guard(vpn.request, name, command)
|
|
68
|
+
except OSError:
|
|
69
|
+
return {"profile": name, "state": "UNKNOWN", "error": "Worker did not answer in time."}
|
|
70
|
+
|
|
71
|
+
|
|
61
72
|
def _rows():
|
|
62
73
|
profiles = _guard(vpn.profiles)
|
|
63
74
|
return [(name, profiles[name], _guard(vpn.request, name)) for name in sorted(profiles)]
|
|
@@ -233,7 +244,9 @@ def import_profile(name, config, replace):
|
|
|
233
244
|
click.echo(f"Imported {name} into evo cred: openvpn.profiles.{name}")
|
|
234
245
|
|
|
235
246
|
|
|
236
|
-
@openvpn_group.command(
|
|
247
|
+
@openvpn_group.command(
|
|
248
|
+
"credentials", help="Save username/password and optionally an OTP source (TOTP or URL); input is hidden."
|
|
249
|
+
)
|
|
237
250
|
@click.argument("name", required=False)
|
|
238
251
|
@click.option("--username", help="VPN account name; password is prompted, never passed as an argument.")
|
|
239
252
|
@click.option("--password-stdin", is_flag=True, help="Read VPN password from stdin rather than the hidden prompt.")
|
|
@@ -241,22 +254,34 @@ def import_profile(name, config, replace):
|
|
|
241
254
|
@click.option(
|
|
242
255
|
"--qr", type=click.Path(exists=True, dir_okay=False, path_type=Path), help="Read OTP from a local QR image."
|
|
243
256
|
)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
257
|
+
@click.option(
|
|
258
|
+
"--otp-url",
|
|
259
|
+
is_flag=True,
|
|
260
|
+
help="Prompt for a link that answers with the current OTP; each login sends the password (PIN) plus a fresh code.",
|
|
261
|
+
)
|
|
262
|
+
def credentials(name, username, password_stdin, otp, qr, otp_url):
|
|
263
|
+
if otp + bool(qr) + otp_url > 1:
|
|
264
|
+
raise click.UsageError("Use only one of --otp, --qr and --otp-url.")
|
|
247
265
|
name = _pick(name)
|
|
248
266
|
profile = _guard(vpn.get_profile, name)
|
|
249
267
|
username = username or click.prompt("Username", default=profile.get("username", ""))
|
|
250
|
-
|
|
268
|
+
label = "VPN password or PIN, without the OTP" if otp_url or profile.get("otp_url") else "VPN password"
|
|
269
|
+
password = sys.stdin.readline().rstrip("\n") if password_stdin else getpass.getpass(f"{label} (hidden): ")
|
|
251
270
|
if not password:
|
|
252
271
|
raise click.ClickException("Empty password; nothing saved.")
|
|
253
272
|
_guard(vpn.quote, username)
|
|
254
273
|
_guard(vpn.quote, password)
|
|
255
274
|
profile.update(username=username, password=password)
|
|
275
|
+
# One OTP source per profile.
|
|
256
276
|
if qr:
|
|
257
277
|
profile["totp"] = _guard(vpn.qr_totp, qr)
|
|
278
|
+
profile.pop("otp_url", None)
|
|
258
279
|
elif otp:
|
|
259
280
|
profile["totp"] = _guard(vpn.parse_totp, getpass.getpass("OTP enrollment secret/URI (hidden): "))
|
|
281
|
+
profile.pop("otp_url", None)
|
|
282
|
+
elif otp_url:
|
|
283
|
+
profile["otp_url"] = _guard(vpn.parse_otp_url, getpass.getpass("OTP URL (hidden): "))
|
|
284
|
+
profile.pop("totp", None)
|
|
260
285
|
_guard(vpn.save_profile, name, profile)
|
|
261
286
|
click.echo(f"Saved credentials for {name}. Sync explicitly with: evo cred sync push")
|
|
262
287
|
|
|
@@ -265,10 +290,10 @@ def credentials(name, username, password_stdin, otp, qr):
|
|
|
265
290
|
@click.argument("name", required=False)
|
|
266
291
|
def otp_code(name):
|
|
267
292
|
name = _pick(name)
|
|
268
|
-
|
|
269
|
-
if not
|
|
293
|
+
profile = _guard(vpn.get_profile, name)
|
|
294
|
+
if not profile.get("totp") and not profile.get("otp_url"):
|
|
270
295
|
raise click.ClickException("No OTP enrollment for this profile.")
|
|
271
|
-
click.echo(_guard(vpn.
|
|
296
|
+
click.echo(_guard(vpn.current_otp, profile))
|
|
272
297
|
|
|
273
298
|
|
|
274
299
|
@openvpn_group.command("status", help="Query the live worker; stale status files never count as connected.")
|
|
@@ -303,13 +328,13 @@ def disconnect(name):
|
|
|
303
328
|
|
|
304
329
|
|
|
305
330
|
def _disconnect(name):
|
|
306
|
-
state =
|
|
331
|
+
state = _ask(name, "disconnect")
|
|
307
332
|
if state["state"] == "STOPPED" and state.get("error"):
|
|
308
333
|
click.echo(f"{name} was not running; last error: {state['error']}", err=True)
|
|
309
334
|
deadline = time.monotonic() + 20
|
|
310
335
|
while state["state"] != "STOPPED" and time.monotonic() < deadline:
|
|
311
336
|
time.sleep(0.2)
|
|
312
|
-
state =
|
|
337
|
+
state = _ask(name)
|
|
313
338
|
if state["state"] != "STOPPED":
|
|
314
339
|
raise click.ClickException(state.get("error") or "Disconnect not confirmed; inspect evo openvpn status.")
|
|
315
340
|
click.echo(f"{name}: stopped (OpenVPN Connect profiles unchanged).")
|
|
@@ -369,7 +394,7 @@ def connect(name, timeout, sudo_password_stdin, watch):
|
|
|
369
394
|
deadline = time.monotonic() + timeout + 20
|
|
370
395
|
try:
|
|
371
396
|
while time.monotonic() < deadline:
|
|
372
|
-
state =
|
|
397
|
+
state = _ask(name)
|
|
373
398
|
if state["state"] == "CONNECTED":
|
|
374
399
|
click.echo(f"{name}: connected, tunnel IP {state.get('vpn_ip', 'unknown')}")
|
|
375
400
|
break
|
|
@@ -380,8 +405,8 @@ def connect(name, timeout, sudo_password_stdin, watch):
|
|
|
380
405
|
raise click.ClickException("OpenVPN worker exited before startup.")
|
|
381
406
|
time.sleep(0.2)
|
|
382
407
|
finally:
|
|
383
|
-
if
|
|
384
|
-
|
|
408
|
+
if _ask(name)["state"] != "CONNECTED":
|
|
409
|
+
_ask(name, "disconnect")
|
|
385
410
|
if state["state"] != "CONNECTED":
|
|
386
411
|
raise click.ClickException("Connection not confirmed before timeout; disconnect requested.")
|
|
387
412
|
_guard(vpn.remember, name)
|
|
@@ -238,12 +238,14 @@ def remote_addresses(remotes):
|
|
|
238
238
|
|
|
239
239
|
|
|
240
240
|
def pin_remotes(command, remotes):
|
|
241
|
-
"""Wrap OpenVPN so the
|
|
241
|
+
"""Wrap OpenVPN so the given hosts (a remote's first word) stay on the physical gateway while it runs.
|
|
242
242
|
|
|
243
243
|
A split-tunnel profile adds no host route for its own server, so a full-tunnel VPN that is (or later comes)
|
|
244
|
-
up would carry this handshake inside itself and break it on either side's reconnect. The
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
up would carry this handshake inside itself and break it on either side's reconnect. The same holds for
|
|
245
|
+
an OTP URL's host, which a reconnect needs while the routes still lead into the tunnel that is down. The
|
|
246
|
+
wrapper runs as root with OpenVPN: pin each host, run OpenVPN, unpin after it exits. Addresses are
|
|
247
|
+
validated IPs, so they are safe to place in the script; the OpenVPN command is passed as arguments, never
|
|
248
|
+
interpolated.
|
|
247
249
|
"""
|
|
248
250
|
gateway, addresses = default_gateway(), remote_addresses(remotes)
|
|
249
251
|
if not gateway or not addresses:
|
|
@@ -310,6 +312,43 @@ def totp(settings, at=None):
|
|
|
310
312
|
return str(number % (10**digits)).zfill(digits)
|
|
311
313
|
|
|
312
314
|
|
|
315
|
+
def parse_otp_url(value):
|
|
316
|
+
"""An https link that answers with the current OTP as bare digits; checked by fetching one code.
|
|
317
|
+
|
|
318
|
+
The link alone yields valid codes, so it is a secret: errors never echo it.
|
|
319
|
+
"""
|
|
320
|
+
value = value.strip()
|
|
321
|
+
uri = urlparse(value)
|
|
322
|
+
if uri.scheme != "https" or not uri.hostname:
|
|
323
|
+
raise CredentialError("The OTP URL must be an https:// link.")
|
|
324
|
+
fetch_otp(value)
|
|
325
|
+
return value
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def fetch_otp(url):
|
|
329
|
+
import http.client
|
|
330
|
+
import urllib.request
|
|
331
|
+
|
|
332
|
+
# Cloudflare answers the default Python-urllib agent with 403.
|
|
333
|
+
request = urllib.request.Request(url, headers={"User-Agent": "evo-cli"})
|
|
334
|
+
try:
|
|
335
|
+
with urllib.request.urlopen(request, timeout=5) as response:
|
|
336
|
+
code = response.read(64).decode(errors="replace").strip()
|
|
337
|
+
except (OSError, ValueError, http.client.HTTPException):
|
|
338
|
+
raise CredentialError("Cannot fetch the OTP from its URL; check the network and the link.") from None
|
|
339
|
+
if not re.fullmatch(r"\d{6,8}", code):
|
|
340
|
+
raise CredentialError("The OTP URL did not answer with a 6-8 digit code.")
|
|
341
|
+
return code
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def current_otp(profile):
|
|
345
|
+
if profile.get("totp"):
|
|
346
|
+
return totp(profile["totp"])
|
|
347
|
+
if profile.get("otp_url"):
|
|
348
|
+
return fetch_otp(profile["otp_url"])
|
|
349
|
+
raise CredentialError("This profile needs OTP; run evo openvpn credentials PROFILE --otp or --otp-url.")
|
|
350
|
+
|
|
351
|
+
|
|
313
352
|
def qr_totp(path):
|
|
314
353
|
decoder = shutil.which("zbarimg")
|
|
315
354
|
if not decoder:
|
|
@@ -423,6 +462,17 @@ def _reap(name, last):
|
|
|
423
462
|
return state
|
|
424
463
|
|
|
425
464
|
|
|
465
|
+
def _saved_state(name):
|
|
466
|
+
path = Path(str(session_path(name)) + ".json")
|
|
467
|
+
if path.exists():
|
|
468
|
+
last = json.loads(path.read_text())
|
|
469
|
+
if last["state"] not in ("FAILED", "STOPPED") or last.get("cleanup_failed"):
|
|
470
|
+
return _reap(name, last)
|
|
471
|
+
if last.get("error"):
|
|
472
|
+
return {"profile": name, "state": "STOPPED", "error": last["error"]}
|
|
473
|
+
return {"profile": name, "state": "STOPPED"}
|
|
474
|
+
|
|
475
|
+
|
|
426
476
|
def request(name, command="status"):
|
|
427
477
|
endpoint = str(session_path(name)) + ".sock"
|
|
428
478
|
with socket.socket(socket.AF_UNIX) as client:
|
|
@@ -430,20 +480,20 @@ def request(name, command="status"):
|
|
|
430
480
|
try:
|
|
431
481
|
client.connect(endpoint)
|
|
432
482
|
except (FileNotFoundError, ConnectionRefusedError):
|
|
433
|
-
|
|
434
|
-
if path.exists():
|
|
435
|
-
last = json.loads(path.read_text())
|
|
436
|
-
if last["state"] not in ("FAILED", "STOPPED") or last.get("cleanup_failed"):
|
|
437
|
-
return _reap(name, last)
|
|
438
|
-
if last.get("error"):
|
|
439
|
-
return {"profile": name, "state": "STOPPED", "error": last["error"]}
|
|
440
|
-
return {"profile": name, "state": "STOPPED"}
|
|
441
|
-
client.sendall(command.encode() + b"\n")
|
|
483
|
+
return _saved_state(name)
|
|
442
484
|
# The worker closes after one reply; a log reply spans several reads.
|
|
443
485
|
chunks = []
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
486
|
+
try:
|
|
487
|
+
client.sendall(command.encode() + b"\n")
|
|
488
|
+
while chunk := client.recv(65536):
|
|
489
|
+
chunks.append(chunk)
|
|
490
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
491
|
+
chunks = []
|
|
492
|
+
if not chunks:
|
|
493
|
+
# A worker that is shutting down publishes its final state, then closes the control socket with
|
|
494
|
+
# this request still queued (EOF on macOS, reset on Linux). Its outcome is in the saved state.
|
|
495
|
+
return _saved_state(name)
|
|
496
|
+
return json.loads(b"".join(chunks))
|
|
447
497
|
|
|
448
498
|
|
|
449
499
|
def quote(value):
|
|
@@ -464,12 +514,11 @@ def auth_commands(line, profile):
|
|
|
464
514
|
if not username or not password:
|
|
465
515
|
raise CredentialError("Missing username/password; run evo openvpn credentials PROFILE.")
|
|
466
516
|
if "SC:" in line:
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
password
|
|
471
|
-
|
|
472
|
-
)
|
|
517
|
+
code = current_otp(profile)
|
|
518
|
+
password = f"SCRV1:{base64.b64encode(password.encode()).decode()}:{base64.b64encode(code.encode()).decode()}"
|
|
519
|
+
elif profile.get("otp_url"):
|
|
520
|
+
# PIN+OTP servers take the code appended to the password; no challenge announces it.
|
|
521
|
+
password += fetch_otp(profile["otp_url"])
|
|
473
522
|
return f'username "Auth" {quote(username)}\npassword "Auth" {quote(password)}\n'
|
|
474
523
|
|
|
475
524
|
|
|
@@ -543,8 +592,11 @@ def worker(name, timeout):
|
|
|
543
592
|
"--verb",
|
|
544
593
|
"3",
|
|
545
594
|
]
|
|
546
|
-
if
|
|
547
|
-
|
|
595
|
+
pinned = [] if takes_default_route(profile["config"]) else remotes
|
|
596
|
+
if profile.get("otp_url"):
|
|
597
|
+
pinned = [*pinned, urlparse(profile["otp_url"]).hostname]
|
|
598
|
+
if pinned:
|
|
599
|
+
command = pin_remotes(command, pinned)
|
|
548
600
|
if os.geteuid() != 0:
|
|
549
601
|
command = ["sudo", "-S", "-p", "", "--"] + command
|
|
550
602
|
process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evo_cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.32.0
|
|
4
4
|
Summary: Evolution CLI - a developer toolbox for setting up dev machines
|
|
5
5
|
Author: maycuatroi
|
|
6
6
|
Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
|
|
@@ -159,6 +159,8 @@ permissions and private-repository access, not an encrypted vault. Do not sync i
|
|
|
159
159
|
a public repository or treat storing the password and OTP together as independent factors.
|
|
160
160
|
|
|
161
161
|
Use `credentials NAME --otp` for hidden manual enrollment input instead of a QR image.
|
|
162
|
+
For a PIN+OTP server whose codes come from a link, `credentials NAME --otp-url` saves the
|
|
163
|
+
https link (hidden input; it is a secret) and each login sends the PIN plus a fresh code.
|
|
162
164
|
`evo openvpn otp NAME` explicitly prints the current code; list and status never print
|
|
163
165
|
secrets. Sudo passwords are not stored. Automation can pass the sudo password on stdin
|
|
164
166
|
with `connect NAME --sudo-password-stdin`, not in an argument or environment variable.
|
|
@@ -266,6 +266,89 @@ def test_auth_token_is_a_notification_not_a_prompt():
|
|
|
266
266
|
assert vpn.auth_commands(">PASSWORD:Auth-Token:ephemeral-secret", {}) == ""
|
|
267
267
|
|
|
268
268
|
|
|
269
|
+
OTP_URL = "https://otp.example/public/link-secret"
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_otp_url_is_appended_without_a_challenge_and_scrv1_with_one(monkeypatch):
|
|
273
|
+
codes = iter(["123456", "654321"])
|
|
274
|
+
monkeypatch.setattr(vpn, "fetch_otp", lambda url: url == OTP_URL and next(codes))
|
|
275
|
+
profile = {"username": "u", "password": "1307", "otp_url": OTP_URL}
|
|
276
|
+
# A fresh code at each login: OpenVPN asks again on every restart (--auth-nocache).
|
|
277
|
+
assert 'password "Auth" "1307123456"' in vpn.auth_commands("Need 'Auth' username/password", profile)
|
|
278
|
+
line = ">PASSWORD:Need 'Auth' username/password SC:1,OTP"
|
|
279
|
+
assert "SCRV1:MTMwNw==:NjU0MzIx" in vpn.auth_commands(line, profile)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def test_fetch_otp_checks_the_answer_without_echoing_the_link(monkeypatch):
|
|
283
|
+
import urllib.error
|
|
284
|
+
|
|
285
|
+
answers = iter([b"042137\n", b"<html>login</html>", urllib.error.HTTPError(OTP_URL, 403, "no", {}, None)])
|
|
286
|
+
|
|
287
|
+
class Response(io.BytesIO):
|
|
288
|
+
def __enter__(self):
|
|
289
|
+
return self
|
|
290
|
+
|
|
291
|
+
def __exit__(self, *_):
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
def urlopen(request, timeout):
|
|
295
|
+
assert request.get_header("User-agent") == "evo-cli" and timeout <= 5
|
|
296
|
+
answer = next(answers)
|
|
297
|
+
if isinstance(answer, BaseException):
|
|
298
|
+
raise answer
|
|
299
|
+
return Response(answer)
|
|
300
|
+
|
|
301
|
+
monkeypatch.setattr("urllib.request.urlopen", urlopen)
|
|
302
|
+
assert vpn.fetch_otp(OTP_URL) == "042137"
|
|
303
|
+
for _ in range(2):
|
|
304
|
+
with pytest.raises(CredentialError) as exc:
|
|
305
|
+
vpn.fetch_otp(OTP_URL)
|
|
306
|
+
assert "link-secret" not in str(exc.value)
|
|
307
|
+
with pytest.raises(CredentialError) as exc:
|
|
308
|
+
vpn.parse_otp_url("http://otp.example/public/link-secret")
|
|
309
|
+
assert "link-secret" not in str(exc.value)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def test_credentials_otp_url_replaces_totp_and_serves_otp(store, monkeypatch):
|
|
313
|
+
vpn.save_profile("m1", {"config": CONFIG, "totp": vpn.parse_totp(SECRET)})
|
|
314
|
+
prompts = []
|
|
315
|
+
|
|
316
|
+
def getpass(prompt):
|
|
317
|
+
prompts.append(prompt)
|
|
318
|
+
return OTP_URL if prompt.startswith("OTP URL") else "1307"
|
|
319
|
+
|
|
320
|
+
monkeypatch.setattr("evo_cli.commands.openvpn.getpass.getpass", getpass)
|
|
321
|
+
monkeypatch.setattr(vpn, "fetch_otp", lambda url: "042137")
|
|
322
|
+
runner = CliRunner()
|
|
323
|
+
result = runner.invoke(cli, ["openvpn", "credentials", "m1", "--username", "u", "--otp-url"])
|
|
324
|
+
assert result.exit_code == 0, result.output
|
|
325
|
+
assert prompts[0].startswith("VPN password or PIN, without the OTP")
|
|
326
|
+
assert "link-secret" not in result.output and "1307" not in result.output
|
|
327
|
+
profile = vpn.get_profile("m1")
|
|
328
|
+
assert (profile["password"], profile["otp_url"], "totp" in profile) == ("1307", OTP_URL, False)
|
|
329
|
+
assert runner.invoke(cli, ["openvpn", "otp", "m1"]).output.strip() == "042137"
|
|
330
|
+
listing = runner.invoke(cli, ["openvpn", "list"]).output
|
|
331
|
+
assert "url" in listing and "link-secret" not in listing
|
|
332
|
+
assert runner.invoke(cli, ["openvpn", "credentials", "m1", "--otp", "--otp-url"]).exit_code == 2
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def test_worker_pins_the_otp_host_even_for_a_full_tunnel(store, monkeypatch):
|
|
336
|
+
vpn.save_profile("m1", {"config": CONFIG, "username": "u", "password": "1307", "otp_url": OTP_URL})
|
|
337
|
+
monkeypatch.setattr(sys, "stdin", io.StringIO("\n"))
|
|
338
|
+
monkeypatch.setattr(vpn.os, "geteuid", lambda: 0)
|
|
339
|
+
monkeypatch.setattr(vpn.shutil, "which", lambda _: "/fake/openvpn")
|
|
340
|
+
pinned = []
|
|
341
|
+
monkeypatch.setattr(vpn, "pin_remotes", lambda command, hosts: pinned.append(hosts) or command)
|
|
342
|
+
|
|
343
|
+
def popen(command, **_):
|
|
344
|
+
raise OSError("stop before running anything")
|
|
345
|
+
|
|
346
|
+
monkeypatch.setattr(vpn.subprocess, "Popen", popen)
|
|
347
|
+
vpn.worker("m1", 10)
|
|
348
|
+
# Not the full-tunnel server (OpenVPN routes it itself), only the host a reconnect needs for its OTP.
|
|
349
|
+
assert pinned == [["otp.example"]]
|
|
350
|
+
|
|
351
|
+
|
|
269
352
|
def test_plain_password_escaping_and_no_unrequested_otp():
|
|
270
353
|
text = vpn.auth_commands("Need 'Auth' username/password", {"username": "u", "password": 'p"\\'})
|
|
271
354
|
assert text == 'username "Auth" "u"\npassword "Auth" "p\\"\\\\"\n'
|
|
@@ -320,6 +403,41 @@ def test_dead_worker_is_reaped_and_disconnect_confirms_it(store, monkeypatch):
|
|
|
320
403
|
assert vpn.request("m1")["state"] == "STOPPED" # so connect and import accept the profile again
|
|
321
404
|
|
|
322
405
|
|
|
406
|
+
def test_connect_reports_the_failure_of_a_worker_closing_with_a_request_queued(store, monkeypatch):
|
|
407
|
+
import socket
|
|
408
|
+
|
|
409
|
+
vpn.save_profile("m1", {"config": CONFIG, "username": "u", "password": "p"})
|
|
410
|
+
base = str(vpn.session_path("m1"))
|
|
411
|
+
monkeypatch.setattr("evo_cli.commands.openvpn.shutil.which", lambda _: "/fake/openvpn")
|
|
412
|
+
monkeypatch.setattr("evo_cli.commands.openvpn.os.geteuid", lambda: 0)
|
|
413
|
+
closed = threading.Event()
|
|
414
|
+
|
|
415
|
+
class ShuttingDown:
|
|
416
|
+
"""A worker in its finally: final state published, control socket listening but never accepting."""
|
|
417
|
+
|
|
418
|
+
def __init__(self):
|
|
419
|
+
self.stdin = io.BytesIO()
|
|
420
|
+
self.control = socket.socket(socket.AF_UNIX)
|
|
421
|
+
self.control.bind(base + ".sock")
|
|
422
|
+
self.control.listen(2)
|
|
423
|
+
error = "VPN authentication rejected; check password/OTP enrollment before retrying."
|
|
424
|
+
vpn.private_write(Path(base + ".json"), json.dumps({"profile": "m1", "state": "FAILED", "error": error}))
|
|
425
|
+
threading.Timer(0.3, self.close).start()
|
|
426
|
+
|
|
427
|
+
def close(self):
|
|
428
|
+
self.control.close()
|
|
429
|
+
Path(base + ".sock").unlink()
|
|
430
|
+
closed.set()
|
|
431
|
+
|
|
432
|
+
def poll(self):
|
|
433
|
+
return 1 if closed.is_set() else None
|
|
434
|
+
|
|
435
|
+
monkeypatch.setattr("evo_cli.commands.openvpn.subprocess.Popen", lambda command, **_: ShuttingDown())
|
|
436
|
+
result = CliRunner().invoke(cli, ["openvpn", "connect", "m1", "--no-watch"])
|
|
437
|
+
assert result.exit_code == 1 and "VPN authentication rejected" in result.output, result.output
|
|
438
|
+
assert "Expecting value" not in result.output
|
|
439
|
+
|
|
440
|
+
|
|
323
441
|
FAKE_OPENVPN = """
|
|
324
442
|
import socket,sys
|
|
325
443
|
sys.stdin.readline()
|
|
@@ -388,11 +506,8 @@ def test_worker_real_sockets_auth_status_shutdown_without_root(store, monkeypatc
|
|
|
388
506
|
try:
|
|
389
507
|
deadline = time.monotonic() + 8
|
|
390
508
|
while thread.is_alive() and time.monotonic() < deadline:
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
except (ConnectionResetError, json.JSONDecodeError):
|
|
394
|
-
time.sleep(0.05)
|
|
395
|
-
continue
|
|
509
|
+
# No retry on reset or empty replies: request() must read through the worker's shutdown.
|
|
510
|
+
state = vpn.request("m1")
|
|
396
511
|
if state["state"] == "CONNECTED" and (mode != "reconnect" or state.get("reconnects")):
|
|
397
512
|
assert state["vpn_ip"] == "10.8.0.2"
|
|
398
513
|
assert state["since"] and (state["bytes_in"], state["bytes_out"]) == (1024, 2048)
|
evo_cli-0.31.3/evo_cli/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.31.3
|
|
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
|