# CyberPanel nginx Feature Reference

This is the canonical feature reference for `nginx-cyberpanel`. It documents
the behavior implemented by the bundled nginx modules and the Plesk takeover
packaging, including the OpenLiteSpeed `.htaccess` compatibility surface.

Current local verification:

```text
bash cyberpanel_nginx/test/run-tests.sh              -> 151 PASS, 0 FAIL
bash cyberpanel_nginx/test/run-ols-docs-parity.sh    -> 55 PASS, 0 GAP, 0 FAIL
```

## Product Scope

`nginx-cyberpanel` is a drop-in nginx replacement for Apache-backed hosting
panels. It reads panel-generated Apache configuration and per-directory
`.htaccess` files directly, then serves sites through nginx with CyberPanel
licensing, LSCache compatibility, HTTP/2, brotli, gzip, and a Plesk extension
UI.

Supported deployment target in this tree:

- Plesk for Linux, with Apache on `:7080` and `:7081` remapped to public
  `:80` and `:443`.
- Ubuntu/Debian binary family and RHEL/Alma/Rocky 8/9 binary families.
- License activation through the CyberPanel platform using product
  `cyberpanel_nginx`.

## License Behavior

A platform license is required before takeover can serve sites.

- Free license tier: 10 registrable domains after activation.
- Pro license tier: 50 registrable domains.
- Business license tier: 100 registrable domains.
- Unlimited license tier: `2147483647` domains.
- Unknown or missing license blocks served domains and shows activation or
  upgrade guidance.
- License identity is stored in `/etc/nginx-cyberpanel/server-id`.
- License key is stored in `/etc/nginx-cyberpanel/license-key`.
- Cached platform response is stored under `/etc/nginx-cyberpanel/cache/`.

The public landing page for purchase or free claim is:

```text
https://cyberpanel.net/cyberpanel-nginx-for-plesk/
```

### Platform License Contract

The product id sent to the platform is:

```text
cyberpanel_nginx
```

The binary enforces license validity and `max_domains`; it does not currently
gate individual runtime modules by feature flag. All activated plans should
therefore return the same feature map, with only `tier`, `max_domains`, and
expiry data varying by license.

Expected plan metadata:

| Plan ID | Tier | max_domains |
|---:|---|---:|
| 25 | free | 10 |
| 26 | pro | 50 |
| 27 | business | 100 |
| 28 | unlimited | 2147483647 |

Required or recommended response fields:

```json
{
  "valid": true,
  "status": "active",
  "tier": "pro",
  "max_domains": 50,
  "expires_at": 1767225600,
  "checked_at": 1710000000,
  "next_check_after": 86400,
  "grace_until": 1710604800,
  "message": null,
  "upgrade_url": "https://cyberpanel.net/cyberpanel-nginx-for-plesk/",
  "manage_url": "https://platform.cyberpersons.com/Billing/licenses/details/123/",
  "features": {
    "nginx_cyberpanel": true,
    "plesk_extension": true,
    "apache_config_translation": true,
    "htaccess": true,
    "ols_htaccess_parity": true,
    "lscache": true,
    "wordpress_lscache": true,
    "ssl_helper": true,
    "http2": true,
    "brotli": true,
    "gzip": true,
    "php_directives": true,
    "cache_purge": true,
    "diagnostics": true
  }
}
```

Live platform verification on 2026-05-14 confirmed plan IDs 25-28 return the
expected `tier`, `max_domains`, `upgrade_url`, `manage_url`, and 14-key feature
map.

## Apache Config Translation

The `ngx_apacheconf_module` exposes:

```nginx
readapacheconf /etc/nginx-cyberpanel/apache-entry.conf portmap=7080:80,7081:443;
```

The parser converts common Apache panel output into nginx server blocks:

- `Listen`, `NameVirtualHost`, `VirtualHost`
- `ServerName`, `ServerAlias`
- `DocumentRoot`
- `ErrorLog`, `CustomLog`
- `SSLEngine`, `SSLCertificateFile`, `SSLCertificateKeyFile`
- `DirectoryIndex`
- `Alias`, `ScriptAlias`
- `ProxyPass`, `ProxyPassReverse`, exclusions
- `ErrorDocument`
- `RewriteEngine`, `RewriteCond`, `RewriteRule`
- `Header`, `RequestHeader`
- `Options Indexes`, `Options -Indexes`
- `Require`, `Allow`, `Deny`
- `Include`, `IncludeOptional`, glob includes
- PHP handler hints from `AddHandler`, `AddType`, `SetHandler`,
  `FcgidWrapper`, `FCGIWrapper`

The emitter includes ACME-safe redirects so `/.well-known/acme-challenge/`
does not get forced to HTTPS before certificate validation.

## `.htaccess` Engine

The `ngx_htaccess_module` walks from document root to request directory and
merges `.htaccess` files per request. Parsed files are cached per worker with
mtime checks and a negative cache for missing `.htaccess` files.

### Headers

Supported response header directives:

