Skip to content

نقل HTTPUpgrade

مقدمة

HTTPUpgrade هو نقل خفيف الوزن يحاكي مصافحة WebSocket (HTTP/1.1 Upgrade: websocket) لكنه لا يستخدم تأطير WebSocket. بعد اكتمال مصافحة الترقية، يصبح الاتصال تدفق TCP خام. هذا يجعله أبسط وأكفأ من نقل WebSocket الكامل، مع الاستمرار في المرور عبر الأجهزة الوسيطة التي تتوقع تدفقات HTTP Upgrade. يدعم TLS وبصمة uTLS وبروتوكول PROXY والبيانات المبكرة.

تسجيل البروتوكول

مسجل باسم "httpupgrade" (transport/internet/httpupgrade/httpupgrade.go:3):

go
const protocolName = "httpupgrade"
  • المتصل: httpupgrade/dialer.go:134-136
  • المستمع: httpupgrade/hub.go:165-167
  • الإعدادات: httpupgrade/config.go:19-23

تدفق الاتصال

المصافحة

dialhttpUpgrade (httpupgrade/dialer.go:46-115) ينفذ ترقية HTTP يدوية:

go
func dialhttpUpgrade(ctx context.Context, dest net.Destination,
    streamSettings *internet.MemoryStreamConfig) (net.Conn, error) {
    // 1. Dial raw TCP via internet.DialSystem
    pconn, _ := internet.DialSystem(ctx, dest, streamSettings.SocketSettings)

    // 2. Optionally wrap with TLS
    if tConfig != nil {
        // Apply TLS with uTLS fingerprinting or standard Go TLS
        conn = tls.UClient(pconn, tlsConfig, fingerprint) // or tls.Client
    }

    // 3. Build HTTP request
    req := &http.Request{
        Method: http.MethodGet,
        URL:    &requestURL,
        Header: make(http.Header),
    }
    req.Header.Set("Connection", "Upgrade")
    req.Header.Set("Upgrade", "websocket")

    // 4. Write request directly to connection
    req.Write(conn)

    // 5. Wrap with ConnRF for response reading
    connRF := &ConnRF{Conn: conn, Req: req, First: true}
    return connRF, nil
}

على عكس نقل WebSocket، هذا لا يستخدم gorilla/websocket. يتم كتابة طلب HTTP مباشرة إلى تدفق TCP ويتم تحليل الاستجابة يدويًا.

قراءة الاستجابة (ConnRF)

بنية ConnRF (httpupgrade/dialer.go:19-44) تعترض أول استدعاء Read لتحليل استجابة HTTP:

go
type ConnRF struct {
    net.Conn
    Req   *http.Request
    First bool
}

func (c *ConnRF) Read(b []byte) (int, error) {
    if c.First {
        c.First = false
        reader := bufio.NewReaderSize(c.Conn, len(b))
        resp, err := http.ReadResponse(reader, c.Req)
        if resp.Status != "101 Switching Protocols" ||
            strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" ||
            strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
            return 0, errors.New("unrecognized reply")
        }
        // Drain buffered bytes from bufreader
        return reader.Read(b[:reader.Buffered()])
    }
    return c.Conn.Read(b)
}

تصميم أساسي: حجم bufio.Reader يساوي بالضبط len(b) لضمان التقاط أي بيانات بعد رؤوس استجابة HTTP (التي قد تصل في نفس مقطع TCP) وإعادتها في هذه القراءة الأولى.

البيانات المبكرة

عندما Ed == 0 (الافتراضي)، تُقرأ الاستجابة فورًا أثناء الاتصال لتأكيد نجاح الترقية (dialer.go:107-112). عندما Ed > 0، يتم تأجيل قراءة الاستجابة إلى أول استدعاء Read من التطبيق، مما يسمح للاتصال بالاكتمال أسرع.

معالجة الرؤوس

يتم إضافة الرؤوس المخصصة عبر دالة AddHeader (dialer.go:120-122) التي تتجاوز تطبيع رؤوس MIME في Go:

go
func AddHeader(header http.Header, key, value string) {
    header[key] = append(header[key], value)
}

هذا يحافظ على حالة أحرف أسماء الرؤوس بالضبط (مثل "Web*S*ocket" بدلاً من "Websocket").

تدفق الاستماع

بنية الخادم

ListenHTTPUpgrade (httpupgrade/hub.go:115-163) ينشئ مستمع TCP خام (ليس خادم HTTP):

go
func ListenHTTPUpgrade(ctx context.Context, address net.Address, port net.Port,
    streamSettings *internet.MemoryStreamConfig, addConn internet.ConnHandler) (internet.Listener, error) {
    // Create TCP/Unix listener via internet.ListenSystem
    // Optionally wrap with TLS
    serverInstance := &server{
        config:         transportConfiguration,
        addConn:        addConn,
        innnerListener: listener,
    }
    go serverInstance.keepAccepting()
    return serverInstance, nil
}

على عكس نقل WebSocket (الذي يستخدم http.Server)، يقبل HTTPUpgrade اتصالات خام ويحلل طلبات HTTP يدويًا.

معالجة الاتصال

server.Handle (httpupgrade/hub.go:34-42) و server.upgrade (hub.go:45-103):

