Skip to content

Commit

Permalink
Add SplitHTTP Browser Dialer support (#3484)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmmray authored Jul 11, 2024
1 parent 308f0c6 commit c8f6ba9
Show file tree
Hide file tree
Showing 11 changed files with 534 additions and 292 deletions.
121 changes: 121 additions & 0 deletions transport/internet/browser_dialer/dialer.go
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
}
136 changes: 136 additions & 0 deletions transport/internet/browser_dialer/dialer.html
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>
39 changes: 39 additions & 0 deletions transport/internet/splithttp/browser_client.go
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
}
Loading

0 comments on commit c8f6ba9

Please # to comment.