```apache
Header set X-Frame-Options "DENY"
Header append X-Multi "two"
Header merge X-Multi "three"
Header add X-Multi "four"
Header unset X-Powered-By
Header always set Cache-Control "no-store"
Header set X-Test: "value"
```

Supported request header directives:

```apache
RequestHeader set X-Backend-Server "production"
RequestHeader unset X-Debug-Token
RequestHeader append X-Trace "edge"
RequestHeader merge X-Trace "edge"
RequestHeader add X-Trace "edge"
```

`RequestHeader` changes are applied before later modules read nginx `$http_*`
variables.

### Environment

Supported environment directives:

```apache
SetEnv APPLICATION_ENV production
SetEnv EMPTY_VALUE
SetEnvIf Request_URI "^/api/" IS_API=1
SetEnvIfNoCase User-Agent "mobile|android|iphone" IS_MOBILE=1
BrowserMatch "bot|crawler|spider" IS_BOT=1
BrowserMatchNoCase "chrome" IS_CHROME=1
```

Supported `SetEnvIf` attributes:

- `Request_URI`
- `Request_Method`
- `User-Agent`
- `Host`
- `Referer`
- `Query_String`
- `Remote_Addr`

Unknown `SetEnvIf` attributes are ignored for OpenLiteSpeed parity.

### Access Control

Supported legacy access directives:

```apache
Order deny,allow
Deny from all
Allow from 127.0.0.1
Allow from 192.168.0.0/16
```

Supported Apache 2.4-style directives:

```apache
Require all granted
Require all denied
Require ip 127.0.0.1
Require ip 192.168.0.0/16
Require not ip 127.0.0.1
```

IPv4, IPv6, and CIDR forms are supported.

### Basic Auth

Supported auth directives:

```apache
AuthType Basic
AuthName "Private Area"
AuthUserFile /absolute/path/.htpasswd
AuthGroupFile /absolute/path/.htgroups
Require valid-user
Require user alice bob
Require group admins editors
```

Supported htpasswd hashes:

- Apache APR1-MD5 (`$apr1$`)
- `{SHA}`
- crypt MD5 (`$1$`)
- crypt SHA-256 (`$5$`)
- crypt SHA-512 (`$6$`)
- legacy DES crypt

Relative auth file paths and paths containing `..` are rejected. `AuthType
Digest` is not enabled.

### Rewrite

Supported per-request rewrite directives:

```apache
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
RewriteRule ^old/(.*)$ /new/$1 [R=301,L]
RewriteRule ^forbidden - [F,L]
RewriteRule ^gone - [G,L]
```

Supported condition variables include:

- `REQUEST_URI`
- `REQUEST_FILENAME`
- `HTTP_HOST`
- `HTTP_USER_AGENT`
- `HTTP_REFERER`
- `QUERY_STRING`
- `REMOTE_ADDR`
- `HTTPS`
- `REQUEST_METHOD`

Supported flags include `L`, `R`, `R=code`, `QSA`, `F`, `G`, `NC`, and
environment-only `-` targets.

### Redirects

Supported redirect directives:

```apache
Redirect /old /new
Redirect 301 /old /new
Redirect permanent /old /new
Redirect temp /old /new
Redirect seeother /old /new
Redirect gone /old
Redirect 307 /old /new
Redirect 308 /old /new
RedirectMatch 302 ^/product-([0-9]+)$ /item/$1
```

Prefix redirects append the remaining path suffix, matching Apache behavior.

### ErrorDocument

Supported forms:

```apache
ErrorDocument 403 /errors/403.html
ErrorDocument 404 "Not Found"
ErrorDocument 500 https://example.com/error
```

Internal paths preserve the original status after internal redirect. Inline
messages are returned directly.

### Files and FilesMatch

Supported overlays:

```apache
<Files "wp-config.php">
    Require all denied
</Files>

<FilesMatch "\.(jpg|png|webp)$">
    Header set Cache-Control "max-age=31536000, public, immutable"
    Header unset ETag
</FilesMatch>
```

Overlays are applied to the request basename.

### Expires

Supported directives:

```apache
ExpiresActive On
ExpiresActive Off
ExpiresDefault "access plus 1 hour"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType text/css "access plus 7 days"
ExpiresByType image/webp A31557600
ExpiresByType image/webp M31557600
```

Supported units: years, months, weeks, days, hours, minutes, seconds.
OpenLiteSpeed shorthand `A<seconds>` and `M<seconds>` is accepted. Zero or
negative max-age values are ignored.

### PHP Directives

Supported directives:

```apache
php_value memory_limit 256M
php_value upload_max_filesize 64M
php_flag display_errors off
php_flag log_errors on
php_admin_value open_basedir /var/www/vhosts/example.com/httpdocs
php_admin_flag expose_php off
```

These are exposed as nginx variables and emitted to PHP-FPM:

```nginx
fastcgi_param PHP_VALUE       $cyberpanel_php_value if_not_empty;
fastcgi_param PHP_ADMIN_VALUE $cyberpanel_php_admin_value if_not_empty;
```

