Files
OwnCord/Server/auth/tls_test.go
T
jevb 6eba999233 feat: add Let's Encrypt ACME support, fix security issues, improve server UX
Server:
- Add Let's Encrypt (ACME) TLS mode with autocert, HTTP-01 challenges on :80,
  and automatic certificate renewal (tls.mode: "acme" in config.yaml)
- Add ASCII art startup banner with server info and endpoint URLs
- Fix CSP blocking admin panel inline styles/scripts (per-route override)
- Suppress TLS handshake error noise in console output
- Fix TOCTOU race in invite consumption (atomic UPDATE with row-count check)
- Fix sendMsg mutex race condition (hold lock for entire send)
- Fix permission override formula (deny-first, allow-wins)
- Fix voice join parsing channelID before permission check
- Add session expiry check at WebSocket auth and periodic revalidation
- Add message length limit (4000 chars) and emoji length validation (32 bytes)
- Add file size enforcement in storage after io.Copy
- Add checksum URL validation in updater
- Add backup path traversal protection (BackupToSafe)
- Add self-modification guard in admin handlePatchUser
- Fix admin ownerOnlyMiddleware to use context user instead of re-auth
- Remove redundant startup log lines (banner shows same info)
- Add periodic expired session cleanup (15-min ticker)
- Add permissions package with bitfield constants and EffectivePerms
- Add rate limiter cleanup goroutine to prevent unbounded growth
- Add auth helpers (IsEffectivelyBanned, IsSessionExpired)
- Add WebSocket origin validation

Client:
- Add TOFU certificate trust service
- Add receive loop error handling
- Fix redundant else-if in OnChatMessage
2026-03-15 07:07:59 +01:00

291 lines
7.8 KiB
Go

