Skip to main content

openlark_core/config/
mod.rs

1use std::{collections::HashMap, ops::Deref, sync::Arc, time::Duration};
2
3use crate::{
4    auth::token_provider::{NoOpTokenProvider, TokenProvider},
5    constants::{AppType, FEISHU_BASE_URL},
6    error::CoreError,
7    performance::OptimizedHttpConfig,
8};
9
10/// 解析 `OPENLARK_*` 布尔环境值。
11///
12/// - 空 / 全空白 → `None`(保留当前配置状态)
13/// - `f*` / `0` / `false`(不区分大小写,trim 后)→ `Some(false)`
14/// - 其它非空 → `Some(true)`
15///
16/// `OPENLARK_ENABLE_TOKEN_CACHE` 与 `OPENLARK_ENABLE_LOG` 共用此规则。
17fn parse_env_bool(value: &str) -> Option<bool> {
18    let s = value.trim().to_lowercase();
19    if s.is_empty() {
20        return None;
21    }
22    Some(!(s.starts_with('f') || s == "0"))
23}
24
25/// 检查 base_url 是否指向已知的飞书/Lark 域名(白名单 SSRF 防护)
26///
27/// 已知域名后缀:`feishu.cn`、`larksuite.com`、`larkoffice.com`。
28/// [`Config::validate`](Config::validate) 用本函数做 base_url 白名单校验;
29/// `allow_custom_base_url` 为 true 时跳过该校验。公开以便下游(如 client 构建校验)
30/// 复用同一白名单判定。
31pub fn is_known_base_url(url: &str) -> bool {
32    let allowed_suffixes = ["feishu.cn", "larksuite.com", "larkoffice.com"];
33    if let Ok(parsed) = url::Url::parse(url)
34        && let Some(host) = parsed.host_str()
35    {
36        return allowed_suffixes
37            .iter()
38            .any(|suffix| host == *suffix || host.ends_with(&format!(".{suffix}")));
39    }
40    false
41}
42
43/// # 零拷贝配置共享实现
44///
45/// `Config` 内部使用 `Arc<ConfigInner>` 实现零拷贝共享:
46///
47/// ## 性能特性
48/// - **内存效率**: 所有克隆共享同一份配置数据(~300-500字节)
49/// - **克隆成本**: `Config::clone()` 只复制Arc指针(8字节 + 原子操作)
50/// - **线程安全**: Arc保证多线程安全的只读访问
51/// - **引用计数**: 自动管理内存,无泄漏风险
52///
53/// ## 使用建议
54/// ```rust
55/// // ✅ 推荐: 克隆Config传递给服务
56/// let service = MyService::new(config.clone());
57///
58/// // ✅ 推荐: 在Request中持有Config
59/// pub struct MyRequest {
60///     config: Config,  // 持有Arc指针,成本低
61/// }
62///
63/// // ⚠️ 不必要: 使用Arc<Config> (Config内部已经是Arc)
64/// // Arc<Arc<ConfigInner>> = 双重Arc,没有额外收益
65/// ```
66///
67/// ## 性能验证
68/// 运行 `cargo test config_arc` 查看基准测试:
69/// - 克隆速度: ~10-20纳秒
70/// - 内存开销: 每个克隆仅8字节
71/// - 引用计数: 自动维护
72#[derive(Debug, Clone)]
73pub struct Config {
74    /// 包装在 Arc 中的共享配置数据
75    ///
76    /// 所有 Config 实例通过 Arc 共享同一份 ConfigInner,
77    /// 实现零拷贝的配置共享。
78    inner: Arc<ConfigInner>,
79}
80
81/// 内部配置数据,被多个服务共享
82#[derive(Clone)]
83pub struct ConfigInner {
84    pub(crate) app_id: String,
85    pub(crate) app_secret: String,
86    /// 域名, 默认为 <https://open.feishu.cn>
87    pub(crate) base_url: String,
88    /// 是否允许 core 在缺少显式 token 时自动获取 token
89    pub(crate) enable_token_cache: bool,
90    /// 应用类型, 默认为自建应用
91    pub(crate) app_type: AppType,
92    pub(crate) http_client: reqwest::Client,
93    /// 客户端超时时间, 默认永不超时
94    pub(crate) req_timeout: Option<Duration>,
95    pub(crate) header: HashMap<String, String>,
96    /// Token 获取抽象(由业务 crate 实现,例如 openlark-auth)
97    pub(crate) token_provider: Arc<dyn TokenProvider>,
98    /// 响应体最大大小(字节),超过返回 ResponseTooLarge 错误,默认 100MB
99    pub(crate) max_response_size: u64,
100    /// 默认重试次数,默认 3
101    pub(crate) retry_count: u32,
102    /// 是否启用日志记录,默认 true
103    pub(crate) enable_log: bool,
104    /// 是否允许自定义 base_url 域名(绕过飞书白名单 SSRF 防护),默认 false
105    pub(crate) allow_custom_base_url: bool,
106}
107
108impl Default for ConfigInner {
109    fn default() -> Self {
110        Self {
111            app_id: "".to_string(),
112            app_secret: "".to_string(),
113            base_url: FEISHU_BASE_URL.to_string(),
114            enable_token_cache: true,
115            app_type: AppType::SelfBuild,
116            http_client: reqwest::Client::new(),
117            req_timeout: None,
118            header: Default::default(),
119            token_provider: Arc::new(NoOpTokenProvider),
120            max_response_size: 100 * 1024 * 1024, // 100MB
121            retry_count: 3,
122            enable_log: true,
123            allow_custom_base_url: false,
124        }
125    }
126}
127
128impl std::fmt::Debug for ConfigInner {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        f.debug_struct("ConfigInner")
131            .field("app_id", &self.app_id)
132            .field("app_secret", &"***")
133            .field("base_url", &self.base_url)
134            .field("enable_token_cache", &self.enable_token_cache)
135            .field("app_type", &self.app_type)
136            .field("req_timeout", &self.req_timeout)
137            .field("max_response_size", &self.max_response_size)
138            .field("retry_count", &self.retry_count)
139            .field("enable_log", &self.enable_log)
140            .field("allow_custom_base_url", &self.allow_custom_base_url)
141            .field("header", &format!("{} headers", self.header.len()))
142            .finish()
143    }
144}
145
146impl Default for Config {
147    fn default() -> Self {
148        Self {
149            inner: Arc::new(ConfigInner::default()),
150        }
151    }
152}
153
154impl Deref for Config {
155    type Target = ConfigInner;
156
157    fn deref(&self) -> &Self::Target {
158        &self.inner
159    }
160}
161
162impl Config {
163    /// 创建配置构建器
164    pub fn builder() -> ConfigBuilder {
165        ConfigBuilder::default()
166    }
167
168    /// 创建新的 Config 实例,直接从 ConfigInner
169    pub fn new(inner: ConfigInner) -> Self {
170        Self {
171            inner: Arc::new(inner),
172        }
173    }
174
175    /// 基于当前配置生成一个“替换 TokenProvider”的新配置
176    ///
177    /// 说明:
178    /// - 这是一个纯拷贝操作(`Config` 本身是 `Arc` 包装),不会修改原配置
179    /// - 推荐用法:先构建一个“基础 Config”(默认 `NoOpTokenProvider`),再用该基础 Config 构建业务 TokenProvider,
180    ///   最后调用此方法把 provider 注入到“业务 Config”中,避免循环引用。
181    pub fn with_token_provider(&self, provider: impl TokenProvider + 'static) -> Self {
182        let mut inner = (*self.inner).clone();
183        inner.token_provider = Arc::new(provider);
184        Config::new(inner)
185    }
186
187    /// 获取内部 Arc 的引用计数
188    pub fn reference_count(&self) -> usize {
189        Arc::strong_count(&self.inner)
190    }
191
192    /// 获取应用 ID
193    pub fn app_id(&self) -> &str {
194        &self.inner.app_id
195    }
196
197    /// 获取应用密钥
198    pub fn app_secret(&self) -> &str {
199        &self.inner.app_secret
200    }
201
202    /// 获取基础 URL
203    pub fn base_url(&self) -> &str {
204        &self.inner.base_url
205    }
206
207    /// 获取超时时间
208    pub fn req_timeout(&self) -> Option<Duration> {
209        self.inner.req_timeout
210    }
211
212    /// 是否启用令牌缓存
213    pub fn enable_token_cache(&self) -> bool {
214        self.inner.enable_token_cache
215    }
216
217    /// 获取应用类型
218    pub fn app_type(&self) -> AppType {
219        self.inner.app_type
220    }
221
222    /// 获取 HTTP 客户端引用
223    pub fn http_client(&self) -> &reqwest::Client {
224        &self.inner.http_client
225    }
226
227    /// 获取自定义 header 引用
228    pub fn header(&self) -> &HashMap<String, String> {
229        &self.inner.header
230    }
231
232    /// 获取 TokenProvider 引用
233    pub fn token_provider(&self) -> &Arc<dyn TokenProvider> {
234        &self.inner.token_provider
235    }
236
237    /// 获取响应体最大大小限制
238    pub fn max_response_size(&self) -> u64 {
239        self.inner.max_response_size
240    }
241
242    /// 获取重试次数
243    pub fn retry_count(&self) -> u32 {
244        self.inner.retry_count
245    }
246
247    /// 是否启用日志记录
248    pub fn enable_log(&self) -> bool {
249        self.inner.enable_log
250    }
251
252    /// 是否允许自定义 base_url 域名(绕过白名单 SSRF 防护)
253    pub fn allow_custom_base_url(&self) -> bool {
254        self.inner.allow_custom_base_url
255    }
256
257    /// 校验配置有效性
258    ///
259    /// # 校验规则
260    /// - `app_id` / `app_secret` 非空
261    /// - `base_url` 非空且以 `<http://>` / `<https://>` 开头
262    /// - `base_url` 域名在飞书/Lark 白名单内(`*.feishu.cn` / `*.larksuite.com` /
263    ///   `*.larkoffice.com`);`allow_custom_base_url` 为 true 时豁免(SSRF 防护)
264    /// - `retry_count` 不超过 10
265    ///
266    /// `builder().build()` **不会**自动调用本方法——与 core 现有行为一致,避免破坏
267    /// 所有现有 `core::Config` 用户;`from_env()` 会在内部调用本方法但失败时仅记录、
268    /// 不阻塞返回。需要强校验的调用方应显式调用 `validate()`。
269    pub fn validate(&self) -> Result<(), CoreError> {
270        if self.app_id.is_empty() {
271            return Err(CoreError::validation_builder()
272                .field("app_id")
273                .message("app_id 不能为空")
274                .build());
275        }
276        if self.app_secret.is_empty() {
277            return Err(CoreError::validation_builder()
278                .field("app_secret")
279                .message("app_secret 不能为空")
280                .build());
281        }
282        if self.base_url.is_empty() {
283            return Err(CoreError::validation_builder()
284                .field("base_url")
285                .message("base_url 不能为空")
286                .build());
287        }
288        if !self.base_url.starts_with("http://") && !self.base_url.starts_with("https://") {
289            return Err(CoreError::validation_builder()
290                .field("base_url")
291                .message("base_url 必须以 http:// 或 https:// 开头")
292                .build());
293        }
294        if !self.allow_custom_base_url && !is_known_base_url(&self.base_url) {
295            tracing::warn!(
296                "base_url '{}' 不在飞书/Lark 已知域名白名单中。\
297                 如需使用自定义域名,请设置 allow_custom_base_url(true)。",
298                self.base_url
299            );
300            return Err(CoreError::validation_builder()
301                .field("base_url")
302                .message(
303                    "base_url 域名不在白名单中,已知域名: *.feishu.cn, *.larksuite.com, \
304                     *.larkoffice.com。如需使用自定义域名,请设置 allow_custom_base_url(true)",
305                )
306                .build());
307        }
308        if self.retry_count > 10 {
309            return Err(CoreError::validation_builder()
310                .field("retry_count")
311                .message("retry_count 不能超过 10")
312                .build());
313        }
314        Ok(())
315    }
316
317    /// 从 `OPENLARK_*` 环境变量加载配置
318    ///
319    /// 读取环境变量构建 Config,缺失变量用默认值。内部调用 [`validate`](Self::validate),
320    /// 校验失败仅记录警告、不阻塞返回(与 `builder().build()` 不校验语义一致)。
321    /// 返回的 Config 可能未通过校验,关键路径应追加显式 `.validate()` 确认。
322    ///
323    /// # 环境变量
324    /// - `OPENLARK_APP_ID` / `OPENLARK_APP_SECRET` / `OPENLARK_APP_TYPE`
325    ///   (`self_build`/`selfbuild`/`self` 或 `marketplace`/`store`)
326    /// - `OPENLARK_BASE_URL` / `OPENLARK_ENABLE_TOKEN_CACHE`
327    /// - `OPENLARK_TIMEOUT`(秒)→ `req_timeout(Some(Duration))`;未设保持 `None`
328    /// - `OPENLARK_RETRY_COUNT` / `OPENLARK_MAX_RESPONSE_SIZE` / `OPENLARK_ENABLE_LOG`
329    pub fn from_env() -> Config {
330        let mut inner = ConfigInner::default();
331        Self::apply_env_vars(&mut inner);
332        let config = Config::new(inner);
333        if let Err(e) = config.validate() {
334            tracing::warn!("from_env 加载的配置未通过校验: {e}");
335        }
336        config
337    }
338
339    /// 从 `OPENLARK_*` 环境变量加载到当前实例(写时复制:独占引用时原地修改)
340    ///
341    /// 仅设置存在且非空的环境变量(`OPENLARK_APP_TYPE` 除外,它对非法值静默忽略)。
342    pub fn load_from_env(&mut self) {
343        let inner = Arc::make_mut(&mut self.inner);
344        Self::apply_env_vars(inner);
345    }
346
347    fn apply_env_vars(inner: &mut ConfigInner) {
348        for (key, value) in std::env::vars() {
349            Self::apply_env_var(inner, &key, &value);
350        }
351    }
352
353    fn apply_env_var(inner: &mut ConfigInner, key: &str, value: &str) {
354        match key {
355            "OPENLARK_APP_ID" if !value.is_empty() => inner.app_id = value.to_string(),
356            "OPENLARK_APP_SECRET" if !value.is_empty() => inner.app_secret = value.to_string(),
357            "OPENLARK_APP_TYPE" => {
358                let v = value.trim().to_lowercase();
359                match v.as_str() {
360                    "self_build" | "selfbuild" | "self" => inner.app_type = AppType::SelfBuild,
361                    "marketplace" | "store" => inner.app_type = AppType::Marketplace,
362                    _ => {}
363                }
364            }
365            "OPENLARK_BASE_URL" if !value.is_empty() => inner.base_url = value.to_string(),
366            "OPENLARK_ENABLE_TOKEN_CACHE" => {
367                if let Some(v) = parse_env_bool(value) {
368                    inner.enable_token_cache = v;
369                }
370            }
371            // 分叉 5:秒数 → req_timeout(Some);未设保持 None
372            "OPENLARK_TIMEOUT" => {
373                if let Ok(secs) = value.parse::<u64>() {
374                    inner.req_timeout = Some(Duration::from_secs(secs));
375                }
376            }
377            "OPENLARK_RETRY_COUNT" => {
378                if let Ok(n) = value.parse::<u32>() {
379                    inner.retry_count = n;
380                }
381            }
382            "OPENLARK_MAX_RESPONSE_SIZE" => {
383                if let Ok(size) = value.parse::<u64>() {
384                    inner.max_response_size = size;
385                }
386            }
387            "OPENLARK_ENABLE_LOG" => {
388                if let Some(v) = parse_env_bool(value) {
389                    inner.enable_log = v;
390                }
391            }
392            _ => {}
393        }
394    }
395
396    /// 生成不含敏感信息的配置摘要
397    ///
398    /// `app_secret` 仅以布尔「是否已设置」表示,不泄露明文。
399    pub fn summary(&self) -> ConfigSummary {
400        ConfigSummary {
401            app_id: self.app_id.clone(),
402            app_secret_set: !self.app_secret.is_empty(),
403            app_type: self.app_type,
404            enable_token_cache: self.enable_token_cache,
405            base_url: self.base_url.clone(),
406            allow_custom_base_url: self.allow_custom_base_url,
407            req_timeout: self.req_timeout,
408            retry_count: self.retry_count,
409            enable_log: self.enable_log,
410            header_count: self.header.len(),
411            max_response_size: self.max_response_size,
412        }
413    }
414}
415
416/// 配置构建器
417///
418/// 直接持有一份 canonical [`ConfigInner`] 状态:所有 setter、环境覆盖与 header
419/// 合并都写同一份状态,`build()` 仅将其移入 [`Config`]。
420///
421/// 不自动调用 [`Config::validate`]——保持 core 宽松构建契约。
422#[derive(Clone, Default)]
423pub struct ConfigBuilder {
424    inner: ConfigInner,
425}
426
427impl std::fmt::Debug for ConfigBuilder {
428    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
429        // 复用 ConfigInner 的脱敏 Debug,避免字段列表双份漂移
430        f.debug_struct("ConfigBuilder")
431            .field("inner", &self.inner)
432            .finish()
433    }
434}
435
436impl ConfigBuilder {
437    /// 设置应用 ID
438    pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
439        self.inner.app_id = app_id.into();
440        self
441    }
442
443    /// 设置应用密钥
444    pub fn app_secret(mut self, app_secret: impl Into<String>) -> Self {
445        self.inner.app_secret = app_secret.into();
446        self
447    }
448
449    /// 设置基础 URL
450    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
451        self.inner.base_url = base_url.into();
452        self
453    }
454
455    /// 设置是否启用令牌缓存
456    pub fn enable_token_cache(mut self, enable: bool) -> Self {
457        self.inner.enable_token_cache = enable;
458        self
459    }
460
461    /// 设置应用类型
462    pub fn app_type(mut self, app_type: AppType) -> Self {
463        self.inner.app_type = app_type;
464        self
465    }
466
467    /// 设置 HTTP 客户端
468    pub fn http_client(mut self, client: reqwest::Client) -> Self {
469        self.inner.http_client = client;
470        self
471    }
472
473    /// 使用优化的HTTP配置构建客户端
474    pub fn optimized_http_client(
475        mut self,
476        config: OptimizedHttpConfig,
477    ) -> Result<Self, reqwest::Error> {
478        let client = config.build_client()?;
479        self.inner.http_client = client;
480        Ok(self)
481    }
482
483    /// 使用生产环境优化配置
484    pub fn production_http_client(self) -> Result<Self, reqwest::Error> {
485        let config = OptimizedHttpConfig::production();
486        self.optimized_http_client(config)
487    }
488
489    /// 使用高吞吐量配置
490    pub fn high_throughput_http_client(self) -> Result<Self, reqwest::Error> {
491        let config = OptimizedHttpConfig::high_throughput();
492        self.optimized_http_client(config)
493    }
494
495    /// 使用低延迟配置
496    pub fn low_latency_http_client(self) -> Result<Self, reqwest::Error> {
497        let config = OptimizedHttpConfig::low_latency();
498        self.optimized_http_client(config)
499    }
500
501    /// 设置请求超时时间
502    pub fn req_timeout(mut self, timeout: Duration) -> Self {
503        self.inner.req_timeout = Some(timeout);
504        self
505    }
506
507    /// 整体替换自定义 HTTP 头
508    ///
509    /// 与 [`Self::add_header`] 混用时按链式调用顺序生效:本方法清除先前增量写入。
510    pub fn header(mut self, header: HashMap<String, String>) -> Self {
511        self.inner.header = header;
512        self
513    }
514
515    /// 增量添加单个 HTTP 头(同名键后写覆盖)
516    pub fn add_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
517        self.inner.header.insert(key.into(), value.into());
518        self
519    }
520
521    /// 在当前链式位置叠加 `OPENLARK_*` 环境变量
522    ///
523    /// 与 [`Config::from_env`] / [`Config::load_from_env`] 共用同一套环境解释逻辑。
524    /// 缺失、空值或不可解析的可选变量保留当前 builder 状态;有效值覆盖当前状态。
525    pub fn load_from_env(mut self) -> Self {
526        Config::apply_env_vars(&mut self.inner);
527        self
528    }
529
530    /// 设置令牌提供者
531    pub fn token_provider(mut self, provider: impl TokenProvider + 'static) -> Self {
532        self.inner.token_provider = Arc::new(provider);
533        self
534    }
535
536    /// 设置响应体最大大小限制(字节),默认 100MB
537    pub fn max_response_size(mut self, size: u64) -> Self {
538        self.inner.max_response_size = size;
539        self
540    }
541
542    /// 设置默认重试次数,默认 3
543    pub fn retry_count(mut self, count: u32) -> Self {
544        self.inner.retry_count = count;
545        self
546    }
547
548    /// 设置是否启用日志记录,默认 true
549    pub fn enable_log(mut self, enable: bool) -> Self {
550        self.inner.enable_log = enable;
551        self
552    }
553
554    /// 设置是否允许自定义 base_url 域名(绕过白名单 SSRF 防护),默认 false
555    pub fn allow_custom_base_url(mut self, allow: bool) -> Self {
556        self.inner.allow_custom_base_url = allow;
557        self
558    }
559
560    /// 构建 Config 实例(不自动校验)
561    pub fn build(self) -> Config {
562        Config::new(self.inner)
563    }
564}
565
566/// 配置摘要(不含敏感信息)
567///
568/// 由 [`Config::summary`] 生成。`app_secret` 仅以 `app_secret_set` 布尔表示是否已设置,
569/// 不泄露明文。便于日志、调试和展示。
570#[derive(Debug, Clone)]
571pub struct ConfigSummary {
572    /// 应用 ID
573    pub app_id: String,
574    /// 应用密钥是否已设置(不泄露明文)
575    pub app_secret_set: bool,
576    /// 应用类型
577    pub app_type: AppType,
578    /// 是否允许自动获取 token
579    pub enable_token_cache: bool,
580    /// API 基础 URL
581    pub base_url: String,
582    /// 是否允许自定义 base_url 域名(绕过白名单 SSRF 防护)
583    pub allow_custom_base_url: bool,
584    /// 请求超时时间(None 表示永不超时)
585    pub req_timeout: Option<Duration>,
586    /// 默认重试次数
587    pub retry_count: u32,
588    /// 是否启用日志记录
589    pub enable_log: bool,
590    /// 自定义 headers 数量
591    pub header_count: usize,
592    /// 响应体最大大小限制(字节)
593    pub max_response_size: u64,
594}
595
596impl ConfigSummary {
597    /// 获取友好的中文配置描述
598    pub fn friendly_description(&self) -> String {
599        format!(
600            "应用ID: {}, 基础URL: {}, 超时: {:?}, 重试: {}, 日志: {}, Headers: {}, 最大响应: {}",
601            self.app_id,
602            self.base_url,
603            self.req_timeout,
604            self.retry_count,
605            if self.enable_log { "启用" } else { "禁用" },
606            self.header_count,
607            self.max_response_size
608        )
609    }
610}
611
612impl std::fmt::Display for ConfigSummary {
613    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
614        // 注意:只输出 app_secret_set 布尔,绝不输出 app_secret 明文
615        write!(
616            f,
617            "Config {{ app_id: {}, app_secret_set: {}, base_url: {}, req_timeout: {:?}, retry_count: {}, enable_log: {}, header_count: {}, max_response_size: {} }}",
618            self.app_id,
619            self.app_secret_set,
620            self.base_url,
621            self.req_timeout,
622            self.retry_count,
623            self.enable_log,
624            self.header_count,
625            self.max_response_size
626        )
627    }
628}
629
630#[cfg(test)]
631mod tests;