### Charset and SSL

Supported directives:

```apache
AddDefaultCharset UTF-8
AddDefaultCharset Off
SSLRequireSSL
```

`SSLRequireSSL` blocks plain HTTP requests with 403.

### Satisfy and Method Limits

Supported directives:

```apache
Satisfy Any
Satisfy All

<Limit POST PUT DELETE>
    Require valid-user
</Limit>

<LimitExcept GET HEAD OPTIONS>
    Require valid-user
</LimitExcept>
```

`Satisfy Any` allows either IP access rules or auth to satisfy protection.
`Satisfy All` requires all configured checks.

### BruteForceProtection

Supported OpenLiteSpeed-compatible directives:

```apache
BruteForceProtection On
BruteForceAllowedAttempts 10
BruteForceMaxAttempts 10
BruteForceWindow 300
BruteForceBaseTimeout 60
BruteForceAction Deny
BruteForceAction block
BruteForceAction throttle
BruteForceAction Drop
BruteForceAction log
BruteForceXForwardedFor On
BruteForceWhitelist 127.0.0.1,192.168.0.0/16
BruteForceProtectPath /custom/login
BruteForceThrottle On
```

Two paths are protected by default for POST requests:

- `wp-login.php`
- `xmlrpc.php`

Additional protected paths are substring matches from `BruteForceProtectPath`.
Whitelist entries bypass brute-force handling, so the normal content handler
decides the response. With static files, a bypassed POST can return nginx's
normal `405 Method Not Allowed`.

Actions:

- `Deny` or `block`: return 403.
- `throttle`: return 429 and `Retry-After`.
- `Drop`: return nginx 444.
- `log`: count only and allow the request to continue.

Basic-auth failures are also counted. Repeated auth failures escalate block
duration using the existing shared-memory counter.

## LSCache Compatibility

The `ngx_lscache_module` implements the LiteSpeed Cache plugin wire protocol:

- Reads `X-LiteSpeed-Cache-Control`.
- Reads and stores `X-LiteSpeed-Tag`.
- Reads `X-LiteSpeed-Vary` and creates vary sidecars.
- Sends `X-LiteSpeed-Cache: miss` on store and `X-LiteSpeed-Cache: hit` on
  replay.
- Supports `PURGE` by URL, `X-LS-Purge: *`, and tag purges.
- Handles `X-LiteSpeed-Purge` response headers from WordPress.
- Replays stored backend headers on cache hits.

Default cache root:

```text
/var/cache/nginx-cyberpanel/lscache
```

The Plesk takeover injects:

```nginx
fastcgi_param HTTP_X_LSCACHE on;
```

This is required for the WordPress LiteSpeed Cache plugin to emit cache-control
headers.

## Plesk Extension

The bundled extension provides:

- License tab with key save, immediate validation, tier, expiry, max domains,
  current domain count, and management/upgrade links.
- Overview/status tab.
- Cache purge and stats tab.
- Config viewer and module toggles.
- SSL helper tab for per-domain Let's Encrypt issuance.
- Diagnose tab.
- Extension icon and static assets in `plesk-extension/cyberpanel-nginx/htdocs/`.

Takeover is not enabled until a valid platform license is saved.

## OS Paths

The installer detects OS and panel paths. Important paths:

```text
/etc/nginx-cyberpanel/nginx.conf
/etc/nginx-cyberpanel/apache-entry.conf
/etc/nginx-cyberpanel/extras.conf
/etc/nginx-cyberpanel/per-vhost-cache.conf
/etc/nginx-cyberpanel/license-key
/etc/nginx-cyberpanel/server-id
/usr/local/sbin/nginx-cyberpanel
/var/cache/nginx-cyberpanel/lscache
/var/log/cyberpanel-nginx-install.log
/var/backups/nginx-cyberpanel/
```

Plesk Apache PID compatibility:

- Debian/Ubuntu: `/var/run/apache2/apache2.pid`
- RHEL/Alma/Rocky: `/run/httpd/httpd.pid`

Both are wired to `/run/nginx-cyberpanel.pid` as needed for Plesk service
adapters.

## Performance Notes

Hot-path decisions:

- `.htaccess` files are cached per worker and reparsed only when mtime changes.
- Missing `.htaccess` lookups use a short negative cache.
- Brute-force POST checks return immediately unless protection is enabled and
  the URI matches `wp-login.php`, `xmlrpc.php`, or a configured custom path.
- Whitelist parsing is only reached for brute-force-protected requests.
- Cache keys use original `unparsed_uri`, not rewritten `r->uri`, to preserve
  WordPress front-controller URL separation.

## Verification Commands

Run these before publishing binaries:

```bash
cd /usr/local/CyberCP
bash cyberpanel_nginx/build.sh
bash cyberpanel_nginx/test/run-ols-docs-parity.sh
bash cyberpanel_nginx/test/run-tests.sh
```

Expected current output:

```text
OLS docs parity: 55 PASS, 0 GAP, 0 FAIL
Full regression: 151 PASS, 0 FAIL
```
