//! atproto-static-web   A simple web viewer for AT Proto.
//! Copyright (C) 2025  AverageHelper
//!
//! This program is free software: you can redistribute it and/or modify
//! it under the terms of the GNU General Public License as published by
//! the Free Software Foundation, either version 3 of the License, or
//! (at your option) any later version.
//!
//! This program is distributed in the hope that it will be useful,
//! but WITHOUT ANY WARRANTY; without even the implied warranty of
//! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//! GNU General Public License for more details.
//!
//! You should have received a copy of the GNU General Public License
//! along with this program.  If not, see <https://www.gnu.org/licenses/>.

use rocket::{
	Request, Response,
	fairing::{Fairing, Info, Kind},
	http::{Header, hyper::header::CONTENT_SECURITY_POLICY},
	shield::{Allow, Feature, Frame, Hsts, NoSniff, Permission, Prefetch, Referrer, Shield},
	time::Duration,
};

/// Middware that sends some security headers on each request.
pub fn shield() -> Shield {
	Shield::new()
		.enable(NoSniff::Enable) // X-Content-Type-Options: nosniff
		.enable(Frame::Deny) // X-Frame-Options: DENY
		.enable(Hsts::Preload(Duration::seconds(31536000))) // Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
		.enable(Referrer::NoReferrer) // Referrer-Policy: no-referrer
		.enable(Prefetch::Off) // X-DNS-Prefetch-Control: off
		.enable(
			Permission::blocked(Feature::Accelerometer) // accelerometer=()
				// .block(Feature::AllScreensCapture) // TODO: all-screens-capture=()
				.block(Feature::AmbientLightSensor) // ambient-light-sensor=()
				// .block(Feature::AttributionReporting) // TODO: attribution-reporting=()
				.block(Feature::Autoplay) // autoplay=()
				.block(Feature::Battery) // battery=()
				// .block(Feature::Bluetooth) // TODO: bluetooth=()
				// .block(Feature::BrowsingTopics) // TODO: browsing-topics=()
				.block(Feature::Camera) // camera=()
				// .block(Feature::CapturedSurfaceControl) // TODO: captured-surface-control=()
				.block(Feature::ClipboardRead) // clipboard-read=()
				.block(Feature::ClipboardWrite) // clipboard-write=()
				.block(Feature::ConversionMeasurement) // conversion-measurement=()
				.block(Feature::CrossOriginIsolated) // cross-origin-isolated=()
				// .block(Feature::DigitalCredentialsGet) // TODO: digital-credentials-get=()
				// .block(Feature::DirectSockets) // TODO: direct-sockets=()
				.block(Feature::Displaycapture) // display-capture=()
				.block(Feature::DocumentDomain) // document-domain=()
				.block(Feature::EncryptedMedia) // encrypted-media=()
				.block(Feature::ExecutionWhileNotRendered) // execution-while-not-rendered=()
				.block(Feature::ExecutionWhileOutOfviewport) // execution-while-out-of-viewport=()
				.block(Feature::FocusWithoutUserActivation) // focus-without-user-activation=()
				.block(Feature::Fullscreen) // fullscreen=()
				.block(Feature::Gamepad) // gemepad=()
				.block(Feature::Geolocation) // geolocation=()
				.block(Feature::Gyroscope) // gyroscope=()
				.block(Feature::Hid) // hid=()
				// .block(Feature::IdentityCredentialsGet) // TODO: identity-credentials-get=()
				.block(Feature::IdleDetection) // idle-detection=()
				.block(Feature::InterestCohort) // interest-cohort=()
				// .block(Feature::JoinAdInterestGroup) // TODO: join-ad-interest-group=()
				// .block(Feature::KeyboardMap) // TODO: keyboard-map=()
				// .block(Feature::LocalFonts) // TODO: local-fonts=()
				.block(Feature::Magnetometer) // magnetometer=()
				.block(Feature::Microphone) // microphone=()
				.block(Feature::Midi) // midi=()
				.block(Feature::NavigationOverride) // navigation-override=()
				.block(Feature::Payment) // payment=()
				.allow(Feature::PictureInPicture, [Allow::Any]) // picture-in-picture=*
				.block(Feature::PublickeyCredentialsGet) // publickey-credentials-get=()
				// .block(Feature::RunAdAuction) // TODO: run-ad-auction=()
				.block(Feature::ScreenWakeLock) // screen-wake-lock=()
				.block(Feature::Serial) // serial=()
				// .block(Feature::SharedAutofill) // TODO: shared-autofill=()
				// .block(Feature::SmartCard) // TODO: smart-card=()
				.block(Feature::SpeakerSelection) // speaker-selection=()
				// .block(Feature::StorageAccess) // TODO: storage-access=()
				.block(Feature::SyncScript) // sync-script=()
				.block(Feature::SyncXhr) // sync-xhr=()
				.block(Feature::TrustTokenRedemption) // trust-token-redemption=()
				// .block(Feature::Unload) // TODO: unload=()
				.block(Feature::Usb) // usb=()
				.block(Feature::VerticalScroll) // vertical-scroll=()
				.allow(Feature::WebShare, [Allow::Any]) // web-share=*
				// .block(Feature::WindowManagement) // TODO: window-management=()
				.block(Feature::XrSpatialTracking), // xr-spatial-tracking=()
		)
}

