ndev-stack 0.3.12__tar.gz → 0.3.14__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.
Files changed (106) hide show
  1. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/PKG-INFO +1 -1
  2. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/__init__.py +1 -1
  3. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/doctor.py +16 -0
  4. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/setup.py +70 -16
  5. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/vhost.py +2 -0
  6. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/runtime/nginx_helper.py +181 -4
  7. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/tui.py +141 -60
  8. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev_stack.egg-info/PKG-INFO +1 -1
  9. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/pyproject.toml +1 -1
  10. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/tests/test_opensuse_compat.py +77 -0
  11. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/README.md +0 -0
  12. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/__main__.py +0 -0
  13. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/cli.py +0 -0
  14. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/__init__.py +0 -0
  15. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/config.py +0 -0
  16. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/constants.py +0 -0
  17. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/distro.py +0 -0
  18. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/github.py +0 -0
  19. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/logger.py +0 -0
  20. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/manifest.py +0 -0
  21. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/modules/__init__.py +0 -0
  22. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/modules/base.py +0 -0
  23. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/modules/builtin/mailpit/manifest.json +0 -0
  24. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/modules/builtin/mailpit/module.py +0 -0
  25. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/modules/builtin/mongodb/manifest.json +0 -0
  26. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/modules/builtin/mongodb/module.py +0 -0
  27. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/modules/builtin/postgres/manifest.json +0 -0
  28. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/modules/builtin/postgres/module.py +0 -0
  29. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/modules/builtin/redis/manifest.json +0 -0
  30. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/modules/builtin/redis/module.py +0 -0
  31. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/modules/manager.py +0 -0
  32. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/common/utils.py +0 -0
  33. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/__init__.py +0 -0
  34. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/chroot/manager.py +0 -0
  35. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/chroot/packages.py +0 -0
  36. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/chroot/shell.py +0 -0
  37. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/cli.py +0 -0
  38. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/available.py +0 -0
  39. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/clean.py +0 -0
  40. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/ctl.py +0 -0
  41. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/current.py +0 -0
  42. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/db.py +0 -0
  43. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/grok.py +0 -0
  44. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/install.py +0 -0
  45. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/list.py +0 -0
  46. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/logs.py +0 -0
  47. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/mailpit.py +0 -0
  48. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/modules.py +0 -0
  49. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/mongodb.py +0 -0
  50. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/postgres.py +0 -0
  51. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/reload.py +0 -0
  52. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/restart.py +0 -0
  53. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/start.py +0 -0
  54. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/status.py +0 -0
  55. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/stop.py +0 -0
  56. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/uninstall.py +0 -0
  57. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/update.py +0 -0
  58. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/upgrade.py +0 -0
  59. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/commands/use.py +0 -0
  60. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/php/builder.py +0 -0
  61. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/php/downloader.py +0 -0
  62. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/php/extensions.py +0 -0
  63. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/php/installer.py +0 -0
  64. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/php/resolver.py +0 -0
  65. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/php/templates.py +0 -0
  66. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/runtime/fpm.py +0 -0
  67. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/runtime/mailpit.py +0 -0
  68. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/runtime/pma.py +0 -0
  69. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/runtime/process.py +0 -0
  70. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/runtime/services.py +0 -0
  71. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/runtime/sockets.py +0 -0
  72. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/linux/runtime/upgrade.py +0 -0
  73. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/main.py +0 -0
  74. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/tui.py +0 -0
  75. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/__init__.py +0 -0
  76. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/bin/shim.exe +0 -0
  77. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/cli.py +0 -0
  78. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/commands/__init__.py +0 -0
  79. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/core/__init__.py +0 -0
  80. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/core/db.py +0 -0
  81. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/core/elevate.py +0 -0
  82. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/core/ext.py +0 -0
  83. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/core/fcgi.py +0 -0
  84. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/core/grok.py +0 -0
  85. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/core/logs.py +0 -0
  86. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/core/mailpit.py +0 -0
  87. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/core/mkcert.py +0 -0
  88. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/core/paths.py +0 -0
  89. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/core/php.py +0 -0
  90. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/core/pma.py +0 -0
  91. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/core/redis_core.py +0 -0
  92. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/core/services.py +0 -0
  93. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/core/setup.py +0 -0
  94. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/core/shim.py +0 -0
  95. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/core/upgrade.py +0 -0
  96. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/core/vhost.py +0 -0
  97. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/templates/vhost.conf.tmpl +0 -0
  98. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/templates/vhost_ssl.conf.tmpl +0 -0
  99. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev/win/tui.py +0 -0
  100. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev_stack.egg-info/SOURCES.txt +0 -0
  101. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev_stack.egg-info/dependency_links.txt +0 -0
  102. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev_stack.egg-info/entry_points.txt +0 -0
  103. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev_stack.egg-info/requires.txt +0 -0
  104. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/ndev_stack.egg-info/top_level.txt +0 -0
  105. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/setup.cfg +0 -0
  106. {ndev_stack-0.3.12 → ndev_stack-0.3.14}/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.12
