mirror of
https://github.com/rishikanthc/Scriberr.git
synced 2026-06-29 15:26:02 +00:00
Adjust cookie security for proxy/HTTP dev
This commit is contained in:
7
Makefile
7
Makefile
@@ -36,7 +36,8 @@ dev: ## Start development environment with Air (backend) and Vite (frontend)
|
||||
echo "placeholder" > internal/web/dist/dummy_asset; \
|
||||
fi; \
|
||||
\
|
||||
trap 'echo ""; echo "🛑 Stopping development servers..."; kill 0; exit 0' INT TERM; \
|
||||
pids=""; \
|
||||
trap 'echo ""; echo "🛑 Stopping development servers..."; for pid in $$pids; do kill $$pid 2>/dev/null || true; done; wait $$pids 2>/dev/null || true; exit 0' INT TERM; \
|
||||
\
|
||||
echo "🧠 Starting ASR engine..."; \
|
||||
ASR_ENGINE_SOCKET=$${ASR_ENGINE_SOCKET:-/tmp/scriberr-asr.sock}; \
|
||||
@@ -47,6 +48,7 @@ dev: ## Start development environment with Air (backend) and Vite (frontend)
|
||||
( cd asr-engines/scriberr-asr-onnx && uv sync ) || true; \
|
||||
fi; \
|
||||
( cd asr-engines/scriberr-asr-onnx && uv run asr-engine-server --socket $$ASR_ENGINE_SOCKET ) & \
|
||||
pids="$$pids $$!"; \
|
||||
else \
|
||||
echo "⚠️ 'uv' not found. ASR engine will not start. Install uv or run make asr-engine-dev separately."; \
|
||||
fi; \
|
||||
@@ -54,13 +56,16 @@ dev: ## Start development environment with Air (backend) and Vite (frontend)
|
||||
if [ "$$USE_GO_RUN" = true ]; then \
|
||||
echo "🔧 Starting Go backend (standard run)..."; \
|
||||
ASR_ENGINE_SOCKET=$$ASR_ENGINE_SOCKET ASR_ENGINE_CMD="$$ASR_ENGINE_CMD" go run cmd/server/main.go & \
|
||||
pids="$$pids $$!"; \
|
||||
else \
|
||||
echo "🔥 Starting Go backend (with Air live reload)..."; \
|
||||
ASR_ENGINE_SOCKET=$$ASR_ENGINE_SOCKET ASR_ENGINE_CMD="$$ASR_ENGINE_CMD" air & \
|
||||
pids="$$pids $$!"; \
|
||||
fi; \
|
||||
\
|
||||
echo "⚛️ Starting React frontend (Vite)..."; \
|
||||
cd web/frontend && npm run dev & \
|
||||
pids="$$pids $$!"; \
|
||||
\
|
||||
wait
|
||||
|
||||
|
||||
11
README.md
11
README.md
@@ -192,7 +192,7 @@ For a containerized setup, you can use Docker. We provide two configurations: on
|
||||
> [!IMPORTANT]
|
||||
> **Permissions:** Ensure you set the `PUID` and `PGID` environment variables to your host user's UID and GID (typically `1000` on Linux) to avoid permission issues with the SQLite database. You can find your UID/GID by running `id` on your host.
|
||||
>
|
||||
> **HTTP vs HTTPS:** By default, Scriberr enables **Secure Cookies** in production. If you are accessing the app via plain HTTP (not HTTPS), you MUST set `SECURE_COOKIES=false` in your environment variables, otherwise you will encounter "Unable to load audio stream" errors.
|
||||
> **HTTP vs HTTPS:** Scriberr now **auto-detects** secure cookies based on TLS or proxy headers (`X-Forwarded-Proto` / `Forwarded`). If you're behind a proxy, keep `TRUST_PROXY_HEADERS=true` (default). To force a mode, set `SECURE_COOKIES=true` (always secure) or `SECURE_COOKIES=false` (always insecure).
|
||||
|
||||
#### Standard Deployment (CPU)
|
||||
|
||||
@@ -215,7 +215,8 @@ services:
|
||||
- APP_ENV=production # DO NOT CHANGE THIS
|
||||
# CORS: comma-separated list of allowed origins for production
|
||||
# - ALLOWED_ORIGINS=https://your-domain.com
|
||||
# - SECURE_COOKIES=false # Uncomment this ONLY if you are not using SSL
|
||||
# - TRUST_PROXY_HEADERS=true # Default; set false if you don't trust proxy headers
|
||||
# - SECURE_COOKIES=false # Force insecure cookies (not recommended in production)
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
@@ -262,7 +263,8 @@ services:
|
||||
- APP_ENV=production # DO NOT CHANGE THIS
|
||||
# CORS: comma-separated list of allowed origins for production
|
||||
# - ALLOWED_ORIGINS=https://your-domain.com
|
||||
# - SECURE_COOKIES=false # Uncomment this ONLY if you are not using SSL
|
||||
# - TRUST_PROXY_HEADERS=true # Default; set false if you don't trust proxy headers
|
||||
# - SECURE_COOKIES=false # Force insecure cookies (not recommended in production)
|
||||
|
||||
volumes:
|
||||
scriberr_data: {}
|
||||
@@ -358,13 +360,14 @@ Replace `1000` with the value you set for `PUID`/`PGID` (default is `1000`).
|
||||
|
||||
If the application loads but you cannot play or see the audio waveform (receiving "Unable to load audio stream"), this is often due to the **Secure Cookies** security flag.
|
||||
|
||||
By default, when `APP_ENV=production`, Scriberr enables `SECURE_COOKIES=true`. This prevents cookies from being sent over insecure (HTTP) connections.
|
||||
By default, Scriberr auto-detects secure cookies based on TLS or trusted proxy headers.
|
||||
|
||||
**Solutions:**
|
||||
- **Recommended:** Deploy Scriberr behind a Reverse Proxy (like Nginx, Caddy, or Traefik) and use SSL/TLS (HTTPS).
|
||||
- **Alternative:** If you must access over plain HTTP, set the following environment variable in your `docker-compose.yml`:
|
||||
```yaml
|
||||
environment:
|
||||
- TRUST_PROXY_HEADERS=true
|
||||
- SECURE_COOKIES=false
|
||||
```
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -76,6 +77,11 @@ func main() {
|
||||
// Load configuration
|
||||
logger.Startup("config", "Loading configuration")
|
||||
cfg := config.Load()
|
||||
if strings.EqualFold(cfg.SecureCookiesMode, "false") {
|
||||
logger.Warn("Secure cookies disabled; auth cookies will be sent over HTTP", "env", cfg.Environment)
|
||||
} else if strings.EqualFold(cfg.SecureCookiesMode, "auto") && !cfg.TrustProxyHeaders {
|
||||
logger.Warn("Secure cookies auto-detect without proxy headers; HTTPS required for auth cookies", "env", cfg.Environment)
|
||||
}
|
||||
|
||||
// Register adapters with config-based paths
|
||||
registerAdapters(cfg)
|
||||
|
||||
63
internal/api/cookies.go
Normal file
63
internal/api/cookies.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (h *Handler) cookieSecure(c *gin.Context) bool {
|
||||
switch strings.ToLower(h.config.SecureCookiesMode) {
|
||||
case "true", "force", "secure":
|
||||
return true
|
||||
case "false", "off", "insecure":
|
||||
return false
|
||||
default:
|
||||
return requestIsSecure(c, h.config.TrustProxyHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
func requestIsSecure(c *gin.Context, trustProxy bool) bool {
|
||||
if c.Request.TLS != nil {
|
||||
return true
|
||||
}
|
||||
if !trustProxy {
|
||||
return false
|
||||
}
|
||||
if proto := firstForwardedProto(c.GetHeader("X-Forwarded-Proto")); proto != "" {
|
||||
return strings.EqualFold(proto, "https")
|
||||
}
|
||||
if forwarded := c.GetHeader("Forwarded"); forwarded != "" {
|
||||
if proto := forwardedProto(forwarded); proto != "" {
|
||||
return strings.EqualFold(proto, "https")
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func firstForwardedProto(value string) string {
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Split(value, ",")
|
||||
return strings.TrimSpace(parts[0])
|
||||
}
|
||||
|
||||
func forwardedProto(header string) string {
|
||||
// Example: Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43
|
||||
parts := strings.Split(header, ";")
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
kv := strings.SplitN(part, "=", 2)
|
||||
if len(kv) != 2 {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(kv[0]), "proto") {
|
||||
return strings.Trim(strings.TrimSpace(kv[1]), "\"")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1567,7 +1567,7 @@ func (h *Handler) Login(c *gin.Context) {
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(24 * time.Hour), // Match your token duration constant
|
||||
HttpOnly: true,
|
||||
Secure: h.config.SecureCookies, // Use explicit secure flag
|
||||
Secure: h.cookieSecure(c),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
@@ -1600,7 +1600,7 @@ func (h *Handler) Logout(c *gin.Context) {
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: h.config.SecureCookies,
|
||||
Secure: h.cookieSecure(c),
|
||||
})
|
||||
// Also clear access token
|
||||
http.SetCookie(c.Writer, &http.Cookie{
|
||||
@@ -1611,7 +1611,7 @@ func (h *Handler) Logout(c *gin.Context) {
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: h.config.SecureCookies,
|
||||
Secure: h.cookieSecure(c),
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"})
|
||||
}
|
||||
@@ -1753,7 +1753,7 @@ func (h *Handler) Refresh(c *gin.Context) {
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(24 * time.Hour),
|
||||
HttpOnly: true,
|
||||
Secure: h.config.SecureCookies,
|
||||
Secure: h.cookieSecure(c),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
@@ -1781,7 +1781,7 @@ func (h *Handler) issueRefreshToken(c *gin.Context, userID uint) error {
|
||||
MaxAge: int((14 * 24 * time.Hour).Seconds()),
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: h.config.SecureCookies,
|
||||
Secure: h.cookieSecure(c),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -33,9 +33,10 @@ type Config struct {
|
||||
WhisperXEnv string
|
||||
|
||||
// Environment configuration
|
||||
Environment string
|
||||
AllowedOrigins []string
|
||||
SecureCookies bool // Explicit control over Secure flag (for HTTPS deployments)
|
||||
Environment string
|
||||
AllowedOrigins []string
|
||||
SecureCookiesMode string // "auto" (default), "true"/"false"
|
||||
TrustProxyHeaders bool // Whether to trust X-Forwarded-Proto/Forwarded
|
||||
// OpenAI configuration
|
||||
OpenAIAPIKey string
|
||||
|
||||
@@ -50,26 +51,26 @@ func Load() *Config {
|
||||
logger.Debug("No .env file found, using system environment variables")
|
||||
}
|
||||
|
||||
// Default SecureCookies to true in production, false otherwise
|
||||
defaultSecure := "false"
|
||||
if strings.ToLower(getEnv("APP_ENV", "development")) == "production" {
|
||||
defaultSecure = "true"
|
||||
defaultSecureMode := strings.ToLower(getEnv("SECURE_COOKIES", "auto"))
|
||||
if defaultSecureMode == "" {
|
||||
defaultSecureMode = "auto"
|
||||
}
|
||||
|
||||
return &Config{
|
||||
Port: getEnv("PORT", "8080"),
|
||||
Host: getEnv("HOST", "0.0.0.0"),
|
||||
Environment: getEnv("APP_ENV", "development"),
|
||||
AllowedOrigins: strings.Split(getEnv("ALLOWED_ORIGINS", "http://localhost:5173,http://localhost:8080"), ","),
|
||||
DatabasePath: getEnv("DATABASE_PATH", "data/scriberr.db"),
|
||||
JWTSecret: getJWTSecret(),
|
||||
UploadDir: getEnv("UPLOAD_DIR", "data/uploads"),
|
||||
TranscriptsDir: getEnv("TRANSCRIPTS_DIR", "data/transcripts"),
|
||||
TempDir: getEnv("TEMP_DIR", "data/temp"),
|
||||
WhisperXEnv: getEnv("WHISPERX_ENV", "data/whisperx-env"),
|
||||
SecureCookies: getEnv("SECURE_COOKIES", defaultSecure) == "true",
|
||||
OpenAIAPIKey: getEnv("OPENAI_API_KEY", ""),
|
||||
HFToken: getEnv("HF_TOKEN", ""),
|
||||
Port: getEnv("PORT", "8080"),
|
||||
Host: getEnv("HOST", "0.0.0.0"),
|
||||
Environment: getEnv("APP_ENV", "development"),
|
||||
AllowedOrigins: strings.Split(getEnv("ALLOWED_ORIGINS", "http://localhost:5173,http://localhost:8080"), ","),
|
||||
DatabasePath: getEnv("DATABASE_PATH", "data/scriberr.db"),
|
||||
JWTSecret: getJWTSecret(),
|
||||
UploadDir: getEnv("UPLOAD_DIR", "data/uploads"),
|
||||
TranscriptsDir: getEnv("TRANSCRIPTS_DIR", "data/transcripts"),
|
||||
TempDir: getEnv("TEMP_DIR", "data/temp"),
|
||||
WhisperXEnv: getEnv("WHISPERX_ENV", "data/whisperx-env"),
|
||||
SecureCookiesMode: defaultSecureMode,
|
||||
TrustProxyHeaders: strings.ToLower(getEnv("TRUST_PROXY_HEADERS", "true")) == "true",
|
||||
OpenAIAPIKey: getEnv("OPENAI_API_KEY", ""),
|
||||
HFToken: getEnv("HF_TOKEN", ""),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ For a containerized setup, you can use Docker. We provide two configurations: on
|
||||
|
||||
> **Permissions:** Ensure you set the `PUID` and `PGID` environment variables to your host user's UID and GID (typically `1000` on Linux) to avoid permission issues with the SQLite database. You can find your UID/GID by running `id` on your host.
|
||||
>
|
||||
> **HTTP vs HTTPS:** By default, Scriberr enables **Secure Cookies** in production. If you are accessing the app via plain HTTP (not HTTPS), you MUST set `SECURE_COOKIES=false` in your environment variables, otherwise you will encounter "Unable to load audio stream" errors.
|
||||
> **HTTP vs HTTPS:** Scriberr auto-detects secure cookies based on TLS or proxy headers (`X-Forwarded-Proto` / `Forwarded`). If you're behind a proxy, keep `TRUST_PROXY_HEADERS=true` (default). To force a mode, set `SECURE_COOKIES=true` (always secure) or `SECURE_COOKIES=false` (always insecure).
|
||||
|
||||
### Standard Deployment (CPU)
|
||||
|
||||
@@ -102,7 +102,8 @@ services:
|
||||
- APP_ENV=production # DO NOT CHANGE THIS
|
||||
# CORS: comma-separated list of allowed origins for production
|
||||
# - ALLOWED_ORIGINS=https://your-domain.com
|
||||
# - SECURE_COOKIES=false # Uncomment this ONLY if you are not using SSL
|
||||
# - TRUST_PROXY_HEADERS=true # Default; set false if you don't trust proxy headers
|
||||
# - SECURE_COOKIES=false # Force insecure cookies (not recommended in production)
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
@@ -149,7 +150,8 @@ services:
|
||||
- APP_ENV=production # DO NOT CHANGE THIS
|
||||
# CORS: comma-separated list of allowed origins for production
|
||||
# - ALLOWED_ORIGINS=https://your-domain.com
|
||||
# - SECURE_COOKIES=false # Uncomment this ONLY if you are not using SSL
|
||||
# - TRUST_PROXY_HEADERS=true # Default; set false if you don't trust proxy headers
|
||||
# - SECURE_COOKIES=false # Force insecure cookies (not recommended in production)
|
||||
|
||||
volumes:
|
||||
scriberr_data: {}
|
||||
|
||||
@@ -23,12 +23,13 @@ Replace `10001` with the value you set for `PUID`/`PGID` (default is `1000`).
|
||||
|
||||
If the application loads but you cannot play or see the audio waveform (receiving "Unable to load audio stream"), this is often due to the **Secure Cookies** security flag.
|
||||
|
||||
By default, when `APP_ENV=production`, Scriberr enables `SECURE_COOKIES=true`. This prevents cookies from being sent over insecure (HTTP) connections.
|
||||
By default, Scriberr auto-detects secure cookies based on TLS or trusted proxy headers.
|
||||
|
||||
### Solutions:
|
||||
- **Recommended:** Deploy Scriberr behind a Reverse Proxy (like Nginx, Caddy, or Traefik) and use SSL/TLS (HTTPS).
|
||||
- **Alternative:** If you must access over plain HTTP, set the following environment variable in your `docker-compose.yml`:
|
||||
```yaml
|
||||
environment:
|
||||
- TRUST_PROXY_HEADERS=true
|
||||
- SECURE_COOKIES=false
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user