SDK 层(pkg/crtsh/)
SDK 是整个项目的心脏。所有 crt.sh 能力的实现都在这里,MCP 与 CLI 只是它的薄包装。
pkg/crtsh唯一事实来源20 方法 + 辅助函数IterateCertificatesClient 结构
go
type Client struct {
BaseURL string // 默认 https://crt.sh/
HTTPClient *http.Client // 默认超时 30s
RetryCount int // 默认 3
Debug bool // 打印请求/响应原文
UserAgent string
rateLimiter <-chan time.Time
}通过 NewClient(opts ...ClientOption) 构造:
⏱️WithTimeout
覆盖默认 30s 超时
🔁WithRetryCount
覆盖默认 3 次重试
🐛WithDebug
打印请求/响应原文
🏷️WithUserAgent
自定义 UA
🌐WithBaseURL
指向自托管 crt.sh
一个方法的内部流程
以 SearchCertificates 为例,每个方法走的都是同一条流水线:
flowchart TD V["Validate()
前置校验搜索类型/匹配模式"] --> B["buildQuery()
QueryParams → crt.sh URL"] B --> R["重试循环
attempt = 0..RetryCount"] R --> H["doRequest()
HTTP 调用 + 限流 + 10MB 限体"] H --> S{状态码} S -->|"5xx 或网络错"| BK["指数退避
500ms × 2^attempt
尊重 ctx.Done"] BK --> R S -->|"4xx/404/429"| E["parseAPIError()
分类为 typed error"] S -->|200| P{返回类型} P -->|JSON| J["json.Unmarshal"] P -->|HTML| HP["parser.go 解析"] P -->|XML| XP["parseAtomFeed"] P -->|PEM| CK["校验 PEM 头"] J --> OUT["返回强类型结果"] HP --> OUT XP --> OUT CK --> OUT
前置校验搜索类型/匹配模式"] --> B["buildQuery()
QueryParams → crt.sh URL"] B --> R["重试循环
attempt = 0..RetryCount"] R --> H["doRequest()
HTTP 调用 + 限流 + 10MB 限体"] H --> S{状态码} S -->|"5xx 或网络错"| BK["指数退避
500ms × 2^attempt
尊重 ctx.Done"] BK --> R S -->|"4xx/404/429"| E["parseAPIError()
分类为 typed error"] S -->|200| P{返回类型} P -->|JSON| J["json.Unmarshal"] P -->|HTML| HP["parser.go 解析"] P -->|XML| XP["parseAtomFeed"] P -->|PEM| CK["校验 PEM 头"] J --> OUT["返回强类型结果"] HP --> OUT XP --> OUT CK --> OUT
关键文件分工
🔌api.go
Client 与全部 20 个方法、buildQuery、重试、错误分类、Link header 分页
📖parser.go
parseCertDetailHTML(HTML→CertificateDetail)、parseAtomFeed(XML→AtomFeed)
📚registry.go
搜索类型/匹配模式/分组/排序/linter 的单一来源
✓validate.go
QueryParams.Validate() 等前置校验
⚙️options.go
ClientOption 函数式选项
🛡️errors.go
Error 类型 + ErrorType 枚举 + IsNotFoundError 等
📄iterate.go
IterateCertificates 自动分页迭代器
🏗️models.go
Certificate / CertificateDetail / QueryParams / AtomFeed
buildQuery:crt.sh URL 语义的忠实翻译
这是整个 SDK 最关键的函数。它把 QueryParams 翻译成 crt.sh 实际接受的 URL 参数,严格遵循 crt.sh 前端 JS 的行为:
flowchart LR subgraph 输入["QueryParams"] ST["SearchType"] FL["ExcludeExpired/Deduplicate"] ICA["IssuerCAID"] LT["Linter/LintType"] end subgraph 输出["crt.sh URL"] U1["搜索类型作 key
?CN=example.com"] U2["exclude=expired
deduplicate=Y"] U3["iCAID=id"] U4["zlint=issues
(linter 名作 key)"] end ST --> U1 FL --> U2 ICA --> U3 LT --> U4
?CN=example.com"] U2["exclude=expired
deduplicate=Y"] U3["iCAID=id"] U4["zlint=issues
(linter 名作 key)"] end ST --> U1 FL --> U2 ICA --> U3 LT --> U4
go
switch params.SearchType {
case "CN": query.Set("CN", params.CN)
case "sha256": query.Set("sha256", params.SHA256)
default: query.Set("q", params.Q) // 通用搜索
}go
if params.ExcludeExpired { query.Set("exclude", "expired") } // 不是 =on
if params.Deduplicate { query.Set("deduplicate", "Y") } // 不是 =on
if params.IssuerCAID != "" { query.Set("iCAID", params.IssuerCAID) }go
if params.Linter != "" { query.Set(params.Linter, params.LintType) }
// ?zlint=issues,不是 ?linter=zlint&linttype=issues跳过 output=json
?id= 与 ?ctid= 端点 crt.sh 不支持 output=json,buildQuery 会据此跳过——这些细节都封装好了。
强类型错误
stateDiagram-v2 [*] --> 发请求 发请求 --> 分类: 拿到状态码 分类 --> NotFound: 404 分类 --> RateLimit: 429 分类 --> Server: 5xx 分类 --> Search: 其他 NotFound --> [*]: IsNotFoundError() RateLimit --> [*]: IsRateLimitError() Server --> [*]: IsServerError() Search --> [*]: 其他判断
parseAPIError 根据状态码分类,调用方用 IsNotFoundError(err) 等做分支处理。
单一来源原则
加一个搜索类型,全层自动同步
搜索类型、匹配模式这些可枚举选项,只在 registry.go 定义一次(SearchTypes()、MatchModes() 等),MCP 工具的 schema、CLI 的 list-* 命令、Skills 的描述都从这里取值。
flowchart LR REG["registry.go — 单一来源"] --> SDK["SDK 方法"] REG --> MCP["MCP 工具 schema"] REG --> CLI["CLI list-* 命令"] REG --> SKILL["Skills 描述"]
下一层