mirror of
https://github.com/XTLS/Xray-core.git
synced 2025-04-29 16:58:34 +00:00
v1.0.0
This commit is contained in:
parent
47d23e9972
commit
c7f7c08ead
711 changed files with 82154 additions and 2 deletions
68
common/protocol/http/headers.go
Normal file
68
common/protocol/http/headers.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
)
|
||||
|
||||
// ParseXForwardedFor parses X-Forwarded-For header in http headers, and return the IP list in it.
|
||||
func ParseXForwardedFor(header http.Header) []net.Address {
|
||||
xff := header.Get("X-Forwarded-For")
|
||||
if xff == "" {
|
||||
return nil
|
||||
}
|
||||
list := strings.Split(xff, ",")
|
||||
addrs := make([]net.Address, 0, len(list))
|
||||
for _, proxy := range list {
|
||||
addrs = append(addrs, net.ParseAddress(proxy))
|
||||
}
|
||||
return addrs
|
||||
}
|
||||
|
||||
// RemoveHopByHopHeaders remove hop by hop headers in http header list.
|
||||
func RemoveHopByHopHeaders(header http.Header) {
|
||||
// Strip hop-by-hop header based on RFC:
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
|
||||
// https://www.mnot.net/blog/2011/07/11/what_proxies_must_do
|
||||
|
||||
header.Del("Proxy-Connection")
|
||||
header.Del("Proxy-Authenticate")
|
||||
header.Del("Proxy-Authorization")
|
||||
header.Del("TE")
|
||||
header.Del("Trailers")
|
||||
header.Del("Transfer-Encoding")
|
||||
header.Del("Upgrade")
|
||||
|
||||
connections := header.Get("Connection")
|
||||
header.Del("Connection")
|
||||
if connections == "" {
|
||||
return
|
||||
}
|
||||
for _, h := range strings.Split(connections, ",") {
|
||||
header.Del(strings.TrimSpace(h))
|
||||
}
|
||||
}
|
||||
|
||||
// ParseHost splits host and port from a raw string. Default port is used when raw string doesn't contain port.
|
||||
func ParseHost(rawHost string, defaultPort net.Port) (net.Destination, error) {
|
||||
port := defaultPort
|
||||
host, rawPort, err := net.SplitHostPort(rawHost)
|
||||
if err != nil {
|
||||
if addrError, ok := err.(*net.AddrError); ok && strings.Contains(addrError.Err, "missing port") {
|
||||
host = rawHost
|
||||
} else {
|
||||
return net.Destination{}, err
|
||||
}
|
||||
} else if len(rawPort) > 0 {
|
||||
intPort, err := strconv.Atoi(rawPort)
|
||||
if err != nil {
|
||||
return net.Destination{}, err
|
||||
}
|
||||
port = net.Port(intPort)
|
||||
}
|
||||
|
||||
return net.TCPDestination(net.ParseAddress(host), port), nil
|
||||
}
|
118
common/protocol/http/headers_test.go
Normal file
118
common/protocol/http/headers_test.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package http_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
. "github.com/xtls/xray-core/v1/common/protocol/http"
|
||||
)
|
||||
|
||||
func TestParseXForwardedFor(t *testing.T) {
|
||||
header := http.Header{}
|
||||
header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103")
|
||||
addrs := ParseXForwardedFor(header)
|
||||
if r := cmp.Diff(addrs, []net.Address{net.ParseAddress("129.78.138.66"), net.ParseAddress("129.78.64.103")}); r != "" {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHopByHopHeadersRemoving(t *testing.T) {
|
||||
rawRequest := `GET /pkg/net/http/ HTTP/1.1
|
||||
Host: golang.org
|
||||
Connection: keep-alive,Foo, Bar
|
||||
Foo: foo
|
||||
Bar: bar
|
||||
Proxy-Connection: keep-alive
|
||||
Proxy-Authenticate: abc
|
||||
Accept-Encoding: gzip
|
||||
Accept-Charset: ISO-8859-1,UTF-8;q=0.7,*;q=0.7
|
||||
Cache-Control: no-cache
|
||||
Accept-Language: de,en;q=0.7,en-us;q=0.3
|
||||
|
||||
`
|
||||
b := bufio.NewReader(strings.NewReader(rawRequest))
|
||||
req, err := http.ReadRequest(b)
|
||||
common.Must(err)
|
||||
headers := []struct {
|
||||
Key string
|
||||
Value string
|
||||
}{
|
||||
{
|
||||
Key: "Foo",
|
||||
Value: "foo",
|
||||
},
|
||||
{
|
||||
Key: "Bar",
|
||||
Value: "bar",
|
||||
},
|
||||
{
|
||||
Key: "Connection",
|
||||
Value: "keep-alive,Foo, Bar",
|
||||
},
|
||||
{
|
||||
Key: "Proxy-Connection",
|
||||
Value: "keep-alive",
|
||||
},
|
||||
{
|
||||
Key: "Proxy-Authenticate",
|
||||
Value: "abc",
|
||||
},
|
||||
}
|
||||
for _, header := range headers {
|
||||
if v := req.Header.Get(header.Key); v != header.Value {
|
||||
t.Error("header ", header.Key, " = ", v, " want ", header.Value)
|
||||
}
|
||||
}
|
||||
|
||||
RemoveHopByHopHeaders(req.Header)
|
||||
|
||||
for _, header := range []string{"Connection", "Foo", "Bar", "Proxy-Connection", "Proxy-Authenticate"} {
|
||||
if v := req.Header.Get(header); v != "" {
|
||||
t.Error("header ", header, " = ", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHost(t *testing.T) {
|
||||
testCases := []struct {
|
||||
RawHost string
|
||||
DefaultPort net.Port
|
||||
Destination net.Destination
|
||||
Error bool
|
||||
}{
|
||||
{
|
||||
RawHost: "example.com:80",
|
||||
DefaultPort: 443,
|
||||
Destination: net.TCPDestination(net.DomainAddress("example.com"), 80),
|
||||
},
|
||||
{
|
||||
RawHost: "tls.example.com",
|
||||
DefaultPort: 443,
|
||||
Destination: net.TCPDestination(net.DomainAddress("tls.example.com"), 443),
|
||||
},
|
||||
{
|
||||
RawHost: "[2401:1bc0:51f0:ec08::1]:80",
|
||||
DefaultPort: 443,
|
||||
Destination: net.TCPDestination(net.ParseAddress("[2401:1bc0:51f0:ec08::1]"), 80),
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
dest, err := ParseHost(testCase.RawHost, testCase.DefaultPort)
|
||||
if testCase.Error {
|
||||
if err == nil {
|
||||
t.Error("for test case: ", testCase.RawHost, " expected error, but actually nil")
|
||||
}
|
||||
} else {
|
||||
if dest != testCase.Destination {
|
||||
t.Error("for test case: ", testCase.RawHost, " expected host: ", testCase.Destination.String(), " but got ", dest.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
94
common/protocol/http/sniff.go
Normal file
94
common/protocol/http/sniff.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
)
|
||||
|
||||
type version byte
|
||||
|
||||
const (
|
||||
HTTP1 version = iota
|
||||
HTTP2
|
||||
)
|
||||
|
||||
type SniffHeader struct {
|
||||
version version
|
||||
host string
|
||||
}
|
||||
|
||||
func (h *SniffHeader) Protocol() string {
|
||||
switch h.version {
|
||||
case HTTP1:
|
||||
return "http1"
|
||||
case HTTP2:
|
||||
return "http2"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SniffHeader) Domain() string {
|
||||
return h.host
|
||||
}
|
||||
|
||||
var (
|
||||
methods = [...]string{"get", "post", "head", "put", "delete", "options", "connect"}
|
||||
|
||||
errNotHTTPMethod = errors.New("not an HTTP method")
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return errNotHTTPMethod
|
||||
}
|
||||
|
||||
func SniffHTTP(b []byte) (*SniffHeader, error) {
|
||||
if err := beginWithHTTPMethod(b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sh := &SniffHeader{
|
||||
version: HTTP1,
|
||||
}
|
||||
|
||||
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]))
|
||||
if key == "host" {
|
||||
rawHost := strings.ToLower(string(bytes.TrimSpace(parts[1])))
|
||||
dest, err := ParseHost(rawHost, net.Port(80))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sh.host = dest.Address.String()
|
||||
}
|
||||
}
|
||||
|
||||
if len(sh.host) > 0 {
|
||||
return sh, nil
|
||||
}
|
||||
|
||||
return nil, common.ErrNoClue
|
||||
}
|
105
common/protocol/http/sniff_test.go
Normal file
105
common/protocol/http/sniff_test.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package http_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/xtls/xray-core/v1/common/protocol/http"
|
||||
)
|
||||
|
||||
func TestHTTPHeaders(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
domain string
|
||||
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`,
|
||||
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`,
|
||||
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`,
|
||||
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`,
|
||||
domain: "",
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
input: `GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1`,
|
||||
domain: "",
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
header, err := SniffHTTP([]byte(test.input))
|
||||
if test.err {
|
||||
if err == nil {
|
||||
t.Errorf("Expect error but nil, in test: %v", test)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Expect no error but actually %s in test %v", err.Error(), test)
|
||||
}
|
||||
if header.Domain() != test.domain {
|
||||
t.Error("expected domain ", test.domain, " but got ", header.Domain())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue