github.com/chainreactors/fingers@v1.2.1/fingerprinthub/fingerprinthub.go (about) 1 package fingerprinthub 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "net/http" 9 "strings" 10 "sync" 11 12 "github.com/chainreactors/fingers/common" 13 "github.com/chainreactors/fingers/resources" 14 "github.com/chainreactors/logs" 15 "github.com/chainreactors/neutron/protocols" 16 http2 "github.com/chainreactors/neutron/protocols/http" 17 "github.com/chainreactors/neutron/templates" 18 "github.com/chainreactors/utils/encode" 19 "github.com/chainreactors/utils/httputils" 20 "gopkg.in/yaml.v3" 21 ) 22 23 // CachedResponse 存储缓存的 HTTP 响应 24 type CachedResponse struct { 25 Response *http.Response // 响应对象(Body 为 nil) 26 Body []byte // 响应体内容 27 } 28 29 // CachedTransport 实现带缓存的 http.RoundTripper 30 // 通过 path 去重,避免重复请求 31 type CachedTransport struct { 32 transport http.RoundTripper 33 cache map[string]*CachedResponse 34 mu sync.Mutex 35 } 36 37 // RoundTrip 实现 http.RoundTripper 接口,带缓存功能 38 func (c *CachedTransport) RoundTrip(req *http.Request) (*http.Response, error) { 39 // 使用 path 作为缓存键 40 cacheKey := req.URL.Path 41 if cacheKey == "" { 42 cacheKey = "/" 43 } 44 45 // 检查缓存 46 c.mu.Lock() 47 if cached, ok := c.cache[cacheKey]; ok { 48 c.mu.Unlock() 49 // 复制响应对象,使用缓存的 Body 50 resp := *cached.Response 51 resp.Body = ioutil.NopCloser(bytes.NewReader(cached.Body)) 52 resp.Request = req 53 return &resp, nil 54 } 55 c.mu.Unlock() 56 57 // 缓存未命中,发送实际请求 58 resp, err := c.transport.RoundTrip(req) 59 if err != nil { 60 return nil, err 61 } 62 63 // 读取响应体 64 bodyBytes, err := ioutil.ReadAll(resp.Body) 65 resp.Body.Close() 66 if err != nil { 67 return nil, err 68 } 69 70 // 保存到缓存(复制响应对象,Body 设为 nil) 71 cachedResp := *resp 72 cachedResp.Body = nil 73 c.mu.Lock() 74 c.cache[cacheKey] = &CachedResponse{ 75 Response: &cachedResp, 76 Body: bodyBytes, 77 } 78 c.mu.Unlock() 79 80 // 返回响应(使用缓存的 Body) 81 resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes)) 82 return resp, nil 83 } 84 85 // FingerPrintHubEngine 基于 neutron 的 FingerprintHub 引擎 86 type FingerPrintHubEngine struct { 87 webTemplates []*templates.Template // Web 指纹模板 88 serviceTemplates []*templates.Template // Service 指纹模板 89 executerOptions *protocols.ExecuterOptions 90 webTemplateIndex *TemplateKeywordIndex // AC keyword index for web template prefiltering 91 92 // CaseInsensitive 控制匹配时是否忽略大小写(默认 true)。 93 // 开启时 event 中的 body/header 统一 ToLower,word matcher keywords 也在编译时 ToLower。 94 // DSL/Regex 模板中的字面量需使用小写以配合此模式。 95 CaseInsensitive bool 96 } 97 98 // NewFingerPrintHubEngine 创建新的引擎实例 99 func NewFingerPrintHubEngine(webData, serviceData []byte) (*FingerPrintHubEngine, error) { 100 engine := &FingerPrintHubEngine{ 101 CaseInsensitive: true, 102 webTemplates: make([]*templates.Template, 0), 103 serviceTemplates: make([]*templates.Template, 0), 104 executerOptions: &protocols.ExecuterOptions{ 105 Options: &protocols.Options{ 106 Timeout: 10, // 默认 10 秒超时 107 }, 108 }, 109 } 110 111 // 加载 web 指纹 112 var webTemplates []map[string]interface{} 113 if err := resources.UnmarshalData(webData, &webTemplates); err != nil { 114 return nil, fmt.Errorf("failed to unmarshal web fingerprints: %w", err) 115 } 116 117 webCount, webErrors := engine.loadTemplates(webTemplates, true) 118 119 // 加载 service 指纹 120 var serviceTemplates []map[string]interface{} 121 if err := resources.UnmarshalData(serviceData, &serviceTemplates); err != nil { 122 return nil, fmt.Errorf("failed to unmarshal service fingerprints: %w", err) 123 } 124 125 serviceCount, serviceErrors := engine.loadTemplates(serviceTemplates, false) 126 127 // 显示前几个错误 128 allErrors := append(webErrors, serviceErrors...) 129 if len(allErrors) > 0 && len(allErrors) < 10 { 130 for _, e := range allErrors { 131 logs.Log.Warn(e) 132 } 133 } 134 135 logs.Log.Infof("resources type=fingerprints source=fingerprinthub templates=%d web=%d service=%d", webCount+serviceCount, webCount, serviceCount) 136 137 engine.webTemplateIndex = NewTemplateKeywordIndex(engine.webTemplates) 138 139 return engine, nil 140 } 141 142 // loadTemplates 加载并编译模板 143 func (engine *FingerPrintHubEngine) loadTemplates(templateData []map[string]interface{}, isWeb bool) (int, []error) { 144 loadedCount := 0 145 var errors []error 146 147 for _, rawTemplate := range templateData { 148 sanitizeTemplateForTinyGo(rawTemplate) 149 150 // 将 map 转为 YAML bytes (neutron 使用 YAML unmarshaler) 151 yamlBytes, err := yaml.Marshal(rawTemplate) 152 if err != nil { 153 errors = append(errors, fmt.Errorf("failed to marshal template: %w", err)) 154 continue 155 } 156 157 // 解析模板 158 tmpl := &templates.Template{} 159 if err := yaml.Unmarshal(yamlBytes, tmpl); err != nil { 160 errors = append(errors, fmt.Errorf("failed to unmarshal template: %w", err)) 161 continue 162 } 163 164 if err := engine.compileTemplate(tmpl); err != nil { 165 errors = append(errors, fmt.Errorf("failed to compile template %s: %w", tmpl.Id, err)) 166 continue 167 } 168 169 // 修复 FingerprintHub 指纹中缺少 ReadSize 和 Input.Read 字段的问题 170 for _, netReq := range tmpl.RequestsNetwork { 171 for _, input := range netReq.Inputs { 172 if input.Read == 0 { 173 input.Read = 1024 174 } 175 } 176 if netReq.ReadSize == 0 { 177 netReq.ReadSize = 1024 178 } 179 } 180 181 // 添加到对应的列表 182 if isWeb { 183 engine.webTemplates = append(engine.webTemplates, tmpl) 184 } else { 185 engine.serviceTemplates = append(engine.serviceTemplates, tmpl) 186 } 187 loadedCount++ 188 } 189 190 return loadedCount, errors 191 } 192 193 // compileTemplate 编译模板。当 CaseInsensitive 开启时,设置 word matcher 194 // 的 CaseInsensitive 标志,使 neutron 在编译时将 keywords ToLower, 195 // 匹配时将 corpus ToLower。所有模板加载路径都必须经过此方法。 196 func (engine *FingerPrintHubEngine) compileTemplate(tmpl *templates.Template) error { 197 if engine.CaseInsensitive { 198 for _, req := range tmpl.GetRequests() { 199 for _, matcher := range req.Matchers { 200 if matcher.Type == "word" { 201 matcher.CaseInsensitive = true 202 } 203 } 204 } 205 } 206 return tmpl.Compile(engine.executerOptions) 207 } 208 209 // LoadFromJSON 从 JSON 数据加载指纹 210 func (engine *FingerPrintHubEngine) LoadFromJSON(data []byte) error { 211 // 解析 JSON 212 var templateData []map[string]interface{} 213 if err := json.Unmarshal(data, &templateData); err != nil { 214 return fmt.Errorf("failed to unmarshal JSON: %w", err) 215 } 216 217 // 转换为 YAML 并加载每个模板 218 loadedCount := 0 219 var errors []error 220 221 for _, rawTemplate := range templateData { 222 sanitizeTemplateForTinyGo(rawTemplate) 223 224 // 将 map 转为 YAML bytes (neutron 使用 YAML unmarshaler) 225 yamlBytes, err := yaml.Marshal(rawTemplate) 226 if err != nil { 227 errors = append(errors, fmt.Errorf("failed to marshal template: %w", err)) 228 continue 229 } 230 231 // 解析模板 232 tmpl := &templates.Template{} 233 err = yaml.Unmarshal(yamlBytes, tmpl) 234 if err != nil { 235 errors = append(errors, fmt.Errorf("failed to unmarshal template: %w", err)) 236 continue 237 } 238 239 if err = engine.compileTemplate(tmpl); err != nil { 240 errors = append(errors, fmt.Errorf("failed to compile template %s: %w", tmpl.Id, err)) 241 continue 242 } 243 244 // 修复 FingerprintHub 指纹中缺少 ReadSize 和 Input.Read 字段的问题 245 for _, netReq := range tmpl.RequestsNetwork { 246 for _, input := range netReq.Inputs { 247 if input.Read == 0 { 248 input.Read = 1024 249 } 250 } 251 if netReq.ReadSize == 0 { 252 netReq.ReadSize = 1024 253 } 254 } 255 256 // 根据模板类型添加到对应的列表 257 // web 指纹包含 HTTP 请求,service 指纹包含 network 请求 258 if len(tmpl.RequestsHTTP) > 0 { 259 engine.webTemplates = append(engine.webTemplates, tmpl) 260 } else if len(tmpl.RequestsNetwork) > 0 { 261 engine.serviceTemplates = append(engine.serviceTemplates, tmpl) 262 } 263 loadedCount++ 264 } 265 266 if len(errors) > 0 && len(errors) < 10 { 267 for _, e := range errors { 268 logs.Log.Warn(e) 269 } 270 } 271 272 return nil 273 } 274 275 // LoadFromFS 从文件系统加载模板(用于开发测试) 276 //func (engine *FingerPrintHubEngine) LoadFromFS(fsys fs.FS, pattern string) error { 277 // var loadedCount int 278 // var errors []error 279 // 280 // err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { 281 // if err != nil { 282 // return err 283 // } 284 // 285 // if d.IsDir() { 286 // return nil 287 // } 288 // 289 // // 只处理 .yaml 和 .yml 文件 290 // ext := filepath.Ext(path) 291 // if ext != ".yaml" && ext != ".yml" { 292 // return nil 293 // } 294 // 295 // // 检查是否匹配 pattern 296 // if pattern != "" { 297 // matched, _ := filepath.Match(pattern, filepath.Base(path)) 298 // if !matched { 299 // return nil 300 // } 301 // } 302 // 303 // // 读取文件内容 304 // content, err := fs.ReadFile(fsys, path) 305 // if err != nil { 306 // errors = append(errors, fmt.Errorf("failed to read %s: %w", path, err)) 307 // return nil // 继续处理其他文件 308 // } 309 // 310 // // 解析模板 311 // tmpl := &templates.Template{} 312 // err = yaml.Unmarshal(content, tmpl) 313 // if err != nil { 314 // errors = append(errors, fmt.Errorf("failed to unmarshal %s: %w", path, err)) 315 // return nil 316 // } 317 // 318 // // 编译模板 319 // // neutron 会自动处理 tcp/udp 字段作为 network 的别名 320 // err = tmpl.Compile(engine.executerOptions) 321 // if err != nil { 322 // errors = append(errors, fmt.Errorf("failed to compile %s: %w", path, err)) 323 // return nil 324 // } 325 // 326 // // 修复 FingerprintHub 指纹中缺少 ReadSize 和 Input.Read 字段的问题 327 // // 这个修复在编译后执行,适用于所有 network 请求(包括从 tcp/udp 转换来的) 328 // for _, netReq := range tmpl.RequestsNetwork { 329 // // 修复 input.Read 字段 330 // for _, input := range netReq.Inputs { 331 // if input.Read == 0 { 332 // input.Read = 1024 333 // } 334 // } 335 // // 修复 ReadSize 字段 336 // if netReq.ReadSize == 0 { 337 // netReq.ReadSize = 1024 338 // } 339 // } 340 // 341 // // 根据模板类型添加到对应的列表 342 // // web 指纹包含 HTTP 请求,service 指纹包含 network 请求 343 // if len(tmpl.RequestsHTTP) > 0 { 344 // engine.webTemplates = append(engine.webTemplates, tmpl) 345 // } else if len(tmpl.RequestsNetwork) > 0 { 346 // engine.serviceTemplates = append(engine.serviceTemplates, tmpl) 347 // } 348 // loadedCount++ 349 // 350 // return nil 351 // }) 352 // 353 // if err != nil { 354 // return fmt.Errorf("failed to walk filesystem: %w", err) 355 // } 356 // 357 // if len(errors) > 0 { 358 // // 记录所有错误 359 // for _, e := range errors { 360 // fmt.Printf("Warning: %v\n", e) 361 // } 362 // } 363 // 364 // fmt.Printf("Loaded %d fingerprint templates from filesystem\n", loadedCount) 365 // return nil 366 //} 367 368 // Name 返回引擎名称 369 func (engine *FingerPrintHubEngine) Name() string { 370 return "fingerprinthub" 371 } 372 373 // Len 返回指纹数量 374 func (engine *FingerPrintHubEngine) Len() int { 375 return len(engine.webTemplates) + len(engine.serviceTemplates) 376 } 377 378 // Compile 编译所有模板(已在加载时完成) 379 func (engine *FingerPrintHubEngine) Compile() error { 380 return nil 381 } 382 383 // Capability 返回引擎能力 384 func (engine *FingerPrintHubEngine) Capability() common.EngineCapability { 385 return common.EngineCapability{ 386 SupportWeb: true, // 支持 HTTP 指纹 387 SupportService: true, // 支持 Service 指纹 (通过 neutron network) 388 } 389 } 390 391 // WebMatch 实现 Web 指纹匹配 392 func (engine *FingerPrintHubEngine) WebMatch(content []byte) common.Frameworks { 393 resp := httputils.NewResponseWithRaw(content) 394 if resp == nil { 395 return make(common.Frameworks) 396 } 397 398 rawBody := httputils.ReadBody(resp) 399 bodyStr := string(rawBody) 400 401 // AC index 始终使用小写进行关键词预过滤 402 lowerBodyStr := strings.ToLower(bodyStr) 403 404 // CaseInsensitive 开启时,event 中的 body/header 使用小写, 405 // 使 word/DSL/regex 等所有 matcher 统一在小写上匹配 406 if engine.CaseInsensitive { 407 bodyStr = lowerBodyStr 408 } 409 410 event := engine.buildInternalEvent(resp, bodyStr, len(content)) 411 412 frames := make(common.Frameworks) 413 414 headerStr, _ := event["all_headers"].(string) 415 lowerHeaderStr := headerStr 416 if !engine.CaseInsensitive { 417 lowerHeaderStr = strings.ToLower(headerStr) 418 } 419 mr := engine.webTemplateIndex.Match(lowerHeaderStr, lowerBodyStr) 420 421 // Fast path: AC keyword hit directly resolves these templates. 422 // All matchers are Word type with OR condition — AC match = matched. 423 // 仅在 CaseInsensitive 模式下可用,因为 AC index 始终用小写匹配。 424 if engine.CaseInsensitive { 425 for ti := range mr.Matched { 426 frames.Add(engine.newFramework(engine.webTemplates[ti])) 427 } 428 } 429 430 // Slow path: templates needing full matchRequest (regex, AND, fallback). 431 // CaseSensitive 模式下 AC fast path 的结果也走 slow path 以确保精确匹配。 432 if !engine.CaseInsensitive { 433 for ti := range mr.Matched { 434 mr.NeedsCheck[ti] = true 435 } 436 } 437 for ti := range mr.NeedsCheck { 438 tmpl := engine.webTemplates[ti] 439 requests := tmpl.GetRequests() 440 if len(requests) == 0 { 441 continue 442 } 443 444 for _, req := range requests { 445 if req.Matchers == nil || len(req.Matchers) == 0 { 446 continue 447 } 448 449 if engine.matchRequest(req, event) { 450 frames.Add(engine.newFramework(tmpl)) 451 break 452 } 453 } 454 } 455 456 return frames 457 } 458 459 func (engine *FingerPrintHubEngine) newFramework(tmpl *templates.Template) *common.Framework { 460 name := tmpl.Info.Name 461 if name == "" { 462 name = tmpl.Id 463 } 464 frame := common.NewFramework(name, common.FrameFromFingerprintHub) 465 if tmpl.Info.Metadata != nil { 466 if vendor, ok := tmpl.Info.Metadata["vendor"].(string); ok { 467 frame.Attributes.Vendor = vendor 468 } 469 if product, ok := tmpl.Info.Metadata["product"].(string); ok { 470 frame.Attributes.Product = product 471 } 472 } 473 return frame 474 } 475 476 // buildInternalEvent 构建 neutron 的 InternalEvent 477 // 复用 neutron 的数据结构,包括 all_headers 等字段 478 func (engine *FingerPrintHubEngine) buildInternalEvent(resp *http.Response, bodyStr string, contentLength int) protocols.InternalEvent { 479 event := make(protocols.InternalEvent) 480 481 // 基础字段 482 event["body"] = bodyStr 483 event["status_code"] = resp.StatusCode 484 event["content_length"] = contentLength 485 486 // header 字段:原始 http.Header 487 event["header"] = resp.Header 488 489 // all_headers 字段:neutron 使用的拼接格式 490 // 复用这个逻辑避免在 matchSingle 中重复拼接 491 event["all_headers"] = engine.buildHeaderString(resp.Header) 492 493 // favicon 字段:提取 favicon hash 用于 favicon matcher 494 faviconData := extractFaviconFromResponse(resp, []byte(bodyStr)) 495 if len(faviconData) > 0 { 496 event["favicon"] = faviconData 497 } 498 499 return event 500 } 501 502 // buildHeaderString 构建 header 字符串。 503 // key 始终小写(HTTP 规范);value 根据 CaseInsensitive 开关决定。 504 func (engine *FingerPrintHubEngine) buildHeaderString(header http.Header) string { 505 var builder strings.Builder 506 for key, values := range header { 507 for _, value := range values { 508 builder.WriteString(strings.ToLower(key)) 509 builder.WriteString(": ") 510 if engine.CaseInsensitive { 511 builder.WriteString(strings.ToLower(value)) 512 } else { 513 builder.WriteString(value) 514 } 515 builder.WriteString("\n") 516 } 517 } 518 return builder.String() 519 } 520 521 // matchRequest 检查请求的所有 matchers 是否匹配 522 // 复用 neutron 的 Request.Match 方法 523 func (engine *FingerPrintHubEngine) matchRequest(req *http2.Request, event protocols.InternalEvent) bool { 524 if req.Matchers == nil || len(req.Matchers) == 0 { 525 return false 526 } 527 528 // 根据 MatchersCondition 决定逻辑 529 matchersCondition := req.MatchersCondition 530 if matchersCondition == "" { 531 matchersCondition = "or" // 默认为 OR 532 } 533 534 matchedCount := 0 535 for _, matcher := range req.Matchers { 536 // 直接使用 neutron 的 Request.Match 方法 537 // 这样可以复用所有的匹配逻辑,包括 getMatchPart 等 538 matched, _ := req.Match(event, matcher) 539 if matched { 540 matchedCount++ 541 if matchersCondition == "or" { 542 return true // OR 条件下,任意匹配即可 543 } 544 } else { 545 if matchersCondition == "and" { 546 return false // AND 条件下,任意不匹配即失败 547 } 548 } 549 } 550 551 // AND 条件下,需要所有 matcher 都匹配 552 if matchersCondition == "and" { 553 return matchedCount == len(req.Matchers) 554 } 555 556 return false 557 } 558 559 // HTTPActiveMatch 实现 HTTP 主动指纹匹配 560 // 使用 http.RoundTripper 进行主动探测,统一发包逻辑 561 // transport: 自定义的 HTTP 传输层,用于发送请求 562 func (engine *FingerPrintHubEngine) HTTPActiveMatch(baseURL string, level int, transport http.RoundTripper, callback func(*common.Framework, *common.Vuln)) (common.Frameworks, common.Vulns) { 563 if baseURL == "" || transport == nil { 564 return nil, nil 565 } 566 567 // 初始化结果集 map 568 allFrameworks := make(common.Frameworks) 569 allVulns := make(common.Vulns) 570 571 // 创建带缓存的 transport,通过 path 去重 572 cachedTransport := &CachedTransport{ 573 transport: transport, 574 cache: make(map[string]*CachedResponse), 575 } 576 577 // 使用带缓存的 transport 创建 HTTP client 578 httpClient := &http.Client{ 579 Transport: cachedTransport, 580 } 581 582 // 创建扫描上下文 583 // Thread the per-call client through the ScanContext so concurrent calls never 584 // mutate the shared compiled templates (httpReq.SetHTTPClient was a data race). 585 scanCtx := &protocols.ScanContext{ 586 Input: baseURL, 587 Client: httpClient, 588 } 589 590 // 遍历所有 web 模板(保留手动遍历 + CachedTransport 加速路径) 591 for _, tmpl := range engine.webTemplates { 592 if len(tmpl.RequestsHTTP) == 0 { 593 continue 594 } 595 596 for _, httpReq := range tmpl.RequestsHTTP { 597 err := httpReq.ExecuteWithResults(scanCtx, make(map[string]interface{}), make(map[string]interface{}), func(event *protocols.InternalWrappedEvent) { 598 if event.OperatorsResult != nil && event.OperatorsResult.Matched { 599 frame := engine.newFramework(tmpl) 600 allFrameworks.Add(frame) 601 if callback != nil { 602 callback(frame, nil) 603 } 604 } 605 }) 606 607 if err != nil { 608 continue 609 } 610 } 611 } 612 613 return allFrameworks, allVulns 614 } 615 616 // ServiceMatch 实现 Service 指纹匹配 617 func (engine *FingerPrintHubEngine) ServiceMatch(host string, portStr string, level int, sender common.ServiceSender, callback common.ServiceCallback) *common.ServiceResult { 618 // 构建目标地址 619 target := fmt.Sprintf("%s:%s", host, portStr) 620 621 // 创建扫描上下文 622 scanCtx := &protocols.ScanContext{ 623 Input: target, 624 } 625 626 // 遍历所有 service 模板,找到包含 network 请求的模板 627 for _, tmpl := range engine.serviceTemplates { 628 // 检查是否有 network 请求 629 if len(tmpl.RequestsNetwork) == 0 { 630 continue 631 } 632 633 // 遍历所有 network 请求 634 for _, networkReq := range tmpl.RequestsNetwork { 635 // 执行 network 请求 636 var matched bool 637 err := networkReq.ExecuteWithResults(scanCtx, make(map[string]interface{}), make(map[string]interface{}), func(event *protocols.InternalWrappedEvent) { 638 // 检查是否有匹配结果 639 // FingerprintHub service-fingerprint 使用 extractors 而不是 matchers 640 // 如果有 extractor 提取到值,说明匹配成功 641 if event.OperatorsResult != nil { 642 // 有 matchers 的情况 643 if event.OperatorsResult.Matched { 644 matched = true 645 } 646 // 有 extractors 的情况 - 提取到值说明匹配成功 647 if len(event.OperatorsResult.OutputExtracts) > 0 { 648 matched = true 649 } 650 } 651 652 if matched { 653 // 构建 Framework 654 name := tmpl.Info.Name 655 if name == "" { 656 name = tmpl.Id 657 } 658 frame := common.NewFramework(name, common.FrameFromFingerprintHub) 659 660 // 添加元数据 661 if tmpl.Info.Metadata != nil { 662 if vendor, ok := tmpl.Info.Metadata["vendor"].(string); ok { 663 frame.Attributes.Vendor = vendor 664 } 665 if product, ok := tmpl.Info.Metadata["product"].(string); ok { 666 frame.Attributes.Product = product 667 } 668 } 669 670 // 创建 ServiceResult 并通过回调返回 671 if callback != nil { 672 callback(&common.ServiceResult{ 673 Framework: frame, 674 }) 675 } 676 } 677 }) 678 679 if err != nil { 680 // 忽略错误继续尝试其他指纹 681 continue 682 } 683 684 // 如果匹配成功,可以选择提前返回 685 if matched { 686 // 这里选择继续匹配其他指纹,以便返回所有可能的匹配 687 // 如果只需要第一个匹配,可以在这里 return 688 } 689 } 690 } 691 692 return nil 693 } 694 695 // calculateFaviconHash 计算 favicon 的 MD5 和 MMH3 hash 696 // 返回 [md5, mmh3] 格式的 hash 数组 697 func calculateFaviconHash(content []byte) []string { 698 if len(content) == 0 { 699 return nil 700 } 701 702 md5Hash := encode.Md5Hash(content) 703 mmh3Hash := encode.Mmh3Hash32(content) 704 705 return []string{md5Hash, mmh3Hash} 706 } 707 708 // extractFaviconFromResponse 从 HTTP 响应中提取 favicon 数据 709 // 返回 map[url][]hash 格式的数据,用于 favicon matcher 710 func extractFaviconFromResponse(resp *http.Response, body []byte) map[string]interface{} { 711 faviconData := make(map[string]interface{}) 712 713 // 检查响应和请求是否有效 714 if resp == nil || resp.Request == nil || resp.Request.URL == nil { 715 return faviconData 716 } 717 718 // 如果响应本身是 favicon.ico 719 if strings.HasSuffix(resp.Request.URL.Path, "/favicon.ico") { 720 if isImageContent(resp, body) { 721 hashes := calculateFaviconHash(body) 722 if hashes != nil { 723 faviconData[resp.Request.URL.String()] = hashes 724 } 725 } 726 } 727 728 // TODO: 未来可以扩展支持从 HTML 中提取 <link rel="icon"> 标签 729 // 目前仅支持直接请求 favicon.ico 的场景 730 731 return faviconData 732 } 733 734 // isImageContent 判断响应内容是否为图片 735 func isImageContent(resp *http.Response, body []byte) bool { 736 // 检查 Content-Type 737 contentType := resp.Header.Get("Content-Type") 738 if strings.Contains(contentType, "image/") { 739 return true 740 } 741 742 // 简单检查:如果内容可以解析为 UTF-8 文本且包含 HTML 标签,则不是图片 743 if len(body) > 0 { 744 bodyStr := string(body) 745 htmlTags := []string{"<html", "<head", "<script", "<div", "<title", "<?xml"} 746 for _, tag := range htmlTags { 747 if strings.Contains(strings.ToLower(bodyStr), tag) { 748 return false 749 } 750 } 751 } 752 753 return true 754 }