Use std parser instead of homemade

This commit is contained in:
Fangliding
2026-09-02 19:17:32 +08:00
parent 5e245b082e
commit f8cdf7d238
2 changed files with 110 additions and 117 deletions
+53 -62
View File
@@ -1,13 +1,16 @@
package http package http
import ( import (
"bufio"
"bytes" "bytes"
"context" "context"
"errors" "errors"
"io"
"net/http"
"strings" "strings"
"unsafe"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
) )
@@ -39,79 +42,67 @@ func (h *SniffHeader) Domain() string {
} }
var ( var (
methods = [...]string{"get", "post", "head", "put", "delete", "options", "connect"} validMethods = map[string]bool{}
errNotHTTP = errors.New("not an HTTP request")
errNotHTTPMethod = errors.New("not an HTTP method")
) )
func beginWithHTTPMethod(b []byte) error { func init() {
for _, m := range &methods { // https://www.iana.org/assignments/http-methods
if len(b) >= len(m) && strings.EqualFold(string(b[:len(m)]), m) { methods := []string{
return nil "ACL", "BASELINE-CONTROL", "BIND", "CHECKIN", "CHECKOUT",
} "CONNECT", "COPY", "DELETE", "GET", "HEAD",
"LABEL", "LINK", "LOCK", "MERGE", "MKACTIVITY",
if len(b) < len(m) { "MKCALENDAR", "MKCOL", "MKREDIRECTREF", "MKWORKSPACE", "MOVE",
return common.ErrNoClue "OPTIONS", "ORDERPATCH", "PATCH", "POST", "PRI",
} "PROPFIND", "PROPPATCH", "PUT", "QUERY", "REBIND",
"REPORT", "SEARCH", "TRACE", "UNBIND", "UNCHECKOUT",
"UNLINK", "UNLOCK", "UPDATE", "UPDATEREDIRECTREF", "VERSION-CONTROL",
} }
for _, m := range methods {
validMethods[m] = true
}
}
return errNotHTTPMethod func isValidHTTPMethod(b []byte) bool {
if len(b) == 0 {
return false
}
idx := bytes.IndexByte(b, ' ')
if idx == -1 {
return false
}
method := unsafe.String(unsafe.SliceData(b), idx)
return validMethods[method]
} }
func SniffHTTP(b []byte, c context.Context) (*SniffHeader, error) { func SniffHTTP(b []byte, c context.Context) (*SniffHeader, error) {
if !isValidHTTPMethod(b) {
return nil, errNotHTTP
}
content := session.ContentFromContext(c) content := session.ContentFromContext(c)
ShouldSniffAttr := true r, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(b)))
// If content.Attributes have information, that means it comes from HTTP inbound PlainHTTP mode. if err != nil {
// It will set attributes, so skip it. if err == io.ErrUnexpectedEOF {
if content == nil || len(content.Attributes) != 0 { return nil, common.ErrNoClue
ShouldSniffAttr = false }
return nil, errNotHTTP
} }
if err := beginWithHTTPMethod(b); err != nil { if r.Host == "" {
return nil, err return nil, common.ErrNoClue
} }
sh := &SniffHeader{ sh := &SniffHeader{
version: HTTP1, version: HTTP1,
host: r.Host,
}
// If content.Attributes have information, that means it comes from HTTP inbound PlainHTTP mode.
// It will set attributes, so skip it.
if content != nil && len(content.Attributes) == 0 {
for key, h := range r.Header {
content.Attributes[key] = strings.Join(h, ",")
}
content.Attributes[":method"] = r.Method
content.Attributes[":path"] = r.URL.Path
} }
headers := bytes.Split(b, []byte{'\n'}) return sh, nil
for i := 1; i < len(headers); i++ {
header := headers[i]
if len(header) == 0 {
break
}
parts := bytes.SplitN(header, []byte{':'}, 2)
if len(parts) != 2 {
continue
}
key := strings.ToLower(string(parts[0]))
value := string(bytes.TrimSpace(parts[1]))
if ShouldSniffAttr {
content.SetAttribute(key, value) // Put header in attribute
}
if key == "host" {
rawHost := strings.ToLower(value)
dest, err := ParseHost(rawHost, net.Port(80))
if err != nil {
return nil, err
}
sh.host = dest.Address.String()
}
}
// Parse request line
// Request line is like this
// "GET /homo/114514 HTTP/1.1"
if len(headers) > 0 && ShouldSniffAttr {
RequestLineParts := bytes.Split(headers[0], []byte{' '})
if len(RequestLineParts) == 3 {
content.SetAttribute(":method", string(RequestLineParts[0]))
content.SetAttribute(":path", string(RequestLineParts[1]))
}
}
if len(sh.host) > 0 {
return sh, nil
}
return nil, common.ErrNoClue
} }
+57 -55
View File
@@ -14,75 +14,76 @@ func TestHTTPHeaders(t *testing.T) {
err bool err bool
}{ }{
{ {
input: `GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1 input: "GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1\r\n" +
Host: net.tutsplus.com "Host: net.tutsplus.com\r\n" +
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729) "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729)\r\n" +
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
Accept-Language: en-us,en;q=0.5 "Accept-Language: en-us,en;q=0.5\r\n" +
Accept-Encoding: gzip,deflate "Accept-Encoding: gzip,deflate\r\n" +
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
Keep-Alive: 300 "Keep-Alive: 300\r\n" +
Connection: keep-alive "Connection: keep-alive\r\n" +
Cookie: PHPSESSID=r2t5uvjq435r4q7ib3vtdjq120 "Cookie: PHPSESSID=r2t5uvjq435r4q7ib3vtdjq120\r\n" +
Pragma: no-cache "Pragma: no-cache\r\n" +
Cache-Control: no-cache`, "Cache-Control: no-cache\r\n" +
"\r\n",
domain: "net.tutsplus.com", domain: "net.tutsplus.com",
}, },
{ {
input: `POST /foo.php HTTP/1.1 input: "POST /foo.php HTTP/1.1\r\n" +
Host: localhost "Host: localhost\r\n" +
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729) "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729)\r\n" +
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
Accept-Language: en-us,en;q=0.5 "Accept-Language: en-us,en;q=0.5\r\n" +
Accept-Encoding: gzip,deflate "Accept-Encoding: gzip,deflate\r\n" +
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
Keep-Alive: 300 "Keep-Alive: 300\r\n" +
Connection: keep-alive "Connection: keep-alive\r\n" +
Referer: http://localhost/test.php "Referer: http://localhost/test.php\r\n" +
Content-Type: application/x-www-form-urlencoded "Content-Type: application/x-www-form-urlencoded\r\n" +
Content-Length: 43 "Content-Length: 43\r\n" +
"\r\n" +
first_name=John&last_name=Doe&action=Submit`, "first_name=John&last_name=Doe&action=Submit",
domain: "localhost", domain: "localhost",
}, },
{ {
input: `X /foo.php HTTP/1.1 input: "X /foo.php HTTP/1.1\r\n" +
Host: localhost "Host: localhost\r\n" +
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729) "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729)\r\n" +
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
Accept-Language: en-us,en;q=0.5 "Accept-Language: en-us,en;q=0.5\r\n" +
Accept-Encoding: gzip,deflate "Accept-Encoding: gzip,deflate\r\n" +
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
Keep-Alive: 300 "Keep-Alive: 300\r\n" +
Connection: keep-alive "Connection: keep-alive\r\n" +
Referer: http://localhost/test.php "Referer: http://localhost/test.php\r\n" +
Content-Type: application/x-www-form-urlencoded "Content-Type: application/x-www-form-urlencoded\r\n" +
Content-Length: 43 "Content-Length: 43\r\n" +
"\r\n" +
first_name=John&last_name=Doe&action=Submit`, "first_name=John&last_name=Doe&action=Submit",
domain: "", domain: "",
err: true, err: true,
}, },
{ {
input: `GET /foo.php HTTP/1.1 input: "GET /foo.php HTTP/1.1\r\n" +
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729) "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729)\r\n" +
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
Accept-Language: en-us,en;q=0.5 "Accept-Language: en-us,en;q=0.5\r\n" +
Accept-Encoding: gzip,deflate "Accept-Encoding: gzip,deflate\r\n" +
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
Keep-Alive: 300 "Keep-Alive: 300\r\n" +
Connection: keep-alive "Connection: keep-alive\r\n" +
Referer: http://localhost/test.php "Referer: http://localhost/test.php\r\n" +
Content-Type: application/x-www-form-urlencoded "Content-Type: application/x-www-form-urlencoded\r\n" +
Content-Length: 43 "Content-Length: 43\r\n" +
"\r\n" +
Host: localhost "Host: localhost\r\n" +
first_name=John&last_name=Doe&action=Submit`, "first_name=John&last_name=Doe&action=Submit",
domain: "", domain: "",
err: true, err: true,
}, },
{ {
input: `GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1`, input: "GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1\r\n",
domain: "", domain: "",
err: true, err: true,
}, },
@@ -97,6 +98,7 @@ first_name=John&last_name=Doe&action=Submit`,
} else { } else {
if err != nil { if err != nil {
t.Errorf("Expect no error but actually %s in test %v", err.Error(), test) t.Errorf("Expect no error but actually %s in test %v", err.Error(), test)
continue
} }
if header.Domain() != test.domain { if header.Domain() != test.domain {
t.Error("expected domain ", test.domain, " but got ", header.Domain()) t.Error("expected domain ", test.domain, " but got ", header.Domain())