diff --git a/Cargo.lock b/Cargo.lock index 4cd8416..55198d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -333,6 +333,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "aws-sdk-sns" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ded45f70b0484f871a31c427bac376bad4028d1236d205a04842694fa4e6658" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + [[package]] name = "aws-sdk-sso" version = "1.22.0" @@ -586,6 +609,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "598e2ade8447dce8d3a15b6159b73354db34257851344b232fb1920c272acc61" dependencies = [ "base64 0.21.7", + "bytes", + "http 1.1.0", + "http-body 1.0.0", + "http-serde", "serde", "serde_json", "serde_with", @@ -809,6 +836,9 @@ name = "bytes" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +dependencies = [ + "serde", +] [[package]] name = "bytes-utils" @@ -1448,6 +1478,19 @@ dependencies = [ "time", ] +[[package]] +name = "email_sender_aws_gateway" +version = "0.1.0" +dependencies = [ + "aws-config", + "aws-sdk-sns", + "aws_lambda_events", + "lambda_runtime", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "email_sender_aws_lambda" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index a474f43..16157d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "rs/canister/impl", "rs/canister_upgrader", "rs/email_sender/aws", + "rs/email_sender/aws/gateway", "rs/email_sender/aws/lambda", "rs/email_sender/aws/template_updater", "rs/email_sender/core", @@ -22,6 +23,7 @@ async-trait = "0.1.79" aws-config = "1.3.0" aws_lambda_events = { version = "0.15.0", default-features = false } aws-sdk-sesv2 = "1.23.0" +aws-sdk-sns = "1.23.0" aws-sign-v4 = "0.3.0" base64 = "0.22.0" candid = "0.10.6" diff --git a/rs/canister/CHANGELOG.md b/rs/canister/CHANGELOG.md index 6999c55..6e61b57 100644 --- a/rs/canister/CHANGELOG.md +++ b/rs/canister/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Add `canister_upgrader` to simplify upgrading the canister ([#23](https://github.com/open-chat-labs/ic-sign-in-with-email/pull/23)) +- Add AWS Lambda gateway function to support IPv6 ([#24](https://github.com/open-chat-labs/ic-sign-in-with-email/pull/24)) ### Changed diff --git a/rs/canister/api/src/lib.rs b/rs/canister/api/src/lib.rs index 1d4462e..b073063 100644 --- a/rs/canister/api/src/lib.rs +++ b/rs/canister/api/src/lib.rs @@ -47,7 +47,7 @@ pub enum EmailSenderConfig { #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] pub struct AwsEmailSenderConfig { pub region: String, - pub target_arn: String, + pub function_url: String, pub access_key: String, pub secret_key: String, } @@ -60,7 +60,7 @@ pub enum EncryptedEmailSenderConfig { #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] pub struct EncryptedAwsEmailSenderConfig { pub region: String, - pub target_arn: String, + pub function_url: String, pub access_key_encrypted: String, pub secret_key_encrypted: String, } @@ -97,7 +97,7 @@ impl AwsEmailSenderConfig { ) -> EncryptedAwsEmailSenderConfig { EncryptedAwsEmailSenderConfig { region: self.region, - target_arn: self.target_arn, + function_url: self.function_url, access_key_encrypted: encrypt(&self.access_key, rsa_public_key, rng), secret_key_encrypted: encrypt(&self.secret_key, rsa_public_key, rng), } @@ -108,7 +108,7 @@ impl EncryptedAwsEmailSenderConfig { pub fn decrypt(self, rsa_private_key: &RsaPrivateKey) -> AwsEmailSenderConfig { AwsEmailSenderConfig { region: self.region, - target_arn: self.target_arn, + function_url: self.function_url, access_key: decrypt(&self.access_key_encrypted, rsa_private_key), secret_key: decrypt(&self.secret_key_encrypted, rsa_private_key), } diff --git a/rs/canister/impl/src/email_sender.rs b/rs/canister/impl/src/email_sender.rs index fc2047f..ccd1f03 100644 --- a/rs/canister/impl/src/email_sender.rs +++ b/rs/canister/impl/src/email_sender.rs @@ -1,4 +1,4 @@ -use crate::{env, rng}; +use crate::env; use candid::Principal; use email_sender_core::EmailSender; use magic_links::EncryptedMagicLink; @@ -17,7 +17,7 @@ pub fn init_from_config(config: EmailSenderConfig, identity_canister_id: Princip init(email_sender_aws::AwsEmailSender::new( identity_canister_id, aws.region, - aws.target_arn, + aws.function_url, aws.access_key, aws.secret_key, )); @@ -40,9 +40,6 @@ pub async fn send_magic_link( magic_link: EncryptedMagicLink, ) -> Result<(), String> { let sender = EMAIL_SENDER.get().expect("Email sender has not been set"); - let idempotency_id = rng::gen(); - sender - .send(email.into(), magic_link, idempotency_id, env::now()) - .await + sender.send(email.into(), magic_link, env::now()).await } diff --git a/rs/canister/impl/src/rng.rs b/rs/canister/impl/src/rng.rs index 46ca215..0e04234 100644 --- a/rs/canister/impl/src/rng.rs +++ b/rs/canister/impl/src/rng.rs @@ -1,6 +1,5 @@ -use rand::distributions::{Distribution, Standard}; use rand::rngs::StdRng; -use rand::{Rng, SeedableRng}; +use rand::SeedableRng; use rsa::RsaPrivateKey; use std::cell::RefCell; @@ -21,13 +20,6 @@ pub fn generate_rsa_private_key() -> RsaPrivateKey { with_rng(|rng| RsaPrivateKey::new(rng, 2048).unwrap()) } -pub fn gen() -> T -where - Standard: Distribution, -{ - with_rng(|rng| rng.gen()) -} - pub fn with_rng T, T>(f: F) -> T { RNG.with_borrow_mut(|rng| f(rng.as_mut().unwrap())) } diff --git a/rs/canister_upgrader/src/main.rs b/rs/canister_upgrader/src/main.rs index 58bae78..e09e573 100644 --- a/rs/canister_upgrader/src/main.rs +++ b/rs/canister_upgrader/src/main.rs @@ -16,7 +16,7 @@ async fn main() { None, EmailSenderConfig::Aws(AwsEmailSenderConfig { region: opts.aws_region, - target_arn: opts.aws_target_arn, + function_url: opts.aws_function_url, access_key: opts.aws_access_key, secret_key: opts.aws_secret_key, }), @@ -39,7 +39,7 @@ struct Opts { aws_region: String, #[arg(long)] - aws_target_arn: String, + aws_function_url: String, #[arg(long)] aws_access_key: String, diff --git a/rs/email_sender/aws/gateway/Cargo.toml b/rs/email_sender/aws/gateway/Cargo.toml new file mode 100644 index 0000000..4e54d2f --- /dev/null +++ b/rs/email_sender/aws/gateway/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "email_sender_aws_gateway" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +aws-config.workspace = true +aws_lambda_events = { workspace = true, features = ["lambda_function_urls"] } +aws-sdk-sns.workspace = true +lambda_runtime.workspace = true +tokio.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true diff --git a/rs/email_sender/aws/gateway/src/main.rs b/rs/email_sender/aws/gateway/src/main.rs new file mode 100644 index 0000000..8853634 --- /dev/null +++ b/rs/email_sender/aws/gateway/src/main.rs @@ -0,0 +1,31 @@ +use aws_config::BehaviorVersion; +use aws_lambda_events::lambda_function_urls::LambdaFunctionUrlRequest; +use aws_sdk_sns::Client as SnsClient; +use lambda_runtime::{run, service_fn, Error, LambdaEvent}; + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_target(false) + .without_time() + .init(); + + run(service_fn(function_handler)).await +} + +async fn function_handler(event: LambdaEvent) -> Result<(), Error> { + let aws_config = aws_config::load_defaults(BehaviorVersion::latest()).await; + let sns_client = SnsClient::new(&aws_config); + let target_arn = std::env::var("SNS_TARGET_ARN").unwrap(); + + sns_client + .publish() + .target_arn(target_arn) + .message(&event.payload.body.unwrap()) + .message_group_id("0") + .send() + .await?; + + Ok(()) +} diff --git a/rs/email_sender/aws/lambda/src/main.rs b/rs/email_sender/aws/lambda/src/main.rs index 32bd0b4..eaf3aed 100644 --- a/rs/email_sender/aws/lambda/src/main.rs +++ b/rs/email_sender/aws/lambda/src/main.rs @@ -26,13 +26,13 @@ async fn main() -> Result<(), Error> { async fn function_handler(event: LambdaEvent) -> Result<(), Error> { let aws_config = aws_config::load_defaults(BehaviorVersion::latest()).await; let ses_client = SesClient::new(&aws_config); - let rsa_private_key_pem = std::env::var("RSA_PRIVATE_KEY_PEM") - .expect("RSA_PRIVATE_KEY_PEM not set") - .replace("\\n", "\n"); + let rsa_private_key_pem = std::env::var("RSA_PRIVATE_KEY_PEM")?.replace("\\n", "\n"); let rsa_private_key = RsaPrivateKey::from_pkcs1_pem(&rsa_private_key_pem)?; for event in event.payload.records { - process_record(event, rsa_private_key.clone(), &ses_client).await?; + if let Err(error) = process_record(event, rsa_private_key.clone(), &ses_client).await { + error!(?error, "Error processing record"); + } } Ok(()) @@ -75,7 +75,10 @@ async fn process_record( .send() .await { - Ok(_) => Ok(()), + Ok(_) => { + info!("Successfully sent email"); + Ok(()) + } Err(error) => { error!(?error, "Failed to send email"); Err(error.into()) diff --git a/rs/email_sender/aws/src/lib.rs b/rs/email_sender/aws/src/lib.rs index 922fcd9..79065c5 100644 --- a/rs/email_sender/aws/src/lib.rs +++ b/rs/email_sender/aws/src/lib.rs @@ -13,7 +13,7 @@ use time::OffsetDateTime; pub struct AwsEmailSender { identity_canister_id: Principal, region: String, - target_arn: String, + function_url: String, access_key: String, secret_key: String, } @@ -25,14 +25,14 @@ impl AwsEmailSender { pub fn new( identity_canister_id: Principal, region: String, - target_arn: String, + function_url: String, access_key: String, secret_key: String, ) -> AwsEmailSender { AwsEmailSender { identity_canister_id, region, - target_arn, + function_url, access_key, secret_key, } @@ -42,16 +42,21 @@ impl AwsEmailSender { &self, email: String, magic_link: EncryptedMagicLink, - idempotency_id: u64, now_millis: u64, ) -> CanisterHttpRequestArgument { let datetime = OffsetDateTime::from_unix_timestamp_nanos(now_millis as i128 * 1_000_000).unwrap(); - let region = &self.region; - let host = format!("sns.{region}.amazonaws.com"); + let host = self.function_url.trim_start_matches("https://"); let url = format!("https://{host}"); + let body = serde_json::to_string(&MagicLinkMessage { + email, + identity_canister_id: self.identity_canister_id, + magic_link, + }) + .unwrap(); + let mut header_map = HeaderMap::new(); header_map.insert( "X-Amz-Date", @@ -60,25 +65,12 @@ impl AwsEmailSender { header_map.insert("host", host.parse().unwrap()); header_map.insert( http::header::CONTENT_TYPE, - "application/x-www-form-urlencoded".parse().unwrap(), + "application/json".parse().unwrap(), + ); + header_map.insert( + http::header::CONTENT_LENGTH, + body.len().to_string().parse().unwrap(), ); - - let message_deduplication_id = idempotency_id.to_string(); - let message = serde_json::to_string(&MagicLinkMessage { - email, - identity_canister_id: self.identity_canister_id, - magic_link, - }) - .unwrap(); - - let body = [ - ("Action", "Publish"), - ("TargetArn", &self.target_arn), - ("Message", &message), - ("MessageDeduplicationId", &message_deduplication_id), - ("MessageGroupId", "0"), - ]; - let body = serde_urlencoded::to_string(body).unwrap(); let signature = aws_sign_v4::AwsSign::new( "POST", @@ -88,7 +80,7 @@ impl AwsEmailSender { &self.region, &self.access_key, &self.secret_key, - "sns", + "lambda", &body, ) .sign(); @@ -120,10 +112,9 @@ impl EmailSender for AwsEmailSender { &self, email: String, magic_link: EncryptedMagicLink, - idempotency_id: u64, now_millis: u64, ) -> Result<(), String> { - let args = self.build_args(email, magic_link, idempotency_id, now_millis); + let args = self.build_args(email, magic_link, now_millis); let resp = ic_cdk::api::management_canister::http_request::http_request(args, 1_000_000_000) diff --git a/rs/email_sender/core/src/lib.rs b/rs/email_sender/core/src/lib.rs index 32c40ac..9c50590 100644 --- a/rs/email_sender/core/src/lib.rs +++ b/rs/email_sender/core/src/lib.rs @@ -7,7 +7,6 @@ pub trait EmailSender: Send + Sync { &self, email: String, magic_link: EncryptedMagicLink, - idempotency_id: u64, now_millis: u64, ) -> Result<(), String>; } @@ -21,7 +20,6 @@ impl EmailSender for NullEmailSender { &self, _email: String, _magic_link: EncryptedMagicLink, - _idempotency_id: u64, _now_millis: u64, ) -> Result<(), String> { Ok(()) diff --git a/scripts/build-aws-gateway-function.sh b/scripts/build-aws-gateway-function.sh new file mode 100755 index 0000000..b73a542 --- /dev/null +++ b/scripts/build-aws-gateway-function.sh @@ -0,0 +1 @@ +cargo lambda build -p email_sender_aws_gateway --release --arm64 --output-format zip \ No newline at end of file diff --git a/scripts/upgrade_canister.sh b/scripts/upgrade_canister.sh index a1c8f94..a59eba4 100755 --- a/scripts/upgrade_canister.sh +++ b/scripts/upgrade_canister.sh @@ -4,7 +4,7 @@ IDENTITY=$1 IC_URL=$2 CANISTER_ID=$3 AWS_REGION=$4 -AWS_TARGET_ARN=$5 +AWS_FUNCTION_URL=$5 AWS_ACCESS_KEY=$6 AWS_SECRET_KEY=$7 @@ -18,6 +18,6 @@ cargo run \ --ic-url $IC_URL \ --canister-id $CANISTER_ID \ --aws-region $AWS_REGION \ - --aws-target-arn $AWS_TARGET_ARN \ + --aws-function-url $AWS_FUNCTION_URL \ --aws-access-key $AWS_ACCESS_KEY \ --aws-secret-key $AWS_SECRET_KEY \ \ No newline at end of file