diff --git a/common/protocol/http/sniff.go b/common/protocol/http/sniff.go index e85a07920..8efe93727 100644 --- a/common/protocol/http/sniff.go +++ b/common/protocol/http/sniff.go @@ -1,13 +1,16 @@ package http import ( + "bufio" "bytes" "context" "errors" + "io" + "net/http" "strings" + "unsafe" "github.com/xtls/xray-core/common" - "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/session" ) @@ -39,79 +42,67 @@ func (h *SniffHeader) Domain() string { } var ( - methods = [...]string{"get", "post", "head", "put", "delete", "options", "connect"} - - errNotHTTPMethod = errors.New("not an HTTP method") + validMethods = map[string]bool{} + errNotHTTP = errors.New("not an HTTP request") ) -func beginWithHTTPMethod(b []byte) error { - for _, m := range &methods { - if len(b) >= len(m) && strings.EqualFold(string(b[:len(m)]), m) { - return nil - } - - if len(b) < len(m) { - return common.ErrNoClue - } +func init() { + // https://www.iana.org/assignments/http-methods + methods := []string{ + "ACL", "BASELINE-CONTROL", "BIND", "CHECKIN", "CHECKOUT", + "CONNECT", "COPY", "DELETE", "GET", "HEAD", + "LABEL", "LINK", "LOCK", "MERGE", "MKACTIVITY", + "MKCALENDAR", "MKCOL", "MKREDIRECTREF", "MKWORKSPACE", "MOVE", + "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) { + if !isValidHTTPMethod(b) { + return nil, errNotHTTP + } content := session.ContentFromContext(c) - ShouldSniffAttr := true - // 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 { - ShouldSniffAttr = false + r, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(b))) + if err != nil { + if err == io.ErrUnexpectedEOF { + return nil, common.ErrNoClue + } + return nil, errNotHTTP } - if err := beginWithHTTPMethod(b); err != nil { - return nil, err + if r.Host == "" { + return nil, common.ErrNoClue } - sh := &SniffHeader{ 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'}) - 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 + return sh, nil } diff --git a/common/protocol/http/sniff_test.go b/common/protocol/http/sniff_test.go index 09ce7d6c1..cc4cf279d 100644 --- a/common/protocol/http/sniff_test.go +++ b/common/protocol/http/sniff_test.go @@ -14,75 +14,76 @@ func TestHTTPHeaders(t *testing.T) { err bool }{ { - input: `GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1 -Host: net.tutsplus.com -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) -Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 -Accept-Language: en-us,en;q=0.5 -Accept-Encoding: gzip,deflate -Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 -Keep-Alive: 300 -Connection: keep-alive -Cookie: PHPSESSID=r2t5uvjq435r4q7ib3vtdjq120 -Pragma: no-cache -Cache-Control: no-cache`, + input: "GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1\r\n" + + "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)\r\n" + + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + + "Accept-Language: en-us,en;q=0.5\r\n" + + "Accept-Encoding: gzip,deflate\r\n" + + "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" + + "Keep-Alive: 300\r\n" + + "Connection: keep-alive\r\n" + + "Cookie: PHPSESSID=r2t5uvjq435r4q7ib3vtdjq120\r\n" + + "Pragma: no-cache\r\n" + + "Cache-Control: no-cache\r\n" + + "\r\n", domain: "net.tutsplus.com", }, { - input: `POST /foo.php HTTP/1.1 -Host: localhost -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) -Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 -Accept-Language: en-us,en;q=0.5 -Accept-Encoding: gzip,deflate -Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 -Keep-Alive: 300 -Connection: keep-alive -Referer: http://localhost/test.php -Content-Type: application/x-www-form-urlencoded -Content-Length: 43 - -first_name=John&last_name=Doe&action=Submit`, + input: "POST /foo.php HTTP/1.1\r\n" + + "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)\r\n" + + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + + "Accept-Language: en-us,en;q=0.5\r\n" + + "Accept-Encoding: gzip,deflate\r\n" + + "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" + + "Keep-Alive: 300\r\n" + + "Connection: keep-alive\r\n" + + "Referer: http://localhost/test.php\r\n" + + "Content-Type: application/x-www-form-urlencoded\r\n" + + "Content-Length: 43\r\n" + + "\r\n" + + "first_name=John&last_name=Doe&action=Submit", domain: "localhost", }, { - input: `X /foo.php HTTP/1.1 -Host: localhost -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) -Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 -Accept-Language: en-us,en;q=0.5 -Accept-Encoding: gzip,deflate -Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 -Keep-Alive: 300 -Connection: keep-alive -Referer: http://localhost/test.php -Content-Type: application/x-www-form-urlencoded -Content-Length: 43 - -first_name=John&last_name=Doe&action=Submit`, + input: "X /foo.php HTTP/1.1\r\n" + + "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)\r\n" + + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + + "Accept-Language: en-us,en;q=0.5\r\n" + + "Accept-Encoding: gzip,deflate\r\n" + + "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" + + "Keep-Alive: 300\r\n" + + "Connection: keep-alive\r\n" + + "Referer: http://localhost/test.php\r\n" + + "Content-Type: application/x-www-form-urlencoded\r\n" + + "Content-Length: 43\r\n" + + "\r\n" + + "first_name=John&last_name=Doe&action=Submit", domain: "", err: true, }, { - input: `GET /foo.php HTTP/1.1 -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) -Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 -Accept-Language: en-us,en;q=0.5 -Accept-Encoding: gzip,deflate -Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 -Keep-Alive: 300 -Connection: keep-alive -Referer: http://localhost/test.php -Content-Type: application/x-www-form-urlencoded -Content-Length: 43 - -Host: localhost -first_name=John&last_name=Doe&action=Submit`, + 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)\r\n" + + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + + "Accept-Language: en-us,en;q=0.5\r\n" + + "Accept-Encoding: gzip,deflate\r\n" + + "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" + + "Keep-Alive: 300\r\n" + + "Connection: keep-alive\r\n" + + "Referer: http://localhost/test.php\r\n" + + "Content-Type: application/x-www-form-urlencoded\r\n" + + "Content-Length: 43\r\n" + + "\r\n" + + "Host: localhost\r\n" + + "first_name=John&last_name=Doe&action=Submit", domain: "", 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: "", err: true, }, @@ -97,6 +98,7 @@ first_name=John&last_name=Doe&action=Submit`, } else { if err != nil { t.Errorf("Expect no error but actually %s in test %v", err.Error(), test) + continue } if header.Domain() != test.domain { t.Error("expected domain ", test.domain, " but got ", header.Domain())