-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
add browser dialer for splithttp
Showing
11 changed files
with
508 additions
and
287 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
package browser_dialer | ||
|
||
import ( | ||
"bytes" | ||
_ "embed" | ||
"encoding/base64" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/gorilla/websocket" | ||
"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 { | ||
newError("Browser dialer http upgrade unexpected error").AtError().WriteToLog() | ||
} | ||
} | ||
} 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 | ||
} | ||
|
||
// send "EOF" | ||
err = conn.WriteMessage(websocket.BinaryMessage, []byte{}) | ||
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 newError(s) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
<!-- vim: set noexpandtab: --> | ||
<!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"; | ||
ws.onmessage = function (event) { | ||
count -= 1; | ||
let [method, url, protocol] = event.data.split(" "); | ||
if (method == "WS") { | ||
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) { | ||
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(); | ||
const response = await fetch(url, {signal: controller.signal}); | ||
const body = await response.body; | ||
const reader = body.getReader(); | ||
|
||
ws.onclose = function (event) { | ||
try { | ||
controller.abort(); | ||
} catch(e) {} | ||
} | ||
|
||
while (true) { | ||
const { done, value } = await reader.read(); | ||
console.log("chunk download"); | ||
ws.send(value); | ||
if (done) break; | ||
} | ||
|
||
ws.close(); | ||
})() | ||
} else if (method == "POST") { | ||
console.log("Dial POST", url); | ||
ws.send("ok"); | ||
let body; | ||
ws.onmessage = function (event) { | ||
if (event.data.byteLength == 0) { | ||
console.log("start upload"); | ||
(async () => { | ||
const response = await fetch( | ||
url, | ||
{method: "POST", body} | ||
); | ||
if (response.ok) { | ||
ws.send("ok"); | ||
ws.close(); | ||
} | ||
})(); | ||
} else { | ||
body = event.data; | ||
} | ||
}; | ||
} | ||
|
||
check() | ||
} | ||
ws.onerror = function (event) { | ||
ws.close() | ||
} | ||
} | ||
} | ||
</script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package browser_dialer | ||
|
||
import "github.com/xtls/xray-core/common/errors" | ||
|
||
type errPathObjHolder struct{} | ||
|
||
func newError(values ...interface{}) *errors.Error { | ||
return errors.New(values...).WithPathObj(errPathObjHolder{}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) error { | ||
bytes, err := ioutil.ReadAll(payload) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
err = browser_dialer.DialPost(url, bytes) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
package splithttp | ||
|
||
import ( | ||
"context" | ||
"io" | ||
gonet "net" | ||
"net/http" | ||
"net/http/httptrace" | ||
"sync" | ||
|
||
"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) 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 { | ||
newError("failed to construct download http request").Base(err).WriteToLog() | ||
gotDownResponse.Close() | ||
return | ||
} | ||
|
||
req.Header = c.TransportConfig.GetRequestHeader() | ||
|
||
response, err := c.Download.Do(req) | ||
gotConn.Close() | ||
if err != nil { | ||
newError("failed to send download http request").Base(err).WriteToLog() | ||
gotDownResponse.Close() | ||
return | ||
} | ||
|
||
if response.StatusCode != 200 { | ||
response.Body.Close() | ||
newError("invalid status code on download:", response.Status).WriteToLog() | ||
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, newError("downResponse failed") | ||
} | ||
return downResponse, nil | ||
}, | ||
} | ||
|
||
return lazyDownload, remoteAddr, localAddr, nil | ||
} | ||
|
||
func (c *DefaultDialerClient) SendUploadRequest(ctx context.Context, url string, payload io.ReadWriteCloser) error { | ||
req, err := http.NewRequest("POST", url, payload) | ||
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 newError("bad status code:", resp.Status) | ||
} | ||
} else { | ||
var err error | ||
var uploadConn any | ||
for i := 0; i < 5; i++ { | ||
uploadConn = c.UploadRawPool.Get() | ||
if uploadConn == nil { | ||
uploadConn, err = c.DialUploadConn(ctx) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
err = req.Write(uploadConn.(net.Conn)) | ||
if err == nil { | ||
break | ||
} | ||
} | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
c.UploadRawPool.Put(uploadConn) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters