Skip to content

Commit

Permalink
Include CSP origin in preview content security policy headers (#3276)
Browse files Browse the repository at this point in the history
  • Loading branch information
bingryan authored Mar 15, 2024
1 parent 479feca commit c4d1c01
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 77 deletions.
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ use {
ciborium::Value,
clap::{ArgGroup, Parser},
html_escaper::{Escape, Trusted},
http::HeaderMap,
lazy_static::lazy_static,
ordinals::{DeserializeFromStr, Epoch, Height, Rarity, Sat, SatPoint},
regex::Regex,
Expand Down Expand Up @@ -117,7 +118,6 @@ pub mod outgoing;
mod re;
mod representation;
pub mod runes;
mod server_config;
mod settings;
pub mod subcommand;
mod tally;
Expand Down
12 changes: 0 additions & 12 deletions src/server_config.rs

This file was deleted.

158 changes: 96 additions & 62 deletions src/subcommand/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,17 @@ use {
error::{OptionExt, ServerError, ServerResult},
},
super::*,
crate::{
server_config::ServerConfig,
templates::{
BlockHtml, BlocksHtml, ChildrenHtml, ClockSvg, CollectionsHtml, HomeHtml, InputHtml,
InscriptionHtml, InscriptionsBlockHtml, InscriptionsHtml, OutputHtml, PageContent, PageHtml,
PreviewAudioHtml, PreviewCodeHtml, PreviewFontHtml, PreviewImageHtml, PreviewMarkdownHtml,
PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml,
RangeHtml, RareTxt, RuneBalancesHtml, RuneHtml, RunesHtml, SatHtml, TransactionHtml,
},
crate::templates::{
BlockHtml, BlocksHtml, ChildrenHtml, ClockSvg, CollectionsHtml, HomeHtml, InputHtml,
InscriptionHtml, InscriptionsBlockHtml, InscriptionsHtml, OutputHtml, PageContent, PageHtml,
PreviewAudioHtml, PreviewCodeHtml, PreviewFontHtml, PreviewImageHtml, PreviewMarkdownHtml,
PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml,
RangeHtml, RareTxt, RuneBalancesHtml, RuneHtml, RunesHtml, SatHtml, TransactionHtml,
},
axum::{
body,
extract::{Extension, Json, Path, Query},
http::{header, HeaderMap, HeaderValue, StatusCode, Uri},
http::{header, HeaderValue, StatusCode, Uri},
response::{IntoResponse, Redirect, Response},
routing::get,
Router,
Expand All @@ -42,10 +39,13 @@ use {
},
};

pub(crate) use server_config::ServerConfig;

mod accept_encoding;
mod accept_json;
mod error;
pub(crate) mod query;
mod server_config;

enum SpawnConfig {
Https(AxumAcceptor),
Expand Down Expand Up @@ -1387,42 +1387,39 @@ impl Server {
.ok_or_not_found(|| format!("delegate {inscription_id}"))?
}

match inscription.media() {
Media::Audio => Ok(PreviewAudioHtml { inscription_id }.into_response()),
let media = inscription.media();

if let Media::Iframe = media {
return Ok(
Self::content_response(inscription, accept_encoding, &server_config)?
.ok_or_not_found(|| format!("inscription {inscription_id} content"))?
.into_response(),
);
}

let content_security_policy = server_config.preview_content_security_policy(media)?;

match media {
Media::Audio => {
Ok((content_security_policy, PreviewAudioHtml { inscription_id }).into_response())
}
Media::Code(language) => Ok(
(
[(
header::CONTENT_SECURITY_POLICY,
"script-src-elem 'self' https://cdn.jsdelivr.net",
)],
content_security_policy,
PreviewCodeHtml {
inscription_id,
language,
},
)
.into_response(),
),
Media::Font => Ok(
(
[(
header::CONTENT_SECURITY_POLICY,
"script-src-elem 'self'; style-src 'self' 'unsafe-inline';",
)],
PreviewFontHtml { inscription_id },
)
.into_response(),
),
Media::Iframe => Ok(
Self::content_response(inscription, accept_encoding, &server_config)?
.ok_or_not_found(|| format!("inscription {inscription_id} content"))?
.into_response(),
),
Media::Font => {
Ok((content_security_policy, PreviewFontHtml { inscription_id }).into_response())
}
Media::Iframe => unreachable!(),
Media::Image(image_rendering) => Ok(
(
[(
header::CONTENT_SECURITY_POLICY,
"default-src 'self' 'unsafe-inline'",
)],
content_security_policy,
PreviewImageHtml {
image_rendering,
inscription_id,
Expand All @@ -1432,37 +1429,24 @@ impl Server {
),
Media::Markdown => Ok(
(
[(
header::CONTENT_SECURITY_POLICY,
"script-src-elem 'self' https://cdn.jsdelivr.net",
)],
content_security_policy,
PreviewMarkdownHtml { inscription_id },
)
.into_response(),
),
Media::Model => Ok(
(
[(
header::CONTENT_SECURITY_POLICY,
"script-src-elem 'self' https://ajax.googleapis.com",
)],
PreviewModelHtml { inscription_id },
)
.into_response(),
),
Media::Pdf => Ok(
(
[(
header::CONTENT_SECURITY_POLICY,
"script-src-elem 'self' https://cdn.jsdelivr.net",
)],
PreviewPdfHtml { inscription_id },
)
.into_response(),
),
Media::Text => Ok(PreviewTextHtml { inscription_id }.into_response()),
Media::Unknown => Ok(PreviewUnknownHtml.into_response()),
Media::Video => Ok(PreviewVideoHtml { inscription_id }.into_response()),
Media::Model => {
Ok((content_security_policy, PreviewModelHtml { inscription_id }).into_response())
}
Media::Pdf => {
Ok((content_security_policy, PreviewPdfHtml { inscription_id }).into_response())
}
Media::Text => {
Ok((content_security_policy, PreviewTextHtml { inscription_id }).into_response())
}
Media::Unknown => Ok((content_security_policy, PreviewUnknownHtml).into_response()),
Media::Video => {
Ok((content_security_policy, PreviewVideoHtml { inscription_id }).into_response())
}
}
})
}
Expand Down Expand Up @@ -3947,6 +3931,56 @@ mod tests {
assert_eq!(headers["content-security-policy"], HeaderValue::from_static("default-src https://ordinals.com/content/ https://ordinals.com/blockheight https://ordinals.com/blockhash https://ordinals.com/blockhash/ https://ordinals.com/blocktime https://ordinals.com/r/ 'unsafe-eval' 'unsafe-inline' data: blob:"));
}

#[test]
fn preview_content_security_policy() {
{
let server = TestServer::builder().chain(Chain::Regtest).build();

server.mine_blocks(1);

let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate {
inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
..Default::default()
});

server.mine_blocks(1);

let inscription_id = InscriptionId { txid, index: 0 };

server.assert_response_csp(
format!("/preview/{}", inscription_id),
StatusCode::OK,
"default-src 'self'",
format!(".*<html lang=en data-inscription={}>.*", inscription_id),
);
}

{
let server = TestServer::builder()
.chain(Chain::Regtest)
.server_option("--csp-origin", "https://ordinals.com")
.build();

server.mine_blocks(1);

let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate {
inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
..Default::default()
});

server.mine_blocks(1);

let inscription_id = InscriptionId { txid, index: 0 };

server.assert_response_csp(
format!("/preview/{}", inscription_id),
StatusCode::OK,
"default-src https://ordinals.com",
format!(".*<html lang=en data-inscription={}>.*", inscription_id),
);
}
}

#[test]
fn code_preview() {
let server = TestServer::builder().chain(Chain::Regtest).build();
Expand Down
2 changes: 1 addition & 1 deletion src/subcommand/server/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub(super) enum ServerError {
NotFound(String),
}

pub(super) type ServerResult<T> = Result<T, ServerError>;
pub(crate) type ServerResult<T> = Result<T, ServerError>;

impl IntoResponse for ServerError {
fn into_response(self) -> Response {
Expand Down
51 changes: 51 additions & 0 deletions src/subcommand/server/server_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use {
super::*,
axum::http::{header, HeaderName},
};

#[derive(Default)]
pub(crate) struct ServerConfig {
pub(crate) chain: Chain,
pub(crate) content_proxy: Option<Url>,
pub(crate) csp_origin: Option<String>,
pub(crate) decompress: bool,
pub(crate) domain: Option<String>,
pub(crate) index_sats: bool,
pub(crate) json_api_enabled: bool,
}

impl ServerConfig {
pub(super) fn preview_content_security_policy(
&self,
media: Media,
) -> ServerResult<[(HeaderName, HeaderValue); 1]> {
let default = match media {
Media::Audio => "default-src 'self'",
Media::Code(_) => "script-src-elem 'self' https://cdn.jsdelivr.net",
Media::Font => "script-src-elem 'self'; style-src 'self' 'unsafe-inline'",
Media::Iframe => {
return Err(
anyhow!("preview_content_security_policy cannot be called with Media::Iframe").into(),
)
}
Media::Image(_) => "default-src 'self' 'unsafe-inline'",
Media::Markdown => "script-src-elem 'self' https://cdn.jsdelivr.net",
Media::Model => "script-src-elem 'self' https://ajax.googleapis.com",
Media::Pdf => "script-src-elem 'self' https://cdn.jsdelivr.net",
Media::Text => "default-src 'self'",
Media::Unknown => "default-src 'self'",
Media::Video => "default-src 'self'",
};

let value = if let Some(csp_origin) = &self.csp_origin {
default
.replace("'self'", csp_origin)
.parse()
.map_err(|err| anyhow!("invalid content-security-policy origin `{csp_origin}`: {err}"))?
} else {
HeaderValue::from_static(default)
};

Ok([(header::CONTENT_SECURITY_POLICY, value)])
}
}
2 changes: 1 addition & 1 deletion src/templates.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use {super::*, boilerplate::Boilerplate};

pub(crate) use {
crate::subcommand::server::ServerConfig,
block::BlockHtml,
children::ChildrenHtml,
clock::ClockSvg,
Expand All @@ -21,7 +22,6 @@ pub(crate) use {
rare::RareTxt,
rune_balances::RuneBalancesHtml,
sat::SatHtml,
server_config::ServerConfig,
};

pub use {
Expand Down

0 comments on commit c4d1c01

Please sign in to comment.