ndev-stack 0.3.1__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/PKG-INFO +1 -1
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/__init__.py +1 -1
- ndev_stack-0.3.2/ndev/linux/commands/grok.py +97 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/cli.py +10 -3
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/fcgi.py +7 -3
- ndev_stack-0.3.2/ndev/win/core/grok.py +95 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/vhost.py +9 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/templates/vhost.conf.tmpl +2 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/templates/vhost_ssl.conf.tmpl +2 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev_stack.egg-info/PKG-INFO +1 -1
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/pyproject.toml +1 -1
- ndev_stack-0.3.1/ndev/linux/commands/grok.py +0 -74
- ndev_stack-0.3.1/ndev/win/core/grok.py +0 -54
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/README.md +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/__main__.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/cli.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/__init__.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/config.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/constants.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/github.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/logger.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/manifest.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/__init__.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/base.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/builtin/mailpit/manifest.json +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/builtin/mailpit/module.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/builtin/mongodb/manifest.json +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/builtin/mongodb/module.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/builtin/postgres/manifest.json +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/builtin/postgres/module.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/builtin/redis/manifest.json +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/builtin/redis/module.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/manager.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/utils.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/__init__.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/chroot/manager.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/chroot/packages.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/chroot/shell.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/cli.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/available.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/clean.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/ctl.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/current.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/db.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/doctor.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/install.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/list.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/logs.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/mailpit.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/modules.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/mongodb.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/postgres.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/reload.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/restart.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/setup.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/start.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/status.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/stop.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/uninstall.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/update.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/upgrade.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/use.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/vhost.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/php/builder.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/php/downloader.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/php/extensions.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/php/installer.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/php/resolver.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/php/templates.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/runtime/fpm.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/runtime/mailpit.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/runtime/pma.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/runtime/process.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/runtime/sockets.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/runtime/upgrade.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/tui.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/main.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/tui.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/__init__.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/bin/shim.exe +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/commands/__init__.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/__init__.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/db.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/elevate.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/ext.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/logs.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/mailpit.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/mkcert.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/paths.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/php.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/pma.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/redis_core.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/services.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/setup.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/shim.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/upgrade.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/tui.py +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev_stack.egg-info/SOURCES.txt +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev_stack.egg-info/dependency_links.txt +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev_stack.egg-info/entry_points.txt +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev_stack.egg-info/requires.txt +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev_stack.egg-info/top_level.txt +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/setup.cfg +0 -0
- {ndev_stack-0.3.1 → ndev_stack-0.3.2}/tests/test_shim.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ndev-stack
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Cross-platform developer stack manager for PHP, FastCGI/PHP-FPM, MariaDB, Redis, PostgreSQL, MongoDB, Nginx, SSL, and Mailpit on Linux and Windows
|
|
5
5
|
Author-email: Ankit Karan <ankit.karan99@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/ankitkaran99/ndev
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import subprocess
|
|
3
|
+
import shutil
|
|
4
|
+
import typer
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from ndev.common.logger import logger
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
def get_vhost_info() -> list[dict]:
|
|
12
|
+
nginx_dir = Path("/etc/nginx/sites-enabled")
|
|
13
|
+
vhosts = []
|
|
14
|
+
if not nginx_dir.exists():
|
|
15
|
+
return vhosts
|
|
16
|
+
|
|
17
|
+
for file in nginx_dir.glob("*.conf"):
|
|
18
|
+
try:
|
|
19
|
+
content = file.read_text()
|
|
20
|
+
domain = None
|
|
21
|
+
for line in content.splitlines():
|
|
22
|
+
line = line.strip()
|
|
23
|
+
if line.startswith("server_name"):
|
|
24
|
+
parts = line.split()
|
|
25
|
+
for p in parts[1:]:
|
|
26
|
+
p = p.rstrip(";").strip()
|
|
27
|
+
if p and p != "_":
|
|
28
|
+
domain = p
|
|
29
|
+
break
|
|
30
|
+
if domain:
|
|
31
|
+
break
|
|
32
|
+
if domain:
|
|
33
|
+
has_ssl = "listen 443 ssl" in content or "listen [::]:443 ssl" in content
|
|
34
|
+
vhosts.append({"domain": domain, "ssl": has_ssl})
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
return vhosts
|
|
38
|
+
|
|
39
|
+
def grok_cmd():
|
|
40
|
+
"""Proxy local Nginx vhosts over public internet using Ngrok."""
|
|
41
|
+
if not shutil.which("ngrok"):
|
|
42
|
+
logger.error("ngrok is not installed or not in PATH.")
|
|
43
|
+
raise typer.Exit(code=1)
|
|
44
|
+
|
|
45
|
+
vhosts = get_vhost_info()
|
|
46
|
+
if not vhosts:
|
|
47
|
+
logger.error("No vhosts found in /etc/nginx/sites-enabled.")
|
|
48
|
+
raise typer.Exit(code=1)
|
|
49
|
+
|
|
50
|
+
console.print("\n[bold]Available VHosts[/bold]")
|
|
51
|
+
console.print("----------------")
|
|
52
|
+
for i, v in enumerate(vhosts):
|
|
53
|
+
ssl_tag = " (HTTPS)" if v["ssl"] else ""
|
|
54
|
+
console.print(f" {i + 1}) {v['domain']}{ssl_tag}")
|
|
55
|
+
|
|
56
|
+
console.print("")
|
|
57
|
+
choice = typer.prompt("Select vhost index", type=int)
|
|
58
|
+
if choice < 1 or choice > len(vhosts):
|
|
59
|
+
logger.error("Invalid selection.")
|
|
60
|
+
raise typer.Exit(code=1)
|
|
61
|
+
|
|
62
|
+
selected = vhosts[choice - 1]
|
|
63
|
+
domain = selected["domain"]
|
|
64
|
+
is_ssl = selected["ssl"]
|
|
65
|
+
|
|
66
|
+
policy_content = f'''on_http_request:
|
|
67
|
+
- actions:
|
|
68
|
+
- type: add-headers
|
|
69
|
+
config:
|
|
70
|
+
headers:
|
|
71
|
+
Host: "{domain}"
|
|
72
|
+
|
|
73
|
+
on_http_response:
|
|
74
|
+
- expressions:
|
|
75
|
+
- 'res.status_code >= 300 && res.status_code < 400 && "location" in res.headers'
|
|
76
|
+
- 'res.headers["location"][0].contains("://{domain}")'
|
|
77
|
+
actions:
|
|
78
|
+
- type: add-headers
|
|
79
|
+
config:
|
|
80
|
+
headers:
|
|
81
|
+
Location: '${{res.headers["location"][0].replace("https://{domain}", conn.server_name.size() > 0 ? "https://" + conn.server_name : "").replace("http://{domain}", conn.server_name.size() > 0 ? "https://" + conn.server_name : "")}}'
|
|
82
|
+
'''
|
|
83
|
+
|
|
84
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
|
|
85
|
+
tmp.write(policy_content)
|
|
86
|
+
policy_file = tmp.name
|
|
87
|
+
|
|
88
|
+
logger.info(f"Selected domain: {domain}{' (HTTPS)' if is_ssl else ''}")
|
|
89
|
+
logger.info("Starting ngrok...")
|
|
90
|
+
target = "https://localhost:443" if is_ssl else "80"
|
|
91
|
+
cmd = ["ngrok", "http", target, "--traffic-policy-file", policy_file]
|
|
92
|
+
if is_ssl:
|
|
93
|
+
cmd.append("--upstream-tls-verify=false")
|
|
94
|
+
try:
|
|
95
|
+
subprocess.run(cmd)
|
|
96
|
+
finally:
|
|
97
|
+
Path(policy_file).unlink(missing_ok=True)
|
|
@@ -1877,7 +1877,7 @@ def redis_cli(port: int):
|
|
|
1877
1877
|
|
|
1878
1878
|
@main.command(name="grok")
|
|
1879
1879
|
@click.option("--domain", default=None, help="Vhost domain to tunnel. Omit to select interactively.")
|
|
1880
|
-
@click.option("--ssl",
|
|
1880
|
+
@click.option("--ssl/--no-ssl", default=None, help="Tunnel HTTPS port 443 (defaults to auto-detecting vhost SSL)")
|
|
1881
1881
|
def grok(domain, ssl):
|
|
1882
1882
|
"""Tunnel a local virtual host to the public web via ngrok."""
|
|
1883
1883
|
if not domain:
|
|
@@ -1897,12 +1897,19 @@ def grok(domain, ssl):
|
|
|
1897
1897
|
raise click.ClickException("Invalid selection.")
|
|
1898
1898
|
|
|
1899
1899
|
domain = re.sub(r"^https?://", "", domain.strip().lower()).rstrip("/")
|
|
1900
|
-
|
|
1901
|
-
|
|
1900
|
+
v_meta = vhost_core.get_vhost(domain)
|
|
1901
|
+
is_ssl = ssl if ssl is not None else bool(v_meta and v_meta.get("ssl"))
|
|
1902
|
+
console.print(f"Starting ngrok tunnel for [bold cyan]{domain}[/bold cyan]{' (HTTPS)' if is_ssl else ''} -- Ctrl+C to stop.")
|
|
1903
|
+
proc, policy_file = grok_core.start_tunnel(domain, ssl=ssl)
|
|
1902
1904
|
try:
|
|
1903
1905
|
proc.wait()
|
|
1904
1906
|
except KeyboardInterrupt:
|
|
1905
1907
|
proc.terminate()
|
|
1908
|
+
finally:
|
|
1909
|
+
try:
|
|
1910
|
+
policy_file.unlink(missing_ok=True)
|
|
1911
|
+
except Exception:
|
|
1912
|
+
pass
|
|
1906
1913
|
|
|
1907
1914
|
|
|
1908
1915
|
@main.command(name="tunnel", hidden=True)
|
|
@@ -209,8 +209,12 @@ def nginx_upstream_name(version: str, domain: str | None = None) -> str:
|
|
|
209
209
|
|
|
210
210
|
def render_upstream_block(version: str, domain: str | None = None) -> str:
|
|
211
211
|
workers = status(version)
|
|
212
|
-
if
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
if workers:
|
|
213
|
+
servers = "\n".join(f" server 127.0.0.1:{w.port};" for w in workers)
|
|
214
|
+
else:
|
|
215
|
+
cfg = paths.load_config()
|
|
216
|
+
ports = ports_for(version, cfg["fcgi_workers_per_version"], cfg["fcgi_base_port"])
|
|
217
|
+
servers = "\n".join(f" server 127.0.0.1:{p};" for p in ports)
|
|
215
218
|
u_name = nginx_upstream_name(version, domain=domain)
|
|
216
219
|
return f"upstream {u_name} {{\n{servers}\n}}\n"
|
|
220
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ngrok tunnel wrapper for ndev-win virtual hosts.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from . import paths, vhost
|
|
12
|
+
|
|
13
|
+
HTTP_PORT = 80
|
|
14
|
+
HTTPS_PORT = 443
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def list_vhosts() -> list[str]:
|
|
18
|
+
"""Return list of domain names with configured Nginx virtual hosts."""
|
|
19
|
+
return [v["domain"] for v in vhost.list_vhosts()]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _ngrok_exe() -> str:
|
|
23
|
+
# 1. Check shim dir
|
|
24
|
+
shim_exe = paths.SHIM_DIR / "ngrok.exe"
|
|
25
|
+
if shim_exe.exists():
|
|
26
|
+
return str(shim_exe)
|
|
27
|
+
|
|
28
|
+
# 2. Check config
|
|
29
|
+
cfg = paths.load_config()
|
|
30
|
+
path = cfg.get("ngrok_path")
|
|
31
|
+
if path and Path(path).exists():
|
|
32
|
+
return str(path)
|
|
33
|
+
|
|
34
|
+
# 3. Check system PATH
|
|
35
|
+
which_path = shutil.which("ngrok") or shutil.which("ngrok.exe")
|
|
36
|
+
if which_path:
|
|
37
|
+
return which_path
|
|
38
|
+
|
|
39
|
+
raise FileNotFoundError("ngrok isn't installed -- run `ndev setup` first")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def start_tunnel(domain: str, ssl: bool | None = None) -> tuple[subprocess.Popen, Path]:
|
|
43
|
+
"""
|
|
44
|
+
Start `ngrok http` against Nginx.
|
|
45
|
+
Rewrites the request Host header to `domain` and rewrites any response
|
|
46
|
+
redirect Location headers from the local domain back to the ngrok public URL.
|
|
47
|
+
Returns (Popen, policy_file_path).
|
|
48
|
+
"""
|
|
49
|
+
import re
|
|
50
|
+
clean_domain = re.sub(r"^https?://", "", domain.strip().lower()).rstrip("/")
|
|
51
|
+
vhost_meta = vhost.get_vhost(clean_domain)
|
|
52
|
+
known = list_vhosts()
|
|
53
|
+
if not vhost_meta and clean_domain not in known:
|
|
54
|
+
raise FileNotFoundError(
|
|
55
|
+
f"No vhost found for '{clean_domain}'. Known vhosts: {known or '(none)'}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Determine SSL mode: use explicit flag if provided, otherwise auto-detect from vhost config
|
|
59
|
+
is_ssl = ssl if ssl is not None else bool(vhost_meta and vhost_meta.get("ssl"))
|
|
60
|
+
|
|
61
|
+
target = f"https://localhost:{HTTPS_PORT}" if is_ssl else str(HTTP_PORT)
|
|
62
|
+
|
|
63
|
+
# Build traffic policy YAML to ensure:
|
|
64
|
+
# 1. Request Host header is rewritten to clean_domain so Nginx routes to the right vhost.
|
|
65
|
+
# 2. Response Location header rewriting prevents external devices from being redirected
|
|
66
|
+
# to the unresolvable local domain (e.g. *.local).
|
|
67
|
+
policy_content = f'''on_http_request:
|
|
68
|
+
- actions:
|
|
69
|
+
- type: add-headers
|
|
70
|
+
config:
|
|
71
|
+
headers:
|
|
72
|
+
Host: "{clean_domain}"
|
|
73
|
+
|
|
74
|
+
on_http_response:
|
|
75
|
+
- expressions:
|
|
76
|
+
- 'res.status_code >= 300 && res.status_code < 400 && "location" in res.headers'
|
|
77
|
+
- 'res.headers["location"][0].contains("://{clean_domain}")'
|
|
78
|
+
actions:
|
|
79
|
+
- type: add-headers
|
|
80
|
+
config:
|
|
81
|
+
headers:
|
|
82
|
+
Location: '${{res.headers["location"][0].replace("https://{clean_domain}", conn.server_name.size() > 0 ? "https://" + conn.server_name : "").replace("http://{clean_domain}", conn.server_name.size() > 0 ? "https://" + conn.server_name : "")}}'
|
|
83
|
+
'''
|
|
84
|
+
|
|
85
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
|
|
86
|
+
tmp.write(policy_content)
|
|
87
|
+
policy_file = Path(tmp.name)
|
|
88
|
+
|
|
89
|
+
cmd = [_ngrok_exe(), "http", target, "--traffic-policy-file", str(policy_file)]
|
|
90
|
+
if is_ssl:
|
|
91
|
+
cmd.append("--upstream-tls-verify=false")
|
|
92
|
+
|
|
93
|
+
proc = subprocess.Popen(cmd)
|
|
94
|
+
return proc, policy_file
|
|
95
|
+
|
|
@@ -286,3 +286,12 @@ def list_vhosts() -> list[dict]:
|
|
|
286
286
|
"conf": str(p),
|
|
287
287
|
})
|
|
288
288
|
return vhosts
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def get_vhost(domain: str) -> dict | None:
|
|
292
|
+
"""Get virtual host metadata for a specific domain name."""
|
|
293
|
+
clean_domain = re.sub(r"^https?://", "", domain.strip().lower()).rstrip("/")
|
|
294
|
+
for v in list_vhosts():
|
|
295
|
+
if v["domain"].lower() == clean_domain:
|
|
296
|
+
return v
|
|
297
|
+
return None
|
|
@@ -20,6 +20,8 @@ server {{
|
|
|
20
20
|
include fastcgi_params;
|
|
21
21
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
|
22
22
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
|
23
|
+
fastcgi_param HTTP_X_FORWARDED_HOST $http_x_forwarded_host if_not_empty;
|
|
24
|
+
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto if_not_empty;
|
|
23
25
|
fastcgi_buffer_size 32k;
|
|
24
26
|
fastcgi_buffers 16 16k;
|
|
25
27
|
fastcgi_busy_buffers_size 64k;
|
|
@@ -30,6 +30,8 @@ server {{
|
|
|
30
30
|
include fastcgi_params;
|
|
31
31
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
|
32
32
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
|
33
|
+
fastcgi_param HTTP_X_FORWARDED_HOST $http_x_forwarded_host if_not_empty;
|
|
34
|
+
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto if_not_empty;
|
|
33
35
|
fastcgi_buffer_size 32k;
|
|
34
36
|
fastcgi_buffers 16 16k;
|
|
35
37
|
fastcgi_busy_buffers_size 64k;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ndev-stack
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Cross-platform developer stack manager for PHP, FastCGI/PHP-FPM, MariaDB, Redis, PostgreSQL, MongoDB, Nginx, SSL, and Mailpit on Linux and Windows
|
|
5
5
|
Author-email: Ankit Karan <ankit.karan99@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/ankitkaran99/ndev
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ndev-stack"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Cross-platform developer stack manager for PHP, FastCGI/PHP-FPM, MariaDB, Redis, PostgreSQL, MongoDB, Nginx, SSL, and Mailpit on Linux and Windows"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import tempfile
|
|
2
|
-
import subprocess
|
|
3
|
-
import shutil
|
|
4
|
-
import typer
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from rich.console import Console
|
|
7
|
-
from ndev.common.logger import logger
|
|
8
|
-
|
|
9
|
-
console = Console()
|
|
10
|
-
|
|
11
|
-
def get_vhosts() -> list[str]:
|
|
12
|
-
nginx_dir = Path("/etc/nginx/sites-enabled")
|
|
13
|
-
domains = []
|
|
14
|
-
if not nginx_dir.exists():
|
|
15
|
-
return domains
|
|
16
|
-
|
|
17
|
-
for file in nginx_dir.glob("*.conf"):
|
|
18
|
-
try:
|
|
19
|
-
content = file.read_text()
|
|
20
|
-
for line in content.splitlines():
|
|
21
|
-
line = line.strip()
|
|
22
|
-
if line.startswith("server_name"):
|
|
23
|
-
parts = line.split()
|
|
24
|
-
for p in parts[1:]:
|
|
25
|
-
p = p.rstrip(";").strip()
|
|
26
|
-
if p and p != "_":
|
|
27
|
-
domains.append(p)
|
|
28
|
-
break
|
|
29
|
-
except Exception:
|
|
30
|
-
pass
|
|
31
|
-
return domains
|
|
32
|
-
|
|
33
|
-
def grok_cmd():
|
|
34
|
-
"""Proxy local Nginx vhosts over public internet using Ngrok."""
|
|
35
|
-
if not shutil.which("ngrok"):
|
|
36
|
-
logger.error("ngrok is not installed or not in PATH.")
|
|
37
|
-
raise typer.Exit(code=1)
|
|
38
|
-
|
|
39
|
-
domains = get_vhosts()
|
|
40
|
-
if not domains:
|
|
41
|
-
logger.error("No vhosts found in /etc/nginx/sites-enabled.")
|
|
42
|
-
raise typer.Exit(code=1)
|
|
43
|
-
|
|
44
|
-
console.print("\n[bold]Available VHosts[/bold]")
|
|
45
|
-
console.print("----------------")
|
|
46
|
-
for i, d in enumerate(domains):
|
|
47
|
-
console.print(f" {i + 1}) {d}")
|
|
48
|
-
|
|
49
|
-
console.print("")
|
|
50
|
-
choice = typer.prompt("Select vhost index", type=int)
|
|
51
|
-
if choice < 1 or choice > len(domains):
|
|
52
|
-
logger.error("Invalid selection.")
|
|
53
|
-
raise typer.Exit(code=1)
|
|
54
|
-
|
|
55
|
-
domain = domains[choice - 1]
|
|
56
|
-
|
|
57
|
-
policy_content = f"""on_http_request:
|
|
58
|
-
- actions:
|
|
59
|
-
- type: add-headers
|
|
60
|
-
config:
|
|
61
|
-
headers:
|
|
62
|
-
Host: {domain}
|
|
63
|
-
"""
|
|
64
|
-
|
|
65
|
-
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
|
|
66
|
-
tmp.write(policy_content)
|
|
67
|
-
policy_file = tmp.name
|
|
68
|
-
|
|
69
|
-
logger.info(f"Selected domain: {domain}")
|
|
70
|
-
logger.info("Starting ngrok...")
|
|
71
|
-
try:
|
|
72
|
-
subprocess.run(["ngrok", "http", "80", "--traffic-policy-file", policy_file])
|
|
73
|
-
finally:
|
|
74
|
-
Path(policy_file).unlink(missing_ok=True)
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
ngrok tunnel wrapper for ndev-win virtual hosts.
|
|
3
|
-
"""
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import shutil
|
|
7
|
-
import subprocess
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
|
|
10
|
-
from . import paths, vhost
|
|
11
|
-
|
|
12
|
-
HTTP_PORT = 80
|
|
13
|
-
HTTPS_PORT = 443
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def list_vhosts() -> list[str]:
|
|
17
|
-
"""Return list of domain names with configured Nginx virtual hosts."""
|
|
18
|
-
return [v["domain"] for v in vhost.list_vhosts()]
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def _ngrok_exe() -> str:
|
|
22
|
-
# 1. Check shim dir
|
|
23
|
-
shim_exe = paths.SHIM_DIR / "ngrok.exe"
|
|
24
|
-
if shim_exe.exists():
|
|
25
|
-
return str(shim_exe)
|
|
26
|
-
|
|
27
|
-
# 2. Check config
|
|
28
|
-
cfg = paths.load_config()
|
|
29
|
-
path = cfg.get("ngrok_path")
|
|
30
|
-
if path and Path(path).exists():
|
|
31
|
-
return str(path)
|
|
32
|
-
|
|
33
|
-
# 3. Check system PATH
|
|
34
|
-
which_path = shutil.which("ngrok") or shutil.which("ngrok.exe")
|
|
35
|
-
if which_path:
|
|
36
|
-
return which_path
|
|
37
|
-
|
|
38
|
-
raise FileNotFoundError("ngrok isn't installed -- run `ndev setup` first")
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def start_tunnel(domain: str, ssl: bool = False) -> subprocess.Popen:
|
|
42
|
-
"""
|
|
43
|
-
Start `ngrok http` against Nginx, rewriting Host header to `domain`.
|
|
44
|
-
"""
|
|
45
|
-
import re
|
|
46
|
-
clean_domain = re.sub(r"^https?://", "", domain.strip().lower()).rstrip("/")
|
|
47
|
-
known = list_vhosts()
|
|
48
|
-
if clean_domain not in known:
|
|
49
|
-
raise FileNotFoundError(
|
|
50
|
-
f"No vhost found for '{clean_domain}'. Known vhosts: {known or '(none)'}"
|
|
51
|
-
)
|
|
52
|
-
target = f"https://localhost:{HTTPS_PORT}" if ssl else str(HTTP_PORT)
|
|
53
|
-
cmd = [_ngrok_exe(), "http", target, f"--host-header={clean_domain}"]
|
|
54
|
-
return subprocess.Popen(cmd)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|