allowed_client.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. package openai
  2. import "strings"
  3. // AllowedClientEntry 描述一个被额外放行的非官方 Codex 客户端签名。
  4. // Originator 必须精确等值匹配(归一化后)。
  5. // UAContains 为必填字段:列表为空,或列表中存在任何空白 marker,均视为非法配置,
  6. // 整体安全失败(return false);每一项都必须出现在 User-Agent 中。
  7. // 这确保双因子匹配不会因缺失 UA 声明而退化为仅凭可伪造的 originator 单因子放行。
  8. // SkipEngineFingerprint 仅对白名单条目有意义:命中此条则跳过引擎指纹门(管理员显式承担
  9. // "纯 UA+originator、无引擎兜底"的后门风险,默认 false)。黑名单忽略此字段。
  10. type AllowedClientEntry struct {
  11. Originator string `json:"originator"`
  12. UAContains []string `json:"ua_contains"`
  13. SkipEngineFingerprint bool `json:"skip_engine_fingerprint"`
  14. }
  15. // IsWhitelistable 报告该条目作为白名单条目是否「有可能命中」——镜像 IsAllowedClientMatch 的结构性
  16. // 前置:originator 非空、ua_contains 至少一项、且无任何空白 marker(空白 marker 会让整条永不命中)。
  17. // 仅供管理端写入校验,避免存入静默失效的白名单规则。黑名单(OR 宽 deny,允许 originator-only)不受此约束。
  18. func (e AllowedClientEntry) IsWhitelistable() bool {
  19. if normalizeCodexClientHeader(e.Originator) == "" {
  20. return false
  21. }
  22. if len(e.UAContains) == 0 {
  23. return false
  24. }
  25. for _, marker := range e.UAContains {
  26. if normalizeCodexClientHeader(marker) == "" {
  27. return false
  28. }
  29. }
  30. return true
  31. }
  32. // IsAllowedClientMatch 判断请求头是否命中给定的额外客户端签名。
  33. // originator 必须精确等值(归一化后);UAContains 中每一项都必须出现在 UA 中。
  34. // UAContains 为必填:列表为空或含任何空白 marker 均视为非法配置,整体安全失败。
  35. func IsAllowedClientMatch(userAgent, originator string, entry AllowedClientEntry) bool {
  36. wantOriginator := normalizeCodexClientHeader(entry.Originator)
  37. if wantOriginator == "" {
  38. return false
  39. }
  40. if normalizeCodexClientHeader(originator) != wantOriginator {
  41. return false
  42. }
  43. // 预设必须声明 UA 特征:否则将退化为仅凭可伪造的 originator 单因子匹配。
  44. if len(entry.UAContains) == 0 {
  45. return false
  46. }
  47. ua := normalizeCodexClientHeader(userAgent)
  48. for _, marker := range entry.UAContains {
  49. normalizedMarker := normalizeCodexClientHeader(marker)
  50. if normalizedMarker == "" {
  51. // 空白 marker 让该项失去校验能力,会让双因子退化为仅 originator
  52. // 单因子;视为非法配置,安全失败。
  53. return false
  54. }
  55. if !strings.Contains(ua, normalizedMarker) {
  56. return false
  57. }
  58. }
  59. return true
  60. }
  61. // MatchClientEntry 同 MatchClientEntries(双因子 AND,复用 IsAllowedClientMatch),但回传命中的
  62. // 那条条目,供调用方读取 SkipEngineFingerprint 等条目级配置。未命中返回零值 + false。
  63. func MatchClientEntry(userAgent, originator string, entries []AllowedClientEntry) (AllowedClientEntry, bool) {
  64. for _, e := range entries {
  65. if IsAllowedClientMatch(userAgent, originator, e) {
  66. return e, true
  67. }
  68. }
  69. return AllowedClientEntry{}, false
  70. }
  71. // MatchClientEntries 判断请求头是否命中任一白名单自由条目(双因子 AND)。薄封装 MatchClientEntry。
  72. // 用于 codex_cli_only 全局白名单:放行官方集未覆盖的 app-server 新 client。
  73. func MatchClientEntries(userAgent, originator string, entries []AllowedClientEntry) bool {
  74. _, ok := MatchClientEntry(userAgent, originator, entries)
  75. return ok
  76. }
  77. // IsDeniedClientMatch 黑名单单条 OR 语义:已声明字段中任一命中即 deny。
  78. // originator 精确等值命中,或任一非空 ua_contains marker 出现在 UA 中。
  79. // 全空字段(originator 与 ua_contains 均空)→ 不 deny(安全忽略)。
  80. // 与白名单 AND 非对称:deny 应宽(挡可疑),allow 应严(防伪造)。
  81. func IsDeniedClientMatch(userAgent, originator string, entry AllowedClientEntry) bool {
  82. if want := normalizeCodexClientHeader(entry.Originator); want != "" {
  83. if normalizeCodexClientHeader(originator) == want {
  84. return true
  85. }
  86. }
  87. ua := normalizeCodexClientHeader(userAgent)
  88. for _, marker := range entry.UAContains {
  89. if m := normalizeCodexClientHeader(marker); m != "" && strings.Contains(ua, m) {
  90. return true
  91. }
  92. }
  93. return false
  94. }
  95. // MatchDenyEntries 判断请求头是否命中任一黑名单条目(OR)。
  96. func MatchDenyEntries(userAgent, originator string, entries []AllowedClientEntry) bool {
  97. for _, e := range entries {
  98. if IsDeniedClientMatch(userAgent, originator, e) {
  99. return true
  100. }
  101. }
  102. return false
  103. }