Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 69 additions & 11 deletions transport/internet/browser_dialer/dialer.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
_ "embed"
"encoding/base64"
"encoding/json"
"net/http"
"time"

Expand All @@ -17,6 +18,12 @@ import (
//go:embed dialer.html
var webpage []byte

type task struct {
Method string `json:"method"`
URL string `json:"url"`
Extra any `json:"extra,omitempty"`
}

var conns chan *websocket.Conn

var upgrader = &websocket.Upgrader{
Expand Down Expand Up @@ -55,23 +62,69 @@ func HasBrowserDialer() bool {
return conns != nil
}

type webSocketExtra struct {
Protocol string `json:"protocol,omitempty"`
}

func DialWS(uri string, ed []byte) (*websocket.Conn, error) {
data := []byte("WS " + uri)
task := task{
Method: "WS",
URL: uri,
}

if ed != nil {
data = append(data, " "+base64.RawURLEncoding.EncodeToString(ed)...)
task.Extra = webSocketExtra{
Protocol: base64.RawURLEncoding.EncodeToString(ed),
}
}

return dialRaw(data)
return dialTask(task)
}

func DialGet(uri string) (*websocket.Conn, error) {
data := []byte("GET " + uri)
return dialRaw(data)
type httpExtra struct {
Referrer string `json:"referrer,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
}

func DialPost(uri string, payload []byte) error {
data := []byte("POST " + uri)
conn, err := dialRaw(data)
func httpExtraFromHeaders(headers http.Header) *httpExtra {
if len(headers) == 0 {
return nil
}

extra := httpExtra{}
if referrer := headers.Get("Referer"); referrer != "" {
extra.Referrer = referrer
headers.Del("Referer")
}

if len(headers) > 0 {
extra.Headers = make(map[string]string)
for header := range headers {
extra.Headers[header] = headers.Get(header)
}
}

return &extra
}

func DialGet(uri string, headers http.Header) (*websocket.Conn, error) {
task := task{
Method: "GET",
URL: uri,
Extra: httpExtraFromHeaders(headers),
}

return dialTask(task)
}

func DialPost(uri string, headers http.Header, payload []byte) error {
task := task{
Method: "POST",
URL: uri,
Extra: httpExtraFromHeaders(headers),
}

conn, err := dialTask(task)
if err != nil {
return err
}
Expand All @@ -90,7 +143,12 @@ func DialPost(uri string, payload []byte) error {
return nil
}

func dialRaw(data []byte) (*websocket.Conn, error) {
func dialTask(task task) (*websocket.Conn, error) {
data, err := json.Marshal(task)
if err != nil {
return nil, err
}

var conn *websocket.Conn
for {
conn = <-conns
Expand All @@ -100,7 +158,7 @@ func dialRaw(data []byte) (*websocket.Conn, error) {
break
}
}
err := CheckOK(conn)
err = CheckOK(conn)
if err != nil {
return nil, err
}
Expand Down
70 changes: 47 additions & 23 deletions transport/internet/browser_dialer/dialer.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,28 @@
let upstreamGetCount = 0;
let upstreamWsCount = 0;
let upstreamPostCount = 0;

function prepareRequestInit(extra) {
const requestInit = {};
if (extra.referrer) {
// note: we have to strip the protocol and host part.
// Browsers disallow that, and will reset the value to current page if attempted.
const referrer = URL.parse(extra.referrer);
requestInit.referrer = referrer.pathname + referrer.search + referrer.hash;
requestInit.referrerPolicy = "unsafe-url";
}

if (extra.headers) {
requestInit.headers = extra.headers;
}

return requestInit;
}

let check = function () {
if (clientIdleCount > 0) {
return;
};
}
clientIdleCount += 1;
console.log("Prepare", url);
let ws = new WebSocket(url);
Expand All @@ -29,12 +47,12 @@
// double-checking that this continues to work
ws.onmessage = function (event) {
clientIdleCount -= 1;
let [method, url, protocol] = event.data.split(" ");
switch (method) {
let task = JSON.parse(event.data);
switch (task.method) {
case "WS": {
upstreamWsCount += 1;
console.log("Dial WS", url, protocol);
const wss = new WebSocket(url, protocol);
console.log("Dial WS", task.url, task.extra.protocol);
const wss = new WebSocket(task.url, task.extra.protocol);
wss.binaryType = "arraybuffer";
let opened = false;
ws.onmessage = function (event) {
Expand All @@ -60,10 +78,12 @@
wss.close()
};
break;
};
}
case "GET": {
(async () => {
console.log("Dial GET", url);
const requestInit = prepareRequestInit(task.extra);

console.log("Dial GET", task.url);
ws.send("ok");
const controller = new AbortController();

Expand All @@ -83,58 +103,62 @@
ws.onclose = (event) => {
try {
reader && reader.cancel();
} catch(e) {};
} catch(e) {}

try {
controller.abort();
} catch(e) {};
} catch(e) {}
};

try {
upstreamGetCount += 1;
const response = await fetch(url, {signal: controller.signal});

requestInit.signal = controller.signal;
const response = await fetch(task.url, requestInit);

const body = await response.body;
reader = body.getReader();

while (true) {
const { done, value } = await reader.read();
ws.send(value);
if (value) ws.send(value); // don't send back "undefined" string when received nothing
if (done) break;
};
}
} finally {
upstreamGetCount -= 1;
console.log("Dial GET DONE, remaining: ", upstreamGetCount);
ws.close();
};
}
})();
break;
};
}
case "POST": {
upstreamPostCount += 1;
console.log("Dial POST", url);

const requestInit = prepareRequestInit(task.extra);
requestInit.method = "POST";

console.log("Dial POST", task.url);
ws.send("ok");
ws.onmessage = async (event) => {
try {
const response = await fetch(
url,
{method: "POST", body: event.data}
);
requestInit.body = event.data;
const response = await fetch(task.url, requestInit);
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();
};
}
};
break;
};
};
}
}

check();
};
Expand Down
11 changes: 6 additions & 5 deletions transport/internet/splithttp/browser_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import (
"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{}
// BrowserDialerClient implements splithttp.DialerClient in terms of browser dialer
type BrowserDialerClient struct {
transportConfig *Config
}

func (c *BrowserDialerClient) IsClosed() bool {
panic("not implemented yet")
Expand All @@ -22,7 +23,7 @@ func (c *BrowserDialerClient) OpenStream(ctx context.Context, url string, body i
panic("not implemented yet")
}

conn, err := browser_dialer.DialGet(url)
conn, err := browser_dialer.DialGet(url, c.transportConfig.GetRequestHeader())
dummyAddr := &gonet.IPAddr{}
if err != nil {
return nil, dummyAddr, dummyAddr, err
Expand All @@ -37,7 +38,7 @@ func (c *BrowserDialerClient) PostPacket(ctx context.Context, url string, body i
return err
}

err = browser_dialer.DialPost(url, bytes)
err = browser_dialer.DialPost(url, c.transportConfig.GetRequestHeader(), bytes)
if err != nil {
return err
}
Expand Down
18 changes: 12 additions & 6 deletions transport/internet/splithttp/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/xtls/xray-core/transport/internet"
)

const referrerHeaderPaddingPrefix = "https://padding.xray.internal/?x_padding="

func (c *Config) GetNormalizedPath() string {
pathAndQuery := strings.SplitN(c.Path, "?", 2)
path := pathAndQuery[0]
Expand Down Expand Up @@ -39,11 +41,6 @@ func (c *Config) GetNormalizedQuery() string {
}
query += "x_version=" + core.Version()

paddingLen := c.GetNormalizedXPaddingBytes().rand()
if paddingLen > 0 {
query += "&x_padding=" + strings.Repeat("0", int(paddingLen))
}

return query
}

Expand All @@ -53,6 +50,15 @@ func (c *Config) GetRequestHeader() http.Header {
header.Add(k, v)
}

paddingLen := c.GetNormalizedXPaddingBytes().rand()
if paddingLen > 0 {
// https://www.rfc-editor.org/rfc/rfc7541.html#appendix-B
// h2's HPACK Header Compression feature employs a huffman encoding using a static table.
// 'X' is assigned an 8 bit code, so HPACK compression won't change actual padding length on the wire.
// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1.2-2
// h3's similar QPACK feature uses the same huffman table.
header.Set("Referer", referrerHeaderPaddingPrefix+strings.Repeat("X", int(paddingLen)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great

}
return header
}

Expand All @@ -63,7 +69,7 @@ func (c *Config) WriteResponseHeader(writer http.ResponseWriter) {
writer.Header().Set("X-Version", core.Version())
paddingLen := c.GetNormalizedXPaddingBytes().rand()
if paddingLen > 0 {
writer.Header().Set("X-Padding", strings.Repeat("0", int(paddingLen)))
writer.Header().Set("X-Padding", strings.Repeat("X", int(paddingLen)))
}
}

Expand Down
2 changes: 1 addition & 1 deletion transport/internet/splithttp/dialer.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func getHTTPClient(ctx context.Context, dest net.Destination, streamSettings *in
realityConfig := reality.ConfigFromStreamSettings(streamSettings)

if browser_dialer.HasBrowserDialer() && realityConfig != nil {
return &BrowserDialerClient{}, nil
return &BrowserDialerClient{transportConfig: streamSettings.ProtocolSettings.(*Config)}, nil
}

globalDialerAccess.Lock()
Expand Down
22 changes: 19 additions & 3 deletions transport/internet/splithttp/hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
gonet "net"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -110,9 +111,24 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
}

validRange := h.config.GetNormalizedXPaddingBytes()
x_padding := int32(len(request.URL.Query().Get("x_padding")))
if validRange.To > 0 && (x_padding < validRange.From || x_padding > validRange.To) {
errors.LogInfo(context.Background(), "invalid x_padding length:", x_padding)
paddingLength := -1

const paddingQuery = "x_padding"
if referrerPadding := request.Header.Get("Referer"); referrerPadding != "" {
// Browser dialer cannot control the host part of referrer header, so not checking it
if referrerURL, err := url.Parse(referrerPadding); err == nil {
if query := referrerURL.Query(); query.Has(paddingQuery) {
paddingLength = len(query.Get(paddingQuery))
}
}
}

if paddingLength == -1 {
paddingLength = len(request.URL.Query().Get(paddingQuery))
}

if validRange.To > 0 && (int32(paddingLength) < validRange.From || int32(paddingLength) > validRange.To) {
errors.LogInfo(context.Background(), "invalid x_padding length:", int32(paddingLength))
writer.WriteHeader(http.StatusBadRequest)
return
}
Expand Down
Loading