# SPDX-FileCopyrightText: 2024 XWiki CryptPad Team <contact@cryptpad.org> and contributors
#
# SPDX-License-Identifier: AGPL-3.0-or-later

#   This file is included strictly as an example of how Caddy can be configured
#   to work with CryptPad. This example WILL NOT WORK AS IS. For best results,
#   compare the sections of this configuration file against a working CryptPad
#   installation (http server by the Nodejs process). If you are using CryptPad
#   in production and require professional support please contact sales@cryptpad.org

(trustedProxies) {
	# Force Caddy to accept `X-Forwarded-For` and other origin headers.
	# Modify the line below if you want to restrict the scope of direct downstream sending these headers.
	trusted_proxies 0.0.0.0/0 ::/0
}

# Caddy does not have variables for server names, so domains need to be hardcoded.
# You can bulk replace "your-main-domain.com" and "your-sandbox-domain.com" safely.
your-main-domain.com:443,
your-sandbox-domain.com:443 {
	# Define your certificates below.
	# No need to adjust TLS configurations, as the defaults in Caddy are already secure.
	tls /path/to/fullchain/publicKey.pem /path/to/certificate/privateKey.pem
	
	# Enable HSTS.
	# Do not enable this line when configuring over mixnet, e.g. Tor.
	header Strict-Transport-Security "max-age=63072000; includeSubDomains"
	
	# Security headers
	header X-XSS-Protection "1; mode=block"
	header X-Content-Type-Options "nosniff"
	header Access-Control-Allow-Credentials "true"
	#header X-Frame-Options "SAMEORIGIN"

	# OnlyOffice fonts may be loaded from both domains.
	@onlyOfficeFonts {
		path_regexp "^\\/common\\/onlyoffice\\/.*\\/fonts\\/.*$"
	}
	header Access-Control-Allow-Origin "*"
	
	# By default CryptPad forbids remote domains from embedding CryptPad documents in iframes.
	# The sandbox domain must always be permitted in order for the platform to function.
	# If you wish to enable remote embedding you may change the value below to "*"
	# as per the commented value.
	header ?Access-Control-Allow-Origin "https://your-sandbox-domain.com"
	#header ?Access-Control-Allow-Origin "*"
	
	# Opt out of Google's FLoC Network
	header Permissions-Policy "interest-cohort=()"
	
	# Enable SharedArrayBuffer in Firefox (for .xlsx export)
	header ?Cross-Origin-Resource-Policy "cross-origin"
	header ?Cross-Origin-Embedder-Policy "require-corp"
	
	# Specify the relative path to root of your custom error page.
	# This error page won't only be served for 404 errors.
	handle_errors {
		rewrite * /error.htm
		header Cache-Control "no-cache, no-store"
		file_server
		templates
	}
	
	# Insert the path to your CryptPad repository root here
	root /home/cryptpad/cryptpad
	
	# Any static assets loaded with "vers=" in their URL will be cached for a year
	@staticAssets {
		query "ver=*"
	}
	header @staticAssets Cache-Control "max-age=31536000"
	
	vars {
		# CSS can be dynamically set inline, loaded from the same domain, or from your main domain.
		styleSrc   "'unsafe-inline' 'self' https://your-main-domain.com"
		
		# connect-src restricts URLs which can be loaded using script interfaces.
		# If you have configured your instance to use a dedicated file delivery domain or API domain,
		# you will need to add them below.
		connectSrc "'self' https://your-main-domain.com blob: wss://api.your-main-domain.com https://your-sandbox-domain.com"
		
		# Fonts can be loaded from data-URLs or the main domain.
		fontSrc	"'self' data: https://your-main-domain.com"
		
		# Images can be loaded from anywhere, though we'd like to deprecate this as it allows
		# the use of images for tracking.
		imgSrc	 "'self' data: blob: https://your-main-domain.com"
		
		# frame-src specifies valid sources for nested browsing contexts.
		# This prevents loading any iframes from anywhere other than the sandbox domain.
		frameSrc   "'self' https://your-sandbox-domain.com blob:"
		
		# media-src specifies valid sources for loading media using video or audio.
		mediaSrc   "blob:"
		
		# child-src defines valid sources for webworkers and nested browser contexts.
		# It is deprecated in favour of worker-src and frame-src.
		childSrc   "https://your-main-domain.com"
		
		# worker-src valid sources for Worker, Shared Worker, or Service Worker scripts.
		# Supercedes child-src, but is unfortunately not yet universally supported.
		workerSrc  "'self'"
		
		# script-src specifies valid sources for JavaScript, including inline handlers.
		scriptSrc  "'self' resource: https://your-main-domain.com"
		
		# frame-ancestors specifies which origins can embed your CryptPad instance.
		# This must include 'self' and your main domain (over HTTPS) in order for CryptPad to work,
		# if you have enabled remote embedding via the admin panel, then this must be more permissive.
		# Note: cryptpad.fr permits web pages served via https: and vector: (element desktop app)
		frameAncestors "'self' https://your-main-domain.com"
		#frameAncestors "'self' https: vector:"
		
		# A few assets are loaded via the sandbox domain.
		# They unfortunately still require exceptions to the sandboxing to work correctly.
		# Everything except the sandbox domain is a privileged scope, as they might be used to handle keys.
		# Unsafe iframes are exceptions. Office file formats are converted outside of the sandboxed scope,
		# because of bugs in Chromium-based browsers that incorrectly ignore headers supposed to enable
		# the use of some modern APIs, that are required when JavaScript is run in a cross-origin context.
		# We've applied other sandboxing techniques to mitigate the risk of running WebAssembly
		# in this privileged scope.
		# Privileged contexts allow a few more rights than unprivileged contexts, though limits are still applied.
		scriptSrcUnsafe "'self' 'unsafe-eval' 'unsafe-inline' resource: https://your-main-domain.com"
	}
	
	# Finally, set all the security rules you have composed above.
	@privilegedScope1 {
		host "your-sandbox-domain.com"
		path_regexp "^\\/(sheet|doc|presentation)\\/inner.html.*$"
	}
	@privilegedScope2 {
		host "your-sandbox-domain.com"
		path_regexp "^\\/common\\/onlyoffice\\/.*\\/.*\\.html.*$"
	}
	@privilegedScope3 {
		host "your-sandbox-domain.com"
		path_regexp "^\\/unsafeiframe\\/inner\\.html.*$"
	}
	header @privilegedScope1 Content-Security-Policy "default-src 'none'; child-src {vars.childSrc}; worker-src {vars.workerSrc}; media-src {vars.mediaSrc}; style-src {vars.styleSrc}; script-src {vars.scriptSrcUnsafe}; connect-src {vars.connectSrc}; font-src {vars.fontSrc}; img-src {vars.imgSrc}; frame-src {vars.frameSrc}; frame-ancestors {vars.frameAncestors}"
	header @privilegedScope2 Content-Security-Policy "default-src 'none'; child-src {vars.childSrc}; worker-src {vars.workerSrc}; media-src {vars.mediaSrc}; style-src {vars.styleSrc}; script-src {vars.scriptSrcUnsafe}; connect-src {vars.connectSrc}; font-src {vars.fontSrc}; img-src {vars.imgSrc}; frame-src {vars.frameSrc}; frame-ancestors {vars.frameAncestors}"
	header @privilegedScope3 Content-Security-Policy "default-src 'none'; child-src {vars.childSrc}; worker-src {vars.workerSrc}; media-src {vars.mediaSrc}; style-src {vars.styleSrc}; script-src {vars.scriptSrcUnsafe}; connect-src {vars.connectSrc}; font-src {vars.fontSrc}; img-src {vars.imgSrc}; frame-src {vars.frameSrc}; frame-ancestors {vars.frameAncestors}"
	header ?Content-Security-Policy "default-src 'none'; child-src {vars.childSrc}; worker-src {vars.workerSrc}; media-src {vars.mediaSrc}; style-src {vars.styleSrc}; script-src {vars.scriptSrc}; connect-src {vars.connectSrc}; font-src {vars.fontSrc}; img-src {vars.imgSrc}; frame-src {vars.frameSrc}; frame-ancestors {vars.frameAncestors}"
	
	# Add support for .mjs files used by pdfjs
	@fileModuleJS {
		path "*.mjs"
	}
	header @fileModuleJS Content-Type "application/javascript"
	
	# The Node.js process can handle all traffic, whether accessed over websocket or as static assets.
	# We prefer to serve static content from Caddy directly, and to leave the API server to handle the
	# the dynamic content that only it can manage. This is primarily for optimization.
	handle /cryptpad_websocket/* {
		reverse_proxy * {
			to 127.0.0.1:3003
			header_up Host "{host}"
			header_up X-Real-IP "{remote_host}"
			
			# Caddy supports WebSockets directly. No additional headers are needed.

			import trustedProxies
		}
	}
	
	handle_path /customize.dist/* {
		# This is needed in order to prevent infinite recursion between /customize/ and the root.
	}
	
	# Try to load customizeable content via /customize/ and fall back to the default content located
	# at /customize.dist/ .
	# This is what allows you to override behaviour.
	handle_path /customize/* {
		try_files /customize/{path} /customize.dist/{path}
		file_server {
			index index.html index.htm default.html default.htm
		}
	}
	
	# /api/config is loaded once per page load, and is used to retrieve the caching variable,
	# which is applied to every other resource loaded during that session.
	@sharedReverseProxy {
		path /api/*
		path /extensions.js
	}
	handle @sharedReverseProxy {
		reverse_proxy * {
			to 127.0.0.1:3000
			header_up Host "{host}"
			header_up X-Real-IP "{remote_host}"
			
			# These settings prevent both Caddy and the API server from setting duplicate headers.
			header_down Cross-Origin-Resource-Policy cross-origin
			header_down Cross-Origin-Embedder-Policy require-corp

			import trustedProxies
		}
	}
	
	# Requests for blobs and blocks are now proxied to the API server.
	# This simplifies Caddy path configuration, in the event they are being hosted in a non-standard location
	# or with odd unexpected permissions. Serving blobs in this manner also means that it will be possible to
	# enforce access control for them, though this is not yet implemented.
	# Access control (via TOTP 2FA) has been added to blocks, so they can be handled with the same directives.
	@blobsAndBlocks {
		path /blob/*
		path /block/*
	}
	handle @blobsAndBlocks {
		@corsPreflight {
			method OPTIONS
		}
		handle @corsPreflight {
			header Access-Control-Allow-Origin "https://your-sandbox-domain.com"
			header Access-Control-Allow-Credentials "true"
			header Access-Control-Allow-Methods "GET, POST, OPTIONS"
			header Access-Control-Allow-Headers "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range"
			header Access-Control-Max-Age "1728000"
			header Content-Type "application/octet-stream; charset=utf-8"
			header Content-Length "0"
			respond 204
		}
		reverse_proxy * {
			to 127.0.0.1:3000
			# Preventing these headers from getting duplicated, since we are proxying to the API server.
			header_down -X-Content-Type-Options
			header_down -Access-Control-Allow-Origin
			header_down -Permissions-Policy
			header_down -X-XSS-Protection
			header_down -Cross-Origin-Resource-Policy
			header_down -Cross-Origin-Embedder-Policy
		}
	}
	
	# The Node.JS server has some built-in forwarding rulesets to prevent URLs not suffixed with a slash
	# from resulting in a 404 error. This simply adds a trailing slash to a variety of applications.
	@preventNotFound {
		path_regexp "^/(register|login|recovery|settings|user|pad|drive|poll|slide|code|whiteboard|file|media|profile|contacts|todo|filepicker|debug|kanban|sheet|support|admin|notifications|teams|calendar|presentation|doc|form|report|convert|checkup|diagram)$"
	}
	redir @preventNotFound "{path}/"
	
	# Enable file serving
	file_server {
		index index.html index.htm default.html default.htm
	}
}