package auth_test
import (
"crypto/tls"
"crypto/x509"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/owncord/server/auth"
"github.com/owncord/server/config"
)
func TestGenerateSelfSignedCreatesFiles(t *testing.T) {
tmpDir := t.TempDir()
certFile := filepath.Join(tmpDir, "cert.pem")
keyFile := filepath.Join(tmpDir, "key.pem")
if err := auth.GenerateSelfSigned(certFile, keyFile); err != nil {
t.Fatalf("GenerateSelfSigned() error: %v", err)
}
if _, err := os.Stat(certFile); os.IsNotExist(err) {
t.Error("cert.pem not created")
}
if _, err := os.Stat(keyFile); os.IsNotExist(err) {
t.Error("key.pem not created")
}
}
func TestGenerateSelfSignedProducesValidCert(t *testing.T) {
tmpDir := t.TempDir()
certFile := filepath.Join(tmpDir, "cert.pem")
keyFile := filepath.Join(tmpDir, "key.pem")
if err := auth.GenerateSelfSigned(certFile, keyFile); err != nil {
t.Fatalf("GenerateSelfSigned() error: %v", err)
}
// Load the generated cert/key pair.
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
t.Fatalf("tls.LoadX509KeyPair error: %v", err)
}
// Parse the leaf certificate.
leaf, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
t.Fatalf("x509.ParseCertificate error: %v", err)
}
// Verify validity period is at least 9 years in the future (10y cert).
minExpiry := time.Now().Add(9 * 365 * 24 * time.Hour)
if leaf.NotAfter.Before(minExpiry) {
t.Errorf("cert expires %v, expected at least 9 years from now (%v)", leaf.NotAfter, minExpiry)
}
// Verify it is a CA/self-signed cert.
if !leaf.IsCA {
t.Error("expected IsCA = true for self-signed cert")
}
}
func TestGenerateSelfSignedInvalidCertPath(t *testing.T) {
err := auth.GenerateSelfSigned("/nonexistent/dir/cert.pem", "/nonexistent/dir/key.pem")
if err == nil {
t.Error("GenerateSelfSigned() should error for invalid cert path")
}
}
func TestGenerateSelfSignedInvalidKeyPath(t *testing.T) {
tmpDir := t.TempDir()
certFile := filepath.Join(tmpDir, "cert.pem")
// Key path in non-existent dir.
err := auth.GenerateSelfSigned(certFile, "/nonexistent/dir/key.pem")
if err == nil {
t.Error("GenerateSelfSigned() should error for invalid key path")
}
}
func TestLoadOrGenerateSelfSigned(t *testing.T) {
tmpDir := t.TempDir()
certFile := filepath.Join(tmpDir, "cert.pem")
keyFile := filepath.Join(tmpDir, "key.pem")
cfg := config.TLSConfig{
Mode: "self_signed",
CertFile: certFile,
KeyFile: keyFile,
}
result, err := auth.LoadOrGenerate(cfg)
if err != nil {
t.Fatalf("LoadOrGenerate() error: %v", err)
}
if result.TLSConfig == nil {
t.Fatal("LoadOrGenerate() returned nil TLSConfig")
}
if len(result.TLSConfig.Certificates) == 0 {
t.Error("LoadOrGenerate() returned TLSConfig with no certificates")
}
if result.HTTPHandler != nil {
t.Error("self_signed mode should not set HTTPHandler")
}
}
func TestLoadOrGenerateLoadsExistingCert(t *testing.T) {
tmpDir := t.TempDir()
certFile := filepath.Join(tmpDir, "cert.pem")
keyFile := filepath.Join(tmpDir, "key.pem")
// Generate a cert first.
if err := auth.GenerateSelfSigned(certFile, keyFile); err != nil {
t.Fatalf("GenerateSelfSigned() error: %v", err)
}
cfg := config.TLSConfig{
Mode: "self_signed",
CertFile: certFile,
KeyFile: keyFile,
}
// Load the existing cert (should not regenerate).
result, err := auth.LoadOrGenerate(cfg)
if err != nil {
t.Fatalf("LoadOrGenerate() error: %v", err)
}
if len(result.TLSConfig.Certificates) == 0 {
t.Error("LoadOrGenerate() returned no certificates")
}
}
func TestLoadOrGenerateModeOff(t *testing.T) {
cfg := config.TLSConfig{Mode: "off"}
result, err := auth.LoadOrGenerate(cfg)
if err != nil {
t.Fatalf("LoadOrGenerate(mode=off) error: %v", err)
}
if result.TLSConfig != nil {
t.Error("LoadOrGenerate(mode=off) should return nil TLSConfig")
}
}
func TestLoadOrGenerateModeManualMissingFiles(t *testing.T) {
cfg := config.TLSConfig{
Mode: "manual",
CertFile: "/nonexistent/cert.pem",
KeyFile: "/nonexistent/key.pem",
}
_, err := auth.LoadOrGenerate(cfg)
if err == nil {
t.Error("LoadOrGenerate(mode=manual) should error when cert/key don't exist")
}
}
func TestLoadOrGenerateModeManualValidFiles(t *testing.T) {
tmpDir := t.TempDir()
certFile := filepath.Join(tmpDir, "cert.pem")
keyFile := filepath.Join(tmpDir, "key.pem")
// Pre-generate cert files.
if err := auth.GenerateSelfSigned(certFile, keyFile); err != nil {
t.Fatalf("GenerateSelfSigned() error: %v", err)
}
cfg := config.TLSConfig{
Mode: "manual",
CertFile: certFile,
KeyFile: keyFile,
}
result, err := auth.LoadOrGenerate(cfg)
if err != nil {
t.Fatalf("LoadOrGenerate(mode=manual) error: %v", err)
}
if len(result.TLSConfig.Certificates) == 0 {
t.Error("LoadOrGenerate(mode=manual) returned no certificates")
}
}
func TestLoadOrGenerateUnknownMode(t *testing.T) {
cfg := config.TLSConfig{Mode: "unknown_mode"}
_, err := auth.LoadOrGenerate(cfg)
if err == nil {
t.Error("LoadOrGenerate() should error for unknown TLS mode")
}
}
// ── ACME mode tests ───────────────────────────────────────────────────────
func TestLoadOrGenerateACME_MissingDomain(t *testing.T) {
cfg := config.TLSConfig{Mode: "acme", Domain: ""}
_, err := auth.LoadOrGenerate(cfg)
if err == nil {
t.Fatal("expected error for ACME mode without domain")
}
if !strings.Contains(err.Error(), "domain") {
t.Errorf("error should mention domain, got: %v", err)
}
}
func TestLoadOrGenerateACME_IPAddress(t *testing.T) {
cfg := config.TLSConfig{Mode: "acme", Domain: "192.168.1.1"}
_, err := auth.LoadOrGenerate(cfg)
if err == nil {
t.Fatal("expected error for ACME mode with IP address")
}
if !strings.Contains(err.Error(), "IP address") {
t.Errorf("error should mention IP address, got: %v", err)
}
}
func TestLoadOrGenerateACME_WildcardDomain(t *testing.T) {
cfg := config.TLSConfig{Mode: "acme", Domain: "*.example.com"}
_, err := auth.LoadOrGenerate(cfg)
if err == nil {
t.Fatal("expected error for ACME mode with wildcard domain")
}
if !strings.Contains(err.Error(), "wildcard") {
t.Errorf("error should mention wildcard, got: %v", err)
}
}
func TestLoadOrGenerateACME_ValidDomain(t *testing.T) {
tmpDir := t.TempDir()
cacheDir := filepath.Join(tmpDir, "acme_certs")
cfg := config.TLSConfig{
Mode: "acme",
Domain: "chat.example.com",
AcmeCacheDir: cacheDir,
}
result, err := auth.LoadOrGenerate(cfg)
if err != nil {
t.Fatalf("LoadOrGenerate(acme) error: %v", err)
}
if result.TLSConfig == nil {
t.Fatal("ACME mode should return non-nil TLSConfig")
}
if result.TLSConfig.GetCertificate == nil {
t.Error("ACME TLSConfig should have GetCertificate set")
}
if result.HTTPHandler == nil {
t.Error("ACME mode should return non-nil HTTPHandler")
}
// Verify cache directory was created.
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
t.Error("ACME cache directory was not created")
}
}
func TestLoadOrGenerateACME_HTTPRedirect(t *testing.T) {
tmpDir := t.TempDir()
cfg := config.TLSConfig{
Mode: "acme",
Domain: "chat.example.com",
AcmeCacheDir: filepath.Join(tmpDir, "acme_certs"),
}
result, err := auth.LoadOrGenerate(cfg)
if err != nil {
t.Fatalf("LoadOrGenerate(acme) error: %v", err)
}
// Non-challenge requests should redirect to HTTPS.
req := httptest.NewRequest(http.MethodGet, "http://chat.example.com/some/path", nil)
rec := httptest.NewRecorder()
result.HTTPHandler.ServeHTTP(rec, req)
if rec.Code != http.StatusMovedPermanently {
t.Errorf("expected 301 redirect, got %d", rec.Code)
}
loc := rec.Header().Get("Location")
if !strings.HasPrefix(loc, "https://chat.example.com/") {
t.Errorf("redirect should point to HTTPS, got: %s", loc)
}
}