go
func (s *server) upgrade(conn net.Conn) (stat.Connection, error) {
    connReader := bufio.NewReader(conn)
    req, _ := http.ReadRequest(connReader)

    // Validate host and path
    if len(s.config.Host) > 0 && !internet.IsValidHTTPHost(host, s.config.Host) {
        return nil, errors.New("bad host")
    }
    if req.URL.Path != path {
        return nil, errors.New("bad path")
    }

    // Validate upgrade headers
    if connection != "upgrade" || upgrade != "websocket" {
        return nil, errors.New("unrecognized request")
    }

    // Send 101 response
    resp := &http.Response{
        Status:     "101 Switching Protocols",
        StatusCode: 101,
        Proto:      "HTTP/1.1",
        ProtoMajor: 1,
        ProtoMinor: 1,
        Header:     http.Header{},
    }
    resp.Header.Set("Connection", "Upgrade")
    resp.Header.Set("Upgrade", "websocket")
    resp.Write(conn)

    return stat.Connection(newConnection(conn, remoteAddr)), nil
}

X-Forwarded-For

يتم استخراج عنوان IP الحقيقي للعميل من رؤوس إعادة التوجيه (hub.go:83-100)، مع احترام إعداد TrustedXForwardedFor في المقبس.

تنسيق السلك

Client -> Server:
GET /path HTTP/1.1
Host: example.com
Connection: Upgrade
Upgrade: websocket
User-Agent: Mozilla/5.0 ...
[custom headers]

Server -> Client:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket

[raw bidirectional TCP stream]

بعد المصافحة، تتدفق البيانات كبايتات خام -- بدون تأطير WebSocket، بدون بادئات طول، بدون تقنيع. هذا هو الفرق الجوهري عن نقل WebSocket الكامل.

mermaid
sequenceDiagram
    participant Client
    participant Server

    Client->>Server: GET /path HTTP/1.1\r\nUpgrade: websocket\r\n...
    Server->>Client: HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n\r\n
    Note over Client,Server: Raw TCP stream begins
    Client->>Server: [proxy data bytes]
    Server->>Client: [proxy data bytes]

مغلف الاتصال

بنية connection (httpupgrade/connection.go:5-19) بسيطة:

go
type connection struct {
    net.Conn
    remoteAddr net.Addr
}

func (c *connection) RemoteAddr() net.Addr {
    return c.remoteAddr
}

تتجاوز فقط RemoteAddr() لدعم X-Forwarded-For. جميع الطرق الأخرى تُفوض إلى net.Conn الأساسي.

دعم بروتوكول PROXY

يدعم كل من العميل والخادم بروتوكول PROXY:

  • الخادم: موروث من إعدادات المقبس. عندما يكون AcceptProxyProtocol مفعلاً، يغلف internet.ListenSystem الأساسي المستمع (hub.go:117-122، hub.go:145-147).
  • دمج الإعدادات: AcceptProxyProtocol من إعدادات النقل أو إعدادات المقبس يُفعّل بروتوكول PROXY (hub.go:121).

خيارات الإعدادات

من httpupgrade/config.go:

  • Path: مسار URL، مُطبّع بـ / في البداية (config.go:8-17)
  • Host: رأس Host المتوقع للتحقق من جانب الخادم
  • Header: رؤوس HTTP مخصصة (قاموس)
  • AcceptProxyProtocol: تفعيل بروتوكول PROXY على المستمع
  • Ed: حجم البيانات المبكرة. عند قيمة غير صفرية، يتم تأجيل تحليل الاستجابة.

ملاحظات التنفيذ

  • بدون تأطير WebSocket: بعد استجابة 101 Switching Protocols، تُرسل البيانات كبايتات خام. هذا يزيل حمل WebSocket (2-14 بايت لكل إطار) ومتطلب التقنيع.
  • بدون اعتماد على gorilla: على عكس نقل WebSocket، لا يستخدم HTTPUpgrade مكتبة gorilla/websocket. يبني ويحلل رسائل HTTP مباشرة.
  • خادم بدون حالة: كل اتصال يُعالج بشكل مستقل. الخادم لا يحتفظ بحالة جلسة.
  • الحفاظ على حالة أحرف الرؤوس: دالة AddHeader تتجاوز textproto.CanonicalMIMEHeaderKey في Go، مما يسمح بأسماء رؤوس بحالة أحرف محددة. هذا يمكن أن يساعد في مطابقة توقعات CDN أو أجهزة وسيطة محددة.
  • خدعة حجم bufio: ConnRF.Read تنشئ bufio.ReaderSize(conn, len(b)) محدودًا بحجم مخزن المستدعي. هذا يضمن أن القارئ المخزن لا يقرأ أكثر مما يمكن إعادته، مما يمنع فقدان البيانات من التخزين المؤقت المزدوج.
  • TLS على المستمع: على عكس gRPC (الذي يستخدم نظام بيانات اعتماد gRPC)، يغلف HTTPUpgrade المستمع net.Listener بـ tls.NewListener مباشرة (hub.go:149-153).
  • المقارنة مع WebSocket: HTTPUpgrade أبسط وأكفأ بشكل صارم لاستخدام البروكسي. يُفضل نقل WebSocket فقط عند الحاجة لميزات خاصة بـ WebSocket (نبضات Ping، ضغط لكل رسالة، متصل المتصفح).

تحليل تقني لأغراض إعادة التنفيذ.