no redux
This commit is contained in:
466
NGINX.md
Normal file
466
NGINX.md
Normal file
@@ -0,0 +1,466 @@
|
||||
# NGINX Configuration Guide for Dashfront & Dashback
|
||||
|
||||
This document provides a production-tested, complete NGINX reverse-proxy and static hosting guide for **Dashfront** (Vite + React 19 PWA) and **Dashback** (Express REST + SSE API).
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Architecture & Request Flow](#1-architecture--request-flow)
|
||||
2. [Key Technical Requirements](#2-key-technical-requirements)
|
||||
3. [Production Configuration (`dashfront.conf`)](#3-production-configuration-dashfrontconf)
|
||||
4. [Detailed Breakdown of Critical Directives](#4-detailed-breakdown-of-critical-directives)
|
||||
- [PWA & Cache Invalidation Strategy](#pwa--cache-invalidation-strategy)
|
||||
- [SPA Routing (`try_files`)](#spa-routing-try_files)
|
||||
- [Server-Sent Events (SSE) Buffering & Timeouts](#server-sent-events-sse-buffering--timeouts)
|
||||
- [File Uploads & Request Body Limits](#file-uploads--request-body-limits)
|
||||
- [WebAssembly (WASM) MIME Types](#webassembly-wasm-mime-types)
|
||||
- [Camera & Hardware Permissions](#camera--hardware-permissions)
|
||||
5. [Alternative: Remote Vite Dev Server Proxy](#5-alternative-remote-vite-dev-server-proxy)
|
||||
6. [Deployment & Operations Runbook](#6-deployment--operations-runbook)
|
||||
7. [Troubleshooting Matrix](#7-troubleshooting-matrix)
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture & Request Flow
|
||||
|
||||
In production, NGINX acts as both the high-performance static web server for Dashfront assets and the secure reverse proxy for the Dashback Express service.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ CLIENT │
|
||||
│ (Desktop Browser, Tablet, Mobile PWA) │
|
||||
└──────────────────────┬───────────────────────┘
|
||||
│ HTTPS (:443)
|
||||
▼
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ NGINX │
|
||||
│ TLS Termination, Gzip, Caching, Auth │
|
||||
└──────────────┬────────────────┬──────────────┘
|
||||
│ │
|
||||
/assets/*, /index.html │ │ /api/*
|
||||
/sw.js, static files │ │ (REST & SSE)
|
||||
▼ ▼
|
||||
┌───────────────────┐ ┌───────────────────┐
|
||||
│ Static Build Dir │ │ Dashback │
|
||||
│ `dashfront/dist` │ │ Express API │
|
||||
│ │ │ (localhost:4208) │
|
||||
└───────────────────┘ └───────────────────┘
|
||||
```
|
||||
|
||||
| Path | Handled By | Cache Policy | Notes |
|
||||
|---|---|---|---|
|
||||
| `/index.html` & `/` | NGINX static file | `no-cache, no-store, must-revalidate` | Always fresh so users get new script hashes |
|
||||
| `/sw.js`, `/registerSW.js` | NGINX static file | `no-cache, no-store, must-revalidate` | **Must never cache**; avoids bricking PWA updates |
|
||||
| `/workbox-*.js` | NGINX static file | `no-cache, no-store, must-revalidate` | Workbox runtime loader |
|
||||
| `/manifest.webmanifest` | NGINX static file | `public, max-age=3600` | PWA manifest definition |
|
||||
| `/assets/*` | NGINX static file | `public, max-age=31536000, immutable` | Content-hashed JS, CSS, WASM, and workers |
|
||||
| `/api/events` | Dashback (`:4208`) | `no-cache, no-buffering` | Server-Sent Events (SSE) live push notifications |
|
||||
| `/api/*` | Dashback (`:4208`) | Dynamic / Backend controlled | REST endpoints, auth cookies, image & PDF uploads |
|
||||
| All other paths | NGINX `try_files` | Fallback to `/index.html` | React Router HTML5 History API deep linking |
|
||||
|
||||
---
|
||||
|
||||
## 2. Key Technical Requirements
|
||||
|
||||
1. **PWA Lifecycles**: A Progressive Web App checks for updates by fetching `sw.js`. If NGINX caches `sw.js` or `index.html`, client devices can remain stuck on obsolete bundles indefinitely.
|
||||
2. **SSE Streaming**: Real-time push updates at `/api/events` require NGINX proxy buffering and proxy caching to be turned **OFF**, with a 24-hour read timeout to prevent arbitrary 60-second connection drops.
|
||||
3. **Payload Limits**: Inbound delivery invoices (PDF receipts scanned client-side) and high-resolution article photos (AVIF) require a request body limit of at least `50m`–`100m` (matching Dashback's `BODY_LIMIT`).
|
||||
4. **Cookie Authentication**: Dashback issues HTTP-only `token` session cookies. NGINX must pass the original `Host`, `X-Real-IP`, and `X-Forwarded-Proto https` headers so Express sets and retains cookies correctly.
|
||||
5. **Camera Access (HTTPS)**: Mobile barcode scanning requires a secure origin (HTTPS) and appropriate `Permissions-Policy` response headers.
|
||||
|
||||
---
|
||||
|
||||
## 3. Production Configuration (`dashfront.conf`)
|
||||
|
||||
Place this configuration file at `/etc/nginx/sites-available/dashfront` (or adapt it to your existing reverse proxy structure):
|
||||
|
||||
```nginx
|
||||
# ==============================================================================
|
||||
# Dashfront + Dashback Production NGINX Server Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# Map Connection header for WebSocket upgrades (if proxying dev server or WebSockets)
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 1. HTTP -> HTTPS Redirect
|
||||
# ------------------------------------------------------------------------------
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name dash.example.com; # <-- Replace with your domain or server IP
|
||||
|
||||
# Let's Encrypt / ACME challenge directory
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 2. Main HTTPS Server Block
|
||||
# ------------------------------------------------------------------------------
|
||||
server {
|
||||
# NGINX >= 1.25.1 uses 'http2 on;'. For older NGINX, use: listen 443 ssl http2;
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
|
||||
server_name dash.example.com; # <-- Replace with your domain or server IP
|
||||
|
||||
# --- SSL / TLS Certificates ---
|
||||
ssl_certificate /etc/letsencrypt/live/dash.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/dash.example.com/privkey.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# --- Static File Root ---
|
||||
# Point directly to the dist/ directory created by `npm run build`
|
||||
root /home/seb/src/dashfront/dist;
|
||||
index index.html;
|
||||
|
||||
# --- Request Body & Uploads ---
|
||||
# Supports large delivery PDF uploads and client-encoded AVIF photos
|
||||
client_max_body_size 100m;
|
||||
client_body_timeout 300s;
|
||||
|
||||
# --- Compression (Gzip) ---
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 5;
|
||||
gzip_min_length 256;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/xml
|
||||
text/javascript
|
||||
application/javascript
|
||||
application/x-javascript
|
||||
application/json
|
||||
application/xml
|
||||
application/manifest+json
|
||||
application/wasm
|
||||
image/svg+xml;
|
||||
|
||||
# --- Security Headers ---
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
# Allow camera access for the built-in barcode scanner
|
||||
add_header Permissions-Policy "camera=(self), microphone=(), geolocation=()" always;
|
||||
|
||||
# ==========================================================================
|
||||
# API & Backend Routing (Dashback on port 4208)
|
||||
# ==========================================================================
|
||||
|
||||
# Dedicated SSE route: Real-time broadcast events
|
||||
location = /api/events {
|
||||
proxy_pass http://127.0.0.1:4208;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
# Clear Connection header to maintain persistent HTTP/1.1 stream
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Disable buffering and caching completely for immediate push
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
chunked_transfer_encoding off;
|
||||
|
||||
# Prevent premature disconnections; keep SSE open for 24 hours
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
}
|
||||
|
||||
# General API endpoints (REST)
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:4208;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Retain cookies across domain/path
|
||||
proxy_cookie_path / /;
|
||||
|
||||
# Streaming uploads (PDF receipts, AVIF photos)
|
||||
proxy_request_buffering off;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
# ==========================================================================
|
||||
# PWA & Service Worker Rules (Zero Caching)
|
||||
# ==========================================================================
|
||||
|
||||
# Service worker scripts MUST NEVER be cached by HTTP cache
|
||||
location ~* ^/(sw\.js|registerSW\.js|workbox-[a-f0-9]+\.js)$ {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# PWA web manifest and browser icons
|
||||
location ~* ^/(manifest\.webmanifest|favicon\.svg|icons/.*)$ {
|
||||
add_header Cache-Control "public, max-age=3600";
|
||||
add_header Vary "Accept-Encoding";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# ==========================================================================
|
||||
# Static Assets & Hashed Bundles (Aggressive Immutable Caching)
|
||||
# ==========================================================================
|
||||
|
||||
# Vite content-hashed assets: JS, CSS, WASM, Web Workers
|
||||
# (Example: /assets/index-CutpK3vd.js, /assets/avif_enc-Co4TcJko.wasm)
|
||||
location /assets/ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
add_header Vary "Accept-Encoding";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Ensure WebAssembly MIME type is correctly set
|
||||
location ~* \.wasm$ {
|
||||
default_type application/wasm;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# ==========================================================================
|
||||
# HTML5 SPA Entry Point & Navigation Fallback
|
||||
# ==========================================================================
|
||||
|
||||
# index.html must never be cached so new release deployments take effect immediately
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
}
|
||||
|
||||
# SPA routing: Serve existing static file or directory; otherwise fallback to /index.html
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Detailed Breakdown of Critical Directives
|
||||
|
||||
### PWA & Cache Invalidation Strategy
|
||||
|
||||
PWA update lifecycles depend on byte-for-byte comparisons of `sw.js` when the user visits the page:
|
||||
1. **`sw.js` and `registerSW.js`**:
|
||||
```nginx
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
```
|
||||
If NGINX serves `sw.js` with a 30-day cache header, browser service worker checks will read from disk cache and never detect new application releases.
|
||||
2. **`index.html`**:
|
||||
Must also have `no-cache, no-store, must-revalidate`. When a new build is run, Vite produces chunks with new hashes (e.g., `index-AbCd12.js`). If `index.html` is cached, users continue loading old references that may no longer exist on disk.
|
||||
3. **`/assets/`**:
|
||||
Because Vite / Rolldown generates content-addressed filenames (hashes change whenever contents change), these files are completely immutable and safe to cache for 1 year (`max-age=31536000, immutable`).
|
||||
|
||||
### SPA Routing (`try_files`)
|
||||
|
||||
Dashfront uses React Router with HTML5 History API pushState:
|
||||
- Direct visits to URLs like `/artikel/10001`, `/kategorien/12`, `/lieferung/neu`, or `/kunde` do not correspond to files on disk.
|
||||
- The directive:
|
||||
```nginx
|
||||
try_files $uri $uri/ /index.html;
|
||||
```
|
||||
instructs NGINX to first check if a matching static file exists (e.g. `/favicon.svg` or `/assets/...`). If not found, it internally rewrites the request to `/index.html` without returning an HTTP 404, allowing React Router to resolve the route in-browser.
|
||||
|
||||
### Server-Sent Events (SSE) Buffering & Timeouts
|
||||
|
||||
The backend provides real-time model invalidation via `GET /api/events`. By default, NGINX buffers HTTP responses. If buffering is enabled:
|
||||
1. Events sent by Dashback stay in NGINX's internal memory buffer until the buffer fills up (often several kilobytes), causing clients to miss instant UI updates.
|
||||
2. NGINX will close idle proxy connections after `proxy_read_timeout` (default 60s).
|
||||
|
||||
The dedicated `/api/events` block fixes both issues:
|
||||
```nginx
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
chunked_transfer_encoding off;
|
||||
proxy_set_header Connection "";
|
||||
proxy_read_timeout 86400s;
|
||||
```
|
||||
|
||||
### File Uploads & Request Body Limits
|
||||
|
||||
In Dashfront:
|
||||
- Inbound deliveries support multi-page PDF invoices and OCR processing (`pdfconvert-wasm`).
|
||||
- Article management supports client-side AVIF image generation.
|
||||
- The default NGINX limit is `1m` (`client_max_body_size 1m;`), which causes HTTP 413 (Payload Too Large) errors on invoice uploads.
|
||||
- Setting:
|
||||
```nginx
|
||||
client_max_body_size 100m;
|
||||
proxy_request_buffering off;
|
||||
```
|
||||
allows uploads up to 100 megabytes and streams the body directly to Express without waiting for NGINX to buffer the entire file on disk.
|
||||
|
||||
### WebAssembly (WASM) MIME Types
|
||||
|
||||
Dashfront uses WebAssembly for:
|
||||
- `@jsquash/avif` (image encoding)
|
||||
- `@jsquash/resize` (squoosh resizing)
|
||||
- `pdfconvert-wasm` (PDF page extraction and rasterization)
|
||||
|
||||
Modern browsers require the HTTP header `Content-Type: application/wasm` for streaming compilation (`WebAssembly.instantiateStreaming`). If missing, NGINX may default to `application/octet-stream`. The WASM block guarantees the correct MIME header:
|
||||
```nginx
|
||||
location ~* \.wasm$ {
|
||||
default_type application/wasm;
|
||||
}
|
||||
```
|
||||
|
||||
### Camera & Hardware Permissions
|
||||
|
||||
The article lookup and delivery receipt features allow scanning barcodes using device cameras. Web browsers enforce two strict rules for camera access:
|
||||
1. **Secure Origin**: The page must be served over HTTPS or `localhost`. Plain HTTP over local IP addresses (e.g., `http://192.168.1.100/`) will automatically disable `navigator.mediaDevices`.
|
||||
2. **Permissions Policy**: The header `Permissions-Policy "camera=(self)"` ensures the browser allows Dashfront to access connected webcams or smartphone cameras.
|
||||
|
||||
---
|
||||
|
||||
## 5. Alternative: Remote Vite Dev Server Proxy
|
||||
|
||||
If you want to access the live **Vite development server** (running on `localhost:4209` with HMR) through NGINX from a smartphone, tablet, or external domain:
|
||||
|
||||
```nginx
|
||||
# Map connection upgrade for Vite HMR WebSockets
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name dev-dash.example.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/dev-dash.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/dev-dash.example.com/privkey.pem;
|
||||
|
||||
client_max_body_size 100m;
|
||||
|
||||
# 1. API Calls -> Dashback (Port 4208)
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:4208;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 86400s;
|
||||
}
|
||||
|
||||
# 2. Vite Dev Server & HMR -> Vite (Port 4209)
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:4209;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
# Crucial for Vite Hot Module Replacement (HMR) WebSocket
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_read_timeout 86400s;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> When proxying to Vite dev server, ensure `server.allowedHosts = true` or add your domain to `allowedHosts` in [vite.config.js](file:///home/seb/src/dashfront/vite.config.js).
|
||||
|
||||
---
|
||||
|
||||
## 6. Deployment & Operations Runbook
|
||||
|
||||
### Step 1: Build Dashfront for Production
|
||||
|
||||
On the development machine or build server:
|
||||
|
||||
```bash
|
||||
cd /home/seb/src/dashfront
|
||||
npm run build
|
||||
```
|
||||
|
||||
This compiles optimized bundles and service worker files into `dashfront/dist`.
|
||||
|
||||
### Step 2: Install and Verify NGINX Configuration
|
||||
|
||||
```bash
|
||||
# 1. Copy config file to sites-available
|
||||
sudo cp deploy/nginx-dashfront.conf /etc/nginx/sites-available/dashfront
|
||||
|
||||
# 2. Enable site by creating a symlink
|
||||
sudo ln -s /etc/nginx/sites-available/dashfront /etc/nginx/sites-enabled/
|
||||
|
||||
# 3. Test configuration syntax
|
||||
sudo nginx -t
|
||||
|
||||
# 4. Reload NGINX without dropping active connections
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### Step 3: Obtain TLS Certificates with Certbot
|
||||
|
||||
```bash
|
||||
sudo apt install certbot python3-certbot-nginx
|
||||
sudo certbot --nginx -d dash.example.com
|
||||
```
|
||||
|
||||
### Step 4: Ensure Filesystem Permissions
|
||||
|
||||
NGINX worker processes run as the `www-data` user by default. Ensure the directory path to `dist/` is readable:
|
||||
|
||||
```bash
|
||||
# Allow www-data execution on parent path components
|
||||
chmod 755 /home/seb /home/seb/src /home/seb/src/dashfront /home/seb/src/dashfront/dist
|
||||
```
|
||||
|
||||
Or deploy build artifacts into a standard web directory:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/www/dashfront
|
||||
sudo rsync -av --delete /home/seb/src/dashfront/dist/ /var/www/dashfront/dist/
|
||||
sudo chown -R www-data:www-data /var/www/dashfront
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Troubleshooting Matrix
|
||||
|
||||
| Symptom | Cause | Solution |
|
||||
|---|---|---|
|
||||
| **HTTP 404 on page reload** on sub-routes (`/artikel`, `/kunde`) | Missing SPA fallback in NGINX | Ensure `try_files $uri $uri/ /index.html;` is present in `location /`. |
|
||||
| **PWA does not update** after deployment | `sw.js` or `index.html` cached in browser | Verify `Cache-Control: no-cache, no-store, must-revalidate` on `sw.js`, `registerSW.js`, and `index.html`. |
|
||||
| **HTTP 413 (Payload Too Large)** when uploading invoice PDF or image | Default NGINX upload limit is 1MB | Add `client_max_body_size 100m;` to the `server` or `location /api/` block. |
|
||||
| **Live SSE events disconnect every 60s** | Default `proxy_read_timeout` timeout | Set `proxy_read_timeout 86400s;` and `proxy_buffering off;` on `/api/events`. |
|
||||
| **Login loop / Cookie not sent** | Secure cookie rejected on plain HTTP, or missing proxy headers | Access via HTTPS; ensure `proxy_set_header Host $http_host;` and `proxy_set_header X-Forwarded-Proto $scheme;` are set. |
|
||||
| **Barcode camera scanner disabled** | Page accessed over insecure HTTP or missing permissions policy | Use HTTPS; check that `Permissions-Policy "camera=(self)"` header is present. |
|
||||
| **WASM compilation error** (`incorrect response MIME type`) | NGINX serving `.wasm` as `application/octet-stream` | Add `default_type application/wasm;` for `\.wasm$` extensions. |
|
||||
| **Vite HMR disconnects immediately** (Dev proxy) | Missing WebSocket upgrade headers | Ensure `proxy_set_header Upgrade $http_upgrade;` and `proxy_set_header Connection $connection_upgrade;` are configured. |
|
||||
Reference in New Issue
Block a user