feat(httpprobe): go-hua/http 迁入并更名 httpprobe(HTTP/HTTPS 协议探测)
- 原包名 http 与标准库 net/http 严重混淆, 实为按首部字节探测协议, 更名 httpprobe; spmux 将基于此做单端口分流, 附测试、例程与 README
This commit is contained in:
@@ -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(...)`。
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user