diff --git a/CHANGELOG.md b/CHANGELOG.md index cf7b52ec9a..9d53548151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,8 +20,12 @@ dfx cycles convert --amount 100 Use `BTreeMap` instead of `HashMap` for headers to guarantee deterministic ordering. -- Module hash: 47927c343a5217f1687d2d60ac7d1cd32b35100f9e24a5c488828857146419e9 +Sets the `ic_env` cookie for html files, which contains the root key and the canister environment variables that are prefixed with `PUBLIC_`. +Please note that this version of the frontend canister is only compatible with PocketIC **v10** and above. + +- Module hash: 2c5ab2cdebe93356e319b36c33abcb49c2162b97353476bb5894540e6e616f13 - https://github.com/dfinity/sdk/pull/4392 +- https://github.com/dfinity/sdk/pull/4387 # 0.29.2 diff --git a/Cargo.lock b/Cargo.lock index 9542760375..63e130f894 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3000,15 +3000,15 @@ dependencies = [ [[package]] name = "ic-cdk" -version = "0.18.6" +version = "0.19.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3694c302426834b1300c095b43dee60aaf07264ca07c8f6456fc6b898c99c72f" +checksum = "e1d13c6162fc7075e901c4df2a6721ed43596e51f2508ff512eaad1dcdc92f34" dependencies = [ "candid", "ic-cdk-executor", "ic-cdk-macros", "ic-error-types 0.2.0", - "ic-management-canister-types 0.3.3", + "ic-management-canister-types 0.4.1", "ic0", "serde", "serde_bytes", @@ -3028,9 +3028,9 @@ dependencies = [ [[package]] name = "ic-cdk-macros" -version = "0.18.6" +version = "0.19.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2cd13805284d5f422012c392a132757c74bd1bb2f7baeda2f3fca5cbe3d8ce7" +checksum = "aaad1e69f8a91ff4cc8ed6513442163a34058dbe1e07b9fba83999b31bb7ca38" dependencies = [ "candid", "darling 0.20.11", @@ -3657,9 +3657,9 @@ dependencies = [ [[package]] name = "ic0" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8877193e1921b5fd16accb0305eb46016868cd1935b05c05eca0ec007b943272" +checksum = "1499d08fd5be8f790d477e1865d63bab6a8d748300e141270c4296e6d5fdd6bc" [[package]] name = "ic_bls12_381" diff --git a/Cargo.toml b/Cargo.toml index 4b8a3f0972..cd5a37f50c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ candid_parser = "0.2.1" dfx-core = { path = "src/dfx-core", version = "0.2.0" } ic-agent = "0.44.0" ic-asset = { path = "src/canisters/frontend/ic-asset", version = "0.25.0" } -ic-cdk = "0.18.4" +ic-cdk = "0.19.0-beta.2" ic-identity-hsm = "0.44.0" ic-utils = "0.44.0" ic-management-canister-types = "0.4.1" diff --git a/src/canisters/frontend/ic-certified-assets/CHANGELOG.md b/src/canisters/frontend/ic-certified-assets/CHANGELOG.md index 8617c0d8e1..115aa34b7e 100644 --- a/src/canisters/frontend/ic-certified-assets/CHANGELOG.md +++ b/src/canisters/frontend/ic-certified-assets/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Changed `CreateAssetArguments.headers` to use `BTreeMap` instead of `HashMap` - Changed `AssetProperties.headers` to use `BTreeMap` instead of `HashMap` - Changed `SetAssetPropertiesArguments.headers` to use `BTreeMap` instead of `HashMap` +- **BREAKING**: Sets the `ic_env` cookie for html files, which contains the root key and the canister environment variables that are prefixed with `PUBLIC_`. Please note that this version of the `ic-certified-assets` is only compatible with PocketIC **v10** and above. #### Migration guide diff --git a/src/canisters/frontend/ic-certified-assets/src/canister_env.rs b/src/canisters/frontend/ic-certified-assets/src/canister_env.rs new file mode 100644 index 0000000000..69af0ed10e --- /dev/null +++ b/src/canisters/frontend/ic-certified-assets/src/canister_env.rs @@ -0,0 +1,60 @@ +use std::collections::HashMap; + +use ic_cdk::api::{env_var_count, env_var_name, env_var_value, root_key}; + +use crate::url::url_encode; + +const PUBLIC_ENV_VAR_NAME_PREFIX: &str = "PUBLIC_"; + +const IC_ROOT_KEY_VALUE_KEY: &str = "ic_root_key"; +const COOKIE_VALUES_SEPARATOR: &str = "&"; + +pub struct CanisterEnv { + pub ic_root_key: Vec, + /// We can expect a maximum of 20 entries, each with a maximum of 128 characters + /// for both the key and the value. Total size: 20 * 128 * 2 = 4096 bytes + /// + /// Numbers from https://github.com/dfinity/ic/blob/34bd4301f941cdfa1596a0eecf9f58ad6407293c/rs/config/src/execution_environment.rs#L175-L183 + pub icp_public_env_vars: HashMap, +} + +impl CanisterEnv { + pub fn load() -> Self { + Self { + ic_root_key: root_key(), + icp_public_env_vars: load_icp_public_env_vars(), + } + } + + pub fn to_cookie_value(&self) -> String { + let hex_root_key = hex::encode(&self.ic_root_key); + let root_key_value = format!("{IC_ROOT_KEY_VALUE_KEY}={hex_root_key}"); + + let mut values = vec![root_key_value]; + + let icp_public_env_vars = self + .icp_public_env_vars + .iter() + .map(|(k, v)| format!("{}={}", k, v)) + .collect::>(); + values.extend(icp_public_env_vars); + + let cookie_value = values.join(COOKIE_VALUES_SEPARATOR); + + url_encode(&cookie_value) + } +} + +fn load_icp_public_env_vars() -> HashMap { + let mut public_env_vars = HashMap::new(); + let env_var_count = env_var_count(); + + for i in 0..env_var_count { + let name = env_var_name(i); + if name.starts_with(PUBLIC_ENV_VAR_NAME_PREFIX) { + let value = env_var_value(&name); + public_env_vars.insert(name, value); + } + } + public_env_vars +} diff --git a/src/canisters/frontend/ic-certified-assets/src/cookies.rs b/src/canisters/frontend/ic-certified-assets/src/cookies.rs new file mode 100644 index 0000000000..ba492015c2 --- /dev/null +++ b/src/canisters/frontend/ic-certified-assets/src/cookies.rs @@ -0,0 +1,10 @@ +use std::collections::BTreeMap; + +const SET_COOKIE_HEADER_NAME: &str = "Set-Cookie"; +const IC_ENV_COOKIE_NAME: &str = "ic_env"; + +pub fn add_ic_env_cookie(headers: &mut BTreeMap, encoded_canister_env: &String) { + let cookie_value = format!("{IC_ENV_COOKIE_NAME}={encoded_canister_env}; SameSite=Lax"); + + headers.insert(SET_COOKIE_HEADER_NAME.to_string(), cookie_value); +} diff --git a/src/canisters/frontend/ic-certified-assets/src/lib.rs b/src/canisters/frontend/ic-certified-assets/src/lib.rs index 97df2c249a..8aec3c6f30 100644 --- a/src/canisters/frontend/ic-certified-assets/src/lib.rs +++ b/src/canisters/frontend/ic-certified-assets/src/lib.rs @@ -1,9 +1,11 @@ //! This module declares canister methods expected by the assets canister client. pub mod asset_certification; +mod canister_env; +mod cookies; pub mod evidence; pub mod state_machine; pub mod types; -mod url_decode; +mod url; #[cfg(test)] mod tests; @@ -14,6 +16,7 @@ use crate::{ CallbackFunc, HttpRequest, HttpResponse, StreamingCallbackHttpResponse, StreamingCallbackToken, }, + canister_env::CanisterEnv, state_machine::{AssetDetails, CertifiedTree, EncodedAsset, State}, types::*, }; @@ -191,8 +194,10 @@ pub fn clear() { } pub fn commit_batch(arg: CommitBatchArguments) { + let canister_env = CanisterEnv::load(); + with_state_mut(|s| { - if let Err(msg) = s.commit_batch(arg, time()) { + if let Err(msg) = s.commit_batch(arg, time(), &canister_env) { trap(&msg); } certified_data_set(s.root_hash()); @@ -216,8 +221,10 @@ pub fn compute_evidence(arg: ComputeEvidenceArguments) -> Option, asset_hashes: CertifiedResponses, + + encoded_canister_env: String, } impl Asset { @@ -356,6 +360,7 @@ impl State { if self.assets.contains_key(&arg.key) { return Err("asset already exists".to_string()); } + self.assets.insert( arg.key, Asset { @@ -422,7 +427,13 @@ impl State { }; asset.encodings.insert(arg.content_encoding, enc); - on_asset_change(&mut self.asset_hashes, &arg.key, asset, dependent_keys); + on_asset_change( + &mut self.asset_hashes, + &arg.key, + asset, + dependent_keys, + Some(&self.encoded_canister_env), + ); Ok(()) } @@ -435,7 +446,13 @@ impl State { .ok_or_else(|| "asset not found".to_string())?; if asset.encodings.remove(&arg.content_encoding).is_some() { - on_asset_change(&mut self.asset_hashes, &arg.key, asset, dependent_keys); + on_asset_change( + &mut self.asset_hashes, + &arg.key, + asset, + dependent_keys, + None, + ); } Ok(()) @@ -458,7 +475,7 @@ impl State { if self.assets.contains_key(&key) { let dependent_keys = self.dependent_keys(&key); if let Some(asset) = self.assets.get_mut(&key) { - on_asset_change(&mut self.asset_hashes, &key, asset, dependent_keys); + on_asset_change(&mut self.asset_hashes, &key, asset, dependent_keys, None); } } } @@ -533,7 +550,13 @@ impl State { encoding.modified = Int::from(time); encoding.sha256 = hash; - on_asset_change(&mut self.asset_hashes, &arg.key, asset, dependent_keys); + on_asset_change( + &mut self.asset_hashes, + &arg.key, + asset, + dependent_keys, + Some(&self.encoded_canister_env), + ); Ok(()) } @@ -684,7 +707,15 @@ impl State { ) } - pub fn commit_batch(&mut self, arg: CommitBatchArguments, now: u64) -> Result<(), String> { + pub fn commit_batch( + &mut self, + arg: CommitBatchArguments, + now: u64, + canister_env: &CanisterEnv, + ) -> Result<(), String> { + // Reload the canister env to get the latest values + self.encoded_canister_env = canister_env.to_cookie_value(); + let (chunks_added, bytes_added) = self.compute_last_chunk_data(&arg); self.check_batch_limits(chunks_added, bytes_added)?; @@ -701,6 +732,9 @@ impl State { } self.batches.remove(&batch_id); self.certify_404_if_required(); + + self.update_ic_env_cookie_in_html_files(); + Ok(()) } @@ -723,11 +757,12 @@ impl State { &mut self, arg: CommitProposedBatchArguments, now: u64, + canister_env: &CanisterEnv, ) -> Result<(), String> { self.validate_commit_proposed_batch_args(&arg)?; let batch = self.batches.get_mut(&arg.batch_id).unwrap(); let proposed_batch_arguments = batch.commit_batch_arguments.take().unwrap(); - self.commit_batch(proposed_batch_arguments, now) + self.commit_batch(proposed_batch_arguments, now, canister_env) } pub fn validate_commit_proposed_batch( @@ -765,6 +800,28 @@ impl State { Ok(()) } + fn update_ic_env_cookie_in_html_files(&mut self) { + let assets_keys: Vec<_> = self + .assets + .keys() + .filter(|key| is_html_key(key)) + .cloned() + .collect(); + + for key in assets_keys { + let dependent_keys = self.dependent_keys(&key); + if let Some(asset) = self.assets.get_mut(&key) { + on_asset_change( + &mut self.asset_hashes, + &key, + asset, + dependent_keys, + Some(&self.encoded_canister_env), + ); + } + } + } + pub fn compute_evidence( &mut self, arg: ComputeEvidenceArguments, @@ -1058,7 +1115,13 @@ impl State { asset.is_aliased = is_aliased } - on_asset_change(&mut self.asset_hashes, &arg.key, asset, dependent_keys); + on_asset_change( + &mut self.asset_hashes, + &arg.key, + asset, + dependent_keys, + Some(&self.encoded_canister_env), + ); Ok(()) } @@ -1167,7 +1230,8 @@ impl From for State { for enc in asset.encodings.values_mut() { enc.certified = false; } - on_asset_change(&mut state.asset_hashes, &key, asset, dependent_keys); + // Do not pass the canister env here, because we want to load the assets as they are (with the old cookie value) + on_asset_change(&mut state.asset_hashes, &key, asset, dependent_keys, None); } else { // shouldn't reach this } @@ -1208,6 +1272,7 @@ fn on_asset_change( key: &str, asset: &mut Asset, dependent_keys: Vec, + encoded_canister_env: Option<&String>, ) { let mut affected_keys = dependent_keys; affected_keys.push(key.to_string()); @@ -1222,6 +1287,14 @@ fn on_asset_change( enc.certified = false; } + // Add ic_env cookie for html files, if the cookie value (canister env) is provided + if let Some(encoded_canister_env) = encoded_canister_env { + if is_html_key(key) { + let headers = asset.headers.get_or_insert_default(); + add_ic_env_cookie(headers, encoded_canister_env); + } + } + asset.update_ic_certificate_expressions(); let most_important_encoding_v1 = asset.most_important_encoding_v1(); @@ -1232,6 +1305,7 @@ fn on_asset_change( headers, .. } = asset; + // Insert certified response values into hash_tree // Once certification v1 support is removed, encoding_certification_order().iter() can be replaced with asset.encodings.iter_mut() for enc_name in encoding_certification_order(encodings.keys()).iter() { @@ -1298,7 +1372,7 @@ fn insert_new_response_hashes_for_encoding( fn aliases_of(key: &AssetKey) -> Vec { if key.ends_with('/') { vec![format!("{}index.html", key)] - } else if !key.ends_with(".html") { + } else if !is_html_key(key) { vec![format!("{}.html", key), format!("{}/index.html", key)] } else { Vec::new() @@ -1319,9 +1393,13 @@ fn aliased_by(key: &AssetKey) -> Vec { key[..(key.len() - 10)].into(), key[..(key.len() - 11)].to_string(), ] - } else if key.ends_with(".html") { + } else if is_html_key(key) { vec![key[..(key.len() - 5)].to_string()] } else { Vec::new() } } + +fn is_html_key>(key: T) -> bool { + key.as_ref().ends_with(".html") +} diff --git a/src/canisters/frontend/ic-certified-assets/src/tests.rs b/src/canisters/frontend/ic-certified-assets/src/tests.rs index f6fa9255dd..0a46286532 100644 --- a/src/canisters/frontend/ic-certified-assets/src/tests.rs +++ b/src/canisters/frontend/ic-certified-assets/src/tests.rs @@ -2,6 +2,7 @@ use crate::CreateChunksArg; use crate::asset_certification::types::http::{ CallbackFunc, HttpRequest, HttpResponse, StreamingCallbackToken, StreamingStrategy, }; +use crate::canister_env::CanisterEnv; use crate::state_machine::{BATCH_EXPIRY_NANOS, StableStateV2, State}; use crate::types::{ AssetProperties, BatchId, BatchOperation, CommitBatchArguments, CommitProposedBatchArguments, @@ -9,7 +10,7 @@ use crate::types::{ DeleteBatchArguments, GetArg, GetChunkArg, SetAssetContentArguments, SetAssetPropertiesArguments, }; -use crate::url_decode::{UrlDecodeError, url_decode}; +use crate::url::{UrlDecodeError, url_decode}; use candid::{Nat, Principal}; use ic_certification_testing::CertificateBuilder; use ic_crypto_tree_hash::Digest; @@ -24,6 +25,9 @@ use std::str::FromStr; // from ic-response-verification tests const MAX_CERT_TIME_OFFSET_NS: u128 = 300_000_000_000; +/// The empty canister env value serialized as a cookie value +const DEFAULT_IC_ENV_COOKIE_VALUE: &str = "ic_env=ic%5Froot%5Fkey%3D; SameSite=Lax"; + fn some_principal() -> Principal { Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap() } @@ -32,6 +36,13 @@ fn unused_callback() -> CallbackFunc { CallbackFunc::new(some_principal(), "unused".to_string()) } +fn empty_canister_env() -> CanisterEnv { + CanisterEnv { + ic_root_key: vec![], + icp_public_env_vars: HashMap::new(), + } +} + pub fn verify_response( state: &State, request: &HttpRequest, @@ -198,7 +209,12 @@ impl RequestBuilder { } } -fn create_assets(state: &mut State, time_now: u64, assets: Vec) -> BatchId { +fn create_assets( + state: &mut State, + time_now: u64, + canister_env: &CanisterEnv, + assets: Vec, +) -> BatchId { let batch_id = state.create_batch(time_now).unwrap(); let operations = @@ -211,6 +227,7 @@ fn create_assets(state: &mut State, time_now: u64, assets: Vec) -> operations, }, time_now, + canister_env, ) .unwrap(); @@ -220,6 +237,7 @@ fn create_assets(state: &mut State, time_now: u64, assets: Vec) -> fn create_assets_by_proposal( state: &mut State, time_now: u64, + canister_env: &CanisterEnv, assets: Vec, ) -> BatchId { let batch_id = state.create_batch(time_now).unwrap(); @@ -249,6 +267,7 @@ fn create_assets_by_proposal( evidence, }, time_now, + canister_env, ) .unwrap(); @@ -332,7 +351,7 @@ impl State { } fn create_test_asset(&mut self, asset: AssetBuilder) { - create_assets(self, 100_000_000_000, vec![asset]); + create_assets(self, 100_000_000_000, &empty_canister_env(), vec![asset]); } } @@ -340,12 +359,14 @@ impl State { fn can_create_assets_using_batch_api() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const BODY: &[u8] = b""; let batch_id = create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents.html", "text/html").with_encoding("identity", vec![BODY]), ], @@ -385,6 +406,7 @@ fn can_create_assets_using_batch_api() { fn serve_correct_encoding_v1() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const IDENTITY_BODY: &[u8] = b""; const GZIP_BODY: &[u8] = b"this is 'gzipped' content"; @@ -392,6 +414,7 @@ fn serve_correct_encoding_v1() { create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents.html", "text/html") .with_encoding("identity", vec![IDENTITY_BODY]) @@ -463,6 +486,7 @@ fn serve_correct_encoding_v1() { fn serve_correct_encoding_v2() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const IDENTITY_BODY: &[u8] = b""; const GZIP_BODY: &[u8] = b"this is 'gzipped' content"; @@ -470,6 +494,7 @@ fn serve_correct_encoding_v2() { create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents.html", "text/html") .with_encoding("identity", vec![IDENTITY_BODY]) @@ -516,6 +541,7 @@ fn serve_correct_encoding_v2() { fn serve_fallback_v2() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const INDEX_BODY: &[u8] = b""; const OTHER_BODY: &[u8] = b"other content"; @@ -523,6 +549,7 @@ fn serve_fallback_v2() { create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/index.html", "text/html") .with_encoding("identity", vec![INDEX_BODY]), @@ -588,12 +615,14 @@ fn serve_fallback_v2() { fn serve_fallback_v1() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const INDEX_BODY: &[u8] = b""; create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/index.html", "text/html") .with_encoding("identity", vec![INDEX_BODY]), @@ -625,12 +654,14 @@ fn serve_fallback_v1() { fn can_create_assets_using_batch_proposal_api() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const BODY: &[u8] = b""; let batch_id = create_assets_by_proposal( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents.html", "text/html").with_encoding("identity", vec![BODY]), ], @@ -896,6 +927,7 @@ fn can_delete_batch_with_chunks() { fn returns_index_file_for_missing_assets() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const INDEX_BODY: &[u8] = b"Index"; const OTHER_BODY: &[u8] = b"Other"; @@ -903,6 +935,7 @@ fn returns_index_file_for_missing_assets() { create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/index.html", "text/html") .with_encoding("identity", vec![INDEX_BODY]), @@ -926,12 +959,14 @@ fn returns_index_file_for_missing_assets() { fn preserves_state_on_stable_roundtrip() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const INDEX_BODY: &[u8] = b"Index"; create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/index.html", "text/html") .with_encoding("identity", vec![INDEX_BODY]), @@ -955,6 +990,7 @@ fn preserves_state_on_stable_roundtrip() { fn uses_streaming_for_multichunk_assets() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const INDEX_BODY_CHUNK_1: &[u8] = b""; const INDEX_BODY_CHUNK_2: &[u8] = b"Index"; @@ -962,6 +998,7 @@ fn uses_streaming_for_multichunk_assets() { create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/index.html", "text/html") .with_encoding("identity", vec![INDEX_BODY_CHUNK_1, INDEX_BODY_CHUNK_2]), @@ -1011,6 +1048,7 @@ fn uses_streaming_for_multichunk_assets() { fn get_and_get_chunk_for_multichunk_assets() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const INDEX_BODY_CHUNK_0: &[u8] = b""; const INDEX_BODY_CHUNK_1: &[u8] = b"Index"; @@ -1018,6 +1056,7 @@ fn get_and_get_chunk_for_multichunk_assets() { create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/index.html", "text/html") .with_encoding("identity", vec![INDEX_BODY_CHUNK_0, INDEX_BODY_CHUNK_1]), @@ -1060,12 +1099,14 @@ fn get_and_get_chunk_for_multichunk_assets() { fn supports_max_age_headers() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const BODY: &[u8] = b""; create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents.html", "text/html").with_encoding("identity", vec![BODY]), AssetBuilder::new("/max-age.html", "text/html") @@ -1140,12 +1181,14 @@ fn check_url_decode() { fn supports_custom_http_headers() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const BODY: &[u8] = b""; create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents.html", "text/html") .with_encoding("identity", vec![BODY]) @@ -1208,12 +1251,15 @@ fn supports_custom_http_headers() { fn supports_getting_and_setting_asset_properties() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const BODY: &[u8] = b""; + let set_cookie_header = ("Set-Cookie".into(), DEFAULT_IC_ENV_COOKIE_VALUE.into()); create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents.html", "text/html") .with_encoding("identity", vec![BODY]) @@ -1229,10 +1275,10 @@ fn supports_getting_and_setting_asset_properties() { state.get_asset_properties("/contents.html".into()), Ok(AssetProperties { max_age: None, - headers: Some(BTreeMap::from([( - "Access-Control-Allow-Origin".into(), - "*".into() - )])), + headers: Some(BTreeMap::from([ + set_cookie_header.clone(), + ("Access-Control-Allow-Origin".into(), "*".into()) + ])), allow_raw_access: None, is_aliased: None }) @@ -1241,10 +1287,10 @@ fn supports_getting_and_setting_asset_properties() { state.get_asset_properties("/max-age.html".into()), Ok(AssetProperties { max_age: Some(604800), - headers: Some(BTreeMap::from([( - "X-Content-Type-Options".into(), - "nosniff".into() - )])), + headers: Some(BTreeMap::from([ + set_cookie_header.clone(), + ("X-Content-Type-Options".into(), "nosniff".into()) + ])), allow_raw_access: None, is_aliased: None }) @@ -1268,10 +1314,10 @@ fn supports_getting_and_setting_asset_properties() { state.get_asset_properties("/max-age.html".into()), Ok(AssetProperties { max_age: Some(1), - headers: Some(BTreeMap::from([( - "X-Content-Type-Options".into(), - "nosniff".into() - )])), + headers: Some(BTreeMap::from([ + set_cookie_header.clone(), + ("X-Content-Type-Options".into(), "nosniff".into()) + ])), allow_raw_access: None, is_aliased: None }) @@ -1292,7 +1338,7 @@ fn supports_getting_and_setting_asset_properties() { state.get_asset_properties("/max-age.html".into()), Ok(AssetProperties { max_age: None, - headers: None, + headers: Some(BTreeMap::from([set_cookie_header.clone()])), allow_raw_access: None, is_aliased: None }) @@ -1316,10 +1362,10 @@ fn supports_getting_and_setting_asset_properties() { state.get_asset_properties("/max-age.html".into()), Ok(AssetProperties { max_age: Some(1), - headers: Some(BTreeMap::from([( - "X-Content-Type-Options".into(), - "nosniff".into() - )])), + headers: Some(BTreeMap::from([ + set_cookie_header.clone(), + ("X-Content-Type-Options".into(), "nosniff".into()) + ])), allow_raw_access: None, is_aliased: None }) @@ -1343,7 +1389,10 @@ fn supports_getting_and_setting_asset_properties() { state.get_asset_properties("/max-age.html".into()), Ok(AssetProperties { max_age: Some(1), - headers: Some(BTreeMap::from([("new-header".into(), "value".into())])), + headers: Some(BTreeMap::from([ + set_cookie_header.clone(), + ("new-header".into(), "value".into()) + ])), allow_raw_access: None, is_aliased: None }) @@ -1364,7 +1413,10 @@ fn supports_getting_and_setting_asset_properties() { state.get_asset_properties("/max-age.html".into()), Ok(AssetProperties { max_age: Some(2), - headers: Some(BTreeMap::from([("new-header".into(), "value".into())])), + headers: Some(BTreeMap::from([ + set_cookie_header.clone(), + ("new-header".into(), "value".into()) + ])), allow_raw_access: None, is_aliased: None }) @@ -1385,7 +1437,10 @@ fn supports_getting_and_setting_asset_properties() { state.get_asset_properties("/max-age.html".into()), Ok(AssetProperties { max_age: Some(2), - headers: Some(BTreeMap::from([("new-header".into(), "value".into())])), + headers: Some(BTreeMap::from([ + set_cookie_header.clone(), + ("new-header".into(), "value".into()) + ])), allow_raw_access: None, is_aliased: Some(false) }) @@ -1406,7 +1461,7 @@ fn supports_getting_and_setting_asset_properties() { state.get_asset_properties("/max-age.html".into()), Ok(AssetProperties { max_age: Some(2), - headers: None, + headers: Some(BTreeMap::from([set_cookie_header.clone()])), allow_raw_access: None, is_aliased: None }) @@ -1417,11 +1472,13 @@ fn supports_getting_and_setting_asset_properties() { fn create_asset_fails_if_asset_exists() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const FILE_BODY: &[u8] = b"file body"; create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents.html", "text/html") .with_encoding("identity", vec![FILE_BODY]), @@ -1447,12 +1504,15 @@ fn create_asset_fails_if_asset_exists() { fn support_aliases() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const INDEX_BODY: &[u8] = b"index"; const SUBDIR_INDEX_BODY: &[u8] = b"subdir index"; const FILE_BODY: &[u8] = b"file body"; + create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents.html", "text/html") .with_encoding("identity", vec![FILE_BODY]), @@ -1495,12 +1555,14 @@ fn support_aliases() { fn alias_enable_and_disable() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const SUBDIR_INDEX_BODY: &[u8] = b"subdir index"; const FILE_BODY: &[u8] = b"file body"; create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents.html", "text/html") .with_encoding("identity", vec![FILE_BODY]), @@ -1538,6 +1600,7 @@ fn alias_enable_and_disable() { create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents.html", "text/html") .with_encoding("identity", vec![FILE_BODY]) @@ -1565,12 +1628,14 @@ fn alias_enable_and_disable() { fn alias_behavior_persists_through_upgrade() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const SUBDIR_INDEX_BODY: &[u8] = b"subdir index"; const FILE_BODY: &[u8] = b"file body"; create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents.html", "text/html") .with_encoding("identity", vec![FILE_BODY]) @@ -1616,12 +1681,14 @@ fn alias_behavior_persists_through_upgrade() { fn aliasing_name_clash() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const FILE_BODY: &[u8] = b"file body"; const FILE_BODY_2: &[u8] = b"second body"; create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents.html", "text/html") .with_encoding("identity", vec![FILE_BODY]), @@ -1634,6 +1701,7 @@ fn aliasing_name_clash() { create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents", "text/html") .with_encoding("identity", vec![FILE_BODY_2]), @@ -1852,12 +1920,14 @@ mod certificate_expression { fn ic_certificate_expression_present_for_new_assets() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const BODY: &[u8] = b""; create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents.html", "text/html") .with_encoding("identity", vec![BODY]) @@ -1893,7 +1963,7 @@ mod certificate_expression { ); assert_eq!( lookup_header(&response, "ic-certificateexpression").unwrap(), - r#"default_certification(ValidationArgs{certification: Certification{no_request_certification: Empty{}, response_certification: ResponseCertification{certified_response_headers: ResponseHeaderList{headers: ["content-type", "cache-control", "Access-Control-Allow-Origin"]}}}})"#, + r#"default_certification(ValidationArgs{certification: Certification{no_request_certification: Empty{}, response_certification: ResponseCertification{certified_response_headers: ResponseHeaderList{headers: ["content-type", "cache-control", "Access-Control-Allow-Origin", "Set-Cookie"]}}}})"#, "Missing ic-certifiedexpression header in response: {:#?}", response, ); @@ -1903,12 +1973,14 @@ mod certificate_expression { fn ic_certificate_expression_gets_updated_on_asset_properties_update() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const BODY: &[u8] = b""; create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents.html", "text/html") .with_encoding("gzip", vec![BODY]) @@ -1932,7 +2004,7 @@ mod certificate_expression { ); assert_eq!( lookup_header(&response, "ic-certificateexpression").unwrap(), - r#"default_certification(ValidationArgs{certification: Certification{no_request_certification: Empty{}, response_certification: ResponseCertification{certified_response_headers: ResponseHeaderList{headers: ["content-type", "content-encoding", "cache-control", "Access-Control-Allow-Origin"]}}}})"#, + r#"default_certification(ValidationArgs{certification: Certification{no_request_certification: Empty{}, response_certification: ResponseCertification{certified_response_headers: ResponseHeaderList{headers: ["content-type", "content-encoding", "cache-control", "Access-Control-Allow-Origin", "Set-Cookie"]}}}})"#, "Missing ic-certificateexpression header in response: {:#?}", response, ); @@ -1963,7 +2035,7 @@ mod certificate_expression { ); assert_eq!( lookup_header(&response, "ic-certificateexpression").unwrap(), - r#"default_certification(ValidationArgs{certification: Certification{no_request_certification: Empty{}, response_certification: ResponseCertification{certified_response_headers: ResponseHeaderList{headers: ["content-type", "content-encoding", "custom-header"]}}}})"#, + r#"default_certification(ValidationArgs{certification: Certification{no_request_certification: Empty{}, response_certification: ResponseCertification{certified_response_headers: ResponseHeaderList{headers: ["content-type", "content-encoding", "Set-Cookie", "custom-header"]}}}})"#, "Missing ic-certifiedexpression header in response: {:#?}", response, ); @@ -1978,6 +2050,7 @@ mod certification_v2 { fn proper_header_structure() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const BODY: &[u8] = b""; const UPDATED_BODY: &[u8] = b"lots of content!"; @@ -1985,6 +2058,7 @@ mod certification_v2 { create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents.html", "text/html") .with_encoding("identity", vec![BODY]) @@ -2017,6 +2091,7 @@ mod certification_v2 { create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents.html", "text/html") .with_encoding("identity", vec![UPDATED_BODY]) @@ -2043,12 +2118,14 @@ mod certification_v2 { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); const BODY: &[u8] = b""; create_assets( &mut state, time_now, + &canister_env, vec![ AssetBuilder::new("/contents.html", "text/html") .with_encoding("identity", vec![BODY]) @@ -3602,6 +3679,7 @@ mod validate_commit_proposed_batch { fn batch_not_found() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); match state.validate_commit_proposed_batch(CommitProposedBatchArguments { batch_id: 1_u8.into(), @@ -3617,6 +3695,7 @@ mod validate_commit_proposed_batch { evidence: Default::default(), }, time_now, + &canister_env, ) { Err(err) if err.contains("batch not found") => (), other => panic!("expected 'batch not found' error, got: {:?}", other), @@ -3627,6 +3706,7 @@ mod validate_commit_proposed_batch { fn no_commit_batch_arguments() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); let batch_id = state.create_batch(time_now).unwrap(); match state.validate_commit_proposed_batch(CommitProposedBatchArguments { @@ -3643,6 +3723,7 @@ mod validate_commit_proposed_batch { evidence: Default::default(), }, time_now, + &canister_env, ) { Err(err) if err.contains("batch does not have CommitBatchArguments") => (), other => panic!("expected 'batch not found' error, got: {:?}", other), @@ -3653,6 +3734,7 @@ mod validate_commit_proposed_batch { fn evidence_not_computed() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); let batch_id = state.create_batch(time_now).unwrap(); assert!( @@ -3677,6 +3759,7 @@ mod validate_commit_proposed_batch { evidence: Default::default(), }, time_now, + &canister_env, ) { Err(err) if err.contains("batch does not have computed evidence") => (), other => panic!("expected 'batch not found' error, got: {:?}", other), @@ -3687,6 +3770,7 @@ mod validate_commit_proposed_batch { fn evidence_does_not_match() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); let batch_id = state.create_batch(time_now).unwrap(); assert!( @@ -3720,6 +3804,7 @@ mod validate_commit_proposed_batch { evidence: Default::default(), }, time_now, + &canister_env, ) { Err(err) if err.contains("does not match presented evidence") => (), other => panic!("expected 'batch not found' error, got: {:?}", other), @@ -3730,6 +3815,7 @@ mod validate_commit_proposed_batch { fn all_good() { let mut state = State::default(); let time_now = 100_000_000_000; + let canister_env = empty_canister_env(); let batch_id = state.create_batch(time_now).unwrap(); assert!( @@ -3767,6 +3853,7 @@ mod validate_commit_proposed_batch { .commit_proposed_batch( CommitProposedBatchArguments { batch_id, evidence }, time_now, + &canister_env, ) .unwrap(); } diff --git a/src/canisters/frontend/ic-certified-assets/src/url_decode.rs b/src/canisters/frontend/ic-certified-assets/src/url.rs similarity index 76% rename from src/canisters/frontend/ic-certified-assets/src/url_decode.rs rename to src/canisters/frontend/ic-certified-assets/src/url.rs index a77589d4c2..3718308ce1 100644 --- a/src/canisters/frontend/ic-certified-assets/src/url_decode.rs +++ b/src/canisters/frontend/ic-certified-assets/src/url.rs @@ -1,6 +1,6 @@ use std::fmt; -use percent_encoding::percent_decode_str; +use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, utf8_percent_encode}; #[derive(Debug, PartialEq, Eq)] pub enum UrlDecodeError { @@ -32,3 +32,10 @@ pub fn url_decode(url: &str) -> Result { Err(_) => Err(UrlDecodeError::InvalidPercentEncoding), } } + +/// Encodes a percent encoded string according to https://url.spec.whatwg.org/#percent-encode +/// +/// This is a wrapper around the percent-encoding crate. +pub fn url_encode(url: &str) -> String { + utf8_percent_encode(url, NON_ALPHANUMERIC).to_string() +} diff --git a/src/distributed/assetstorage.wasm.gz b/src/distributed/assetstorage.wasm.gz index 664c2146c6..8ef383c0ca 100755 Binary files a/src/distributed/assetstorage.wasm.gz and b/src/distributed/assetstorage.wasm.gz differ