add plain text WIP
This commit is contained in:
@@ -0,0 +1,426 @@
|
||||
--- /dev/null
|
||||
+++ b/protocol/obfhttp/outbound.go
|
||||
@@ -0,0 +1,423 @@
|
||||
+// OMV
|
||||
+package obfhttp
|
||||
+
|
||||
+import (
|
||||
+ "bytes"
|
||||
+ "compress/gzip"
|
||||
+ "context"
|
||||
+ "encoding/json"
|
||||
+ "io"
|
||||
+ "net"
|
||||
+ "net/http"
|
||||
+ "sync"
|
||||
+ "time"
|
||||
+
|
||||
+ "github.com/sagernet/sing-box/adapter"
|
||||
+ "github.com/sagernet/sing-box/adapter/outbound"
|
||||
+ "github.com/sagernet/sing-box/common/dialer"
|
||||
+ boxTLS "github.com/sagernet/sing-box/common/tls"
|
||||
+ C "github.com/sagernet/sing-box/constant"
|
||||
+ "github.com/sagernet/sing-box/log"
|
||||
+ "github.com/sagernet/sing-box/option"
|
||||
+ "github.com/sagernet/sing/common"
|
||||
+ E "github.com/sagernet/sing/common/exceptions"
|
||||
+ "github.com/sagernet/sing/common/logger"
|
||||
+ M "github.com/sagernet/sing/common/metadata"
|
||||
+ N "github.com/sagernet/sing/common/network"
|
||||
+ "github.com/sagernet/sing/common/uot"
|
||||
+)
|
||||
+
|
||||
+func RegisterOutbound(registry *outbound.Registry) {
|
||||
+ outbound.Register[option.ObfHTTPOutboundOptions](registry, C.TypeObfHTTP, NewOutbound)
|
||||
+}
|
||||
+
|
||||
+type Outbound struct {
|
||||
+ outbound.Adapter
|
||||
+ ctx context.Context
|
||||
+ logger logger.ContextLogger
|
||||
+ dialer N.Dialer
|
||||
+ serverAddr M.Socksaddr
|
||||
+ tlsConfig boxTLS.Config
|
||||
+ client *http.Client
|
||||
+ encryptor Encryptor
|
||||
+ codec TextCodec
|
||||
+ path string
|
||||
+ username string
|
||||
+ password string
|
||||
+ scheme string
|
||||
+ longPollTimeout time.Duration
|
||||
+}
|
||||
+
|
||||
+func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ObfHTTPOutboundOptions) (adapter.Outbound, error) {
|
||||
+ outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+
|
||||
+ ob := &Outbound{
|
||||
+ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeObfHTTP, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
|
||||
+ ctx: ctx,
|
||||
+ logger: logger,
|
||||
+ dialer: outboundDialer,
|
||||
+ serverAddr: options.ServerOptions.Build(),
|
||||
+ codec: newTextCodec(options.Encoding),
|
||||
+ path: options.Path,
|
||||
+ username: options.Username,
|
||||
+ password: options.Password,
|
||||
+ scheme: "http",
|
||||
+ }
|
||||
+
|
||||
+ if ob.path == "" {
|
||||
+ ob.path = "/"
|
||||
+ }
|
||||
+ if ob.serverAddr.Port == 0 {
|
||||
+ ob.serverAddr.Port = 80
|
||||
+ }
|
||||
+
|
||||
+ // Encryption
|
||||
+ if options.Encryption != nil {
|
||||
+ enc, err := NewEncryptor(options.Encryption.Method, options.Encryption.Password)
|
||||
+ if err != nil {
|
||||
+ return nil, E.Cause(err, "create encryptor")
|
||||
+ }
|
||||
+ ob.encryptor = enc
|
||||
+ } else {
|
||||
+ ob.encryptor = &noneEncryptor{}
|
||||
+ }
|
||||
+
|
||||
+ ob.longPollTimeout = time.Duration(options.LongPollTimeout)
|
||||
+ if ob.longPollTimeout == 0 {
|
||||
+ ob.longPollTimeout = 30 * time.Second
|
||||
+ }
|
||||
+
|
||||
+ // TLS
|
||||
+ var tlsConfig boxTLS.Config
|
||||
+ if options.TLS != nil && options.TLS.Enabled {
|
||||
+ tlsConfig, err = boxTLS.NewClient(ctx, logger, ob.serverAddr.AddrString(), common.PtrValueOrDefault(options.TLS))
|
||||
+ if err != nil {
|
||||
+ return nil, E.Cause(err, "create TLS config")
|
||||
+ }
|
||||
+ ob.tlsConfig = tlsConfig
|
||||
+ ob.scheme = "https"
|
||||
+ }
|
||||
+
|
||||
+ transport := &http.Transport{
|
||||
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
+ conn, err := ob.dialer.DialContext(ctx, N.NetworkTCP, ob.serverAddr)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ if ob.tlsConfig != nil {
|
||||
+ tlsConn, err := boxTLS.ClientHandshake(ctx, conn, ob.tlsConfig)
|
||||
+ if err != nil {
|
||||
+ conn.Close()
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ return tlsConn, nil
|
||||
+ }
|
||||
+ return conn, nil
|
||||
+ },
|
||||
+ MaxIdleConns: 100,
|
||||
+ MaxIdleConnsPerHost: 100,
|
||||
+ IdleConnTimeout: 90 * time.Second,
|
||||
+ DisableCompression: true,
|
||||
+ }
|
||||
+
|
||||
+ ob.client = &http.Client{
|
||||
+ Transport: transport,
|
||||
+ Timeout: ob.longPollTimeout + 10*time.Second,
|
||||
+ }
|
||||
+
|
||||
+ return ob, nil
|
||||
+}
|
||||
+
|
||||
+func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
+ switch N.NetworkName(network) {
|
||||
+ case N.NetworkTCP:
|
||||
+ h.logger.InfoContext(ctx, "outbound connection to ", destination)
|
||||
+ return h.openConnection(ctx, "tcp", destination)
|
||||
+ case N.NetworkUDP:
|
||||
+ h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
|
||||
+ conn, err := h.openConnection(ctx, "tcp", uot.RequestDestination(uot.Version))
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ return uot.NewLazyConn(conn, uot.Request{
|
||||
+ IsConnect: true,
|
||||
+ Destination: destination,
|
||||
+ }), nil
|
||||
+ default:
|
||||
+ return nil, E.New("unsupported network: ", network)
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
+ h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
|
||||
+ conn, err := h.openConnection(ctx, "tcp", uot.RequestDestination(uot.Version))
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ return uot.NewLazyConn(conn, uot.Request{
|
||||
+ IsConnect: false,
|
||||
+ Destination: destination,
|
||||
+ }), nil
|
||||
+}
|
||||
+
|
||||
+func (h *Outbound) openConnection(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
+ // Send open request
|
||||
+ resp, err := h.doRequest(&request{
|
||||
+ Action: actionOpen,
|
||||
+ Destination: destination.String(),
|
||||
+ Network: network,
|
||||
+ })
|
||||
+ if err != nil {
|
||||
+ return nil, E.Cause(err, "open session")
|
||||
+ }
|
||||
+ if !resp.OK {
|
||||
+ return nil, E.New("open session failed: ", resp.Error)
|
||||
+ }
|
||||
+
|
||||
+ cc := &clientConn{
|
||||
+ outbound: h,
|
||||
+ sessionID: resp.Session,
|
||||
+ ctx: ctx,
|
||||
+ }
|
||||
+ cc.cond = sync.NewCond(&cc.mu)
|
||||
+
|
||||
+ // Process piggybacked data from open response
|
||||
+ if resp.Payload != "" {
|
||||
+ data, err := h.decodePayload(resp.Payload)
|
||||
+ if err != nil {
|
||||
+ return nil, E.Cause(err, "decode open response")
|
||||
+ }
|
||||
+ cc.readBuf = data
|
||||
+ }
|
||||
+
|
||||
+ // Start background long-poll goroutine
|
||||
+ go cc.pollLoop()
|
||||
+
|
||||
+ return cc, nil
|
||||
+}
|
||||
+
|
||||
+func (h *Outbound) doRequest(req *request) (*response, error) {
|
||||
+ // Encode payload if present
|
||||
+ if req.Payload != "" {
|
||||
+ // Payload is already encoded at this point (by clientConn)
|
||||
+ }
|
||||
+
|
||||
+ // Add padding
|
||||
+ req.Padding = h.codec.GeneratePadding(randomPaddingLength())
|
||||
+
|
||||
+ body, err := json.Marshal(req)
|
||||
+ if err != nil {
|
||||
+ return nil, E.Cause(err, "marshal request")
|
||||
+ }
|
||||
+
|
||||
+ url := h.scheme + "://" + h.serverAddr.String() + h.path
|
||||
+ httpReq, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
||||
+ if err != nil {
|
||||
+ return nil, E.Cause(err, "create http request")
|
||||
+ }
|
||||
+ httpReq.Header.Set("Content-Type", "application/json")
|
||||
+
|
||||
+ if h.username != "" || h.password != "" {
|
||||
+ httpReq.SetBasicAuth(h.username, h.password)
|
||||
+ }
|
||||
+
|
||||
+ httpResp, err := h.client.Do(httpReq)
|
||||
+ if err != nil {
|
||||
+ return nil, E.Cause(err, "http request")
|
||||
+ }
|
||||
+ defer httpResp.Body.Close()
|
||||
+
|
||||
+ respBody, err := h.readResponseBody(httpResp)
|
||||
+ if err != nil {
|
||||
+ return nil, E.Cause(err, "read response body")
|
||||
+ }
|
||||
+
|
||||
+ var resp response
|
||||
+ if err := json.Unmarshal(respBody, &resp); err != nil {
|
||||
+ return nil, E.Cause(err, "unmarshal response")
|
||||
+ }
|
||||
+
|
||||
+ return &resp, nil
|
||||
+}
|
||||
+
|
||||
+func (h *Outbound) readResponseBody(resp *http.Response) ([]byte, error) {
|
||||
+ var reader io.Reader = resp.Body
|
||||
+ if resp.Header.Get("Content-Encoding") == "gzip" {
|
||||
+ gr, err := gzip.NewReader(resp.Body)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ defer gr.Close()
|
||||
+ reader = gr
|
||||
+ }
|
||||
+ return io.ReadAll(reader)
|
||||
+}
|
||||
+
|
||||
+func (h *Outbound) encodePayload(data []byte) (string, error) {
|
||||
+ encrypted, err := h.encryptor.Encrypt(data)
|
||||
+ if err != nil {
|
||||
+ return "", err
|
||||
+ }
|
||||
+ return h.codec.Encode(encrypted), nil
|
||||
+}
|
||||
+
|
||||
+func (h *Outbound) decodePayload(payload string) ([]byte, error) {
|
||||
+ decoded, err := h.codec.Decode(payload)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ return h.encryptor.Decrypt(decoded)
|
||||
+}
|
||||
+
|
||||
+func (h *Outbound) Close() error {
|
||||
+ h.client.CloseIdleConnections()
|
||||
+ return common.Close(h.tlsConfig)
|
||||
+}
|
||||
+
|
||||
+// clientConn implements net.Conn, bridging to the HTTP-based obfhttp protocol.
|
||||
+type clientConn struct {
|
||||
+ outbound *Outbound
|
||||
+ sessionID string
|
||||
+ ctx context.Context
|
||||
+
|
||||
+ mu sync.Mutex
|
||||
+ cond *sync.Cond
|
||||
+ readBuf []byte
|
||||
+ closed bool
|
||||
+ pollDone bool
|
||||
+}
|
||||
+
|
||||
+func (c *clientConn) Read(p []byte) (int, error) {
|
||||
+ c.mu.Lock()
|
||||
+ defer c.mu.Unlock()
|
||||
+
|
||||
+ for len(c.readBuf) == 0 && !c.closed {
|
||||
+ c.cond.Wait()
|
||||
+ }
|
||||
+ if len(c.readBuf) == 0 && c.closed {
|
||||
+ return 0, net.ErrClosed
|
||||
+ }
|
||||
+
|
||||
+ n := copy(p, c.readBuf)
|
||||
+ c.readBuf = c.readBuf[n:]
|
||||
+ return n, nil
|
||||
+}
|
||||
+
|
||||
+func (c *clientConn) Write(p []byte) (int, error) {
|
||||
+ if c.closed {
|
||||
+ return 0, net.ErrClosed
|
||||
+ }
|
||||
+
|
||||
+ encoded, err := c.outbound.encodePayload(p)
|
||||
+ if err != nil {
|
||||
+ return 0, err
|
||||
+ }
|
||||
+
|
||||
+ resp, err := c.outbound.doRequest(&request{
|
||||
+ Session: c.sessionID,
|
||||
+ Action: actionData,
|
||||
+ Payload: encoded,
|
||||
+ })
|
||||
+ if err != nil {
|
||||
+ return 0, err
|
||||
+ }
|
||||
+ if !resp.OK {
|
||||
+ return 0, E.New("data request failed: ", resp.Error)
|
||||
+ }
|
||||
+
|
||||
+ // Process piggybacked downstream data
|
||||
+ if resp.Payload != "" {
|
||||
+ data, err := c.outbound.decodePayload(resp.Payload)
|
||||
+ if err != nil {
|
||||
+ return 0, E.Cause(err, "decode piggyback data")
|
||||
+ }
|
||||
+ c.mu.Lock()
|
||||
+ c.readBuf = append(c.readBuf, data...)
|
||||
+ c.cond.Broadcast()
|
||||
+ c.mu.Unlock()
|
||||
+ }
|
||||
+
|
||||
+ return len(p), nil
|
||||
+}
|
||||
+
|
||||
+func (c *clientConn) Close() error {
|
||||
+ c.mu.Lock()
|
||||
+ if c.closed {
|
||||
+ c.mu.Unlock()
|
||||
+ return nil
|
||||
+ }
|
||||
+ c.closed = true
|
||||
+ c.cond.Broadcast()
|
||||
+ c.mu.Unlock()
|
||||
+
|
||||
+ // Send close to server (best effort)
|
||||
+ c.outbound.doRequest(&request{
|
||||
+ Session: c.sessionID,
|
||||
+ Action: actionClose,
|
||||
+ })
|
||||
+ return nil
|
||||
+}
|
||||
+
|
||||
+func (c *clientConn) pollLoop() {
|
||||
+ defer func() {
|
||||
+ c.mu.Lock()
|
||||
+ c.pollDone = true
|
||||
+ c.cond.Broadcast()
|
||||
+ c.mu.Unlock()
|
||||
+ }()
|
||||
+
|
||||
+ for {
|
||||
+ c.mu.Lock()
|
||||
+ closed := c.closed
|
||||
+ c.mu.Unlock()
|
||||
+ if closed {
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
+ resp, err := c.outbound.doRequest(&request{
|
||||
+ Session: c.sessionID,
|
||||
+ Action: actionRecv,
|
||||
+ })
|
||||
+ if err != nil {
|
||||
+ c.mu.Lock()
|
||||
+ c.closed = true
|
||||
+ c.cond.Broadcast()
|
||||
+ c.mu.Unlock()
|
||||
+ return
|
||||
+ }
|
||||
+ if !resp.OK {
|
||||
+ c.mu.Lock()
|
||||
+ c.closed = true
|
||||
+ c.cond.Broadcast()
|
||||
+ c.mu.Unlock()
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
+ if resp.Payload != "" {
|
||||
+ data, err := c.outbound.decodePayload(resp.Payload)
|
||||
+ if err != nil {
|
||||
+ c.mu.Lock()
|
||||
+ c.closed = true
|
||||
+ c.cond.Broadcast()
|
||||
+ c.mu.Unlock()
|
||||
+ return
|
||||
+ }
|
||||
+ c.mu.Lock()
|
||||
+ c.readBuf = append(c.readBuf, data...)
|
||||
+ c.cond.Broadcast()
|
||||
+ c.mu.Unlock()
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func (c *clientConn) LocalAddr() net.Addr { return &simpleAddr{network: "tcp", address: "obfhttp-client"} }
|
||||
+func (c *clientConn) RemoteAddr() net.Addr { return &simpleAddr{network: "tcp", address: c.outbound.serverAddr.String()} }
|
||||
+func (c *clientConn) SetDeadline(t time.Time) error { return nil }
|
||||
+func (c *clientConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
+func (c *clientConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
+
|
||||
+// compile-time check
|
||||
+var _ net.Conn = (*clientConn)(nil)
|
||||
Reference in New Issue
Block a user