Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ examples/
├── fingers/ # 指纹识别工具
├── neutron/ # POC 扫描工具
├── gogo/ # 端口扫描和指纹识别工具
└── spray/ # HTTP 批量探测工具
├── spray/ # HTTP 批量探测工具
└── cases/ # 小颗粒度使用案例(cookbook)
├── match_detail/ # 获取 matcher 详情和命中资源 URL(cmd + test)
└── match_detail_helper/ # 同上,FingerMatch 封装风格(library + test)
```

## 快速开始
Expand Down Expand Up @@ -257,6 +260,40 @@ echo "http://127.0.0.1:8080" >> test_urls.txt

---

## Cases - 小颗粒度使用案例

`examples/cases/` 下放的是 cookbook 风格的最小可运行片段,每个 case 只演示一个 API 或一个用法要点,复制即可融入到自己的工程里。

### match_detail - 获取 matcher 详情和命中资源 URL

演示如何通过 SDK public API 输出 `MatchDetail`(matcher 类型/值、rule_index、send_data)以及命中的资源 URL。提供两种风格,二选一即可:

**风格 ① 直接用 SDK 原生类型(推荐)** —— `cases/match_detail/`

```bash
# 跑命令行版(被动匹配真实 target)
go run ./cases/match_detail -url http://127.0.0.1:8080 -key your_api_key -target http://127.0.0.1:3000

