-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add SplitHTTP Browser Dialer support (#3484)
- Loading branch information
Showing
11 changed files
with
534 additions
and
292 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,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 | ||
} |
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,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> |
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, 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 | ||
} |
Oops, something went wrong.