mirror of
				https://github.com/optim-enterprises-bv/Xray-core.git
				synced 2025-10-31 02:27:53 +00:00 
			
		
		
		
	Add SplitHTTP Browser Dialer support (#3484)
This commit is contained in:
		
							
								
								
									
										121
									
								
								transport/internet/browser_dialer/dialer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								transport/internet/browser_dialer/dialer.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| package browser_dialer | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	_ "embed" | ||||
| 	"encoding/base64" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gorilla/websocket" | ||||
| 	"github.com/xtls/xray-core/common/errors" | ||||
| 	"github.com/xtls/xray-core/common/platform" | ||||
| 	"github.com/xtls/xray-core/common/uuid" | ||||
| ) | ||||
|  | ||||
| //go:embed dialer.html | ||||
| var webpage []byte | ||||
|  | ||||
| var conns chan *websocket.Conn | ||||
|  | ||||
| var upgrader = &websocket.Upgrader{ | ||||
| 	ReadBufferSize:   0, | ||||
| 	WriteBufferSize:  0, | ||||
| 	HandshakeTimeout: time.Second * 4, | ||||
| 	CheckOrigin: func(r *http.Request) bool { | ||||
| 		return true | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	addr := platform.NewEnvFlag(platform.BrowserDialerAddress).GetValue(func() string { return "" }) | ||||
| 	if addr != "" { | ||||
| 		token := uuid.New() | ||||
| 		csrfToken := token.String() | ||||
| 		webpage = bytes.ReplaceAll(webpage, []byte("csrfToken"), []byte(csrfToken)) | ||||
| 		conns = make(chan *websocket.Conn, 256) | ||||
| 		go http.ListenAndServe(addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 			if r.URL.Path == "/websocket" { | ||||
| 				if r.URL.Query().Get("token") == csrfToken { | ||||
| 					if conn, err := upgrader.Upgrade(w, r, nil); err == nil { | ||||
| 						conns <- conn | ||||
| 					} else { | ||||
| 						errors.LogError(context.Background(), "Browser dialer http upgrade unexpected error") | ||||
| 					} | ||||
| 				} | ||||
| 			} else { | ||||
| 				w.Write(webpage) | ||||
| 			} | ||||
| 		})) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func HasBrowserDialer() bool { | ||||
| 	return conns != nil | ||||
| } | ||||
|  | ||||
| func DialWS(uri string, ed []byte) (*websocket.Conn, error) { | ||||
| 	data := []byte("WS " + uri) | ||||
| 	if ed != nil { | ||||
| 		data = append(data, " "+base64.RawURLEncoding.EncodeToString(ed)...) | ||||
| 	} | ||||
|  | ||||
| 	return dialRaw(data) | ||||
| } | ||||
|  | ||||
| func DialGet(uri string) (*websocket.Conn, error) { | ||||
| 	data := []byte("GET " + uri) | ||||
| 	return dialRaw(data) | ||||
| } | ||||
|  | ||||
| func DialPost(uri string, payload []byte) error { | ||||
| 	data := []byte("POST " + uri) | ||||
| 	conn, err := dialRaw(data) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	err = conn.WriteMessage(websocket.BinaryMessage, payload) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	err = CheckOK(conn) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	conn.Close() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func dialRaw(data []byte) (*websocket.Conn, error) { | ||||
| 	var conn *websocket.Conn | ||||
| 	for { | ||||
| 		conn = <-conns | ||||
| 		if conn.WriteMessage(websocket.TextMessage, data) != nil { | ||||
| 			conn.Close() | ||||
| 		} else { | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 	err := CheckOK(conn) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return conn, nil | ||||
| } | ||||
|  | ||||
| func CheckOK(conn *websocket.Conn) error { | ||||
| 	if _, p, err := conn.ReadMessage(); err != nil { | ||||
| 		conn.Close() | ||||
| 		return err | ||||
| 	} else if s := string(p); s != "ok" { | ||||
| 		conn.Close() | ||||
| 		return errors.New(s) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										136
									
								
								transport/internet/browser_dialer/dialer.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								transport/internet/browser_dialer/dialer.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,136 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
| 	<title>Browser Dialer</title> | ||||
| </head> | ||||
| <body> | ||||
| 	<script> | ||||
| 		// Copyright (c) 2021 XRAY. Mozilla Public License 2.0. | ||||
| 		var url = "ws://" + window.location.host + "/websocket?token=csrfToken"; | ||||
| 		var clientIdleCount = 0; | ||||
| 		var upstreamGetCount = 0; | ||||
| 		var upstreamWsCount = 0; | ||||
| 		var upstreamPostCount = 0; | ||||
| 		setInterval(check, 1000); | ||||
| 		function check() { | ||||
| 			if (clientIdleCount > 0) { | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			clientIdleCount += 1; | ||||
| 			console.log("Prepare", url); | ||||
| 			var ws = new WebSocket(url); | ||||
| 			// arraybuffer is significantly faster in chrome than default | ||||
| 			// blob, tested with chrome 123 | ||||
| 			ws.binaryType = "arraybuffer"; | ||||
| 			ws.onmessage = function (event) { | ||||
| 				clientIdleCount -= 1; | ||||
| 				let [method, url, protocol] = event.data.split(" "); | ||||
| 				if (method == "WS") { | ||||
| 					upstreamWsCount += 1; | ||||
| 					console.log("Dial WS", url, protocol); | ||||
| 					const wss = new WebSocket(url, protocol); | ||||
| 					wss.binaryType = "arraybuffer"; | ||||
| 					var opened = false; | ||||
| 					ws.onmessage = function (event) { | ||||
| 						wss.send(event.data) | ||||
| 					} | ||||
| 					wss.onopen = function (event) { | ||||
| 						opened = true; | ||||
| 						ws.send("ok") | ||||
| 					} | ||||
| 					wss.onmessage = function (event) { | ||||
| 						ws.send(event.data) | ||||
| 					} | ||||
| 					wss.onclose = function (event) { | ||||
| 						upstreamWsCount -= 1; | ||||
| 						console.log("Dial WS DONE, remaining: ", upstreamWsCount); | ||||
| 						ws.close() | ||||
| 					} | ||||
| 					wss.onerror = function (event) { | ||||
| 						!opened && ws.send("fail") | ||||
| 						wss.close() | ||||
| 					} | ||||
| 					ws.onclose = function (event) { | ||||
| 						wss.close() | ||||
| 					} | ||||
| 				} else if (method == "GET") { | ||||
| 					(async () => { | ||||
| 						console.log("Dial GET", url); | ||||
| 						ws.send("ok"); | ||||
| 						const controller = new AbortController(); | ||||
|  | ||||
| 						/* | ||||
| 						Aborting a streaming response in JavaScript | ||||
| 						requires two levers to be pulled: | ||||
|  | ||||
| 						First, the streaming read itself has to be cancelled using | ||||
| 						reader.cancel(), only then controller.abort() will actually work. | ||||
|  | ||||
| 						If controller.abort() alone is called while a | ||||
| 						reader.read() is ongoing, it will block until the server closes the | ||||
| 						response, the page is refreshed or the network connection is lost. | ||||
| 						*/ | ||||
|  | ||||
| 						let reader = null; | ||||
| 						ws.onclose = (event) => { | ||||
| 							try { | ||||
| 								reader && reader.cancel(); | ||||
| 							} catch(e) {} | ||||
|  | ||||
| 							try { | ||||
| 								controller.abort(); | ||||
| 							} catch(e) {} | ||||
| 						} | ||||
|  | ||||
| 						try { | ||||
| 							upstreamGetCount += 1; | ||||
| 							const response = await fetch(url, {signal: controller.signal}); | ||||
|  | ||||
| 							const body = await response.body; | ||||
| 							reader = body.getReader(); | ||||
|  | ||||
| 							while (true) { | ||||
| 								const { done, value } = await reader.read(); | ||||
| 								ws.send(value); | ||||
| 								if (done) break; | ||||
| 							} | ||||
| 						} finally { | ||||
| 							upstreamGetCount -= 1; | ||||
| 							console.log("Dial GET DONE, remaining: ", upstreamGetCount); | ||||
| 							ws.close(); | ||||
| 						} | ||||
| 					})() | ||||
| 				} else if (method == "POST") { | ||||
| 					upstreamPostCount += 1; | ||||
| 					console.log("Dial POST", url); | ||||
| 					ws.send("ok"); | ||||
| 					ws.onmessage = async (event) => { | ||||
| 						try { | ||||
| 							const response = await fetch( | ||||
| 								url, | ||||
| 								{method: "POST", body: event.data} | ||||
| 							); | ||||
| 							if (response.ok) { | ||||
| 								ws.send("ok"); | ||||
| 							} else { | ||||
| 								console.error("bad status code"); | ||||
| 								ws.send("fail"); | ||||
| 							} | ||||
| 						} finally { | ||||
| 							upstreamPostCount -= 1; | ||||
| 							console.log("Dial POST DONE, remaining: ", upstreamPostCount); | ||||
| 							ws.close(); | ||||
| 						} | ||||
| 					}; | ||||
| 				} | ||||
|  | ||||
| 				check() | ||||
| 			} | ||||
| 			ws.onerror = function (event) { | ||||
| 				ws.close() | ||||
| 			} | ||||
| 		} | ||||
| 	</script> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										39
									
								
								transport/internet/splithttp/browser_client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								transport/internet/splithttp/browser_client.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| package splithttp | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	gonet "net" | ||||
|  | ||||
| 	"github.com/xtls/xray-core/transport/internet/browser_dialer" | ||||
| 	"github.com/xtls/xray-core/transport/internet/websocket" | ||||
| ) | ||||
|  | ||||
| // implements splithttp.DialerClient in terms of browser dialer | ||||
| // has no fields because everything is global state :O) | ||||
| type BrowserDialerClient struct{} | ||||
|  | ||||
| func (c *BrowserDialerClient) OpenDownload(ctx context.Context, baseURL string) (io.ReadCloser, gonet.Addr, gonet.Addr, error) { | ||||
| 	conn, err := browser_dialer.DialGet(baseURL) | ||||
| 	dummyAddr := &gonet.IPAddr{} | ||||
| 	if err != nil { | ||||
| 		return nil, dummyAddr, dummyAddr, err | ||||
| 	} | ||||
|  | ||||
| 	return websocket.NewConnection(conn, dummyAddr, nil), conn.RemoteAddr(), conn.LocalAddr(), nil | ||||
| } | ||||
|  | ||||
| func (c *BrowserDialerClient) SendUploadRequest(ctx context.Context, url string, payload io.ReadWriteCloser, contentLength int64) error { | ||||
| 	bytes, err := ioutil.ReadAll(payload) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	err = browser_dialer.DialPost(url, bytes) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										169
									
								
								transport/internet/splithttp/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								transport/internet/splithttp/client.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,169 @@ | ||||
| package splithttp | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"io" | ||||
| 	gonet "net" | ||||
| 	"net/http" | ||||
| 	"net/http/httptrace" | ||||
| 	"sync" | ||||
|  | ||||
| 	"github.com/xtls/xray-core/common" | ||||
| 	"github.com/xtls/xray-core/common/errors" | ||||
| 	"github.com/xtls/xray-core/common/net" | ||||
| 	"github.com/xtls/xray-core/common/signal/done" | ||||
| ) | ||||
|  | ||||
| // interface to abstract between use of browser dialer, vs net/http | ||||
| type DialerClient interface { | ||||
| 	// (ctx, baseURL, payload) -> err | ||||
| 	// baseURL already contains sessionId and seq | ||||
| 	SendUploadRequest(context.Context, string, io.ReadWriteCloser, int64) error | ||||
|  | ||||
| 	// (ctx, baseURL) -> (downloadReader, remoteAddr, localAddr) | ||||
| 	// baseURL already contains sessionId | ||||
| 	OpenDownload(context.Context, string) (io.ReadCloser, net.Addr, net.Addr, error) | ||||
| } | ||||
|  | ||||
| // implements splithttp.DialerClient in terms of direct network connections | ||||
| type DefaultDialerClient struct { | ||||
| 	transportConfig *Config | ||||
| 	download        *http.Client | ||||
| 	upload          *http.Client | ||||
| 	isH2            bool | ||||
| 	// pool of net.Conn, created using dialUploadConn | ||||
| 	uploadRawPool  *sync.Pool | ||||
| 	dialUploadConn func(ctxInner context.Context) (net.Conn, error) | ||||
| } | ||||
|  | ||||
| func (c *DefaultDialerClient) OpenDownload(ctx context.Context, baseURL string) (io.ReadCloser, gonet.Addr, gonet.Addr, error) { | ||||
| 	var remoteAddr gonet.Addr | ||||
| 	var localAddr gonet.Addr | ||||
| 	// this is done when the TCP/UDP connection to the server was established, | ||||
| 	// and we can unblock the Dial function and print correct net addresses in | ||||
| 	// logs | ||||
| 	gotConn := done.New() | ||||
|  | ||||
| 	var downResponse io.ReadCloser | ||||
| 	gotDownResponse := done.New() | ||||
|  | ||||
| 	go func() { | ||||
| 		trace := &httptrace.ClientTrace{ | ||||
| 			GotConn: func(connInfo httptrace.GotConnInfo) { | ||||
| 				remoteAddr = connInfo.Conn.RemoteAddr() | ||||
| 				localAddr = connInfo.Conn.LocalAddr() | ||||
| 				gotConn.Close() | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		// in case we hit an error, we want to unblock this part | ||||
| 		defer gotConn.Close() | ||||
|  | ||||
| 		req, err := http.NewRequestWithContext( | ||||
| 			httptrace.WithClientTrace(ctx, trace), | ||||
| 			"GET", | ||||
| 			baseURL, | ||||
| 			nil, | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			errors.LogInfoInner(ctx, err, "failed to construct download http request") | ||||
| 			gotDownResponse.Close() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		req.Header = c.transportConfig.GetRequestHeader() | ||||
|  | ||||
| 		response, err := c.download.Do(req) | ||||
| 		gotConn.Close() | ||||
| 		if err != nil { | ||||
| 			errors.LogInfoInner(ctx, err, "failed to send download http request") | ||||
| 			gotDownResponse.Close() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if response.StatusCode != 200 { | ||||
| 			response.Body.Close() | ||||
| 			errors.LogInfo(ctx, "invalid status code on download:", response.Status) | ||||
| 			gotDownResponse.Close() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		downResponse = response.Body | ||||
| 		gotDownResponse.Close() | ||||
| 	}() | ||||
|  | ||||
| 	// we want to block Dial until we know the remote address of the server, | ||||
| 	// for logging purposes | ||||
| 	<-gotConn.Wait() | ||||
|  | ||||
| 	lazyDownload := &LazyReader{ | ||||
| 		CreateReader: func() (io.ReadCloser, error) { | ||||
| 			<-gotDownResponse.Wait() | ||||
| 			if downResponse == nil { | ||||
| 				return nil, errors.New("downResponse failed") | ||||
| 			} | ||||
| 			return downResponse, nil | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	return lazyDownload, remoteAddr, localAddr, nil | ||||
| } | ||||
|  | ||||
| func (c *DefaultDialerClient) SendUploadRequest(ctx context.Context, url string, payload io.ReadWriteCloser, contentLength int64) error { | ||||
| 	req, err := http.NewRequest("POST", url, payload) | ||||
| 	req.ContentLength = contentLength | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	req.Header = c.transportConfig.GetRequestHeader() | ||||
|  | ||||
| 	if c.isH2 { | ||||
| 		resp, err := c.upload.Do(req) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		defer resp.Body.Close() | ||||
|  | ||||
| 		if resp.StatusCode != 200 { | ||||
| 			return errors.New("bad status code:", resp.Status) | ||||
| 		} | ||||
| 	} else { | ||||
| 		// stringify the entire HTTP/1.1 request so it can be | ||||
| 		// safely retried. if instead req.Write is called multiple | ||||
| 		// times, the body is already drained after the first | ||||
| 		// request | ||||
| 		requestBytes := new(bytes.Buffer) | ||||
| 		common.Must(req.Write(requestBytes)) | ||||
|  | ||||
| 		var uploadConn any | ||||
|  | ||||
| 		for { | ||||
| 			uploadConn = c.uploadRawPool.Get() | ||||
| 			newConnection := uploadConn == nil | ||||
| 			if newConnection { | ||||
| 				uploadConn, err = c.dialUploadConn(context.WithoutCancel(ctx)) | ||||
| 				if err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			_, err = uploadConn.(net.Conn).Write(requestBytes.Bytes()) | ||||
|  | ||||
| 			// if the write failed, we try another connection from | ||||
| 			// the pool, until the write on a new connection fails. | ||||
| 			// failed writes to a pooled connection are normal when | ||||
| 			// the connection has been closed in the meantime. | ||||
| 			if err == nil { | ||||
| 				break | ||||
| 			} else if newConnection { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		c.uploadRawPool.Put(uploadConn) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
| @@ -1,13 +1,10 @@ | ||||
| package splithttp | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	gotls "crypto/tls" | ||||
| 	"io" | ||||
| 	gonet "net" | ||||
| 	"net/http" | ||||
| 	"net/http/httptrace" | ||||
| 	"net/url" | ||||
| 	"strconv" | ||||
| 	"sync" | ||||
| @@ -17,10 +14,10 @@ import ( | ||||
| 	"github.com/xtls/xray-core/common/buf" | ||||
| 	"github.com/xtls/xray-core/common/errors" | ||||
| 	"github.com/xtls/xray-core/common/net" | ||||
| 	"github.com/xtls/xray-core/common/signal/done" | ||||
| 	"github.com/xtls/xray-core/common/signal/semaphore" | ||||
| 	"github.com/xtls/xray-core/common/uuid" | ||||
| 	"github.com/xtls/xray-core/transport/internet" | ||||
| 	"github.com/xtls/xray-core/transport/internet/browser_dialer" | ||||
| 	"github.com/xtls/xray-core/transport/internet/stat" | ||||
| 	"github.com/xtls/xray-core/transport/internet/tls" | ||||
| 	"github.com/xtls/xray-core/transport/pipe" | ||||
| @@ -32,32 +29,31 @@ type dialerConf struct { | ||||
| 	*internet.MemoryStreamConfig | ||||
| } | ||||
|  | ||||
| type reusedClient struct { | ||||
| 	download *http.Client | ||||
| 	upload   *http.Client | ||||
| 	isH2     bool | ||||
| 	// pool of net.Conn, created using dialUploadConn | ||||
| 	uploadRawPool  *sync.Pool | ||||
| 	dialUploadConn func(ctxInner context.Context) (net.Conn, error) | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	globalDialerMap    map[dialerConf]reusedClient | ||||
| 	globalDialerMap    map[dialerConf]DialerClient | ||||
| 	globalDialerAccess sync.Mutex | ||||
| ) | ||||
|  | ||||
| func getHTTPClient(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) reusedClient { | ||||
| func getHTTPClient(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) DialerClient { | ||||
| 	if browser_dialer.HasBrowserDialer() { | ||||
| 		return &BrowserDialerClient{} | ||||
| 	} | ||||
|  | ||||
| 	globalDialerAccess.Lock() | ||||
| 	defer globalDialerAccess.Unlock() | ||||
|  | ||||
| 	if globalDialerMap == nil { | ||||
| 		globalDialerMap = make(map[dialerConf]reusedClient) | ||||
| 		globalDialerMap = make(map[dialerConf]DialerClient) | ||||
| 	} | ||||
|  | ||||
| 	if client, found := globalDialerMap[dialerConf{dest, streamSettings}]; found { | ||||
| 		return client | ||||
| 	} | ||||
|  | ||||
| 	if browser_dialer.HasBrowserDialer() { | ||||
| 		return &BrowserDialerClient{} | ||||
| 	} | ||||
|  | ||||
| 	tlsConfig := tls.ConfigFromStreamSettings(streamSettings) | ||||
| 	isH2 := tlsConfig != nil && !(len(tlsConfig.NextProtocol) == 1 && tlsConfig.NextProtocol[0] == "http/1.1") | ||||
|  | ||||
| @@ -116,7 +112,8 @@ func getHTTPClient(ctx context.Context, dest net.Destination, streamSettings *in | ||||
| 		uploadTransport = nil | ||||
| 	} | ||||
|  | ||||
| 	client := reusedClient{ | ||||
| 	client := &DefaultDialerClient{ | ||||
| 		transportConfig: streamSettings.ProtocolSettings.(*Config), | ||||
| 		download: &http.Client{ | ||||
| 			Transport: downloadTransport, | ||||
| 		}, | ||||
| @@ -160,80 +157,9 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me | ||||
|  | ||||
| 	httpClient := getHTTPClient(ctx, dest, streamSettings) | ||||
|  | ||||
| 	var remoteAddr gonet.Addr | ||||
| 	var localAddr gonet.Addr | ||||
| 	// this is done when the TCP/UDP connection to the server was established, | ||||
| 	// and we can unblock the Dial function and print correct net addresses in | ||||
| 	// logs | ||||
| 	gotConn := done.New() | ||||
|  | ||||
| 	var downResponse io.ReadCloser | ||||
| 	gotDownResponse := done.New() | ||||
|  | ||||
| 	sessionIdUuid := uuid.New() | ||||
| 	sessionId := sessionIdUuid.String() | ||||
|  | ||||
| 	go func() { | ||||
| 		trace := &httptrace.ClientTrace{ | ||||
| 			GotConn: func(connInfo httptrace.GotConnInfo) { | ||||
| 				remoteAddr = connInfo.Conn.RemoteAddr() | ||||
| 				localAddr = connInfo.Conn.LocalAddr() | ||||
| 				gotConn.Close() | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		// in case we hit an error, we want to unblock this part | ||||
| 		defer gotConn.Close() | ||||
|  | ||||
| 		req, err := http.NewRequestWithContext( | ||||
| 			httptrace.WithClientTrace(context.WithoutCancel(ctx), trace), | ||||
| 			"GET", | ||||
| 			requestURL.String()+sessionId, | ||||
| 			nil, | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			errors.LogInfoInner(ctx, err, "failed to construct download http request") | ||||
| 			gotDownResponse.Close() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		req.Header = transportConfiguration.GetRequestHeader() | ||||
|  | ||||
| 		response, err := httpClient.download.Do(req) | ||||
| 		gotConn.Close() | ||||
| 		if err != nil { | ||||
| 			errors.LogInfoInner(ctx, err, "failed to send download http request") | ||||
| 			gotDownResponse.Close() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if response.StatusCode != 200 { | ||||
| 			response.Body.Close() | ||||
| 			errors.LogInfo(ctx, "invalid status code on download:", response.Status) | ||||
| 			gotDownResponse.Close() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// skip "ooooooooook" response | ||||
| 		trashHeader := []byte{0} | ||||
| 		for { | ||||
| 			_, err = io.ReadFull(response.Body, trashHeader) | ||||
| 			if err != nil { | ||||
| 				response.Body.Close() | ||||
| 				errors.LogInfoInner(ctx, err, "failed to read initial response") | ||||
| 				gotDownResponse.Close() | ||||
| 				return | ||||
| 			} | ||||
| 			if trashHeader[0] == 'k' { | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		downResponse = response.Body | ||||
| 		gotDownResponse.Close() | ||||
| 	}() | ||||
|  | ||||
| 	uploadUrl := requestURL.String() + sessionId + "/" | ||||
| 	baseURL := requestURL.String() + sessionId | ||||
|  | ||||
| 	uploadPipeReader, uploadPipeWriter := pipe.New(pipe.WithSizeLimit(maxUploadSize)) | ||||
|  | ||||
| @@ -252,97 +178,55 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me | ||||
|  | ||||
| 			<-requestsLimiter.Wait() | ||||
|  | ||||
| 			url := uploadUrl + strconv.FormatInt(requestCounter, 10) | ||||
| 			seq := requestCounter | ||||
| 			requestCounter += 1 | ||||
|  | ||||
| 			go func() { | ||||
| 				defer requestsLimiter.Signal() | ||||
| 				req, err := http.NewRequest("POST", url, &buf.MultiBufferContainer{MultiBuffer: chunk}) | ||||
|  | ||||
| 				err := httpClient.SendUploadRequest( | ||||
| 					context.WithoutCancel(ctx), | ||||
| 					baseURL+"/"+strconv.FormatInt(seq, 10), | ||||
| 					&buf.MultiBufferContainer{MultiBuffer: chunk}, | ||||
| 					int64(chunk.Len()), | ||||
| 				) | ||||
|  | ||||
| 				if err != nil { | ||||
| 					errors.LogInfoInner(ctx, err, "failed to send upload") | ||||
| 					uploadPipeReader.Interrupt() | ||||
| 					return | ||||
| 				} | ||||
|  | ||||
| 				req.ContentLength = int64(chunk.Len()) | ||||
| 				req.Header = transportConfiguration.GetRequestHeader() | ||||
|  | ||||
| 				if httpClient.isH2 { | ||||
| 					resp, err := httpClient.upload.Do(req) | ||||
| 					if err != nil { | ||||
| 						errors.LogInfoInner(ctx, err, "failed to send upload") | ||||
| 						uploadPipeReader.Interrupt() | ||||
| 						return | ||||
| 					} | ||||
| 					defer resp.Body.Close() | ||||
|  | ||||
| 					if resp.StatusCode != 200 { | ||||
| 						errors.LogInfo(ctx, "failed to send upload, bad status code:", resp.Status) | ||||
| 						uploadPipeReader.Interrupt() | ||||
| 						return | ||||
| 					} | ||||
| 				} else { | ||||
| 					var uploadConn any | ||||
|  | ||||
| 					// stringify the entire HTTP/1.1 request so it can be | ||||
| 					// safely retried. if instead req.Write is called multiple | ||||
| 					// times, the body is already drained after the first | ||||
| 					// request | ||||
| 					requestBytes := new(bytes.Buffer) | ||||
| 					common.Must(req.Write(requestBytes)) | ||||
|  | ||||
| 					for { | ||||
| 						uploadConn = httpClient.uploadRawPool.Get() | ||||
| 						newConnection := uploadConn == nil | ||||
| 						if newConnection { | ||||
| 							uploadConn, err = httpClient.dialUploadConn(context.WithoutCancel(ctx)) | ||||
| 							if err != nil { | ||||
| 								errors.LogInfoInner(ctx, err, "failed to connect upload") | ||||
| 								uploadPipeReader.Interrupt() | ||||
| 								return | ||||
| 							} | ||||
| 						} | ||||
|  | ||||
| 						_, err = uploadConn.(net.Conn).Write(requestBytes.Bytes()) | ||||
|  | ||||
| 						// if the write failed, we try another connection from | ||||
| 						// the pool, until the write on a new connection fails. | ||||
| 						// failed writes to a pooled connection are normal when | ||||
| 						// the connection has been closed in the meantime. | ||||
| 						if err == nil { | ||||
| 							break | ||||
| 						} else if newConnection { | ||||
| 							errors.LogInfoInner(ctx, err, "failed to send upload") | ||||
| 							uploadPipeReader.Interrupt() | ||||
| 							return | ||||
| 						} | ||||
| 					} | ||||
|  | ||||
| 					httpClient.uploadRawPool.Put(uploadConn) | ||||
| 				} | ||||
| 			}() | ||||
|  | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	// we want to block Dial until we know the remote address of the server, | ||||
| 	// for logging purposes | ||||
| 	<-gotConn.Wait() | ||||
| 	lazyRawDownload, remoteAddr, localAddr, err := httpClient.OpenDownload(context.WithoutCancel(ctx), baseURL) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	lazyDownload := &LazyReader{ | ||||
| 		CreateReader: func() (io.ReadCloser, error) { | ||||
| 			// skip "ooooooooook" response | ||||
| 			trashHeader := []byte{0} | ||||
| 			for { | ||||
| 				_, err := io.ReadFull(lazyRawDownload, trashHeader) | ||||
| 				if err != nil { | ||||
| 					return nil, errors.New("failed to read initial response").Base(err) | ||||
| 				} | ||||
| 				if trashHeader[0] == 'k' { | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			return lazyRawDownload, nil | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	// necessary in order to send larger chunks in upload | ||||
| 	bufferedUploadPipeWriter := buf.NewBufferedWriter(uploadPipeWriter) | ||||
| 	bufferedUploadPipeWriter.SetBuffered(false) | ||||
|  | ||||
| 	lazyDownload := &LazyReader{ | ||||
| 		CreateReader: func() (io.ReadCloser, error) { | ||||
| 			<-gotDownResponse.Wait() | ||||
| 			if downResponse == nil { | ||||
| 				return nil, errors.New("downResponse failed") | ||||
| 			} | ||||
| 			return downResponse, nil | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	conn := splitConn{ | ||||
| 		writer:     bufferedUploadPipeWriter, | ||||
| 		reader:     lazyDownload, | ||||
|   | ||||
| @@ -32,7 +32,7 @@ type requestHandler struct { | ||||
| } | ||||
|  | ||||
| type httpSession struct { | ||||
| 	uploadQueue *UploadQueue | ||||
| 	uploadQueue *uploadQueue | ||||
| 	// for as long as the GET request is not opened by the client, this will be | ||||
| 	// open ("undone"), and the session may be expired within a certain TTL. | ||||
| 	// after the client connects, this becomes "done" and the session lives as | ||||
| @@ -163,7 +163,7 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req | ||||
| 		writer.Header().Set("X-Accel-Buffering", "no") | ||||
| 		// magic header to make the HTTP middle box consider this as SSE to disable buffer | ||||
| 		writer.Header().Set("Content-Type", "text/event-stream") | ||||
| 		 | ||||
|  | ||||
| 		writer.WriteHeader(http.StatusOK) | ||||
| 		// send a chunk immediately to enable CDN streaming. | ||||
| 		// many CDN buffer the response headers until the origin starts sending | ||||
|   | ||||
| @@ -15,7 +15,7 @@ type Packet struct { | ||||
| 	Seq     uint64 | ||||
| } | ||||
|  | ||||
| type UploadQueue struct { | ||||
| type uploadQueue struct { | ||||
| 	pushedPackets chan Packet | ||||
| 	heap          uploadHeap | ||||
| 	nextSeq       uint64 | ||||
| @@ -23,8 +23,8 @@ type UploadQueue struct { | ||||
| 	maxPackets    int | ||||
| } | ||||
|  | ||||
| func NewUploadQueue(maxPackets int) *UploadQueue { | ||||
| 	return &UploadQueue{ | ||||
| func NewUploadQueue(maxPackets int) *uploadQueue { | ||||
| 	return &uploadQueue{ | ||||
| 		pushedPackets: make(chan Packet, maxPackets), | ||||
| 		heap:          uploadHeap{}, | ||||
| 		nextSeq:       0, | ||||
| @@ -33,7 +33,7 @@ func NewUploadQueue(maxPackets int) *UploadQueue { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (h *UploadQueue) Push(p Packet) error { | ||||
| func (h *uploadQueue) Push(p Packet) error { | ||||
| 	if h.closed { | ||||
| 		return errors.New("splithttp packet queue closed") | ||||
| 	} | ||||
| @@ -42,13 +42,13 @@ func (h *UploadQueue) Push(p Packet) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (h *UploadQueue) Close() error { | ||||
| func (h *uploadQueue) Close() error { | ||||
| 	h.closed = true | ||||
| 	close(h.pushedPackets) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (h *UploadQueue) Read(b []byte) (int, error) { | ||||
| func (h *uploadQueue) Read(b []byte) (int, error) { | ||||
| 	if h.closed { | ||||
| 		return 0, io.EOF | ||||
| 	} | ||||
|   | ||||
| @@ -15,16 +15,14 @@ var _ buf.Writer = (*connection)(nil) | ||||
|  | ||||
| // connection is a wrapper for net.Conn over WebSocket connection. | ||||
| type connection struct { | ||||
| 	conn       *websocket.Conn | ||||
| 	reader     io.Reader | ||||
| 	remoteAddr net.Addr | ||||
| 	conn   *websocket.Conn | ||||
| 	reader io.Reader | ||||
| } | ||||
|  | ||||
| func newConnection(conn *websocket.Conn, remoteAddr net.Addr, extraReader io.Reader) *connection { | ||||
| func NewConnection(conn *websocket.Conn, remoteAddr net.Addr, extraReader io.Reader) *connection { | ||||
| 	return &connection{ | ||||
| 		conn:       conn, | ||||
| 		remoteAddr: remoteAddr, | ||||
| 		reader:     extraReader, | ||||
| 		conn:   conn, | ||||
| 		reader: extraReader, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -92,7 +90,7 @@ func (c *connection) LocalAddr() net.Addr { | ||||
| } | ||||
|  | ||||
| func (c *connection) RemoteAddr() net.Addr { | ||||
| 	return c.remoteAddr | ||||
| 	return c.conn.RemoteAddr() | ||||
| } | ||||
|  | ||||
| func (c *connection) SetDeadline(t time.Time) error { | ||||
|   | ||||
| @@ -1,54 +1,23 @@ | ||||
| package websocket | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	_ "embed" | ||||
| 	"encoding/base64" | ||||
| 	"io" | ||||
| 	gonet "net" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gorilla/websocket" | ||||
| 	"github.com/xtls/xray-core/common" | ||||
| 	"github.com/xtls/xray-core/common/errors" | ||||
| 	"github.com/xtls/xray-core/common/net" | ||||
| 	"github.com/xtls/xray-core/common/platform" | ||||
| 	"github.com/xtls/xray-core/common/uuid" | ||||
| 	"github.com/xtls/xray-core/transport/internet" | ||||
| 	"github.com/xtls/xray-core/transport/internet/browser_dialer" | ||||
| 	"github.com/xtls/xray-core/transport/internet/stat" | ||||
| 	"github.com/xtls/xray-core/transport/internet/tls" | ||||
| ) | ||||
|  | ||||
| //go:embed dialer.html | ||||
| var webpage []byte | ||||
|  | ||||
| var conns chan *websocket.Conn | ||||
|  | ||||
| func init() { | ||||
| 	addr := platform.NewEnvFlag(platform.BrowserDialerAddress).GetValue(func() string { return "" }) | ||||
| 	if addr != "" { | ||||
| 		token := uuid.New() | ||||
| 		csrfToken := token.String() | ||||
| 		webpage = bytes.ReplaceAll(webpage, []byte("csrfToken"), []byte(csrfToken)) | ||||
| 		conns = make(chan *websocket.Conn, 256) | ||||
| 		go http.ListenAndServe(addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 			if r.URL.Path == "/websocket" { | ||||
| 				if r.URL.Query().Get("token") == csrfToken { | ||||
| 					if conn, err := upgrader.Upgrade(w, r, nil); err == nil { | ||||
| 						conns <- conn | ||||
| 					} else { | ||||
| 						errors.LogError(context.Background(), "Browser dialer http upgrade unexpected error") | ||||
| 					} | ||||
| 				} | ||||
| 			} else { | ||||
| 				w.Write(webpage) | ||||
| 			} | ||||
| 		})) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Dial dials a WebSocket connection to the given destination. | ||||
| func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) { | ||||
| 	errors.LogInfo(ctx, "creating connection to ", dest) | ||||
| @@ -98,18 +67,18 @@ func dialWebSocket(ctx context.Context, dest net.Destination, streamSettings *in | ||||
| 				// Like the NetDial in the dialer | ||||
| 				pconn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings) | ||||
| 				if err != nil { | ||||
| 					errors.LogErrorInner(ctx, err, "failed to dial to " + addr) | ||||
| 					errors.LogErrorInner(ctx, err, "failed to dial to "+addr) | ||||
| 					return nil, err | ||||
| 				} | ||||
| 				// TLS and apply the handshake | ||||
| 				cn := tls.UClient(pconn, tlsConfig, fingerprint).(*tls.UConn) | ||||
| 				if err := cn.WebsocketHandshakeContext(ctx); err != nil { | ||||
| 					errors.LogErrorInner(ctx, err, "failed to dial to " + addr) | ||||
| 					errors.LogErrorInner(ctx, err, "failed to dial to "+addr) | ||||
| 					return nil, err | ||||
| 				} | ||||
| 				if !tlsConfig.InsecureSkipVerify { | ||||
| 					if err := cn.VerifyHostname(tlsConfig.ServerName); err != nil { | ||||
| 						errors.LogErrorInner(ctx, err, "failed to dial to " + addr) | ||||
| 						errors.LogErrorInner(ctx, err, "failed to dial to "+addr) | ||||
| 						return nil, err | ||||
| 					} | ||||
| 				} | ||||
| @@ -124,28 +93,13 @@ func dialWebSocket(ctx context.Context, dest net.Destination, streamSettings *in | ||||
| 	} | ||||
| 	uri := protocol + "://" + host + wsSettings.GetNormalizedPath() | ||||
|  | ||||
| 	if conns != nil { | ||||
| 		data := []byte(uri) | ||||
| 		if ed != nil { | ||||
| 			data = append(data, " "+base64.RawURLEncoding.EncodeToString(ed)...) | ||||
| 		} | ||||
| 		var conn *websocket.Conn | ||||
| 		for { | ||||
| 			conn = <-conns | ||||
| 			if conn.WriteMessage(websocket.TextMessage, data) != nil { | ||||
| 				conn.Close() | ||||
| 			} else { | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		if _, p, err := conn.ReadMessage(); err != nil { | ||||
| 			conn.Close() | ||||
| 	if browser_dialer.HasBrowserDialer() { | ||||
| 		conn, err := browser_dialer.DialWS(uri, ed) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} else if s := string(p); s != "ok" { | ||||
| 			conn.Close() | ||||
| 			return nil, errors.New(s) | ||||
| 		} | ||||
| 		return newConnection(conn, conn.RemoteAddr(), nil), nil | ||||
|  | ||||
| 		return NewConnection(conn, conn.RemoteAddr(), nil), nil | ||||
| 	} | ||||
|  | ||||
| 	header := wsSettings.GetRequestHeader() | ||||
| @@ -163,7 +117,7 @@ func dialWebSocket(ctx context.Context, dest net.Destination, streamSettings *in | ||||
| 		return nil, errors.New("failed to dial to (", uri, "): ", reason).Base(err) | ||||
| 	} | ||||
|  | ||||
| 	return newConnection(conn, conn.RemoteAddr(), nil), nil | ||||
| 	return NewConnection(conn, conn.RemoteAddr(), nil), nil | ||||
| } | ||||
|  | ||||
| type delayDialConn struct { | ||||
|   | ||||
| @@ -1,59 +0,0 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
| 	<title>Browser Dialer</title> | ||||
| </head> | ||||
| <body> | ||||
| 	<script> | ||||
| 		// Copyright (c) 2021 XRAY. Mozilla Public License 2.0. | ||||
| 		var url = "ws://" + window.location.host + "/websocket?token=csrfToken" | ||||
| 		var count = 0 | ||||
| 		setInterval(check, 1000) | ||||
| 		function check() { | ||||
| 			if (count <= 0) { | ||||
| 				count += 1 | ||||
| 				console.log("Prepare", url) | ||||
| 				var ws = new WebSocket(url) | ||||
| 				// arraybuffer is significantly faster in chrome than default | ||||
| 				// blob, tested with chrome 123 | ||||
| 				ws.binaryType = "arraybuffer"; | ||||
| 				var wss = undefined | ||||
| 				var first = true | ||||
| 				ws.onmessage = function (event) { | ||||
| 					if (first) { | ||||
| 						first = false | ||||
| 						count -= 1 | ||||
| 						var arr = event.data.split(" ") | ||||
| 						console.log("Dial", arr[0], arr[1]) | ||||
| 						wss = new WebSocket(arr[0], arr[1]) | ||||
| 						wss.binaryType = "arraybuffer"; | ||||
| 						var opened = false | ||||
| 						wss.onopen = function (event) { | ||||
| 							opened = true | ||||
| 							ws.send("ok") | ||||
| 						} | ||||
| 						wss.onmessage = function (event) { | ||||
| 							ws.send(event.data) | ||||
| 						} | ||||
| 						wss.onclose = function (event) { | ||||
| 							ws.close() | ||||
| 						} | ||||
| 						wss.onerror = function (event) { | ||||
| 							!opened && ws.send("fail") | ||||
| 							wss.close() | ||||
| 						} | ||||
| 						check() | ||||
| 					} else wss.send(event.data) | ||||
| 				} | ||||
| 				ws.onclose = function (event) { | ||||
| 					if (first) count -= 1 | ||||
| 					else wss.close() | ||||
| 				} | ||||
| 				ws.onerror = function (event) { | ||||
| 					ws.close() | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	</script> | ||||
| </body> | ||||
| </html> | ||||
| @@ -73,7 +73,7 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	h.ln.addConn(newConnection(conn, remoteAddr, extraReader)) | ||||
| 	h.ln.addConn(NewConnection(conn, remoteAddr, extraReader)) | ||||
| } | ||||
|  | ||||
| type Listener struct { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 mmmray
					mmmray