##
# Caddy 2 Configuration - us.onetimesecret.com (2026-02-27)
#
# Usage:
#
#   $ envsubst < config/Caddyfile.template > /etc/caddy/Caddyfile
#   $ ots-containers proxy render
#
# caddy add-package github.com/caddy-dns/cloudflare github.com/caddyserver/transform-encoder github.com/mholt/caddy-l4
#
# journalctl -t caddy -o json -f | jq
# journalctl -t caddy -o json-pretty -f
#
# Permissions issues:
#
#    $ sudo -u caddy ls -la /var/www/public/web/
#
#    Instead of a symlink, create a bind mount which works at the
#    kernel level.
#
#    $ sudo mkdir -p /var/www/public
#    $ sudo mount --bind /var/lib/containers/storage/volumes/static_assets/_data /var/www/public
#
#    To survive restarts, add it to fstab
#    $ vi /etc/fstab
#    /var/lib/containers/storage/volumes/static_assets/_data /var/www/public none bind 0 0
{
  # This email address is used for ACME (Let's Encrypt) contacts
  # and depending on your customer domain privacy settings, may be
  # publicly visible in the certificate transparency logs.
  email "domains@onetimesecret.com"

  admin off

  servers {
    # Optimize keepalive settings
    keepalive_interval 20s
    max_header_size 4kb

    # Set explicit timeouts (updated 2025-05-18)
    timeouts {
      read_body 30s
      read_header 10s
      write 60s
      idle 5m
    }

    # Protect against slow DoS attacks
    protocols h1 h2 h3

    # If we're behind a proxy, this tells caddy that it's not unsafe
    # to trust the client IP address(es) in the forwarded-for
    # header. It's a security measure to prevent someone from
    # spoofing a different IP address than their own.
    trusted_proxies static private_ranges
    client_ip_headers X-Forwarded-For X-Real-IP
  }

  # TLS configuration with secure defaults
  default_sni ${JURISDICTION}.onetimesecret.com

  # NOTE: Do not use global DNS-01 validation for custom domains
  # i.e. "acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}"
}

(cors_headers) {
  header {
    Access-Control-Allow-Origin "{args[0]}"
    Access-Control-Allow-Methods "GET, POST, OPTIONS"
    Access-Control-Allow-Headers "Content-Type, Authorization, O-*"
    Access-Control-Allow-Credentials "true"
    Access-Control-Max-Age "86400"  # Increased from 1200 to 24 hours
  }
}

(cors) {
  # Main production site
  @origin_main header Origin https://onetimesecret.com
  handle @origin_main {
    import cors_headers https://onetimesecret.com
  }

  # Development environments
  @origin_web_onetime header Origin https://web.onetime.dev
  handle @origin_web_onetime {
    import cors_headers https://web.onetime.dev
  }

  @origin_staging header Origin https://onetimesecret.dev
  handle @origin_staging {
    import cors_headers https://onetimesecret.dev
  }

  # IMPORTANT: Add a default handler for unmatched origins (for logging only)
  @no_cors_match {
    header Origin *
    not header Origin https://onetimesecret.com
    not header Origin https://web.onetime.dev
    not header Origin https://onetimesecret.dev
  }
  handle @no_cors_match {
    # Log and reject unmatched origins
    # Just log it - no CORS headers sent
  }
}