# 跑测试版(inline finger + httptest,离线可跑)
go test ./cases/match_detail -v
```

**风格 ② 封装一层调用方友好结构** —— `cases/match_detail_helper/`

`FingerMatch` 演示如何把 SDK 返回的 `fingers.MatchResult` 转成业务自己的 JSON DTO。`match_url`、matcher detail、fallback URL 都来自 SDK,不需要调用方复制解析逻辑。

```bash
go test ./cases/match_detail_helper -v
```

**两种风格共通的要点:**

- 推荐直接调用 `eng.MatchHTTPWithDetail(resp)`,返回 `[]fingers.MatchResult`。
- `MatchHTTPWithDetail` 会自动打开底层 `MatchDetail` 开关,调用方不需要手动调用 `GetFingersEngine().EnableMatchDetail()`。
- `match_url` 取值优先级:`MatchDetail.SendData` 中的 `url=` > 当前请求 URL(`resp.Request.URL`,已处理重定向)。

---

## 常见问题

### Q: 如何获取 Cyberhub API Key?
Expand Down
75 changes: 75 additions & 0 deletions examples/cases/match_detail/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// match_detail 演示:在 sdk/fingers 上直接拿到指纹命中的 matcher 详情 + 命中的资源 URL。
//
// 关键点:
// 1. 调用 MatchHTTPWithDetail(resp),SDK 会自动打开 MatchDetail。
// 2. 结果里直接读 MatchResult.MatchURL / MatcherType / MatcherValue。
// 3. match_url 取值优先级:MatchDetail.SendData 中的 "url=" > 当前请求 URL。
//
// 用法:
//
// go run ./examples/cases/match_detail -url http://127.0.0.1:8080 -key <api_key> -target http://example.com
package main

import (
"flag"
"fmt"
"net/http"
"os"

"github.com/chainreactors/sdk/fingers"
)

func main() {
cyberhubURL := flag.String("url", "", "Cyberhub URL (optional, local fingers used if empty)")
apiKey := flag.String("key", "", "Cyberhub API Key")
target := flag.String("target", "", "Target URL to match (required)")
flag.Parse()
if *target == "" {
flag.Usage()
os.Exit(1)
}

// 1. 构建 engine
cfg := fingers.NewConfig()
if *cyberhubURL != "" {
cfg.WithCyberhub(*cyberhubURL, *apiKey)
}
eng, err := fingers.NewEngine(cfg)
if err != nil {
fmt.Printf("engine init failed: %v\n", err)
os.Exit(1)
}

// 2. 抓 + 匹配。MatchHTTPWithDetail 会自动打开 MatchDetail。
resp, err := http.Get(*target)
if err != nil {
fmt.Printf("http get failed: %v\n", err)
os.Exit(1)
}
defer resp.Body.Close()

results, err := eng.MatchHTTPWithDetail(resp)
if err != nil {
fmt.Printf("match failed: %v\n", err)
os.Exit(1)
}

if len(results) == 0 {
fmt.Println("no fingerprints matched")
return
}
for _, r := range results {
name := ""
if r.Framework != nil {
name = r.Framework.Name
}
fmt.Printf("[%s]\n", name)
fmt.Printf(" match_url : %s\n", r.MatchURL)
fmt.Printf(" matcher_type : %s\n", r.MatcherType)
fmt.Printf(" matcher_value : %s\n", r.MatcherValue)
fmt.Printf(" rule_index : %d\n", r.RuleIndex)
if r.SendData != "" {
fmt.Printf(" send_data : %s\n", r.SendData)
}
}
}
99 changes: 99 additions & 0 deletions examples/cases/match_detail/match_detail_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Test-based 演示:直接用 go test 跑通整条链路,证明
// MatchHTTPWithDetail 会返回 match_url 和 matcher 详情。
//
// Run with:
//
// go test ./examples/cases/match_detail -v
package main

import (
"net/http"
"net/http/httptest"
"testing"

fingersEngine "github.com/chainreactors/fingers/fingers"
sdkfingers "github.com/chainreactors/sdk/fingers"
)

// TestMatchDetailUsage 演示获取 matcher 详情的最小调用流程:
//
// 1. 构造 Engine
// 2. 调 MatchHTTPWithDetail(resp)
// 3. 直接读 MatchResult.MatchURL / MatcherType / MatcherValue
func TestMatchDetailUsage(t *testing.T) {
// —— 演示用:inline finger + httptest。
// 实际工程里换成 WithCyberhub / WithLocalFile 即可。
finger := &fingersEngine.Finger{
Name: "demo-app",
Protocol: "http",
Rules: fingersEngine.Rules{
{Regexps: &fingersEngine.Regexps{Body: []string{"DemoMarker"}}},
},
}
eng, err := sdkfingers.NewEngine(
sdkfingers.NewConfig().WithFingers(fingersEngine.Fingers{finger}),
)
if err != nil {
t.Fatal(err)
}

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("response with DemoMarker"))
}))
defer srv.Close()

resp, err := http.Get(srv.URL)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()

// MatchHTTPWithDetail 会自动打开 MatchDetail,并返回扁平结果。
results, err := eng.MatchHTTPWithDetail(resp)
if err != nil {
t.Fatal(err)
}
if len(results) != 1 {
t.Fatalf("expected 1 result, got %d", len(results))
}
r := results[0]
if r.Framework == nil || r.Framework.Name != "demo-app" {
t.Fatalf("Framework not preserved: %+v", r)
}
if r.MatchURL != srv.URL {
t.Fatalf("MatchURL should fall back to request URL %q, got %q", srv.URL, r.MatchURL)
}
if r.MatcherType == "" || r.MatcherValue == "" {
t.Fatalf("expected matcher fields, got %+v", r)
}
t.Logf("[%s] match_url=%s matcher_type=%s matcher_value=%s rule_index=%d",
r.Framework.Name, r.MatchURL, r.MatcherType, r.MatcherValue, r.RuleIndex)
}

// TestPlainMatchWithoutDetail 反向对照:plain MatchHTTP 仍保持兼容,
// 不会自动填 MatchDetail。
func TestPlainMatchWithoutDetail(t *testing.T) {
finger := &fingersEngine.Finger{
Name: "demo-app-disabled",
Protocol: "http",
Rules: fingersEngine.Rules{
{Regexps: &fingersEngine.Regexps{Body: []string{"DemoMarker2"}}},
},
}
eng, _ := sdkfingers.NewEngine(
sdkfingers.NewConfig().WithFingers(fingersEngine.Fingers{finger}),
)
// 故意不调 EnableMatchDetail()

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("DemoMarker2"))
}))
defer srv.Close()
resp, _ := http.Get(srv.URL)
defer resp.Body.Close()

frames, _ := eng.MatchHTTP(resp)
if fw, ok := frames["demo-app-disabled"]; ok && fw.MatchDetail != nil {
t.Fatalf("expected MatchDetail=nil for plain MatchHTTP, got %+v", *fw.MatchDetail)
}
}
87 changes: 87 additions & 0 deletions examples/cases/match_detail_helper/helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// match_detail_helper 演示调用方如何把 SDK 的 MatchResult 转成自己的 DTO。
//
// 核心 matcher detail / match_url 逻辑已经在 sdk/fingers 公共 API 中:
//
// eng.MatchHTTPWithDetail(resp) -> []fingers.MatchResult
//
// 这一份只保留应用层字段映射,不再让调用方复制 EnableMatchDetail、
// SendData 解析、fallback URL 等 SDK 内部细节。
package main

import (
"fmt"
"net/http"

"github.com/chainreactors/fingers/common"
sdkfingers "github.com/chainreactors/sdk/fingers"
)

func main() {
fmt.Println("This example is exercised via `go test ./examples/cases/match_detail_helper`.")
}

// FingerMatch 是给上层的扁平、易序列化结构。
type FingerMatch struct {
Name string `json:"name"`
Version string `json:"version,omitempty"`
Tags []string `json:"tags,omitempty"`
Attributes *common.Attributes `json:"attributes,omitempty"`
MatchURL string `json:"match_url,omitempty"`
MatcherType string `json:"matcher_type,omitempty"`
MatcherValue string `json:"matcher_value,omitempty"`
RuleIndex int `json:"rule_index"`
SendData string `json:"send_data,omitempty"`
}

// FromMatchResults 把 SDK MatchResult 拍平成调用方自己的结构。
func FromMatchResults(results []sdkfingers.MatchResult) []FingerMatch {
out := make([]FingerMatch, 0, len(results))
for _, r := range results {
out = append(out, FromMatchResult(r))
}
return out
}

// FromMatchResult 转换单条 SDK MatchResult。
func FromMatchResult(r sdkfingers.MatchResult) FingerMatch {
fm := FingerMatch{
MatchURL: r.MatchURL,
MatcherType: r.MatcherType,
MatcherValue: r.MatcherValue,
RuleIndex: r.RuleIndex,
SendData: r.SendData,
}
if r.Framework == nil {
return fm
}
fm.Name = r.Framework.Name
fm.Tags = r.Framework.Tags
fm.Attributes = r.Framework.Attributes
if r.Framework.Attributes != nil {
fm.Version = r.Framework.Attributes.Version
}
return fm
}

// DetectFingersDetail 演示单次 HTTP 被动匹配,返回调用方自己的 FingerMatch 列表。
func DetectFingersDetail(target, cyberhubURL, apiKey string) ([]FingerMatch, error) {
cfg := sdkfingers.NewConfig()
if cyberhubURL != "" {
cfg.WithCyberhub(cyberhubURL, apiKey)
}
eng, err := sdkfingers.NewEngine(cfg)
if err != nil {
return nil, err
}
resp, err := http.Get(target)
if err != nil {
return nil, err
}
defer resp.Body.Close()

results, err := eng.MatchHTTPWithDetail(resp)
if err != nil {
return nil, err
}
return FromMatchResults(results), nil
}
Loading