ndev-stack 0.3.11__tar.gz → 0.3.13__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.11 → ndev_stack-0.3.13}/PKG-INFO +1 -1
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/__init__.py +1 -1
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/doctor.py +16 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/setup.py +6 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/vhost.py +20 -5
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/runtime/nginx_helper.py +195 -7
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/tui.py +157 -62
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev_stack.egg-info/PKG-INFO +1 -1
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/pyproject.toml +1 -1
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/tests/test_opensuse_compat.py +98 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/README.md +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/__main__.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/cli.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/__init__.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/config.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/constants.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/distro.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/github.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/logger.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/manifest.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/modules/__init__.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/modules/base.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/modules/builtin/mailpit/manifest.json +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/modules/builtin/mailpit/module.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/modules/builtin/mongodb/manifest.json +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/modules/builtin/mongodb/module.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/modules/builtin/postgres/manifest.json +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/modules/builtin/postgres/module.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/modules/builtin/redis/manifest.json +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/modules/builtin/redis/module.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/modules/manager.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/common/utils.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/__init__.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/chroot/manager.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/chroot/packages.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/chroot/shell.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/cli.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/available.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/clean.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/ctl.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/current.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/db.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/grok.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/install.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/list.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/logs.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/mailpit.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/modules.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/mongodb.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/postgres.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/reload.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/restart.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/start.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/status.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/stop.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/uninstall.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/update.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/upgrade.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/commands/use.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/php/builder.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/php/downloader.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/php/extensions.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/php/installer.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/php/resolver.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/php/templates.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/runtime/fpm.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/runtime/mailpit.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/runtime/pma.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/runtime/process.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/runtime/services.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/runtime/sockets.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/linux/runtime/upgrade.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/main.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/tui.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/__init__.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/bin/shim.exe +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/cli.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/commands/__init__.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/core/__init__.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/core/db.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/core/elevate.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/core/ext.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/core/fcgi.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/core/grok.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/core/logs.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/core/mailpit.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/core/mkcert.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/core/paths.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/core/php.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/core/pma.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/core/redis_core.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/core/services.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/core/setup.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/core/shim.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/core/upgrade.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/core/vhost.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/templates/vhost.conf.tmpl +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/templates/vhost_ssl.conf.tmpl +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev/win/tui.py +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev_stack.egg-info/SOURCES.txt +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev_stack.egg-info/dependency_links.txt +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev_stack.egg-info/entry_points.txt +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev_stack.egg-info/requires.txt +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/ndev_stack.egg-info/top_level.txt +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/setup.cfg +0 -0
- {ndev_stack-0.3.11 → ndev_stack-0.3.13}/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.13
|
|
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
|
|
@@ -74,6 +74,22 @@ def doctor_cmd():
|
|
|
74
74
|
else:
|
|
75
75
|
table.add_row("mkcert Local SSL", "[yellow]NOT INSTALLED[/yellow]", "Optional for SSL. Run 'ndev upgrade mkcert' to install.")
|
|
76
76
|
|
|
77
|
+
try:
|
|
78
|
+
from ndev.linux.runtime.nginx_helper import is_selinux_enforcing, is_selinux_httpd_permissive
|
|
79
|
+
if is_selinux_enforcing():
|
|
80
|
+
if is_selinux_httpd_permissive():
|
|
81
|
+
table.add_row("SELinux Web Server Access", "[green]OK[/green]", "Enforcing mode, httpd_t is permissive")
|
|
82
|
+
else:
|
|
83
|
+
table.add_row(
|
|
84
|
+
"SELinux Web Server Access",
|
|
85
|
+
"[yellow]RESTRICTED[/yellow]",
|
|
86
|
+
"Enforcing. Nginx cannot access ~/Sites or FastCGI sockets. Run: sudo /usr/sbin/semanage permissive -a httpd_t"
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
table.add_row("SELinux Web Server Access", "[green]OK[/green]", "SELinux is permissive or disabled")
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
|
|
77
93
|
if NDEV_DIR.exists():
|
|
78
94
|
table.add_row("Layout Directory (~/.ndev)", "[green]OK[/green]", f"Exists at {NDEV_DIR}")
|
|
79
95
|
else:
|
|
@@ -71,6 +71,12 @@ def setup_cmd():
|
|
|
71
71
|
except Exception:
|
|
72
72
|
pass
|
|
73
73
|
|
|
74
|
+
try:
|
|
75
|
+
from ndev.linux.runtime.nginx_helper import ensure_selinux_httpd_permissions
|
|
76
|
+
ensure_selinux_httpd_permissions()
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
|
|
74
80
|
console.print("[bold green]MariaDB, Nginx, and Bubblewrap installed and configured successfully![/bold green]\n")
|
|
75
81
|
else:
|
|
76
82
|
# 1. Update Package Lists
|
|
@@ -319,6 +319,9 @@ def vhost_cmd(
|
|
|
319
319
|
get_nginx_vhost_dirs,
|
|
320
320
|
generate_vhost_config,
|
|
321
321
|
ensure_nginx_sites_enabled_included,
|
|
322
|
+
install_nginx_ssl_cert,
|
|
323
|
+
ensure_docroot_accessible,
|
|
324
|
+
ensure_selinux_httpd_permissions,
|
|
322
325
|
add_to_hosts_privileged,
|
|
323
326
|
reload_nginx,
|
|
324
327
|
)
|
|
@@ -338,14 +341,26 @@ def vhost_cmd(
|
|
|
338
341
|
if enabled_dir:
|
|
339
342
|
ensure_nginx_sites_enabled_included()
|
|
340
343
|
|
|
344
|
+
# Install SSL certificates into /etc/nginx/ssl for systemd / SELinux compatibility
|
|
345
|
+
final_cert_path = cert_path
|
|
346
|
+
final_key_path = key_path
|
|
347
|
+
if ssl and cert_path and key_path:
|
|
348
|
+
try:
|
|
349
|
+
final_cert_path, final_key_path = install_nginx_ssl_cert(domain, cert_path, key_path)
|
|
350
|
+
except Exception as e:
|
|
351
|
+
logger.warning(f"Could not install SSL certificate to /etc/nginx/ssl: {e}")
|
|
352
|
+
|
|
353
|
+
ensure_docroot_accessible(root_path)
|
|
354
|
+
ensure_selinux_httpd_permissions()
|
|
355
|
+
|
|
341
356
|
conf_file = vhost_dir / f"{domain}.conf"
|
|
342
357
|
config_template = generate_vhost_config(
|
|
343
358
|
domain=domain,
|
|
344
|
-
root=
|
|
359
|
+
root=str(root_path),
|
|
345
360
|
selected_sock=selected_sock,
|
|
346
361
|
ssl=bool(ssl),
|
|
347
|
-
cert_path=
|
|
348
|
-
key_path=
|
|
362
|
+
cert_path=final_cert_path,
|
|
363
|
+
key_path=final_key_path,
|
|
349
364
|
)
|
|
350
365
|
|
|
351
366
|
enabled_link = None
|
|
@@ -382,8 +397,8 @@ def vhost_cmd(
|
|
|
382
397
|
if enabled_link:
|
|
383
398
|
console.print(f"Symlink : {enabled_link}")
|
|
384
399
|
if ssl:
|
|
385
|
-
console.print(f"SSL Cert : {
|
|
386
|
-
console.print(f"SSL Key : {
|
|
400
|
+
console.print(f"SSL Cert : {final_cert_path}")
|
|
401
|
+
console.print(f"SSL Key : {final_key_path}")
|
|
387
402
|
console.print(f"\nOpen: https://{domain}")
|
|
388
403
|
else:
|
|
389
404
|
console.print(f"\nOpen: http://{domain}")
|
|
@@ -66,6 +66,96 @@ def check_sudo_needs_password() -> bool:
|
|
|
66
66
|
return True
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def run_privileged_command(
|
|
70
|
+
cmd: list[str],
|
|
71
|
+
password: Optional[str] = None,
|
|
72
|
+
check: bool = True,
|
|
73
|
+
timeout: int = 60,
|
|
74
|
+
capture_output: bool = True
|
|
75
|
+
) -> subprocess.CompletedProcess:
|
|
76
|
+
"""
|
|
77
|
+
Run a command with elevated privileges using sudo -n if non-root.
|
|
78
|
+
Validates sudo password first if provided to refresh the sudo credential timestamp.
|
|
79
|
+
"""
|
|
80
|
+
full_cmd = list(cmd)
|
|
81
|
+
if os.geteuid() != 0:
|
|
82
|
+
if password:
|
|
83
|
+
validate_sudo_password(password)
|
|
84
|
+
full_cmd = ["sudo", "-n"] + full_cmd
|
|
85
|
+
return subprocess.run(
|
|
86
|
+
full_cmd,
|
|
87
|
+
check=check,
|
|
88
|
+
timeout=timeout,
|
|
89
|
+
capture_output=capture_output,
|
|
90
|
+
text=True
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def is_selinux_enforcing() -> bool:
|
|
95
|
+
"""Check if SELinux is present and in Enforcing mode."""
|
|
96
|
+
enforce_file = Path("/sys/fs/selinux/enforce")
|
|
97
|
+
if enforce_file.exists():
|
|
98
|
+
try:
|
|
99
|
+
return enforce_file.read_text().strip() == "1"
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
getenforce = shutil.which("getenforce") or "/usr/sbin/getenforce"
|
|
103
|
+
if Path(getenforce).exists():
|
|
104
|
+
try:
|
|
105
|
+
res = subprocess.run([getenforce], capture_output=True, text=True, timeout=2)
|
|
106
|
+
return res.stdout.strip().lower() == "enforcing"
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def is_selinux_httpd_permissive() -> bool:
|
|
113
|
+
"""Check if httpd_t is in SELinux permissive types."""
|
|
114
|
+
if not is_selinux_enforcing():
|
|
115
|
+
return True
|
|
116
|
+
seinfo = shutil.which("seinfo") or "/usr/bin/seinfo"
|
|
117
|
+
if Path(seinfo).exists():
|
|
118
|
+
try:
|
|
119
|
+
res = subprocess.run([seinfo, "--permissive"], capture_output=True, text=True, timeout=3)
|
|
120
|
+
return "httpd_t" in res.stdout
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def ensure_selinux_httpd_permissions(password: Optional[str] = None) -> bool:
|
|
127
|
+
"""
|
|
128
|
+
Ensure SELinux allows Nginx (httpd_t) to access user document roots and FastCGI sockets.
|
|
129
|
+
If SELinux is enforcing and httpd_t is not permissive, add httpd_t to permissive types
|
|
130
|
+
via semanage and enable relevant SELinux booleans.
|
|
131
|
+
"""
|
|
132
|
+
if not is_selinux_enforcing() or is_selinux_httpd_permissive():
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
success = False
|
|
136
|
+
semanage = shutil.which("semanage") or "/usr/sbin/semanage"
|
|
137
|
+
if Path(semanage).exists():
|
|
138
|
+
try:
|
|
139
|
+
run_privileged_command([semanage, "permissive", "-a", "httpd_t"], password=password, check=False)
|
|
140
|
+
success = is_selinux_httpd_permissive()
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.warning(f"Failed to set httpd_t permissive via semanage: {e}")
|
|
143
|
+
|
|
144
|
+
# Also enable booleans as supplementary fallback
|
|
145
|
+
setsebool = shutil.which("setsebool") or "/usr/sbin/setsebool"
|
|
146
|
+
if Path(setsebool).exists():
|
|
147
|
+
try:
|
|
148
|
+
run_privileged_command(
|
|
149
|
+
[setsebool, "-P", "httpd_read_user_content=1", "httpd_enable_homedirs=1", "httpd_can_network_connect=1"],
|
|
150
|
+
password=password,
|
|
151
|
+
check=False
|
|
152
|
+
)
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
return success or is_selinux_httpd_permissive()
|
|
157
|
+
|
|
158
|
+
|
|
69
159
|
def write_privileged_file(target: Path, content: str, password: Optional[str] = None) -> None:
|
|
70
160
|
"""
|
|
71
161
|
Write content to a file, using sudo tee if current process lacks permissions.
|
|
@@ -426,13 +516,24 @@ server {{
|
|
|
426
516
|
|
|
427
517
|
def reload_nginx(password: Optional[str] = None) -> bool:
|
|
428
518
|
"""
|
|
429
|
-
Reload Nginx configuration
|
|
519
|
+
Reload Nginx configuration if active, or start Nginx if stopped.
|
|
430
520
|
"""
|
|
521
|
+
if os.geteuid() != 0 and password:
|
|
522
|
+
validate_sudo_password(password)
|
|
523
|
+
|
|
431
524
|
cmd = None
|
|
432
525
|
if shutil.which("systemctl"):
|
|
433
|
-
|
|
526
|
+
res = subprocess.run(["systemctl", "is-active", "--quiet", "nginx"])
|
|
527
|
+
if res.returncode == 0:
|
|
528
|
+
cmd = ["systemctl", "reload", "nginx"]
|
|
529
|
+
else:
|
|
530
|
+
cmd = ["systemctl", "start", "nginx"]
|
|
434
531
|
elif shutil.which("service"):
|
|
435
|
-
|
|
532
|
+
res = subprocess.run(["service", "nginx", "status"], capture_output=True)
|
|
533
|
+
if res.returncode == 0:
|
|
534
|
+
cmd = ["service", "nginx", "reload"]
|
|
535
|
+
else:
|
|
536
|
+
cmd = ["service", "nginx", "start"]
|
|
436
537
|
else:
|
|
437
538
|
nginx_bin = which_binary("nginx") or shutil.which("nginx")
|
|
438
539
|
if nginx_bin:
|
|
@@ -442,18 +543,102 @@ def reload_nginx(password: Optional[str] = None) -> bool:
|
|
|
442
543
|
return False
|
|
443
544
|
|
|
444
545
|
if os.geteuid() != 0:
|
|
445
|
-
if password:
|
|
446
|
-
validate_sudo_password(password)
|
|
447
546
|
cmd = ["sudo", "-n"] + cmd
|
|
448
547
|
|
|
449
548
|
try:
|
|
450
549
|
res = subprocess.run(cmd, capture_output=True, timeout=10)
|
|
451
550
|
return res.returncode == 0
|
|
452
551
|
except Exception as e:
|
|
453
|
-
logger.warning(f"Could not reload Nginx: {e}")
|
|
552
|
+
logger.warning(f"Could not reload/start Nginx: {e}")
|
|
454
553
|
return False
|
|
455
554
|
|
|
456
555
|
|
|
556
|
+
def install_nginx_ssl_cert(
|
|
557
|
+
domain: str,
|
|
558
|
+
src_cert: Path | str,
|
|
559
|
+
src_key: Path | str,
|
|
560
|
+
password: Optional[str] = None
|
|
561
|
+
) -> Tuple[Path, Path]:
|
|
562
|
+
"""
|
|
563
|
+
Install SSL certificate and key into /etc/nginx/ssl/{domain}.* with proper permissions and SELinux label.
|
|
564
|
+
Fixes 'BIO_new_file() failed (SSL: error:8000000d:system library:BIO_new_file:Permission denied)'
|
|
565
|
+
caused by systemd ProtectHome and SELinux restricting access to ~/.ndev/certs.
|
|
566
|
+
Returns (installed_cert_path, installed_key_path).
|
|
567
|
+
"""
|
|
568
|
+
ssl_dir = Path("/etc/nginx/ssl")
|
|
569
|
+
dest_cert = ssl_dir / f"{domain}.crt"
|
|
570
|
+
dest_key = ssl_dir / f"{domain}.key"
|
|
571
|
+
|
|
572
|
+
cert_content = Path(src_cert).read_text(encoding="utf-8")
|
|
573
|
+
key_content = Path(src_key).read_text(encoding="utf-8")
|
|
574
|
+
|
|
575
|
+
write_privileged_file(dest_cert, cert_content, password=password)
|
|
576
|
+
write_privileged_file(dest_key, key_content, password=password)
|
|
577
|
+
|
|
578
|
+
if os.geteuid() == 0:
|
|
579
|
+
try:
|
|
580
|
+
os.chmod(str(dest_cert), 0o644)
|
|
581
|
+
os.chmod(str(dest_key), 0o644)
|
|
582
|
+
except Exception:
|
|
583
|
+
pass
|
|
584
|
+
else:
|
|
585
|
+
try:
|
|
586
|
+
os.chmod(str(dest_cert), 0o644)
|
|
587
|
+
os.chmod(str(dest_key), 0o644)
|
|
588
|
+
except (PermissionError, OSError):
|
|
589
|
+
subprocess.run(["sudo", "-n", "chmod", "644", str(dest_cert), str(dest_key)], check=False)
|
|
590
|
+
|
|
591
|
+
if shutil.which("restorecon"):
|
|
592
|
+
cmd = ["restorecon", "-Rv", str(ssl_dir)]
|
|
593
|
+
if os.geteuid() != 0:
|
|
594
|
+
cmd = ["sudo", "-n"] + cmd
|
|
595
|
+
try:
|
|
596
|
+
subprocess.run(cmd, capture_output=True)
|
|
597
|
+
except Exception:
|
|
598
|
+
pass
|
|
599
|
+
|
|
600
|
+
return dest_cert, dest_key
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def ensure_docroot_accessible(root_path: Path) -> None:
|
|
604
|
+
"""
|
|
605
|
+
Ensure the parent directories up to root_path have execute permission (+x)
|
|
606
|
+
so the nginx worker process (e.g. user nginx) can traverse into document root.
|
|
607
|
+
"""
|
|
608
|
+
home = Path.home()
|
|
609
|
+
sudo_user = os.environ.get("SUDO_USER")
|
|
610
|
+
if sudo_user:
|
|
611
|
+
try:
|
|
612
|
+
import pwd
|
|
613
|
+
home = Path(pwd.getpwnam(sudo_user).pw_dir)
|
|
614
|
+
except Exception:
|
|
615
|
+
pass
|
|
616
|
+
|
|
617
|
+
try:
|
|
618
|
+
resolved_root = root_path.resolve()
|
|
619
|
+
resolved_home = home.resolve()
|
|
620
|
+
|
|
621
|
+
# Traverse every directory from root_path up to home and ensure +x (0o001)
|
|
622
|
+
curr = resolved_root
|
|
623
|
+
while curr != curr.parent:
|
|
624
|
+
try:
|
|
625
|
+
st = curr.stat()
|
|
626
|
+
if not (st.st_mode & 0o001):
|
|
627
|
+
os.chmod(str(curr), st.st_mode | 0o001)
|
|
628
|
+
except Exception:
|
|
629
|
+
pass
|
|
630
|
+
if curr == resolved_home:
|
|
631
|
+
break
|
|
632
|
+
curr = curr.parent
|
|
633
|
+
|
|
634
|
+
# Also ensure home directory itself has +x
|
|
635
|
+
st = resolved_home.stat()
|
|
636
|
+
if not (st.st_mode & 0o001):
|
|
637
|
+
os.chmod(str(resolved_home), st.st_mode | 0o001)
|
|
638
|
+
except Exception:
|
|
639
|
+
pass
|
|
640
|
+
|
|
641
|
+
|
|
457
642
|
def list_vhosts() -> List[Dict[str, Any]]:
|
|
458
643
|
"""List all configured virtual hosts across all searched Nginx config directories."""
|
|
459
644
|
vhosts = []
|
|
@@ -513,8 +698,11 @@ def remove_vhost(domain: str, password: Optional[str] = None) -> bool:
|
|
|
513
698
|
except Exception as e:
|
|
514
699
|
logger.warning(f"Could not remove {conf_file}: {e}")
|
|
515
700
|
|
|
516
|
-
# Remove optional certificates
|
|
701
|
+
# Remove system and optional certificates
|
|
517
702
|
for cert_ext in [".crt", ".key"]:
|
|
703
|
+
sys_cert = Path(f"/etc/nginx/ssl/{clean_domain}{cert_ext}")
|
|
704
|
+
unlink_privileged(sys_cert, password=password)
|
|
705
|
+
|
|
518
706
|
cfile = NDEV_DIR / "certs" / f"{clean_domain}{cert_ext}"
|
|
519
707
|
if cfile.exists():
|
|
520
708
|
try:
|