diff --git a/osssendfile-rs/src/main.rs b/osssendfile-rs/src/main.rs index 7c9427b..958e420 100755 --- a/osssendfile-rs/src/main.rs +++ b/osssendfile-rs/src/main.rs @@ -34,6 +34,9 @@ use std::fs::File; use std::io::{ErrorKind, Read, Write}; use std::path::PathBuf; use std::{fs, io}; +use std::fmt::format; +use std::process::Command; +use std::str::FromStr; use tokio::fs::File as TokioFile; use tokio_util::bytes::BytesMut; use tokio_util::codec::{Decoder, FramedRead}; @@ -42,6 +45,13 @@ const OSS_SEND_FILE_CONFIG_FILE: &str = "~/.jssp/config/osssendfile.json"; const CREATE_STS_URL: &str = "https://global.hatter.ink/oidc/create_sts.json"; const ADD_DOC_URL: &str = "https://play.hatter.me/doc/addDoc.jsonp"; +const ASSUME_ROLE_BY_KEY: &str = "https://global.hatter.ink/cloud/alibaba_cloud/assume_role_by_key.json"; + +const INSTANCE_IDENTITY_TOKEN: &str = "http://100.100.100.200/latest/api/token"; +const INSTANCE_IDENTITY_TTL: &str = "X-aliyun-ecs-metadata-token-ttl-seconds"; +const INSTANCE_IDENTITY_PKCS7: &str = "http://100.100.100.200/latest/dynamic/instance-identity/pkcs7"; +const INSTANCE_IDENTITY_PKCS7_TOKEN: &str = "X-aliyun-ecs-metadata-token"; + const ENV_OSS_SEND_FILE_CONFIG: &str = "OSS_SEND_FILE_CONFIG"; #[derive(Debug, Parser)] @@ -77,7 +87,8 @@ struct OssSendFileConfig { endpoint: String, bucket: String, token: String, - oidc: OidcConfig, + oidc: Option, + pkcs7: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -146,7 +157,6 @@ impl Decoder for CustomBytesCodec { } } } - #[tokio::main] async fn main() -> XResult<()> { let args = OssSendFileArgs::parse(); @@ -157,9 +167,26 @@ async fn main() -> XResult<()> { let sts = request_sts(&client, &oss_send_file_config).await?; information!("Get STS success"); - let source_file = args.file.clone(); + let mut pending_remove_file = None::; + let source_file = args.file.clone(); // TODO + + let metadata = fs::metadata(&source_file)?; let filename = source_file.file_name().unwrap().to_str().unwrap(); let filename_ext = get_filename_ext(filename); + + let temp_zip_file = format!("{}.zip", filename); + if metadata.is_dir() { + // TODO check temp_zip_file exists + match zip_dir(&temp_zip_file, source_file.to_str().unwrap()) { + Ok(_) => { + information!("Zip {} to {} success", source_file.display(), temp_zip_file); + // TODO source_file = PathBuf::from_str(&temp_zip_file)?; + pending_remove_file = Some(temp_zip_file.clone()); + } + Err(e) => return simple_error!("Zip {} to {} failed: {}", source_file.display(), temp_zip_file, e), + } + } + let temp_file = match source_file.parent() { None => PathBuf::from(format!("{}.tmp", filename)), Some(parent_source_file) => { @@ -247,6 +274,11 @@ async fn main() -> XResult<()> { success!("CDN URL: {}", cnd_url); } + if let Some(pending_remove_file) = pending_remove_file { + fs::remove_file(&pending_remove_file)?; + success!("Remove pending remove file: {} success", &pending_remove_file); + } + success!("File {} upload success", filename); Ok(()) } @@ -348,10 +380,20 @@ fn digest_sha256(source_file: &PathBuf) -> XResult> { } async fn request_sts(client: &Client, oss_send_file_config: &OssSendFileConfig) -> XResult { + if let Some(oidc_config) = &oss_send_file_config.oidc { + Ok(request_sts_via_oidc(client, oidc_config).await?) + } else if let Some(role_arn) = &oss_send_file_config.pkcs7 { + Ok(request_sts_via_pkcs7(client, role_arn).await?) + } else { + simple_error!("OIDC or PKCS#7 config not found.") + } +} + +async fn request_sts_via_oidc(client: &Client, oidc_config: &OidcConfig) -> XResult { let mut params = HashMap::new(); - params.insert("client_id", &oss_send_file_config.oidc.client_id); - params.insert("client_secret", &oss_send_file_config.oidc.client_secret); - params.insert("sub", &oss_send_file_config.oidc.sub); + params.insert("client_id", &oidc_config.client_id); + params.insert("client_secret", &oidc_config.client_secret); + params.insert("sub", &oidc_config.sub); let response = client.post(CREATE_STS_URL) .form(¶ms) .send() @@ -360,6 +402,47 @@ async fn request_sts(client: &Client, oss_send_file_config: &OssSendFileConfig) parse_sts_response(response).await } +#[derive(Debug, Deserialize)] +struct StsTokenForAssumeRoleByKeyResponse { + pub mode: String, + pub expiration: String, + pub access_key_id: String, + pub access_key_secret: String, + pub sts_token: String, +} + +#[derive(Debug, Deserialize)] +struct AssumeRoleByKeyResponse { + pub status: i32, + pub message: Option, + pub data: StsTokenForAssumeRoleByKeyResponse, +} + +async fn request_sts_via_pkcs7(client: &Client, role_arn: &str) -> XResult { + let pkcs7 = fetch_alibaba_cloud_instance_identity_v1( + client, "hatter.ink:/cloud/alibaba_cloud/assume_role_by_key.json").await?; + + let assumed_role_response = client.get(format!("{}?roleArn={}", ASSUME_ROLE_BY_KEY, role_arn)) + .header("Authorization", format!("PKCS7 {}", pkcs7)) + .send() + .await?; + + if !assumed_role_response.status().is_success() { + return simple_error!("Assume role by key failed, status: {}", assumed_role_response.status()); + } + + let assumed_role: AssumeRoleByKeyResponse = serde_json::from_str(&assumed_role_response.text().await?)?; + if assumed_role.status != 200 { + return simple_error!("Assume role by key failed, response: {:?}", assumed_role); + } + Ok(Sts{ + access_key_id: assumed_role.data.access_key_id, + access_key_secret: assumed_role.data.access_key_secret, + expiration: assumed_role.data.expiration, + security_token: assumed_role.data.sts_token, + }) +} + async fn send_file(client: &Client, token: &str, url: &str, title: &str, keywords: &str, key: [u8; 16]) -> XResult { let mut params = HashMap::new(); params.insert("jsonp", "1".to_string()); @@ -400,5 +483,60 @@ async fn parse_sts_response(response: Response) -> XResult { Ok(sts) } +fn encrypt_file() { + // config == "base64:" + // tiny-encrypt ::: encrypt file +} + +fn zip_dir(temp_zip_file: &str, target: &str) -> XResult<()> { + let mut cmd = Command::new("zip"); + cmd.args(&["-r", temp_zip_file, target]); + if let Err(e) = rust_util::util_cmd::run_command_and_wait(&mut cmd) { + return simple_error!("Zip {} to {} failed: {}", target, temp_zip_file, e); + } + Ok(()) +} + +#[derive(Debug, Serialize)] +struct AlibabaCloudInstanceIdentityAudienceMeta { + pub ver: i32, + iat: i64, + exp: i64, + aud: String, + jti: String, +} + +async fn fetch_alibaba_cloud_instance_identity_v1(client: &Client, audience: &str) -> XResult { + let instance_identity_token = client.get(INSTANCE_IDENTITY_TOKEN) + .header(INSTANCE_IDENTITY_TTL, "60") + .send() + .await?; + if !instance_identity_token.status().is_success() { + return simple_error!("Get instance identity token failed, status: {}", instance_identity_token.status()); + } + let token = opt_result!(instance_identity_token.text().await, "Get instance identity token failed: {}"); + + let rand_bytes: [u8; 8] = rand::random(); + let audience_meta = AlibabaCloudInstanceIdentityAudienceMeta { + ver: 1, + iat: util_time::get_current_secs() as i64, + exp: (util_time::get_current_secs() + 60) as i64, + aud: audience.to_string(), + jti: format!("jti-{}-{}", util_time::get_current_millis(), hex::encode(rand_bytes)), + }; + let audience_meta_json = serde_json::to_string(&audience_meta)?; + let instance_identity_pkcs7 = client.get( + format!("{}?audience={}", INSTANCE_IDENTITY_PKCS7, audience_meta_json)) + .header(INSTANCE_IDENTITY_PKCS7_TOKEN, token) + .send() + .await?; + if !instance_identity_pkcs7.status().is_success() { + return simple_error!("Get instance identity PKCS#7 failed, status: {}", instance_identity_pkcs7.status()); + } + let pkcs7 = opt_result!( instance_identity_pkcs7.text().await, "Get instance identity PKCS#7 failed: {}"); + + Ok(pkcs7) +} + // @SCRIPT-SIGNATURE-V1: yk-r1.ES256.20251015T193822+08:00.MEYCIQCHVvU0SXeLuxIAD19G // PbuX9ZsYWBpsHL2aSo7MeelVhAIhAPMrEXIaiyaEDB8YtQfun1Sb+nwiAUeJvIOc9pKd5NwM