mirror of
https://github.com/XTLS/Xray-core.git
synced 2024-11-22 08:31:28 +00:00
Add SplitHTTP Browser Dialer support (#3484)
This commit is contained in:
parent
308f0c64c3
commit
c8f6ba9ff0
121
transport/internet/browser_dialer/dialer.go
Normal file
121
transport/internet/browser_dialer/dialer.go
Normal file
|
@ -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
transport/internet/browser_dialer/dialer.html
Normal file
136
transport/internet/browser_dialer/dialer.html
Normal file
|
@ -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
transport/internet/splithttp/browser_client.go
Normal file
39
transport/internet/splithttp/browser_client.go
Normal file
|
@ -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
|
||||||
|
}
|
169
transport/internet/splithttp/client.go
Normal file
169
transport/internet/splithttp/client.go
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
package splithttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
gonet "net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptrace"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common"
|
||||||
|
"github.com/xtls/xray-core/common/errors"
|
||||||
|
"github.com/xtls/xray-core/common/net"
|
||||||
|
"github.com/xtls/xray-core/common/signal/done"
|
||||||
|
)
|
||||||
|
|
||||||
|
// interface to abstract between use of browser dialer, vs net/http
|
||||||
|
type DialerClient interface {
|
||||||
|
// (ctx, baseURL, payload) -> err
|
||||||
|
// baseURL already contains sessionId and seq
|
||||||
|
SendUploadRequest(context.Context, string, io.ReadWriteCloser, int64) error
|
||||||
|
|
||||||
|
// (ctx, baseURL) -> (downloadReader, remoteAddr, localAddr)
|
||||||
|
// baseURL already contains sessionId
|
||||||
|
OpenDownload(context.Context, string) (io.ReadCloser, net.Addr, net.Addr, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// implements splithttp.DialerClient in terms of direct network connections
|
||||||
|
type DefaultDialerClient struct {
|
||||||
|
transportConfig *Config
|
||||||
|
download *http.Client
|
||||||
|
upload *http.Client
|
||||||
|
isH2 bool
|
||||||
|
// pool of net.Conn, created using dialUploadConn
|
||||||
|
uploadRawPool *sync.Pool
|
||||||
|
dialUploadConn func(ctxInner context.Context) (net.Conn, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DefaultDialerClient) OpenDownload(ctx context.Context, baseURL string) (io.ReadCloser, gonet.Addr, gonet.Addr, error) {
|
||||||
|
var remoteAddr gonet.Addr
|
||||||
|
var localAddr gonet.Addr
|
||||||
|
// this is done when the TCP/UDP connection to the server was established,
|
||||||
|
// and we can unblock the Dial function and print correct net addresses in
|
||||||
|
// logs
|
||||||
|
gotConn := done.New()
|
||||||
|
|
||||||
|
var downResponse io.ReadCloser
|
||||||
|
gotDownResponse := done.New()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
trace := &httptrace.ClientTrace{
|
||||||
|
GotConn: func(connInfo httptrace.GotConnInfo) {
|
||||||
|
remoteAddr = connInfo.Conn.RemoteAddr()
|
||||||
|
localAddr = connInfo.Conn.LocalAddr()
|
||||||
|
gotConn.Close()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// in case we hit an error, we want to unblock this part
|
||||||
|
defer gotConn.Close()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
httptrace.WithClientTrace(ctx, trace),
|
||||||
|
"GET",
|
||||||
|
baseURL,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
errors.LogInfoInner(ctx, err, "failed to construct download http request")
|
||||||
|
gotDownResponse.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header = c.transportConfig.GetRequestHeader()
|
||||||
|
|
||||||
|
response, err := c.download.Do(req)
|
||||||
|
gotConn.Close()
|
||||||
|
if err != nil {
|
||||||
|
errors.LogInfoInner(ctx, err, "failed to send download http request")
|
||||||
|
gotDownResponse.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.StatusCode != 200 {
|
||||||
|
response.Body.Close()
|
||||||
|
errors.LogInfo(ctx, "invalid status code on download:", response.Status)
|
||||||
|
gotDownResponse.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
downResponse = response.Body
|
||||||
|
gotDownResponse.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// we want to block Dial until we know the remote address of the server,
|
||||||
|
// for logging purposes
|
||||||
|
<-gotConn.Wait()
|
||||||
|
|
||||||
|
lazyDownload := &LazyReader{
|
||||||
|
CreateReader: func() (io.ReadCloser, error) {
|
||||||
|
<-gotDownResponse.Wait()
|
||||||
|
if downResponse == nil {
|
||||||
|
return nil, errors.New("downResponse failed")
|
||||||
|
}
|
||||||
|
return downResponse, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return lazyDownload, remoteAddr, localAddr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DefaultDialerClient) SendUploadRequest(ctx context.Context, url string, payload io.ReadWriteCloser, contentLength int64) error {
|
||||||
|
req, err := http.NewRequest("POST", url, payload)
|
||||||
|
req.ContentLength = contentLength
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header = c.transportConfig.GetRequestHeader()
|
||||||
|
|
||||||
|
if c.isH2 {
|
||||||
|
resp, err := c.upload.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return errors.New("bad status code:", resp.Status)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// stringify the entire HTTP/1.1 request so it can be
|
||||||
|
// safely retried. if instead req.Write is called multiple
|
||||||
|
// times, the body is already drained after the first
|
||||||
|
// request
|
||||||
|
requestBytes := new(bytes.Buffer)
|
||||||
|
common.Must(req.Write(requestBytes))
|
||||||
|
|
||||||
|
var uploadConn any
|
||||||
|
|
||||||
|
for {
|
||||||
|
uploadConn = c.uploadRawPool.Get()
|
||||||
|
newConnection := uploadConn == nil
|
||||||
|
if newConnection {
|
||||||
|
uploadConn, err = c.dialUploadConn(context.WithoutCancel(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = uploadConn.(net.Conn).Write(requestBytes.Bytes())
|
||||||
|
|
||||||
|
// if the write failed, we try another connection from
|
||||||
|
// the pool, until the write on a new connection fails.
|
||||||
|
// failed writes to a pooled connection are normal when
|
||||||
|
// the connection has been closed in the meantime.
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
} else if newConnection {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.uploadRawPool.Put(uploadConn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,13 +1,10 @@
|
||||||
package splithttp
|
package splithttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
gotls "crypto/tls"
|
gotls "crypto/tls"
|
||||||
"io"
|
"io"
|
||||||
gonet "net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptrace"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -17,10 +14,10 @@ import (
|
||||||
"github.com/xtls/xray-core/common/buf"
|
"github.com/xtls/xray-core/common/buf"
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
"github.com/xtls/xray-core/common/signal/done"
|
|
||||||
"github.com/xtls/xray-core/common/signal/semaphore"
|
"github.com/xtls/xray-core/common/signal/semaphore"
|
||||||
"github.com/xtls/xray-core/common/uuid"
|
"github.com/xtls/xray-core/common/uuid"
|
||||||
"github.com/xtls/xray-core/transport/internet"
|
"github.com/xtls/xray-core/transport/internet"
|
||||||
|
"github.com/xtls/xray-core/transport/internet/browser_dialer"
|
||||||
"github.com/xtls/xray-core/transport/internet/stat"
|
"github.com/xtls/xray-core/transport/internet/stat"
|
||||||
"github.com/xtls/xray-core/transport/internet/tls"
|
"github.com/xtls/xray-core/transport/internet/tls"
|
||||||
"github.com/xtls/xray-core/transport/pipe"
|
"github.com/xtls/xray-core/transport/pipe"
|
||||||
|
@ -32,32 +29,31 @@ type dialerConf struct {
|
||||||
*internet.MemoryStreamConfig
|
*internet.MemoryStreamConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type reusedClient struct {
|
|
||||||
download *http.Client
|
|
||||||
upload *http.Client
|
|
||||||
isH2 bool
|
|
||||||
// pool of net.Conn, created using dialUploadConn
|
|
||||||
uploadRawPool *sync.Pool
|
|
||||||
dialUploadConn func(ctxInner context.Context) (net.Conn, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
globalDialerMap map[dialerConf]reusedClient
|
globalDialerMap map[dialerConf]DialerClient
|
||||||
globalDialerAccess sync.Mutex
|
globalDialerAccess sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
func getHTTPClient(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) reusedClient {
|
func getHTTPClient(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) DialerClient {
|
||||||
|
if browser_dialer.HasBrowserDialer() {
|
||||||
|
return &BrowserDialerClient{}
|
||||||
|
}
|
||||||
|
|
||||||
globalDialerAccess.Lock()
|
globalDialerAccess.Lock()
|
||||||
defer globalDialerAccess.Unlock()
|
defer globalDialerAccess.Unlock()
|
||||||
|
|
||||||
if globalDialerMap == nil {
|
if globalDialerMap == nil {
|
||||||
globalDialerMap = make(map[dialerConf]reusedClient)
|
globalDialerMap = make(map[dialerConf]DialerClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
if client, found := globalDialerMap[dialerConf{dest, streamSettings}]; found {
|
if client, found := globalDialerMap[dialerConf{dest, streamSettings}]; found {
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if browser_dialer.HasBrowserDialer() {
|
||||||
|
return &BrowserDialerClient{}
|
||||||
|
}
|
||||||
|
|
||||||
tlsConfig := tls.ConfigFromStreamSettings(streamSettings)
|
tlsConfig := tls.ConfigFromStreamSettings(streamSettings)
|
||||||
isH2 := tlsConfig != nil && !(len(tlsConfig.NextProtocol) == 1 && tlsConfig.NextProtocol[0] == "http/1.1")
|
isH2 := tlsConfig != nil && !(len(tlsConfig.NextProtocol) == 1 && tlsConfig.NextProtocol[0] == "http/1.1")
|
||||||
|
|
||||||
|
@ -116,7 +112,8 @@ func getHTTPClient(ctx context.Context, dest net.Destination, streamSettings *in
|
||||||
uploadTransport = nil
|
uploadTransport = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
client := reusedClient{
|
client := &DefaultDialerClient{
|
||||||
|
transportConfig: streamSettings.ProtocolSettings.(*Config),
|
||||||
download: &http.Client{
|
download: &http.Client{
|
||||||
Transport: downloadTransport,
|
Transport: downloadTransport,
|
||||||
},
|
},
|
||||||
|
@ -160,80 +157,9 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
|
||||||
|
|
||||||
httpClient := getHTTPClient(ctx, dest, streamSettings)
|
httpClient := getHTTPClient(ctx, dest, streamSettings)
|
||||||
|
|
||||||
var remoteAddr gonet.Addr
|
|
||||||
var localAddr gonet.Addr
|
|
||||||
// this is done when the TCP/UDP connection to the server was established,
|
|
||||||
// and we can unblock the Dial function and print correct net addresses in
|
|
||||||
// logs
|
|
||||||
gotConn := done.New()
|
|
||||||
|
|
||||||
var downResponse io.ReadCloser
|
|
||||||
gotDownResponse := done.New()
|
|
||||||
|
|
||||||
sessionIdUuid := uuid.New()
|
sessionIdUuid := uuid.New()
|
||||||
sessionId := sessionIdUuid.String()
|
sessionId := sessionIdUuid.String()
|
||||||
|
baseURL := requestURL.String() + sessionId
|
||||||
go func() {
|
|
||||||
trace := &httptrace.ClientTrace{
|
|
||||||
GotConn: func(connInfo httptrace.GotConnInfo) {
|
|
||||||
remoteAddr = connInfo.Conn.RemoteAddr()
|
|
||||||
localAddr = connInfo.Conn.LocalAddr()
|
|
||||||
gotConn.Close()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// in case we hit an error, we want to unblock this part
|
|
||||||
defer gotConn.Close()
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(
|
|
||||||
httptrace.WithClientTrace(context.WithoutCancel(ctx), trace),
|
|
||||||
"GET",
|
|
||||||
requestURL.String()+sessionId,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
errors.LogInfoInner(ctx, err, "failed to construct download http request")
|
|
||||||
gotDownResponse.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header = transportConfiguration.GetRequestHeader()
|
|
||||||
|
|
||||||
response, err := httpClient.download.Do(req)
|
|
||||||
gotConn.Close()
|
|
||||||
if err != nil {
|
|
||||||
errors.LogInfoInner(ctx, err, "failed to send download http request")
|
|
||||||
gotDownResponse.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.StatusCode != 200 {
|
|
||||||
response.Body.Close()
|
|
||||||
errors.LogInfo(ctx, "invalid status code on download:", response.Status)
|
|
||||||
gotDownResponse.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip "ooooooooook" response
|
|
||||||
trashHeader := []byte{0}
|
|
||||||
for {
|
|
||||||
_, err = io.ReadFull(response.Body, trashHeader)
|
|
||||||
if err != nil {
|
|
||||||
response.Body.Close()
|
|
||||||
errors.LogInfoInner(ctx, err, "failed to read initial response")
|
|
||||||
gotDownResponse.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if trashHeader[0] == 'k' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
downResponse = response.Body
|
|
||||||
gotDownResponse.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
uploadUrl := requestURL.String() + sessionId + "/"
|
|
||||||
|
|
||||||
uploadPipeReader, uploadPipeWriter := pipe.New(pipe.WithSizeLimit(maxUploadSize))
|
uploadPipeReader, uploadPipeWriter := pipe.New(pipe.WithSizeLimit(maxUploadSize))
|
||||||
|
|
||||||
|
@ -252,97 +178,55 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
|
||||||
|
|
||||||
<-requestsLimiter.Wait()
|
<-requestsLimiter.Wait()
|
||||||
|
|
||||||
url := uploadUrl + strconv.FormatInt(requestCounter, 10)
|
seq := requestCounter
|
||||||
requestCounter += 1
|
requestCounter += 1
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer requestsLimiter.Signal()
|
defer requestsLimiter.Signal()
|
||||||
req, err := http.NewRequest("POST", url, &buf.MultiBufferContainer{MultiBuffer: chunk})
|
|
||||||
|
err := httpClient.SendUploadRequest(
|
||||||
|
context.WithoutCancel(ctx),
|
||||||
|
baseURL+"/"+strconv.FormatInt(seq, 10),
|
||||||
|
&buf.MultiBufferContainer{MultiBuffer: chunk},
|
||||||
|
int64(chunk.Len()),
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errors.LogInfoInner(ctx, err, "failed to send upload")
|
errors.LogInfoInner(ctx, err, "failed to send upload")
|
||||||
uploadPipeReader.Interrupt()
|
uploadPipeReader.Interrupt()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
req.ContentLength = int64(chunk.Len())
|
}
|
||||||
req.Header = transportConfiguration.GetRequestHeader()
|
}()
|
||||||
|
|
||||||
if httpClient.isH2 {
|
lazyRawDownload, remoteAddr, localAddr, err := httpClient.OpenDownload(context.WithoutCancel(ctx), baseURL)
|
||||||
resp, err := httpClient.upload.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errors.LogInfoInner(ctx, err, "failed to send upload")
|
return nil, err
|
||||||
uploadPipeReader.Interrupt()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
errors.LogInfo(ctx, "failed to send upload, bad status code:", resp.Status)
|
|
||||||
uploadPipeReader.Interrupt()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var uploadConn any
|
|
||||||
|
|
||||||
// stringify the entire HTTP/1.1 request so it can be
|
|
||||||
// safely retried. if instead req.Write is called multiple
|
|
||||||
// times, the body is already drained after the first
|
|
||||||
// request
|
|
||||||
requestBytes := new(bytes.Buffer)
|
|
||||||
common.Must(req.Write(requestBytes))
|
|
||||||
|
|
||||||
|
lazyDownload := &LazyReader{
|
||||||
|
CreateReader: func() (io.ReadCloser, error) {
|
||||||
|
// skip "ooooooooook" response
|
||||||
|
trashHeader := []byte{0}
|
||||||
for {
|
for {
|
||||||
uploadConn = httpClient.uploadRawPool.Get()
|
_, err := io.ReadFull(lazyRawDownload, trashHeader)
|
||||||
newConnection := uploadConn == nil
|
|
||||||
if newConnection {
|
|
||||||
uploadConn, err = httpClient.dialUploadConn(context.WithoutCancel(ctx))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errors.LogInfoInner(ctx, err, "failed to connect upload")
|
return nil, errors.New("failed to read initial response").Base(err)
|
||||||
uploadPipeReader.Interrupt()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
if trashHeader[0] == 'k' {
|
||||||
|
|
||||||
_, err = uploadConn.(net.Conn).Write(requestBytes.Bytes())
|
|
||||||
|
|
||||||
// if the write failed, we try another connection from
|
|
||||||
// the pool, until the write on a new connection fails.
|
|
||||||
// failed writes to a pooled connection are normal when
|
|
||||||
// the connection has been closed in the meantime.
|
|
||||||
if err == nil {
|
|
||||||
break
|
break
|
||||||
} else if newConnection {
|
|
||||||
errors.LogInfoInner(ctx, err, "failed to send upload")
|
|
||||||
uploadPipeReader.Interrupt()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
httpClient.uploadRawPool.Put(uploadConn)
|
return lazyRawDownload, nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// we want to block Dial until we know the remote address of the server,
|
|
||||||
// for logging purposes
|
|
||||||
<-gotConn.Wait()
|
|
||||||
|
|
||||||
// necessary in order to send larger chunks in upload
|
// necessary in order to send larger chunks in upload
|
||||||
bufferedUploadPipeWriter := buf.NewBufferedWriter(uploadPipeWriter)
|
bufferedUploadPipeWriter := buf.NewBufferedWriter(uploadPipeWriter)
|
||||||
bufferedUploadPipeWriter.SetBuffered(false)
|
bufferedUploadPipeWriter.SetBuffered(false)
|
||||||
|
|
||||||
lazyDownload := &LazyReader{
|
|
||||||
CreateReader: func() (io.ReadCloser, error) {
|
|
||||||
<-gotDownResponse.Wait()
|
|
||||||
if downResponse == nil {
|
|
||||||
return nil, errors.New("downResponse failed")
|
|
||||||
}
|
|
||||||
return downResponse, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
conn := splitConn{
|
conn := splitConn{
|
||||||
writer: bufferedUploadPipeWriter,
|
writer: bufferedUploadPipeWriter,
|
||||||
reader: lazyDownload,
|
reader: lazyDownload,
|
||||||
|
|
|
@ -32,7 +32,7 @@ type requestHandler struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type httpSession struct {
|
type httpSession struct {
|
||||||
uploadQueue *UploadQueue
|
uploadQueue *uploadQueue
|
||||||
// for as long as the GET request is not opened by the client, this will be
|
// for as long as the GET request is not opened by the client, this will be
|
||||||
// open ("undone"), and the session may be expired within a certain TTL.
|
// open ("undone"), and the session may be expired within a certain TTL.
|
||||||
// after the client connects, this becomes "done" and the session lives as
|
// after the client connects, this becomes "done" and the session lives as
|
||||||
|
|
|
@ -15,7 +15,7 @@ type Packet struct {
|
||||||
Seq uint64
|
Seq uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
type UploadQueue struct {
|
type uploadQueue struct {
|
||||||
pushedPackets chan Packet
|
pushedPackets chan Packet
|
||||||
heap uploadHeap
|
heap uploadHeap
|
||||||
nextSeq uint64
|
nextSeq uint64
|
||||||
|
@ -23,8 +23,8 @@ type UploadQueue struct {
|
||||||
maxPackets int
|
maxPackets int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUploadQueue(maxPackets int) *UploadQueue {
|
func NewUploadQueue(maxPackets int) *uploadQueue {
|
||||||
return &UploadQueue{
|
return &uploadQueue{
|
||||||
pushedPackets: make(chan Packet, maxPackets),
|
pushedPackets: make(chan Packet, maxPackets),
|
||||||
heap: uploadHeap{},
|
heap: uploadHeap{},
|
||||||
nextSeq: 0,
|
nextSeq: 0,
|
||||||
|
@ -33,7 +33,7 @@ func NewUploadQueue(maxPackets int) *UploadQueue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UploadQueue) Push(p Packet) error {
|
func (h *uploadQueue) Push(p Packet) error {
|
||||||
if h.closed {
|
if h.closed {
|
||||||
return errors.New("splithttp packet queue closed")
|
return errors.New("splithttp packet queue closed")
|
||||||
}
|
}
|
||||||
|
@ -42,13 +42,13 @@ func (h *UploadQueue) Push(p Packet) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UploadQueue) Close() error {
|
func (h *uploadQueue) Close() error {
|
||||||
h.closed = true
|
h.closed = true
|
||||||
close(h.pushedPackets)
|
close(h.pushedPackets)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UploadQueue) Read(b []byte) (int, error) {
|
func (h *uploadQueue) Read(b []byte) (int, error) {
|
||||||
if h.closed {
|
if h.closed {
|
||||||
return 0, io.EOF
|
return 0, io.EOF
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,11 @@ var _ buf.Writer = (*connection)(nil)
|
||||||
type connection struct {
|
type connection struct {
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
reader io.Reader
|
reader io.Reader
|
||||||
remoteAddr net.Addr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newConnection(conn *websocket.Conn, remoteAddr net.Addr, extraReader io.Reader) *connection {
|
func NewConnection(conn *websocket.Conn, remoteAddr net.Addr, extraReader io.Reader) *connection {
|
||||||
return &connection{
|
return &connection{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
remoteAddr: remoteAddr,
|
|
||||||
reader: extraReader,
|
reader: extraReader,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,7 +90,7 @@ func (c *connection) LocalAddr() net.Addr {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *connection) RemoteAddr() net.Addr {
|
func (c *connection) RemoteAddr() net.Addr {
|
||||||
return c.remoteAddr
|
return c.conn.RemoteAddr()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *connection) SetDeadline(t time.Time) error {
|
func (c *connection) SetDeadline(t time.Time) error {
|
||||||
|
|
|
@ -1,54 +1,23 @@
|
||||||
package websocket
|
package websocket
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"io"
|
"io"
|
||||||
gonet "net"
|
gonet "net"
|
||||||
"net/http"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/xtls/xray-core/common"
|
"github.com/xtls/xray-core/common"
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
"github.com/xtls/xray-core/common/platform"
|
|
||||||
"github.com/xtls/xray-core/common/uuid"
|
|
||||||
"github.com/xtls/xray-core/transport/internet"
|
"github.com/xtls/xray-core/transport/internet"
|
||||||
|
"github.com/xtls/xray-core/transport/internet/browser_dialer"
|
||||||
"github.com/xtls/xray-core/transport/internet/stat"
|
"github.com/xtls/xray-core/transport/internet/stat"
|
||||||
"github.com/xtls/xray-core/transport/internet/tls"
|
"github.com/xtls/xray-core/transport/internet/tls"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed dialer.html
|
|
||||||
var webpage []byte
|
|
||||||
|
|
||||||
var conns chan *websocket.Conn
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial dials a WebSocket connection to the given destination.
|
// Dial dials a WebSocket connection to the given destination.
|
||||||
func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) {
|
func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) {
|
||||||
errors.LogInfo(ctx, "creating connection to ", dest)
|
errors.LogInfo(ctx, "creating connection to ", dest)
|
||||||
|
@ -124,28 +93,13 @@ func dialWebSocket(ctx context.Context, dest net.Destination, streamSettings *in
|
||||||
}
|
}
|
||||||
uri := protocol + "://" + host + wsSettings.GetNormalizedPath()
|
uri := protocol + "://" + host + wsSettings.GetNormalizedPath()
|
||||||
|
|
||||||
if conns != nil {
|
if browser_dialer.HasBrowserDialer() {
|
||||||
data := []byte(uri)
|
conn, err := browser_dialer.DialWS(uri, ed)
|
||||||
if ed != nil {
|
if err != nil {
|
||||||
data = append(data, " "+base64.RawURLEncoding.EncodeToString(ed)...)
|
|
||||||
}
|
|
||||||
var conn *websocket.Conn
|
|
||||||
for {
|
|
||||||
conn = <-conns
|
|
||||||
if conn.WriteMessage(websocket.TextMessage, data) != nil {
|
|
||||||
conn.Close()
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if _, p, err := conn.ReadMessage(); err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if s := string(p); s != "ok" {
|
|
||||||
conn.Close()
|
|
||||||
return nil, errors.New(s)
|
|
||||||
}
|
}
|
||||||
return newConnection(conn, conn.RemoteAddr(), nil), nil
|
|
||||||
|
return NewConnection(conn, conn.RemoteAddr(), nil), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
header := wsSettings.GetRequestHeader()
|
header := wsSettings.GetRequestHeader()
|
||||||
|
@ -163,7 +117,7 @@ func dialWebSocket(ctx context.Context, dest net.Destination, streamSettings *in
|
||||||
return nil, errors.New("failed to dial to (", uri, "): ", reason).Base(err)
|
return nil, errors.New("failed to dial to (", uri, "): ", reason).Base(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return newConnection(conn, conn.RemoteAddr(), nil), nil
|
return NewConnection(conn, conn.RemoteAddr(), nil), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type delayDialConn struct {
|
type delayDialConn struct {
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
<!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 count = 0
|
|
||||||
setInterval(check, 1000)
|
|
||||||
function check() {
|
|
||||||
if (count <= 0) {
|
|
||||||
count += 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";
|
|
||||||
var wss = undefined
|
|
||||||
var first = true
|
|
||||||
ws.onmessage = function (event) {
|
|
||||||
if (first) {
|
|
||||||
first = false
|
|
||||||
count -= 1
|
|
||||||
var arr = event.data.split(" ")
|
|
||||||
console.log("Dial", arr[0], arr[1])
|
|
||||||
wss = new WebSocket(arr[0], arr[1])
|
|
||||||
wss.binaryType = "arraybuffer";
|
|
||||||
var opened = false
|
|
||||||
wss.onopen = function (event) {
|
|
||||||
opened = true
|
|
||||||
ws.send("ok")
|
|
||||||
}
|
|
||||||
wss.onmessage = function (event) {
|
|
||||||
ws.send(event.data)
|
|
||||||
}
|
|
||||||
wss.onclose = function (event) {
|
|
||||||
ws.close()
|
|
||||||
}
|
|
||||||
wss.onerror = function (event) {
|
|
||||||
!opened && ws.send("fail")
|
|
||||||
wss.close()
|
|
||||||
}
|
|
||||||
check()
|
|
||||||
} else wss.send(event.data)
|
|
||||||
}
|
|
||||||
ws.onclose = function (event) {
|
|
||||||
if (first) count -= 1
|
|
||||||
else wss.close()
|
|
||||||
}
|
|
||||||
ws.onerror = function (event) {
|
|
||||||
ws.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -73,7 +73,7 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h.ln.addConn(newConnection(conn, remoteAddr, extraReader))
|
h.ln.addConn(NewConnection(conn, remoteAddr, extraReader))
|
||||||
}
|
}
|
||||||
|
|
||||||
type Listener struct {
|
type Listener struct {
|
||||||
|
|
Loading…
Reference in a new issue