From caa65209acaceb5bc3563868d2fe01243cec54ff Mon Sep 17 00:00:00 2001 From: 4566704 <4566704@qq.com> Date: Sun, 20 Sep 2026 12:25:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(httpprobe):=20go-hua/http=20=E8=BF=81?= =?UTF-8?q?=E5=85=A5=E5=B9=B6=E6=9B=B4=E5=90=8D=20httpprobe(HTTP/HTTPS=20?= =?UTF-8?q?=E5=8D=8F=E8=AE=AE=E6=8E=A2=E6=B5=8B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 原包名 http 与标准库 net/http 严重混淆, 实为按首部字节探测协议, 更名 httpprobe; spmux 将基于此做单端口分流, 附测试、例程与 README --- examples/httpprobe/main.go | 26 +++++++++++++ httpprobe/README.md | 27 ++++++++++++++ httpprobe/httpprobe.go | 58 +++++++++++++++++++++++++++++ httpprobe/httpprobe_test.go | 74 +++++++++++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+) create mode 100644 examples/httpprobe/main.go create mode 100644 httpprobe/README.md create mode 100644 httpprobe/httpprobe.go create mode 100644 httpprobe/httpprobe_test.go diff --git a/examples/httpprobe/main.go b/examples/httpprobe/main.go new file mode 100644 index 0000000..94ed476 --- /dev/null +++ b/examples/httpprobe/main.go @@ -0,0 +1,26 @@ +// httpprobe 包示例:按报文首部字节识别流量是 HTTP 还是 HTTPS(常用于网关/代理分流) +package main + +import ( + "fmt" + + "git.zeroonesoft.cn/golib/zogo/httpprobe" +) + +func main() { + // HTTP 请求报文(以请求方法开头) + httpData := []byte("GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n") + fmt.Println("IsHttp(HTTP报文):", httpprobe.IsHttp(httpData)) + + // TLS 握手报文(首字节 0x16 0x03) + httpsData := make([]byte, 32) + httpsData[0] = 0x16 + httpsData[1] = 0x03 + httpsData[3] = 0x01 + copy(httpsData[8:], "http") // ClientHello 的 SNI 中会出现 host 字符 + fmt.Println("IsHttps(TLS报文):", httpprobe.IsHttps(httpsData)) + + // 普通数据两者都不是 + fmt.Println("IsHttp(普通数据):", httpprobe.IsHttp([]byte("hello world"))) + fmt.Println("IsHttps(普通数据):", httpprobe.IsHttps([]byte("hello world"))) +} diff --git a/httpprobe/README.md b/httpprobe/README.md new file mode 100644 index 0000000..e85be25 --- /dev/null +++ b/httpprobe/README.md @@ -0,0 +1,27 @@ +# httpprobe + +按报文首部字节识别流量是 HTTP 还是 HTTPS(TLS ClientHello),常用于网关/代理 +在同一端口上做协议分流(本仓库 `spmux` 即基于此实现)。 + +> 迁移自 go-hua/http 并更名:原包名 `http` 与标准库 `net/http` 严重混淆, +> 实际功能是"协议探测"而非 HTTP 客户端,故更名 `httpprobe`。 + +## 用法 + +```go +import "git.zeroonesoft.cn/golib/zogo/httpprobe" + +httpprobe.IsHttp([]byte("GET /index.html HTTP/1.1\r\nHost: a.com\r\n\r\n")) // true +httpprobe.IsHttp([]byte("hello world")) // false + +// TLS ClientHello(首字节 0x16 0x03) +httpprobe.IsHttps(clientHello) // true +``` + +完整可运行例程:[examples/httpprobe/main.go](../examples/httpprobe/main.go) + +## 注意 + +- 识别基于首部字节的启发式规则:HTTP 按合法请求方法前缀判断,HTTPS 按 + TLS record 头(0x16 0x03)判断,均为工程实践口径而非完整协议解析。 +- 迁移自 go-hua 的消费方:`http.IsHttp(...)` → `httpprobe.IsHttp(...)`。 diff --git a/httpprobe/httpprobe.go b/httpprobe/httpprobe.go new file mode 100644 index 0000000..2e156db --- /dev/null +++ b/httpprobe/httpprobe.go @@ -0,0 +1,58 @@ +// Package httpprobe 按报文首部字节识别流量是 HTTP 还是 HTTPS,常用于网关/代理协议分流。 +package httpprobe + +import ( + "bytes" + "fmt" + "strings" +) + +// IsHttps 判断报文是否为 TLS 握手(首字节 0x16 0x03 且包含 host 特征) +func IsHttps(data []byte) bool { + if len(data) == 0 { + return false + } else if data[0] != 0x16 { + return false + } else if data[1] != 0x3 { + return false + } else if data[3] != 0x1 && data[3] != 0x2 && data[3] != 0x3 && data[3] != 0x4 { + return false + } else if !bytes.ContainsAny(data, "http") { + return false + } + return true +} + +// IsHttp 判断报文是否为 HTTP 请求(按 GET/POST/PUT 等方法前缀识别) +func IsHttp(data []byte) bool { + fmt.Print(string(data)) + if len(data) < 8 { + fmt.Println("<8") + return false + } else if strings.EqualFold(string(data[:3]), "GET") { + fmt.Println("GET") + return true + } else if strings.EqualFold(string(data[:4]), "HEAD") { + fmt.Println("HEAD") + return true + } else if strings.EqualFold(string(data[:4]), "POST") { + fmt.Println("POST") + return true + } else if strings.EqualFold(string(data[:3]), "PUT") { + fmt.Println("PUT") + return true + } else if strings.EqualFold(string(data[:6]), "DELETE") { + fmt.Println("DELETE") + return true + } else if strings.EqualFold(string(data[:7]), "CONNECT") { + fmt.Println("CONNECT") + return true + } else if strings.EqualFold(string(data[:7]), "OPTIONS") { + fmt.Println("OPTIONS") + return true + } else if strings.EqualFold(string(data[:5]), "TRACE") { + fmt.Println("TRACE") + return true + } + return false +} diff --git a/httpprobe/httpprobe_test.go b/httpprobe/httpprobe_test.go new file mode 100644 index 0000000..d01b58d --- /dev/null +++ b/httpprobe/httpprobe_test.go @@ -0,0 +1,74 @@ +package httpprobe + +import "testing" + +func TestIsHttpMethods(t *testing.T) { + cases := map[string][]byte{ + "GET": []byte("GET /index.html HTTP/1.1\r\nHost: a.com\r\n\r\n"), + "POST": []byte("POST /api HTTP/1.1\r\nHost: a.com\r\n\r\n{}"), + "PUT": []byte("PUT /api HTTP/1.1\r\nHost: a.com\r\n\r\n"), + "HEAD": []byte("HEAD / HTTP/1.1\r\nHost: a.com\r\n\r\n"), + "DELETE": []byte("DELETE /api HTTP/1.1\r\nHost: a.com\r\n\r\n"), + "CONNECT": []byte("CONNECT a.com:443 HTTP/1.1\r\n\r\n"), + "OPTIONS": []byte("OPTIONS / HTTP/1.1\r\nHost: a.com\r\n\r\n"), + "TRACE": []byte("TRACE / HTTP/1.1\r\nHost: a.com\r\n\r\n"), + } + for method, data := range cases { + if !IsHttp(data) { + t.Errorf("IsHttp(%s...) 应为 true", method) + } + } +} + +func TestIsHttpNegative(t *testing.T) { + if IsHttp([]byte("hello world! This is plain data")) { + t.Error("普通文本不应识别为 HTTP") + } + if IsHttp(nil) { + t.Error("空数据不应识别为 HTTP") + } + if IsHttp([]byte("GET")) { + t.Error("短于 8 字节不应识别为 HTTP") + } + // TLS 握手不是 HTTP + tls := []byte{0x16, 0x03, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05} + if IsHttp(tls) { + t.Error("TLS 握手不应识别为 HTTP") + } +} + +func TestIsHttpCaseInsensitive(t *testing.T) { + if !IsHttp([]byte("get / HTTP/1.1")) { + t.Error("IsHttp 应大小写不敏感") + } + if !IsHttp([]byte("trace / HTTP/1.1")) { + t.Error("小写 trace 应识别为 HTTP") + } +} + +func TestIsHttps(t *testing.T) { + data := make([]byte, 64) + data[0] = 0x16 + data[1] = 0x03 + data[3] = 0x01 + copy(data[40:], "http") + if !IsHttps(data) { + t.Error("TLS ClientHello 应识别为 HTTPS") + } +} + +func TestIsHttpsNegative(t *testing.T) { + if IsHttps([]byte("hello world")) { + t.Error("普通文本不应识别为 HTTPS") + } + if IsHttps(nil) { + t.Error("空数据不应识别为 HTTPS") + } + // 非 TLS 握手类型(第一字节不是 0x16) + wrong := make([]byte, 16) + wrong[0] = 0x17 + wrong[1] = 0x03 + if IsHttps(wrong) { + t.Error("非握手记录不应识别为 HTTPS") + } +}