Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add origin to CSP so Safari can do recursion #2708

Merged
merged 15 commits into from
Nov 22, 2023
3 changes: 2 additions & 1 deletion src/page_config.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use super::*;

#[derive(Clone)]
#[derive(Clone, Default)]
pub(crate) struct PageConfig {
pub(crate) chain: Chain,
pub(crate) csp_origin: Option<String>,
pub(crate) domain: Option<String>,
pub(crate) index_sats: bool,
}
79 changes: 67 additions & 12 deletions src/subcommand/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ pub(crate) struct Server {
help = "Request ACME TLS certificate for <ACME_DOMAIN>. This ord instance must be reachable at <ACME_DOMAIN>:443 to respond to Let's Encrypt ACME challenges."
)]
acme_domain: Vec<String>,
#[arg(
long,
help = "Use <CSP_ORIGIN> in Content-Security-Policy header. Set this to the public-facing URL of your ord instance."
)]
csp_origin: Option<String>,
#[arg(
long,
help = "Listen on <HTTP_PORT> for incoming HTTP requests. [default: 80]."
Expand Down Expand Up @@ -182,6 +187,7 @@ impl Server {

let page_config = Arc::new(PageConfig {
chain: options.chain(),
csp_origin: self.csp_origin.clone(),
domain: acme_domains.first().cloned(),
index_sats: index.has_sat_index(),
});
Expand Down Expand Up @@ -985,6 +991,7 @@ impl Server {
async fn content(
Extension(index): Extension<Arc<Index>>,
Extension(config): Extension<Arc<Config>>,
Extension(page_config): Extension<Arc<PageConfig>>,
Path(inscription_id): Path<InscriptionId>,
accept_encoding: AcceptEncoding,
) -> ServerResult<Response> {
Expand All @@ -997,7 +1004,7 @@ impl Server {
.ok_or_not_found(|| format!("inscription {inscription_id}"))?;

Ok(
Self::content_response(inscription, accept_encoding)?
Self::content_response(inscription, accept_encoding, &page_config)?
.ok_or_not_found(|| format!("inscription {inscription_id} content"))?
.into_response(),
)
Expand All @@ -1006,6 +1013,7 @@ impl Server {
fn content_response(
inscription: Inscription,
accept_encoding: AcceptEncoding,
page_config: &PageConfig,
) -> ServerResult<Option<(HeaderMap, Vec<u8>)>> {
let mut headers = HeaderMap::new();

Expand All @@ -1027,15 +1035,25 @@ impl Server {
}
}

headers.insert(
header::CONTENT_SECURITY_POLICY,
HeaderValue::from_static("default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:"),
);

headers.append(
header::CONTENT_SECURITY_POLICY,
HeaderValue::from_static("default-src *:*/content/ *:*/blockheight *:*/blockhash *:*/blockhash/ *:*/blocktime *:*/r/ 'unsafe-eval' 'unsafe-inline' data: blob:"),
);
match &page_config.csp_origin {
None => {
headers.insert(
header::CONTENT_SECURITY_POLICY,
HeaderValue::from_static("default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:"),
);
headers.append(
header::CONTENT_SECURITY_POLICY,
HeaderValue::from_static("default-src *:*/content/ *:*/blockheight *:*/blockhash *:*/blockhash/ *:*/blocktime *:*/r/ 'unsafe-eval' 'unsafe-inline' data: blob:"),
);
}
Some(origin) => {
let csp = format!("default-src {origin}/content/ {origin}/blockheight {origin}/blockhash {origin}/blockhash/ {origin}/blocktime {origin}/r/ 'unsafe-eval' 'unsafe-inline' data: blob:");
headers.insert(
header::CONTENT_SECURITY_POLICY,
HeaderValue::from_str(&csp).map_err(|err| ServerError::Internal(Error::from(err)))?,
);
}
}

headers.insert(
header::CACHE_CONTROL,
Expand All @@ -1052,6 +1070,7 @@ impl Server {
async fn preview(
Extension(index): Extension<Arc<Index>>,
Extension(config): Extension<Arc<Config>>,
Extension(page_config): Extension<Arc<PageConfig>>,
Path(inscription_id): Path<InscriptionId>,
accept_encoding: AcceptEncoding,
) -> ServerResult<Response> {
Expand Down Expand Up @@ -1089,7 +1108,7 @@ impl Server {
.into_response(),
),
Media::Iframe => Ok(
Self::content_response(inscription, accept_encoding)?
Self::content_response(inscription, accept_encoding, &page_config)?
.ok_or_not_found(|| format!("inscription {inscription_id} content"))?
.into_response(),
),
Expand Down Expand Up @@ -3131,7 +3150,8 @@ mod tests {
assert_eq!(
Server::content_response(
Inscription::new(Some("text/plain".as_bytes().to_vec()), None),
AcceptEncoding::default()
AcceptEncoding::default(),
&PageConfig::default(),
)
.unwrap(),
None
Expand All @@ -3143,6 +3163,7 @@ mod tests {
let (headers, body) = Server::content_response(
Inscription::new(Some("text/plain".as_bytes().to_vec()), Some(vec![1, 2, 3])),
AcceptEncoding::default(),
&PageConfig::default(),
)
.unwrap()
.unwrap();
Expand All @@ -3151,6 +3172,38 @@ mod tests {
assert_eq!(body, vec![1, 2, 3]);
}

#[test]
fn content_security_policy_no_origin() {
let (headers, _) = Server::content_response(
Inscription::new(Some("text/plain".as_bytes().to_vec()), Some(vec![1, 2, 3])),
AcceptEncoding::default(),
&PageConfig::default(),
)
.unwrap()
.unwrap();

assert_eq!(
headers["content-security-policy"],
HeaderValue::from_static("default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:")
);
}

#[test]
fn content_security_policy_with_origin() {
let (headers, _) = Server::content_response(
Inscription::new(Some("text/plain".as_bytes().to_vec()), Some(vec![1, 2, 3])),
AcceptEncoding::default(),
&PageConfig {
csp_origin: Some("https://ordinals.com".into()),
..Default::default()
},
)
.unwrap()
.unwrap();

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 code_preview() {
let server = TestServer::new_with_regtest();
Expand Down Expand Up @@ -3181,6 +3234,7 @@ mod tests {
let (headers, body) = Server::content_response(
Inscription::new(None, Some(Vec::new())),
AcceptEncoding::default(),
&PageConfig::default(),
)
.unwrap()
.unwrap();
Expand All @@ -3194,6 +3248,7 @@ mod tests {
let (headers, body) = Server::content_response(
Inscription::new(Some("\n".as_bytes().to_vec()), Some(Vec::new())),
AcceptEncoding::default(),
&PageConfig::default(),
)
.unwrap()
.unwrap();
Expand Down
10 changes: 7 additions & 3 deletions src/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ mod tests {
assert_regex_match!(
Foo.page(Arc::new(PageConfig {
chain: Chain::Mainnet,
csp_origin: Some("https://signet.ordinals.com".into()),
domain: Some("signet.ordinals.com".into()),
index_sats: true,
}),),
Expand Down Expand Up @@ -162,9 +163,10 @@ mod tests {
assert_regex_match!(
Foo.page(Arc::new(PageConfig {
chain: Chain::Mainnet,
csp_origin: None,
domain: None,
index_sats: true,
}),),
})),
r".*<nav class=links>\s*<a href=/>Ordinals<sup>alpha</sup></a>.*"
);
}
Expand All @@ -174,9 +176,10 @@ mod tests {
assert_regex_match!(
Foo.page(Arc::new(PageConfig {
chain: Chain::Mainnet,
csp_origin: None,
domain: None,
index_sats: false,
}),),
})),
r".*<nav class=links>\s*<a href=/>Ordinals<sup>alpha</sup></a>.*<a href=/clock>Clock</a>\s*<form action=/search.*",
);
}
Expand All @@ -186,9 +189,10 @@ mod tests {
assert_regex_match!(
Foo.page(Arc::new(PageConfig {
chain: Chain::Signet,
csp_origin: None,
domain: None,
index_sats: true,
}),),
})),
r".*<nav class=links>\s*<a href=/>Ordinals<sup>signet</sup></a>.*"
);
}
Expand Down
Loading