3
+ Version: 0.3.14
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
@@ -1,7 +1,7 @@
1
1
  """
2
2
  ndev: Fast, cross-platform PHP developer environment for Windows and Linux.
3
3
  """
4
- __version__ = "0.3.12"
4
+ __version__ = "0.3.14"
5
5
 
6
6
  from . import common as common
7
7
  from .common.logger import logger as logger
@@ -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:
@@ -51,9 +51,9 @@ def setup_cmd():
51
51
  console.print("[bold yellow]Refreshing openSUSE repositories (sudo zypper refresh)...[/bold yellow]")
52
52
  subprocess.run(["sudo", "zypper", "--non-interactive", "refresh"], check=True)
53
53
 
54
- # 2. Install MariaDB, Nginx, curl, and Bubblewrap
55
- console.print("\n[bold yellow]Installing MariaDB, Nginx, curl, and Bubblewrap (sudo zypper install)...[/bold yellow]")
56
- packages = ["mariadb", "mariadb-client", "nginx", "curl", "bubblewrap"]
54
+ # 2. Install MariaDB, Nginx, curl, Bubblewrap, and SELinux tools
55
+ console.print("\n[bold yellow]Installing MariaDB, Nginx, curl, Bubblewrap, and system tools (sudo zypper install)...[/bold yellow]")
56
+ packages = ["mariadb", "mariadb-client", "nginx", "curl", "bubblewrap", "policycoreutils-python-utils"]
57
57
  subprocess.run(["sudo", "zypper", "--non-interactive", "install", "-y"] + packages, check=True)
58
58
 
59
59
  # Optional mkcert package
@@ -65,21 +65,16 @@ def setup_cmd():
65
65
  except Exception:
66
66
  pass
67
67
 
68
- try:
69
- subprocess.run(["sudo", "systemctl", "enable", "--now", "mariadb"], check=False, capture_output=True)
70
- subprocess.run(["sudo", "systemctl", "enable", "--now", "nginx"], check=False, capture_output=True)
71
- except Exception:
72
- pass
73
-
74
- console.print("[bold green]MariaDB, Nginx, and Bubblewrap installed and configured successfully![/bold green]\n")
68
+ console.print("[bold green]System packages installed successfully![/bold green]\n")
75
69
  else:
76
70
  # 1. Update Package Lists
77
71
  console.print("[bold yellow]Updating package lists (sudo apt-get update)...[/bold yellow]")
78
72
  subprocess.run(["sudo", "apt-get", "update"], check=True)
79
73
 
80
74
  # 2. Install MariaDB and Nginx
81
- console.print("\n[bold yellow]Installing MariaDB and Nginx (sudo apt-get install)...[/bold yellow]")
82
- subprocess.run(["sudo", "apt-get", "install", "-y", "mariadb-server", "nginx", "curl"], check=True)
75
+ console.print("\n[bold yellow]Installing MariaDB, Nginx, curl, Bubblewrap, and system tools (sudo apt-get install)...[/bold yellow]")
76
+ packages = ["mariadb-server", "nginx", "curl", "bubblewrap", "policycoreutils-python-utils"]
77
+ subprocess.run(["sudo", "apt-get", "install", "-y"] + packages, check=True)
83
78
 
84
79
  # Optional mkcert package