# NOTE: If you see `$ HOSTNAME` and `$ JURISDICTION` on the next
# line, you're looking at the template caddy configuration. See
# note above about calling envsubst to generate the final Caddyfile.
# (the vars above have spaces added so that they don't get replaced
# too and make this inline docs very confusing).
${HOSTNAME}.onetimesecret.com ${JURISDICTION}.onetimesecret.com ca-tor-lb-02.onetimesecret.com ${JURISDICTION}.onetime.co ca-tor-lb-02.onetime.co {
  tls {
    dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    protocols tls1.3
  }

  root * /var/www/public/web

  # Add a unique ID for every request for enhanced debugging and logging.
  # Excludes favicon requests for efficiency.
  @always {
    not path /favicon.ico
  }
  # Adds the O-Request-ID header to all requests (excluding
  # favicon.ico) using a UUID.
  header @always O-Request-ID "{http.request.uuid}"

  # CRITICAL FIX: Handle OPTIONS preflight requests FIRST with proper CORS
  @options {
    method OPTIONS
    path /api/*  # Match all API paths, not just specific endpoints
  }
  handle @options {
    # Apply CORS headers based on Origin
    import cors
    # MUST respond 200 or 204 with headers
    respond "" 204
  }

  # Serve static files if they exist
  @exists file
  handle @exists {
    file_server
  }

  header {
    # Don't send referrer information when leaving the site
    Referrer-Policy "no-referrer"
    X-Content-Type-Options nosniff
    X-Frame-Options DENY
    X-XSS-Protection "1; mode=block"
    X-Via tor

    # Prevent caching of sensitive content
    Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
    Pragma "no-cache"

    # Prevent search engines from indexing
    X-Robots-Tag "noindex, nofollow, noarchive, nosnippet, noimageindex"

    # Remove Server header to minimize fingerprinting
    -Server
  }

  encode {
    minimum_length 1024
    zstd
    gzip 7
  }

  # Block access to sensitive files
  @sensitive {
    path */wp-config.php */.env */.git/* */config.php */.htaccess */readme.html */readme.md
    path */phpinfo.php */info.php */test.php */server-status */server-info
    path */admin* */wp-admin
  }
  respond @sensitive 404

  # Block known bad user agents and common attack patterns
  @blocklist {
    header_regexp User-Agent (nmap|nikto|sqlmap|gobuster|masscan|zmap|zgrab|wpscan|dirbuster)
    path */wp-login.php */xmlrpc.php */eval-stdin.php */install.php
  }
  respond @blocklist 404

  # API endpoints handler - MUST come after OPTIONS handler
  @api_endpoints {
    path /api/v2/secret/conceal /api/v2/secret/generate /api/v2/status
    not method OPTIONS  # Exclude OPTIONS as it's handled above
  }
  handle @api_endpoints {
    # Apply CORS headers to actual API requests
    import cors

    # Reverse proxy to your backend here if needed
    # reverse_proxy localhost:8080  # Example
  }


  # Forward all other requests to backend
  reverse_proxy {
    # Pass custom domain relevant headers to backend
    header_up Host {http.request.host}
    header_up Apx-Incoming-Host {http.request.header.Apx-Incoming-Host}
    header_up X-Forwarded-Host {http.request.header.X-Forwarded-Host}
    header_up X-Original-Host {http.request.host}
    header_up X-Real-IP {http.request.remote.host}

    # Filter request headers to prevent header smuggling
    header_down -Server

    to 127.0.0.1:7043-7044

    # Load Balancer
    #
    # @see https://caddyserver.com/docs/caddyfile/directives/reverse_proxy#load-balancing
    lb_policy least_conn

    # How many times to retry selecting available backends (default: 0).
    # retries may stop early if the duration is reached. In other words,
    # the retry duration takes precedence over the retry count.
    #lb_retries 1

    # how long to try selecting available backends for each request if
    # the next available host is down. (default: 0)
    #lb_try_duration 5s

    # Active health checking
    #
    # @see https://caddyserver.com/docs/caddyfile/directives/reverse_proxy#active-health-checks
    health_uri /api/v3/status

    # Substring or regular expression to match
    health_body nominal

    # How often to check (default: 30s)
    health_interval 20s

    # Consecutive checks to mark healthy/unhealthy (default: 1)
    health_passes 1
    health_fails 1

    # How long to wait before marking down (default: 5s)
    health_timeout 5s

    # Passive health checks
    #
    # @see https://caddyserver.com/docs/caddyfile/directives/reverse_proxy#passive-health-checks

    # How long to remember a failed request; a duration > 0 enables
    # passive health checking. (default: 30s)
    fail_duration 30s

    transport http {
      compression off

      # May 18: Something to try if the Gateway 504 errors are still
      # happening after increasing the timeouts.
      #
      keepalive off  # Add this to test if connection reuse is the issue

      read_timeout 15s  # Increase from 5s
      write_timeout 60s # from 30s
      dial_timeout 5s # from 2s
    }
  }

  # Site-wide logging configuration
  # https://github.com/caddyserver/transform-encoder?tab=readme-ov-file
  log {
    output stdout
    format json {
      time_format unix_milli_float
      duration_format string
    }

    level INFO
    #exclude_headers authorization cookie set-cookie
  }

  # HSTS configuration
  #
  # The host matcher only fires when a request actually arrives with a matching
  # Host header. If no traffic for onetime.dev reaches this Caddy instance, the
  # matcher never triggers for it, and the header directive is inert for that
  # domain. There's no overhead, no failed lookups, no error logging.
  #
  # NOTE: For domains that are proxied through CloudFlare, these settings are
  # overridden and configured in the CloudFlare SSL/TLS Dashboard.
  #
  # A conservative default (max-age in the hours-to-days range rather than two
  # years, no includeSubDomains, no preload) lets customers get the security
  # benefit while limiting blast radius if they change their mind. Customers
  # who want the full max-age=63072000; includeSubDomains; preload treatment
  # can opt in explicitly, understanding it's not easily reversible.
  #
  @hsts_domains {
    host onetimesecret.com *.onetimesecret.com onetime.co *.onetime.co onetime.dev *.onetime.dev
  }
  header @hsts_domains Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"

  # re: Content Security Policies (CSP)
  #
  # How will these headers work with custom hostnames? Approximated?
  #
  #
  # The Content-Security-Policy header helps prevent cross-site scripting
  # attacks by allowing us to control what resources the browser is allowed
  # to load for a particular page. The header is a string consisting of one
  # or more directives that specify the allowed sources for a type of resource.
  #
  # See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
  #
  #
  # Very restrictive
  #
  # header Content-Security-Policy "default-src 'self'; connect-src 'self' dev.example.com; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self';"
  #
  # Moderately restrictive
  #
  # header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';"
  #
  # Least restrictive
  #
  # header Content-Security-Policy "default-src 'self' dev.example.com; connect-src 'self' dev.example.com; script-src 'self' 'unsafe-inline' dev.example.com; style-src 'self' 'unsafe-inline' dev.example.com; img-src 'self' data: dev.example.com; font-src 'self' dev.example.com;"

  #header Access-Control-Allow-Origin *
  #header Access-Control-Allow-Methods GET, POST, PUT, OPTIONS
  #header Access-Control-Allow-Headers Content-Type, Authorization, X-CSRF-Token
}

# Caddy relies on this to be available for hot reloads
#
# i.e. systemctl reload caddy.service
:2019 {
  @local_only {
    remote_ip 127.0.0.1 ::1
  }

  # (Optional) Custom Debugging endpoint - only available locally
  handle /debug/headers {
    @allowed {
      expression {remote_ip} == '127.0.0.1' || {remote_ip} == '::1'
    }
    respond @allowed "Headers debug endpoint" 200 {
      close
    }
    respond 403
  }

  log {
    output stdout
    level DEBUG
  }
}