/// A Rocket [Fairing] that sets additional security headers, to be used in conjunction with [`shield`].
///
/// [Fairing]: https://rocket.rs/guide/v0.5/fairings/#fairings
pub fn more_shield() -> ExtraHeaders {
	ExtraHeaders
}

pub struct ExtraHeaders;

#[rocket::async_trait]
impl Fairing for ExtraHeaders {
	fn info(&self) -> Info {
		Info {
			name: "Security Headers",
			kind: Kind::Response,
		}
	}

	async fn on_response<'r>(&self, _: &'r Request<'_>, res: &mut Response<'r>) {
		res.set_header(ContentSecurityPolicy);
		res.set_header(CrossOriginEmbedderPolicy);
		res.set_header(CrossOriginOpenerPolicy);
		res.set_header(CrossOriginResourcePolicy);
		res.set_header(DownloadOptions);
		res.set_header(PermittedCrossDomainPolicies);
	}
}

struct ContentSecurityPolicy;
impl From<ContentSecurityPolicy> for Header<'static> {
	fn from(_: ContentSecurityPolicy) -> Self {
		Header::new(
			CONTENT_SECURITY_POLICY.as_str(),
			// includes script-src-attr hash for language selector, and media-src permitting video embeds
			// TODO: Disallow media-src unless we know we're embedding external video/audio
			"base-uri 'none'; default-src 'none'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' https://* data:; sandbox allow-same-origin allow-downloads allow-forms allow-scripts; style-src 'self' 'unsafe-inline'; media-src https://*; script-src 'none'; script-src-attr 'unsafe-hashes' 'sha256-osjxnKEPL/pQJbFk1dKsF7PYFmTyMWGmVSiL9inhxJY='; upgrade-insecure-requests",
		)
	}
}

// Standard headers that aren't in the http crate yet:
const CROSS_ORIGIN_EMBEDDER_POLICY: &str = "Cross-Origin-Embedder-Policy";
const CROSS_ORIGIN_OPENER_POLICY: &str = "Cross-Origin-Opener-Policy";
const CROSS_ORIGIN_RESOURCE_POLICY: &str = "Cross-Origin-Resource-Policy";
const X_DOWNLOAD_OPTIONS: &str = "X-Download-Options";
const X_PERMITTED_CROSS_DOMAIN_POLICIES: &str = "X-Permitted-Cross-Domain-Policies";

struct CrossOriginEmbedderPolicy;
impl From<CrossOriginEmbedderPolicy> for Header<'static> {
	fn from(_: CrossOriginEmbedderPolicy) -> Self {
		// 'credentialless' so we can load external images
		Header::new(CROSS_ORIGIN_EMBEDDER_POLICY, "credentialless")
	}
}

struct CrossOriginOpenerPolicy;
impl From<CrossOriginOpenerPolicy> for Header<'static> {
	fn from(_: CrossOriginOpenerPolicy) -> Self {
		Header::new(CROSS_ORIGIN_OPENER_POLICY, "same-origin")
	}
}

struct CrossOriginResourcePolicy;
impl From<CrossOriginResourcePolicy> for Header<'static> {
	fn from(_: CrossOriginResourcePolicy) -> Self {
		// 'cross-origin' so we can load external images, if CrossOriginEmbedderPolicy agrees and the remote doesn't forbid us using Access-Control-Allow-Origin
		Header::new(CROSS_ORIGIN_RESOURCE_POLICY, "cross-origin")
	}
}

struct DownloadOptions;
impl From<DownloadOptions> for Header<'static> {
	fn from(_: DownloadOptions) -> Self {
		Header::new(X_DOWNLOAD_OPTIONS, "noopen")
	}
}

struct PermittedCrossDomainPolicies;
impl From<PermittedCrossDomainPolicies> for Header<'static> {
	fn from(_: PermittedCrossDomainPolicies) -> Self {
		Header::new(X_PERMITTED_CROSS_DOMAIN_POLICIES, "none")
	}
}