85
80
  try:
@@ -90,9 +85,63 @@ def setup_cmd():
90
85
  except Exception:
91
86
  pass
92
87
 
93
- console.print("[bold green]MariaDB and Nginx installed successfully![/bold green]\n")
94
-
95
- # 3. Install Composer
88
+ console.print("[bold green]System packages installed successfully![/bold green]\n")
89
+
90
+ # 3. Auto-configure Stack Services, Permissions, SSL, and SELinux
91
+ console.print("[bold yellow]Auto-configuring stack services, directories, SSL, and SELinux...[/bold yellow]")
92
+ from ndev.linux.runtime.nginx_helper import (
93
+ ensure_selinux_httpd_permissions,
94
+ ensure_nginx_ssl_dir,
95
+ ensure_nginx_sites_enabled_included,
96
+ ensure_development_directories,
97
+ reload_nginx,
98
+ )
99
+
100
+ # Enable & start system services
101
+ try:
102
+ subprocess.run(["sudo", "systemctl", "enable", "--now", "mariadb"], check=False, capture_output=True)
103
+ subprocess.run(["sudo", "systemctl", "enable", "--now", "nginx"], check=False, capture_output=True)
104
+ console.print(" [bold green]✓[/bold green] Enabled and started MariaDB and Nginx system services.")
105
+ except Exception:
106
+ pass
107
+
108
+ # SELinux Permissions
109
+ try:
110
+ ok = ensure_selinux_httpd_permissions()
111
+ if ok:
112
+ console.print(" [bold green]✓[/bold green] Configured SELinux permissions for Nginx (httpd_t set to permissive).")
113
+ except Exception as e:
114
+ logger.warning(f"SELinux configuration notice: {e}")
115
+
116
+ # Nginx SSL Directory (/etc/nginx/ssl)
117
+ try:
118
+ ensure_nginx_ssl_dir()
119
+ console.print(" [bold green]✓[/bold green] Configured Nginx SSL certificate directory (/etc/nginx/ssl).")
120
+ except Exception as e:
121
+ logger.warning(f"SSL directory configuration notice: {e}")
122
+
123
+ # Nginx Sites-Enabled / Vhosts Bridge
124
+ try:
125
+ ensure_nginx_sites_enabled_included()
126
+ console.print(" [bold green]✓[/bold green] Configured Nginx sites-available/sites-enabled directory bridge.")
127
+ except Exception as e:
128
+ logger.warning(f"Nginx bridge configuration notice: {e}")
129
+
130
+ # Web development root permissions and docroot traversal
131
+ try:
132
+ ensure_development_directories()
133
+ console.print(" [bold green]✓[/bold green] Configured web development root permissions (~/Sites).")
134
+ except Exception as e:
135
+ logger.warning(f"Docroot permission notice: {e}")
136
+
137
+ # Reload Nginx
138
+ try:
139
+ reload_nginx()
140
+ console.print(" [bold green]✓[/bold green] Nginx web server configuration reloaded.")
141
+ except Exception:
142
+ pass
143
+
144
+ # 4. Install Composer
96
145
  local_bin = get_user_local_bin_dir()
97
146
  composer_bin = local_bin / "composer"
98
147
  if composer_bin.exists():
@@ -144,7 +193,12 @@ def setup_cmd():
144
193
  chown_to_sudo_user(ndev_link)
145
194
  console.print(f"[bold green]Created symlink {ndev_link} -> {ndev_venv_path}[/bold green]")
146
195
 
147
- console.print("\n[bold green]System setup completed successfully![/bold green]")
196
+ console.print("\n[bold green]System setup completed successfully![/bold green]\n")
197
+ try:
198
+ from ndev.linux.commands.doctor import doctor_cmd
199
+ doctor_cmd()
200
+ except Exception:
201
+ pass
148
202
 
149
203
  except subprocess.CalledProcessError as e:
150
204
  logger.error(f"Command execution failed: {e}")
@@ -321,6 +321,7 @@ def vhost_cmd(
321
321
  ensure_nginx_sites_enabled_included,
322
322
  install_nginx_ssl_cert,
323
323
  ensure_docroot_accessible,
324
+ ensure_selinux_httpd_permissions,
324
325
  add_to_hosts_privileged,
325
326
  reload_nginx,
326
327
  )
