diff --git a/Cargo.lock b/Cargo.lock index 3a29edcd5..9e8261c71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1358,6 +1358,7 @@ dependencies = [ "chrono", "g3-histogram", "g3-types", + "hex", "humanize-rs", "idna 1.0.2", "ip_network", @@ -1405,10 +1406,12 @@ dependencies = [ "anyhow", "g3-socket", "g3-types", + "g3-yaml", "redis", "rustls-pki-types", "tokio", "tokio-rustls", + "yaml-rust2", ] [[package]] @@ -1561,11 +1564,17 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", + "g3-json", + "g3-redis-client", "g3-types", + "g3-yaml", "log", + "redis", "rustc-hash 2.0.0", + "serde_json", "tokio", "tokio-util", + "yaml-rust2", ] [[package]] diff --git a/g3proxy/Cargo.toml b/g3proxy/Cargo.toml index 2a0884aa8..01d3a3aa6 100644 --- a/g3proxy/Cargo.toml +++ b/g3proxy/Cargo.toml @@ -77,7 +77,7 @@ g3-ip-locate.workspace = true g3-json = { workspace = true, features = ["acl-rule", "resolve", "http", "rustls", "openssl", "histogram"] } g3-msgpack.workspace = true g3-openssl.workspace = true -g3-redis-client.workspace = true +g3-redis-client = { workspace = true, features = ["yaml"] } g3-resolver.workspace = true g3-slog-types = { workspace = true, features = ["http"] } g3-smtp-proto.workspace = true diff --git a/g3proxy/src/config/escaper/proxy_float/source/redis.rs b/g3proxy/src/config/escaper/proxy_float/source/redis.rs index 66852468b..340734521 100644 --- a/g3proxy/src/config/escaper/proxy_float/source/redis.rs +++ b/g3proxy/src/config/escaper/proxy_float/source/redis.rs @@ -99,60 +99,15 @@ impl ProxyFloatRedisSource { fn set(&mut self, k: &str, v: &Yaml, position: Option<&YamlDocPosition>) -> anyhow::Result<()> { match g3_yaml::key::normalize(k).as_str() { super::CONFIG_KEY_SOURCE_TYPE => Ok(()), - "addr" | "address" => { - let addr = g3_yaml::value::as_upstream_addr(v, g3_redis_client::REDIS_DEFAULT_PORT) - .context(format!("invalid upstream address value for key {k}"))?; - self.client_builder.set_addr(addr); - Ok(()) - } - "tls" | "tls_client" => { - let lookup_dir = g3_daemon::config::get_lookup_dir(position)?; - let tls = g3_yaml::value::as_rustls_client_config_builder(v, Some(lookup_dir)) - .context(format!( - "invalid rustls tls client config value for key {k}" - ))?; - self.client_builder.set_tls_client(tls); - Ok(()) - } - "tls_name" => { - let name = g3_yaml::value::as_rustls_server_name(v) - .context(format!("invalid rustls server name value for key {k}"))?; - self.client_builder.set_tls_name(name); - Ok(()) - } - "db" => { - let db = - g3_yaml::value::as_i64(v).context(format!("invalid int value for key {k}"))?; - self.client_builder.set_db(db); - Ok(()) - } - "username" => { - let username = g3_yaml::value::as_string(v)?; - self.client_builder.set_username(username); - Ok(()) - } - "password" => { - let password = g3_yaml::value::as_string(v)?; - self.client_builder.set_password(password); - Ok(()) - } - "connect_timeout" => { - let timeout = g3_yaml::humanize::as_duration(v) - .context(format!("invalid humanize duration value for key {k}"))?; - self.client_builder.set_connect_timeout(timeout); - Ok(()) - } - "response_timeout" | "read_timeout" => { - let timeout = g3_yaml::humanize::as_duration(v) - .context(format!("invalid humanize duration value for key {k}"))?; - self.client_builder.set_response_timeout(timeout); - Ok(()) - } "sets_key" => { self.sets_key = g3_yaml::value::as_string(v)?; Ok(()) } - _ => Err(anyhow!("invalid key {}", k)), + normalized_key => { + let lookup_dir = g3_daemon::config::get_lookup_dir(position)?; + self.client_builder + .set_yaml_kv(normalized_key, v, Some(lookup_dir)) + } } } } diff --git a/lib/g3-json/Cargo.toml b/lib/g3-json/Cargo.toml index 228643970..52018ccbc 100644 --- a/lib/g3-json/Cargo.toml +++ b/lib/g3-json/Cargo.toml @@ -15,6 +15,7 @@ humanize-rs.workspace = true idna.workspace = true ascii.workspace = true rand.workspace = true +hex.workspace = true ip_network = { workspace = true, optional = true } regex = { workspace = true, optional = true } rustls-pki-types = { workspace = true, optional = true, features = ["std"] } diff --git a/lib/g3-json/src/value/mod.rs b/lib/g3-json/src/value/mod.rs index 564d0641f..8df9cb44a 100644 --- a/lib/g3-json/src/value/mod.rs +++ b/lib/g3-json/src/value/mod.rs @@ -28,8 +28,8 @@ pub use datetime::as_rfc3339_datetime; pub use metrics::{as_metrics_name, as_weighted_metrics_name}; pub use net::*; pub use primary::{ - as_ascii, as_bool, as_f64, as_hashmap, as_i32, as_list, as_nonzero_u32, as_string, as_u16, - as_u32, as_u8, as_usize, + as_ascii, as_bool, as_bytes, as_f64, as_hashmap, as_i32, as_list, as_nonzero_u32, as_string, + as_u16, as_u32, as_u8, as_usize, }; pub use random::as_random_ratio; pub use rate_limit::as_rate_limit_quota; diff --git a/lib/g3-json/src/value/primary.rs b/lib/g3-json/src/value/primary.rs index c66bdfa5c..dd16c12e8 100644 --- a/lib/g3-json/src/value/primary.rs +++ b/lib/g3-json/src/value/primary.rs @@ -159,6 +159,14 @@ pub fn as_bool(v: &Value) -> anyhow::Result { } } +pub fn as_bytes(v: &Value, out: &mut [u8]) -> anyhow::Result<()> { + if let Value::String(s) = v { + hex::decode_to_slice(s, out).map_err(|e| anyhow!("invalid hex string: {e}")) + } else { + Err(anyhow!("json value type for bytes should be 'hex string'")) + } +} + pub fn as_ascii(v: &Value) -> anyhow::Result { let s = as_string(v).context("the base type for AsciiString should be String")?; AsciiString::from_str(&s).map_err(|e| anyhow!("invalid ascii string: {e}")) diff --git a/lib/g3-redis-client/Cargo.toml b/lib/g3-redis-client/Cargo.toml index 67541aef9..209bc479e 100644 --- a/lib/g3-redis-client/Cargo.toml +++ b/lib/g3-redis-client/Cargo.toml @@ -10,5 +10,11 @@ redis = { workspace = true, features = ["aio", "tokio-comp"] } tokio = { workspace = true, features = ["net"] } tokio-rustls.workspace = true rustls-pki-types.workspace = true +yaml-rust = { workspace = true, optional = true } g3-types = { workspace = true, features = ["rustls"] } g3-socket.workspace = true +g3-yaml = { workspace = true, optional = true, features = ["rustls"] } + +[features] +default = [] +yaml = ["dep:g3-yaml", "dep:yaml-rust"] diff --git a/lib/g3-redis-client/src/lib.rs b/lib/g3-redis-client/src/lib.rs index ac6dbca3d..617ddf5d3 100644 --- a/lib/g3-redis-client/src/lib.rs +++ b/lib/g3-redis-client/src/lib.rs @@ -26,6 +26,9 @@ use tokio_rustls::TlsConnector; use g3_types::net::{Host, RustlsClientConfig, RustlsClientConfigBuilder, UpstreamAddr}; +#[cfg(feature = "yaml")] +mod yaml; + pub const REDIS_DEFAULT_PORT: u16 = 6379; #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/lib/g3-redis-client/src/yaml.rs b/lib/g3-redis-client/src/yaml.rs new file mode 100644 index 000000000..f5ee80c90 --- /dev/null +++ b/lib/g3-redis-client/src/yaml.rs @@ -0,0 +1,82 @@ +/* + * Copyright 2024 ByteDance and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::path::Path; + +use anyhow::{anyhow, Context}; +use yaml_rust::Yaml; + +use super::RedisClientConfigBuilder; + +impl RedisClientConfigBuilder { + pub fn set_yaml_kv( + &mut self, + k: &str, + v: &Yaml, + lookup_dir: Option<&Path>, + ) -> anyhow::Result<()> { + match k { + "addr" | "address" => { + let addr = g3_yaml::value::as_upstream_addr(v, crate::REDIS_DEFAULT_PORT) + .context(format!("invalid upstream address value for key {k}"))?; + self.set_addr(addr); + Ok(()) + } + "tls" | "tls_client" => { + let tls = g3_yaml::value::as_rustls_client_config_builder(v, lookup_dir).context( + format!("invalid rustls tls client config value for key {k}"), + )?; + self.set_tls_client(tls); + Ok(()) + } + "tls_name" => { + let name = g3_yaml::value::as_rustls_server_name(v) + .context(format!("invalid rustls server name value for key {k}"))?; + self.set_tls_name(name); + Ok(()) + } + "db" => { + let db = + g3_yaml::value::as_i64(v).context(format!("invalid int value for key {k}"))?; + self.set_db(db); + Ok(()) + } + "username" => { + let username = g3_yaml::value::as_string(v)?; + self.set_username(username); + Ok(()) + } + "password" => { + let password = g3_yaml::value::as_string(v)?; + self.set_password(password); + Ok(()) + } + "connect_timeout" => { + let timeout = g3_yaml::humanize::as_duration(v) + .context(format!("invalid humanize duration value for key {k}"))?; + self.set_connect_timeout(timeout); + Ok(()) + } + "response_timeout" | "read_timeout" => { + let timeout = g3_yaml::humanize::as_duration(v) + .context(format!("invalid humanize duration value for key {k}"))?; + self.set_response_timeout(timeout); + Ok(()) + } + _ => Err(anyhow!("invalid key {}", k)), + } + } +} diff --git a/lib/g3-tls-ticket/Cargo.toml b/lib/g3-tls-ticket/Cargo.toml index 1c83d0718..4acadb128 100644 --- a/lib/g3-tls-ticket/Cargo.toml +++ b/lib/g3-tls-ticket/Cargo.toml @@ -8,7 +8,17 @@ edition.workspace = true anyhow.workspace = true log.workspace = true rustc-hash.workspace = true -chrono.workspace = true +chrono = { workspace = true, features = ["now"] } tokio = { workspace = true, features = ["rt", "time", "macros"] } tokio-util = { workspace = true, features = ["time"] } +serde_json.workspace = true +yaml-rust = { workspace = true, optional = true } +redis = { workspace = true, features = ["aio", "tokio-comp"] } g3-types = { workspace = true, features = ["openssl"] } +g3-json.workspace = true +g3-redis-client.workspace = true +g3-yaml = { workspace = true, optional = true } + +[features] +default = [] +yaml = ["g3-redis-client/yaml", "dep:g3-yaml", "dep:yaml-rust"] diff --git a/lib/g3-tls-ticket/src/config.rs b/lib/g3-tls-ticket/src/config/mod.rs similarity index 97% rename from lib/g3-tls-ticket/src/config.rs rename to lib/g3-tls-ticket/src/config/mod.rs index 4d092c945..51291190e 100644 --- a/lib/g3-tls-ticket/src/config.rs +++ b/lib/g3-tls-ticket/src/config/mod.rs @@ -23,6 +23,9 @@ use g3_types::net::{OpensslTicketKey, RollingTicketKey, RollingTicketer}; use super::{TicketKeyUpdate, TicketSourceConfig}; +#[cfg(feature = "yaml")] +mod yaml; + #[derive(Clone)] pub struct TlsTicketConfig { pub(crate) check_interval: Duration, diff --git a/lib/g3-tls-ticket/src/config/yaml.rs b/lib/g3-tls-ticket/src/config/yaml.rs new file mode 100644 index 000000000..fdfbc2529 --- /dev/null +++ b/lib/g3-tls-ticket/src/config/yaml.rs @@ -0,0 +1,55 @@ +/* + * Copyright 2024 ByteDance and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::path::Path; + +use anyhow::{anyhow, Context}; +use yaml_rust::Yaml; + +use super::TlsTicketConfig; +use crate::source::TicketSourceConfig; + +impl TlsTicketConfig { + pub fn parse_yaml(value: &Yaml, lookup_dir: Option<&Path>) -> anyhow::Result { + if let Yaml::Hash(map) = value { + let mut config = TlsTicketConfig::default(); + g3_yaml::foreach_kv(map, |k, v| match g3_yaml::key::normalize(k).as_str() { + "check_interval" => { + config.check_interval = g3_yaml::humanize::as_duration(v) + .context(format!("invalid humanize duration value for key {k}"))?; + Ok(()) + } + "local_lifetime" => { + config.local_lifetime = g3_yaml::value::as_u32(v)?; + Ok(()) + } + "source" => { + let source = TicketSourceConfig::parse_yaml(v, lookup_dir).context(format!( + "invalid remote tls ticket source config for key {k}" + ))?; + config.remote_source = Some(source); + Ok(()) + } + _ => Err(anyhow!("invalid key {k}")), + })?; + Ok(config) + } else { + Err(anyhow!( + "yaml value type for 'tls ticket config' should be 'map'" + )) + } + } +} diff --git a/lib/g3-tls-ticket/src/source/json.rs b/lib/g3-tls-ticket/src/source/json.rs new file mode 100644 index 000000000..faaa9f8f7 --- /dev/null +++ b/lib/g3-tls-ticket/src/source/json.rs @@ -0,0 +1,135 @@ +/* + * Copyright 2024 ByteDance and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use anyhow::{anyhow, Context}; +use chrono::{DateTime, Utc}; +use serde_json::Value; + +use g3_types::net::OpensslTicketKeyBuilder; + +use super::{RemoteDecryptKey, RemoteEncryptKey, RemoteKeys}; + +impl RemoteEncryptKey { + pub(super) fn parse_json(value: &Value) -> anyhow::Result { + if let Value::Object(map) = value { + let mut builder = OpensslTicketKeyBuilder::default(); + for (k, v) in map { + match g3_json::key::normalize(k).as_str() { + "name" => g3_json::value::as_bytes(v, &mut builder.name) + .context(format!("invalid bytes value for key {k}"))?, + "aes" | "aes_key" => g3_json::value::as_bytes(v, &mut builder.aes_key) + .context(format!("invalid bytes value for key {k}"))?, + "hmac" | "hmac_key" => g3_json::value::as_bytes(v, &mut builder.hmac_key) + .context(format!("invalid bytes value for key {k}"))?, + "lifetime" => { + let lifetime = g3_json::value::as_u32(v) + .context(format!("invalid u32 value for key {k}"))?; + builder.set_lifetime(lifetime); + } + _ => return Err(anyhow!("invalid key {k}")), + } + } + Ok(RemoteEncryptKey { + key: builder.build(), + }) + } else { + Err(anyhow!( + "json value type for 'openssl ticket key' should be 'map'" + )) + } + } +} + +impl RemoteDecryptKey { + pub(super) fn parse_json(value: &Value) -> anyhow::Result { + if let Value::Object(map) = value { + let mut expire: Option> = None; + let mut builder = OpensslTicketKeyBuilder::default(); + for (k, v) in map { + match g3_json::key::normalize(k).as_str() { + "name" => g3_json::value::as_bytes(v, &mut builder.name) + .context(format!("invalid bytes value for key {k}"))?, + "aes" | "aes_key" => g3_json::value::as_bytes(v, &mut builder.aes_key) + .context(format!("invalid bytes value for key {k}"))?, + "hmac" | "hmac_key" => g3_json::value::as_bytes(v, &mut builder.hmac_key) + .context(format!("invalid bytes value for key {k}"))?, + "lifetime" => {} + "expire" => { + let time = g3_json::value::as_rfc3339_datetime(v) + .context(format!("invalid rfc3339 datetime value for key {k}"))?; + expire = Some(time); + } + _ => return Err(anyhow!("invalid key {k}")), + } + } + match expire { + Some(expire) => Ok(RemoteDecryptKey { + key: builder.build(), + expire, + }), + None => Err(anyhow!("no expire datetime set")), + } + } else { + Err(anyhow!( + "json value type for 'openssl ticket key' should be 'map'" + )) + } + } +} + +impl RemoteKeys { + #[allow(dead_code)] + pub(super) fn parse_json(value: &Value) -> anyhow::Result { + if let Value::Object(map) = value { + let mut enc_key: Option = None; + let mut dec_keys = Vec::new(); + for (k, v) in map { + match g3_json::key::normalize(k).as_str() { + "enc" | "encrypt" | "enc_key" | "encrypt_key" => { + let key = RemoteEncryptKey::parse_json(v) + .context(format!("invalid remote encrypt key value for key {k}"))?; + enc_key = Some(key); + } + "dec" | "decrypt" | "dec_keys" | "decrypt_keys" => { + if let Value::Array(seq) = v { + for (i, v) in seq.iter().enumerate() { + let key = RemoteDecryptKey::parse_json(v).context(format!( + "invalid single remote decrypt key value for {k}#{i}" + ))?; + dec_keys.push(key); + } + } else { + let key = RemoteDecryptKey::parse_json(v).context(format!( + "invalid single remote decrypt key value for key {k}" + ))?; + dec_keys.push(key); + } + } + _ => return Err(anyhow!("invalid key {k}")), + } + } + match enc_key { + Some(enc_key) => Ok(RemoteKeys { + enc: enc_key, + dec: dec_keys, + }), + None => Err(anyhow!("no encrypt key set")), + } + } else { + Err(anyhow!("json value type for 'remote keys' should be 'map'")) + } + } +} diff --git a/lib/g3-tls-ticket/src/source/mod.rs b/lib/g3-tls-ticket/src/source/mod.rs index b3416a971..24197c81a 100644 --- a/lib/g3-tls-ticket/src/source/mod.rs +++ b/lib/g3-tls-ticket/src/source/mod.rs @@ -14,13 +14,25 @@ * limitations under the License. */ +use anyhow::Context; use chrono::{DateTime, Utc}; use std::time::Duration; use g3_types::net::OpensslTicketKey; +mod json; +#[cfg(feature = "yaml")] +mod yaml; + mod redis; +use redis::{RedisSource, RedisSourceConfig}; + +const CONFIG_KEY_SOURCE_TYPE: &str = "type"; + +pub(crate) struct RemoteEncryptKey { + pub(crate) key: OpensslTicketKey, +} pub(crate) struct RemoteDecryptKey { pub(crate) key: OpensslTicketKey, @@ -34,17 +46,39 @@ impl RemoteDecryptKey { } pub(crate) struct RemoteKeys { - pub(crate) enc_key: OpensslTicketKey, - pub(crate) dec_keys: Vec, + pub(crate) enc: RemoteEncryptKey, + pub(crate) dec: Vec, } #[derive(Clone)] pub(crate) enum TicketSourceConfig { - Redis, + Redis(RedisSourceConfig), } impl TicketSourceConfig { + pub(crate) fn build(&self) -> anyhow::Result { + match self { + TicketSourceConfig::Redis(s) => { + let source = s + .build() + .context("failed to build redis remote key source")?; + Ok(TicketSource::Redis(source)) + } + } + } +} + +pub(crate) enum TicketSource { + Redis(RedisSource), +} + +impl TicketSource { pub(crate) async fn fetch_remote_keys(&self) -> anyhow::Result { - todo!() + match self { + TicketSource::Redis(s) => s + .fetch_remote_keys() + .await + .context("failed to fetch remote keys from redis"), + } } } diff --git a/lib/g3-tls-ticket/src/source/redis.rs b/lib/g3-tls-ticket/src/source/redis.rs deleted file mode 100644 index 867e1a183..000000000 --- a/lib/g3-tls-ticket/src/source/redis.rs +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 2024 ByteDance and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ diff --git a/lib/g3-tls-ticket/src/source/redis/mod.rs b/lib/g3-tls-ticket/src/source/redis/mod.rs new file mode 100644 index 000000000..797a36db4 --- /dev/null +++ b/lib/g3-tls-ticket/src/source/redis/mod.rs @@ -0,0 +1,116 @@ +/* + * Copyright 2024 ByteDance and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use anyhow::{anyhow, Context}; +use redis::AsyncCommands; + +use g3_redis_client::{RedisClientConfig, RedisClientConfigBuilder}; + +use super::{RemoteDecryptKey, RemoteEncryptKey, RemoteKeys}; + +#[cfg(feature = "yaml")] +mod yaml; + +#[derive(Clone, Default)] +pub(crate) struct RedisSourceConfig { + redis: RedisClientConfigBuilder, + enc_key_name: String, + dec_set_name: String, +} + +impl RedisSourceConfig { + pub(super) fn build(&self) -> anyhow::Result { + let redis = self.redis.build()?; + Ok(RedisSource { + redis, + enc_key_name: self.enc_key_name.clone(), + dec_set_name: self.dec_set_name.clone(), + }) + } + + fn check(&self) -> anyhow::Result<()> { + if self.enc_key_name.is_empty() { + return Err(anyhow!("no enc redis keys name set")); + } + if self.dec_set_name.is_empty() { + return Err(anyhow!("no dec redis set name set")); + } + Ok(()) + } +} + +pub(crate) struct RedisSource { + redis: RedisClientConfig, + enc_key_name: String, + dec_set_name: String, +} + +impl RedisSource { + pub(crate) async fn fetch_remote_keys(&self) -> anyhow::Result { + let mut conn = self + .redis + .connect() + .await + .context("failed to connect to redis")?; + + let enc_key = conn + .get(&self.enc_key_name) + .await + .map_err(|e| anyhow!("failed to get redis key {}: {e}", self.enc_key_name))?; + let redis::Value::BulkString(b) = enc_key else { + return Err(anyhow!( + "invalid data type for redis key {}", + self.enc_key_name + )); + }; + let record = serde_json::from_slice(&b).map_err(|e| { + anyhow!( + "invalid json string in redis key {}: {e}", + self.enc_key_name + ) + })?; + let enc_key = RemoteEncryptKey::parse_json(&record).context("invalid encrypt key")?; + + let members: Vec = conn.smembers(&self.dec_set_name).await.map_err(|e| { + anyhow!( + "failed to get all members of sets {}: {e}", + self.dec_set_name + ) + })?; + let mut dec_keys = Vec::with_capacity(members.len()); + for (i, m) in members.into_iter().enumerate() { + let redis::Value::BulkString(b) = m else { + return Err(anyhow!( + "invalid data type for redis set value {}#{i}", + self.dec_set_name + )); + }; + let record = serde_json::from_slice(&b).map_err(|e| { + anyhow!( + "invalid json string in redis set value {}#{i}: {e}", + self.dec_set_name + ) + })?; + let dec_key = RemoteDecryptKey::parse_json(&record).context("invalid decrypt key")?; + dec_keys.push(dec_key); + } + + Ok(RemoteKeys { + enc: enc_key, + dec: dec_keys, + }) + } +} diff --git a/lib/g3-tls-ticket/src/source/redis/yaml.rs b/lib/g3-tls-ticket/src/source/redis/yaml.rs new file mode 100644 index 000000000..1015ed94b --- /dev/null +++ b/lib/g3-tls-ticket/src/source/redis/yaml.rs @@ -0,0 +1,47 @@ +/* + * Copyright 2024 ByteDance and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::path::Path; + +use yaml_rust::yaml; + +use super::RedisSourceConfig; +use crate::source::CONFIG_KEY_SOURCE_TYPE; + +impl RedisSourceConfig { + pub(crate) fn parse_yaml_map( + map: &yaml::Hash, + lookup_dir: Option<&Path>, + ) -> anyhow::Result { + let mut config = RedisSourceConfig::default(); + + g3_yaml::foreach_kv(map, |k, v| match g3_yaml::key::normalize(k).as_str() { + CONFIG_KEY_SOURCE_TYPE => Ok(()), + "enc_key" => { + config.enc_key_name = g3_yaml::value::as_string(v)?; + Ok(()) + } + "dec_set" => { + config.dec_set_name = g3_yaml::value::as_string(v)?; + Ok(()) + } + normalized_key => config.redis.set_yaml_kv(normalized_key, v, lookup_dir), + })?; + + config.check()?; + Ok(config) + } +} diff --git a/lib/g3-tls-ticket/src/source/yaml.rs b/lib/g3-tls-ticket/src/source/yaml.rs new file mode 100644 index 000000000..9c86de345 --- /dev/null +++ b/lib/g3-tls-ticket/src/source/yaml.rs @@ -0,0 +1,42 @@ +/* + * Copyright 2024 ByteDance and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::path::Path; + +use anyhow::anyhow; +use yaml_rust::Yaml; + +use super::{TicketSourceConfig, CONFIG_KEY_SOURCE_TYPE}; + +impl TicketSourceConfig { + pub(crate) fn parse_yaml(value: &Yaml, lookup_dir: Option<&Path>) -> anyhow::Result { + if let Yaml::Hash(map) = value { + let source_type = g3_yaml::hash_get_required_str(map, CONFIG_KEY_SOURCE_TYPE)?; + + match g3_yaml::key::normalize(source_type).as_str() { + "redis" => { + let source = super::RedisSourceConfig::parse_yaml_map(map, lookup_dir)?; + Ok(TicketSourceConfig::Redis(source)) + } + _ => Err(anyhow!("unsupported source type {source_type}")), + } + } else { + Err(anyhow!( + "yaml value type for tls ticket source should be 'map'" + )) + } + } +} diff --git a/lib/g3-tls-ticket/src/update.rs b/lib/g3-tls-ticket/src/update.rs index 5fc02f765..fbca60dea 100644 --- a/lib/g3-tls-ticket/src/update.rs +++ b/lib/g3-tls-ticket/src/update.rs @@ -59,19 +59,30 @@ impl TicketKeyUpdate { async fn run(mut self) { let mut check_interval = tokio::time::interval(self.config.check_interval); + let remote_source = match &self.config.remote_source { + Some(config) => match config.build() { + Ok(source) => Some(source), + Err(e) => { + warn!("remote source disabled, dur to: {e}"); + None + } + }, + None => None, + }; + loop { tokio::select! { biased; _ = check_interval.tick() => { let mut roll_local = true; - if let Some(source) = &self.config.remote_source { + if let Some(source) = &remote_source { match source.fetch_remote_keys().await { Ok(data ) => { roll_local = false; - self.ticketer.set_encrypt_key(Arc::new(data.enc_key)); + self.ticketer.set_encrypt_key(Arc::new(data.enc.key)); let now = Utc::now(); - for dec_key in data.dec_keys { + for dec_key in data.dec { if let Some(expire_dur) = dec_key.expire_duration(&now) { let key = dec_key.key; let key_name = key.name(); diff --git a/lib/g3-types/src/net/openssl/mod.rs b/lib/g3-types/src/net/openssl/mod.rs index 4023605c5..87f107b83 100644 --- a/lib/g3-types/src/net/openssl/mod.rs +++ b/lib/g3-types/src/net/openssl/mod.rs @@ -24,7 +24,7 @@ mod server; pub use server::{ OpensslInterceptionServerConfig, OpensslInterceptionServerConfigBuilder, OpensslServerConfig, OpensslServerConfigBuilder, OpensslServerSessionCache, OpensslSessionIdContext, - OpensslTicketKey, + OpensslTicketKey, OpensslTicketKeyBuilder, }; mod cert_pair; diff --git a/lib/g3-types/src/net/openssl/server/mod.rs b/lib/g3-types/src/net/openssl/server/mod.rs index 6e4ad7717..a41255f11 100644 --- a/lib/g3-types/src/net/openssl/server/mod.rs +++ b/lib/g3-types/src/net/openssl/server/mod.rs @@ -37,7 +37,7 @@ mod intercept; pub use intercept::{OpensslInterceptionServerConfig, OpensslInterceptionServerConfigBuilder}; mod ticket_key; -pub use ticket_key::OpensslTicketKey; +pub use ticket_key::{OpensslTicketKey, OpensslTicketKeyBuilder}; mod ticketer; diff --git a/lib/g3-types/src/net/openssl/server/ticket_key.rs b/lib/g3-types/src/net/openssl/server/ticket_key.rs index b4acfd408..9b12f2ae5 100644 --- a/lib/g3-types/src/net/openssl/server/ticket_key.rs +++ b/lib/g3-types/src/net/openssl/server/ticket_key.rs @@ -51,6 +51,44 @@ impl OpensslTicketContext { } } +pub struct OpensslTicketKeyBuilder { + pub name: [u8; TICKET_KEY_NAME_LENGTH], + pub aes_key: [u8; TICKET_AES_KEY_LENGTH], + pub hmac_key: [u8; TICKET_HMAC_KEY_LENGTH], + lifetime: u32, +} + +impl OpensslTicketKeyBuilder { + pub fn new(lifetime: u32) -> Self { + OpensslTicketKeyBuilder { + name: [0u8; TICKET_KEY_NAME_LENGTH], + aes_key: [0u8; TICKET_AES_KEY_LENGTH], + hmac_key: [0u8; TICKET_HMAC_KEY_LENGTH], + lifetime, + } + } + + #[inline] + pub fn set_lifetime(&mut self, lifetime: u32) { + self.lifetime = lifetime; + } + + pub fn build(self) -> OpensslTicketKey { + OpensslTicketKey { + name: self.name.into(), + lifetime: self.lifetime, + aes_key: self.aes_key, + hmac_key: self.hmac_key, + } + } +} + +impl Default for OpensslTicketKeyBuilder { + fn default() -> Self { + Self::new(3600 * 24) + } +} + pub struct OpensslTicketKey { name: TicketKeyName, lifetime: u32,