Skip to content

Commit

Permalink
Move x_padding to Referer header
Browse files Browse the repository at this point in the history
  • Loading branch information
rPDmYQ committed Jan 18, 2025
1 parent 30cb22a commit 1f5b32a
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 48 deletions.
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
68 changes: 46 additions & 22 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,16 +103,18 @@
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();
Expand All @@ -101,40 +123,42 @@
const { done, value } = await reader.read();
ws.send(value);
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)))
}
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

0 comments on commit 1f5b32a

Please sign in to comment.