package browser_dialer import ( "bytes" "context" _ "embed" "encoding/base64" "encoding/json" "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 type task struct { Method string `json:"method"` URL string `json:"url"` Extra any `json:"extra,omitempty"` StreamResponse bool `json:"streamResponse"` } 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.Header().Set("Access-Control-Allow-Origin", "*"); w.Write(webpage) } })) } } func HasBrowserDialer() bool { return conns != nil } type webSocketExtra struct { Protocol string `json:"protocol,omitempty"` } func DialWS(uri string, ed []byte) (*websocket.Conn, error) { task := task{ Method: "WS", URL: uri, StreamResponse: true, } if ed != nil { task.Extra = webSocketExtra{ Protocol: base64.RawURLEncoding.EncodeToString(ed), } } return dialTask(task) } type httpExtra struct { Referrer string `json:"referrer,omitempty"` Headers map[string]string `json:"headers,omitempty"` Cookies map[string]string `json:"cookies,omitempty"` } func httpExtraFromHeadersAndCookies(headers http.Header, cookies []*http.Cookie) *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) } } if len(cookies) > 0 { extra.Cookies = make(map[string]string) for _, cookie := range cookies { extra.Cookies[cookie.Name] = cookie.Value } } return &extra } func DialGet(uri string, headers http.Header, cookies []*http.Cookie) (*websocket.Conn, error) { task := task{ Method: "GET", URL: uri, Extra: httpExtraFromHeadersAndCookies(headers, cookies), StreamResponse: true, } return dialTask(task) } func DialPacket(method string, uri string, headers http.Header, cookies []*http.Cookie, payload []byte) error { return dialWithBody(method, uri, headers, cookies, payload) } func dialWithBody(method string, uri string, headers http.Header, cookies []*http.Cookie, payload []byte) error { task := task{ Method: method, URL: uri, Extra: httpExtraFromHeadersAndCookies(headers, cookies), StreamResponse: false, } conn, err := dialTask(task) 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 dialTask(task task) (*websocket.Conn, error) { data, err := json.Marshal(task) if err != nil { return nil, err } 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 }