@@ -350,6 +351,7 @@ def vhost_cmd(
350
351
  logger.warning(f"Could not install SSL certificate to /etc/nginx/ssl: {e}")
351
352
 
352
353
  ensure_docroot_accessible(root_path)
354
+ ensure_selinux_httpd_permissions()
353
355
 
354
356
  conf_file = vhost_dir / f"{domain}.conf"
355
357
  config_template = generate_vhost_config(
@@ -66,6 +66,169 @@ 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 if non-root.
78
+ Validates sudo password first if provided to refresh the sudo credential timestamp.
79
+ If no password provided:
80
+ - Uses sudo -n if not in an interactive terminal or if sudo timestamp is active.
81
+ - Falls back to interactive sudo if in an interactive terminal.
82
+ """
83
+ full_cmd = list(cmd)
84
+ if os.geteuid() != 0:
85
+ if password:
86
+ validate_sudo_password(password)
87
+ full_cmd = ["sudo", "-n"] + full_cmd
88
+ else:
89
+ if not check_sudo_needs_password():
90
+ full_cmd = ["sudo", "-n"] + full_cmd
91
+ elif sys.stdin.isatty():
92
+ full_cmd = ["sudo"] + full_cmd
93
+ capture_output = False
94
+ else:
95
+ full_cmd = ["sudo", "-n"] + full_cmd
96
+
97
+ return subprocess.run(
98
+ full_cmd,
99
+ check=check,
100
+ timeout=timeout,
101
+ capture_output=capture_output,
102
+ text=True
103
+ )
104
+
105
+
106
+ def is_selinux_enforcing() -> bool:
107
+ """Check if SELinux is present and in Enforcing mode."""
108
+ enforce_file = Path("/sys/fs/selinux/enforce")
109
+ if enforce_file.exists():
110
+ try:
111
+ return enforce_file.read_text().strip() == "1"
112
+ except Exception:
113
+ pass
114
+ getenforce = shutil.which("getenforce") or "/usr/sbin/getenforce"
115
+ if Path(getenforce).exists():
116
+ try:
117
+ res = subprocess.run([getenforce], capture_output=True, text=True, timeout=2)
118
+ return res.stdout.strip().lower() == "enforcing"
119
+ except Exception:
120
+ pass
121
+ return False
122
+
123
+
124
+ def is_selinux_httpd_permissive() -> bool:
125
+ """Check if httpd_t is in SELinux permissive types."""
126
+ if not is_selinux_enforcing():
127
+ return True
128
+ seinfo = shutil.which("seinfo") or "/usr/bin/seinfo"
129
+ if Path(seinfo).exists():
130
+ try:
131
+ res = subprocess.run([seinfo, "--permissive"], capture_output=True, text=True, timeout=3)
132
+ return "httpd_t" in res.stdout
133
+ except Exception:
134
+ pass
135
+ return False
136
+
137
+
138
+ def ensure_selinux_httpd_permissions(password: Optional[str] = None) -> bool:
139
+ """
140
+ Ensure SELinux allows Nginx (httpd_t) to access user document roots and FastCGI sockets.
141
+ If SELinux is enforcing and httpd_t is not permissive, add httpd_t to permissive types
142
+ via semanage and enable relevant SELinux booleans.
143
+ """
144
+ if not is_selinux_enforcing() or is_selinux_httpd_permissive():
145
+ return True
146
+
147
+ success = False
148
+ semanage = shutil.which("semanage") or "/usr/sbin/semanage"
149
+ if Path(semanage).exists():
150
+ try:
151
+ run_privileged_command([semanage, "permissive", "-a", "httpd_t"], password=password, check=False)
152
+ success = is_selinux_httpd_permissive()
153
+ except Exception as e:
154
+ logger.warning(f"Failed to set httpd_t permissive via semanage: {e}")
155
+
156
+ # Also enable booleans as supplementary fallback
157
+ setsebool = shutil.which("setsebool") or "/usr/sbin/setsebool"
158
+ if Path(setsebool).exists():
159
+ try:
160
+ run_privileged_command(
161
+ [setsebool, "-P", "httpd_read_user_content=1", "httpd_enable_homedirs=1", "httpd_can_network_connect=1"],
162
+ password=password,
163
+ check=False
164
+ )
165
+ except Exception:
166
+ pass
167
+
168
+ return success or is_selinux_httpd_permissive()
169
+
170
+
171
+ def ensure_nginx_ssl_dir(password: Optional[str] = None) -> Path:
172
+ """
173
+ Ensure /etc/nginx/ssl directory exists with 0755 permissions and proper SELinux context.
174
+ """
175
+ ssl_dir = Path("/etc/nginx/ssl")
176
+ if os.geteuid() == 0:
177
+ ssl_dir.mkdir(parents=True, exist_ok=True)
178
+ try:
179
+ os.chmod(str(ssl_dir), 0o755)
180
+ except Exception:
181
+ pass
182
+ else:
183
+ run_privileged_command(["mkdir", "-p", str(ssl_dir)], password=password, check=False)
184
+ run_privileged_command(["chmod", "755", str(ssl_dir)], password=password, check=False)
185
+
186
+ if shutil.which("restorecon"):
187
+ run_privileged_command(["restorecon", "-Rv", str(ssl_dir)], password=password, check=False)
188
+
189
+ return ssl_dir
190
+
191
+
192
+ def ensure_development_directories(password: Optional[str] = None) -> None:
193
+ """
194
+ Ensure the user home directory has +x traversal permissions,
195
+ ~/Sites directory exists with 0755 permissions, and any configured vhost docroots are accessible.
196
+ """
197
+ home = Path.home()
198
+ sudo_user = os.environ.get("SUDO_USER")
199
+ if sudo_user:
200
+ try:
201
+ import pwd
202
+ home = Path(pwd.getpwnam(sudo_user).pw_dir)
203
+ except Exception:
204
+ pass
205
+
206
+ # Ensure home has traversal (+x)
207
+ try:
208
+ st = home.stat()
209
+ if not (st.st_mode & 0o001):
210
+ os.chmod(str(home), st.st_mode | 0o001)
211
+ except Exception:
212
+ pass
213
+
214
+ # Ensure ~/Sites exists
215
+ sites_dir = home / "Sites"
216
+ try:
217
+ sites_dir.mkdir(parents=True, exist_ok=True)
218
+ os.chmod(str(sites_dir), 0o755)
219
+ except Exception:
220
+ pass
221
+
222
+ # Ensure all configured vhost docroots are accessible
223
+ try:
224
+ for vhost in list_vhosts():
225
+ docroot = vhost.get("root")
226
+ if docroot and Path(docroot).exists():
227
+ ensure_docroot_accessible(Path(docroot))
228
+ except Exception:
229
+ pass
230
+
231
+
69
232
  def write_privileged_file(target: Path, content: str, password: Optional[str] = None) -> None:
70
233
  """
71
234
  Write content to a file, using sudo tee if current process lacks permissions.
@@ -527,10 +690,24 @@ def ensure_docroot_accessible(root_path: Path) -> None:
527
690
  try:
528
691
  resolved_root = root_path.resolve()
529
692
  resolved_home = home.resolve()
530
- if resolved_root == resolved_home or resolved_root.is_relative_to(resolved_home):
531
- st = home.stat()
532
- if not (st.st_mode & 0o001):
533
- os.chmod(str(home), st.st_mode | 0o001)
693
+
694
+ # Traverse every directory from root_path up to home and ensure +x (0o001)
695
+ curr = resolved_root
696
+ while curr != curr.parent:
697
+ try:
698
+ st = curr.stat()
699
+ if not (st.st_mode & 0o001):
700
+ os.chmod(str(curr), st.st_mode | 0o001)
701
+ except Exception:
702
+ pass
703
+ if curr == resolved_home:
704
+ break
705
+ curr = curr.parent
706
+
707
+ # Also ensure home directory itself has +x
708
+ st = resolved_home.stat()
709
+ if not (st.st_mode & 0o001):
710
+ os.chmod(str(resolved_home), st.st_mode | 0o001)
534
711
  except Exception:
535
712
  pass
536
713