mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 03:27:02 +08:00
346 lines
11 KiB
Go
346 lines
11 KiB
Go
// capture_pipeline.go 负责服务请求响应体的捕获、解码和事件追加。
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// exchangeIDContextKey 隔离反向服务内部使用的捕获编号。
|
|
type exchangeIDContextKey struct{}
|
|
|
|
// captureRequest 捕获请求元数据并安装请求体读取器。
|
|
func (server *Server) captureRequest(request *http.Request) *http.Request {
|
|
if request == nil {
|
|
return request
|
|
}
|
|
server.captureMu.RLock()
|
|
id := strconv.FormatUint(server.counter.Add(1), 10)
|
|
path := request.URL.Path
|
|
upstreamURL := *request.URL
|
|
upstreamURL.Scheme = server.upstream.Scheme
|
|
upstreamURL.Host = server.upstream.Host
|
|
upstreamURL.User = nil
|
|
requestContentType := request.Header.Get("Content-Type")
|
|
requestCodec := requestContentCodec(path, request.Header)
|
|
exchange := &Exchange{
|
|
ExchangeSummary: ExchangeSummary{
|
|
ID: id,
|
|
StartedAt: time.Now(),
|
|
Method: request.Method,
|
|
URL: upstreamURL.String(),
|
|
Host: server.upstream.Host,
|
|
Path: path,
|
|
State: "pending",
|
|
},
|
|
Request: Payload{
|
|
Headers: sortedHeaders(request.Header),
|
|
ContentType: requestContentType,
|
|
ContentCodec: requestCodec,
|
|
Frames: make([]FrameView, 0),
|
|
},
|
|
Response: Payload{Headers: make([]Header, 0), Frames: make([]FrameView, 0)},
|
|
}
|
|
server.store.create(exchange)
|
|
server.captureMu.RUnlock()
|
|
request = request.WithContext(context.WithValue(request.Context(), exchangeIDContextKey{}, id))
|
|
request.Close = false
|
|
|
|
if request.Body == nil {
|
|
server.finishRequestBody(id, path, requestContentType, requestCodec, nil, 0, false, nil)
|
|
return request
|
|
}
|
|
var frameDecoder *connectFrameDecoder
|
|
if messageType := streamingRequestMessageType(path); messageType != "" {
|
|
frameDecoder = newConnectFrameDecoder(
|
|
messageType,
|
|
requestCodec,
|
|
server.config.MaxFrames,
|
|
func(frame FrameView) { server.appendRequestFrame(id, frame) },
|
|
)
|
|
}
|
|
request.Body = newCaptureReadCloser(
|
|
request.Body,
|
|
server.config.MaxCaptureBytes,
|
|
func(chunk []byte) {
|
|
if frameDecoder != nil {
|
|
frameDecoder.Write(chunk)
|
|
}
|
|
},
|
|
func(captured []byte, size int64, truncated bool, readErr error) {
|
|
if frameDecoder != nil {
|
|
frameDecoder.Close()
|
|
}
|
|
server.finishRequestBody(id, path, requestContentType, requestCodec, captured, size, truncated, readErr)
|
|
},
|
|
)
|
|
return request
|
|
}
|
|
|
|
// clearExchanges 清空内存和持久化捕获,并重置递增编号。
|
|
func (server *Server) clearExchanges() error {
|
|
server.captureMu.Lock()
|
|
defer server.captureMu.Unlock()
|
|
if err := server.store.clear(); err != nil {
|
|
return err
|
|
}
|
|
server.counter.Store(0)
|
|
return nil
|
|
}
|
|
|
|
// captureResponse 创建响应记录更新并包装响应体捕获器。
|
|
func (server *Server) captureResponse(response *http.Response) error {
|
|
if response == nil {
|
|
return nil
|
|
}
|
|
id := exchangeID(response.Request)
|
|
if id == "" {
|
|
return nil
|
|
}
|
|
path := ""
|
|
if response.Request != nil && response.Request.URL != nil {
|
|
path = response.Request.URL.Path
|
|
}
|
|
responseCodec := responseContentCodec(path, response.Header)
|
|
responseContentType := response.Header.Get("Content-Type")
|
|
server.store.update(id, func(exchange *Exchange) {
|
|
exchange.Status = response.StatusCode
|
|
exchange.State = "streaming"
|
|
exchange.DurationMS = elapsedMS(exchange.StartedAt)
|
|
exchange.Response.Headers = sortedHeaders(response.Header)
|
|
exchange.Response.ContentType = responseContentType
|
|
exchange.Response.ContentCodec = responseCodec
|
|
})
|
|
if response.Body == nil {
|
|
server.finishResponseBody(id, path, responseContentType, responseCodec, nil, 0, false, nil)
|
|
return nil
|
|
}
|
|
|
|
var frameDecoder *connectFrameDecoder
|
|
if messageType := streamingResponseMessageType(path); messageType != "" {
|
|
frameDecoder = newConnectFrameDecoder(
|
|
messageType,
|
|
responseCodec,
|
|
server.config.MaxFrames,
|
|
func(frame FrameView) { server.appendResponseFrame(id, frame) },
|
|
)
|
|
}
|
|
response.Body = newCaptureReadCloser(
|
|
response.Body,
|
|
server.config.MaxCaptureBytes,
|
|
func(chunk []byte) {
|
|
if frameDecoder != nil {
|
|
frameDecoder.Write(chunk)
|
|
}
|
|
},
|
|
func(captured []byte, size int64, truncated bool, readErr error) {
|
|
if frameDecoder != nil {
|
|
frameDecoder.Close()
|
|
}
|
|
server.finishResponseBody(id, path, responseContentType, responseCodec, captured, size, truncated, readErr)
|
|
},
|
|
)
|
|
return nil
|
|
}
|
|
|
|
// failExchange 保存反向转发失败状态。
|
|
func (server *Server) failExchange(request *http.Request, upstreamErr error) {
|
|
id := exchangeID(request)
|
|
if id == "" || upstreamErr == nil {
|
|
return
|
|
}
|
|
server.store.update(id, func(exchange *Exchange) {
|
|
exchange.State = "error"
|
|
exchange.Error = upstreamErr.Error()
|
|
exchange.DurationMS = elapsedMS(exchange.StartedAt)
|
|
})
|
|
}
|
|
|
|
// finishRequestBody 解压、解码并保存完整请求体的最终状态。
|
|
func (server *Server) finishRequestBody(id, path, contentType, codec string, captured []byte, size int64, truncated bool, readErr error) {
|
|
decodePayload := captured
|
|
var contentDecodeErr error
|
|
decodeProto := decodesUnaryRequest(path) && isUnaryProtoContentType(contentType)
|
|
if decodeProto && truncated {
|
|
contentDecodeErr = errors.New("请求正文超过抓取上限,无法完整解码")
|
|
} else if decodeProto && codec != "" && !strings.EqualFold(codec, "identity") {
|
|
decodePayload, contentDecodeErr = decompressPayload(captured, codec)
|
|
}
|
|
decodedJSON, decodedLang, kind, requestID, conversationID, decodeErr := "", "", "", "", "", contentDecodeErr
|
|
if decodeProto && decodeErr == nil {
|
|
decodedJSON, kind, requestID, conversationID, decodeErr = decodeUnaryRequest(path, decodePayload)
|
|
}
|
|
if decodeErr == nil && decodedJSON != "" {
|
|
decodedLang = "json"
|
|
} else if !decodeProto {
|
|
decodedJSON, decodedLang, decodeErr = decodeCapturedContent(captured, contentType, codec)
|
|
}
|
|
server.store.update(id, func(exchange *Exchange) {
|
|
exchange.RequestBytes = size
|
|
exchange.Request.Size = size
|
|
exchange.Request.RawHex = rawHex(captured)
|
|
exchange.Request.RawTruncated = truncated
|
|
if decodedJSON != "" {
|
|
exchange.Request.DecodedJSON = decodedJSON
|
|
exchange.Request.DecodedLang = decodedLang
|
|
}
|
|
if kind != "" {
|
|
exchange.RequestKind = kind
|
|
}
|
|
if requestID != "" {
|
|
exchange.RequestID = requestID
|
|
}
|
|
if conversationID != "" {
|
|
exchange.ConversationID = conversationID
|
|
}
|
|
if decodeErr != nil {
|
|
exchange.Request.DecodeError = decodeErr.Error()
|
|
}
|
|
if readErr != nil && !errors.Is(readErr, io.EOF) {
|
|
exchange.Error = readErr.Error()
|
|
}
|
|
})
|
|
}
|
|
|
|
// requestContentCodec 读取请求方向的 Connect 或 HTTP 压缩编码。
|
|
func requestContentCodec(path string, headers http.Header) string {
|
|
if streamingRequestMessageType(path) != "" {
|
|
return strings.TrimSpace(headers.Get("Connect-Content-Encoding"))
|
|
}
|
|
return strings.TrimSpace(headers.Get("Content-Encoding"))
|
|
}
|
|
|
|
// responseContentCodec 读取响应方向的 Connect 或 HTTP 压缩编码。
|
|
func responseContentCodec(path string, headers http.Header) string {
|
|
if streamingResponseMessageType(path) != "" {
|
|
return strings.TrimSpace(headers.Get("Connect-Content-Encoding"))
|
|
}
|
|
if !decodesUnaryResponse(path) {
|
|
if codec := strings.TrimSpace(headers.Get("Connect-Content-Encoding")); codec != "" {
|
|
return codec
|
|
}
|
|
}
|
|
return strings.TrimSpace(headers.Get("Content-Encoding"))
|
|
}
|
|
|
|
// finishResponseBody 解压、解码并保存完整响应体的最终状态。
|
|
func (server *Server) finishResponseBody(id, path, contentType, codec string, captured []byte, size int64, truncated bool, readErr error) {
|
|
decodePayload := captured
|
|
var contentDecodeErr error
|
|
decodeProto := decodesUnaryResponse(path) && isUnaryProtoContentType(contentType)
|
|
if decodeProto && truncated {
|
|
contentDecodeErr = errors.New("响应正文超过抓取上限,无法完整解码")
|
|
} else if decodeProto && codec != "" && !strings.EqualFold(codec, "identity") {
|
|
decodePayload, contentDecodeErr = decompressPayload(captured, codec)
|
|
}
|
|
decodedJSON, decodedLang, kind, decodeErr := "", "", "", contentDecodeErr
|
|
if decodeProto && decodeErr == nil {
|
|
decodedJSON, kind, decodeErr = decodeUnaryResponse(path, decodePayload)
|
|
}
|
|
if decodeErr == nil && decodedJSON != "" {
|
|
decodedLang = "json"
|
|
} else if !decodeProto {
|
|
decodedJSON, decodedLang, decodeErr = decodeCapturedContent(captured, contentType, codec)
|
|
}
|
|
server.store.update(id, func(exchange *Exchange) {
|
|
exchange.ResponseBytes = size
|
|
exchange.Response.Size = size
|
|
exchange.Response.RawHex = rawHex(captured)
|
|
exchange.Response.RawTruncated = truncated
|
|
if decodedJSON != "" {
|
|
exchange.Response.DecodedJSON = decodedJSON
|
|
exchange.Response.DecodedLang = decodedLang
|
|
}
|
|
if kind != "" {
|
|
exchange.ResponseKind = kind
|
|
}
|
|
if decodeErr != nil {
|
|
exchange.Response.DecodeError = decodeErr.Error()
|
|
}
|
|
exchange.DurationMS = elapsedMS(exchange.StartedAt)
|
|
exchange.State = "completed"
|
|
if readErr != nil && !errors.Is(readErr, io.EOF) {
|
|
exchange.State = "error"
|
|
exchange.Error = readErr.Error()
|
|
}
|
|
})
|
|
}
|
|
|
|
// appendRequestFrame 把请求方向的流式帧追加到临时快照。
|
|
func (server *Server) appendRequestFrame(id string, frame FrameView) {
|
|
server.store.updateTransient(id, func(exchange *Exchange) {
|
|
if len(exchange.Request.Frames) < server.config.MaxFrames {
|
|
exchange.Request.Frames = append(exchange.Request.Frames, frame)
|
|
}
|
|
if frame.Kind != "" {
|
|
exchange.RequestKind = frame.Kind
|
|
}
|
|
if frame.RequestID != "" {
|
|
exchange.RequestID = frame.RequestID
|
|
}
|
|
})
|
|
}
|
|
|
|
// appendResponseFrame 把响应方向的流式帧追加到临时快照。
|
|
func (server *Server) appendResponseFrame(id string, frame FrameView) {
|
|
server.store.updateTransient(id, func(exchange *Exchange) {
|
|
if len(exchange.Response.Frames) < server.config.MaxFrames {
|
|
exchange.Response.Frames = append(exchange.Response.Frames, frame)
|
|
}
|
|
exchange.FrameCount = len(exchange.Response.Frames)
|
|
if frame.Kind != "" && frame.Kind != "end_stream" {
|
|
exchange.ResponseKind = frame.Kind
|
|
}
|
|
if frame.Error != "" {
|
|
exchange.Response.DecodeError = frame.Error
|
|
}
|
|
})
|
|
}
|
|
|
|
// exchangeID 从请求上下文读取捕获记录编号。
|
|
func exchangeID(request *http.Request) string {
|
|
if request == nil {
|
|
return ""
|
|
}
|
|
value, ok := request.Context().Value(exchangeIDContextKey{}).(string)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return value
|
|
}
|
|
|
|
// browserAddress 把通配监听地址转换为浏览器可访问的回环地址。
|
|
func browserAddress(address string) string {
|
|
host, port, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return address
|
|
}
|
|
if host == "" || host == "0.0.0.0" || host == "::" {
|
|
host = "127.0.0.1"
|
|
}
|
|
return net.JoinHostPort(host, port)
|
|
}
|
|
|
|
// validateLoopbackAddress 拒绝把调试服务暴露到非回环网卡。
|
|
func validateLoopbackAddress(address string) error {
|
|
host, _, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return fmt.Errorf("调试服务监听地址无效:%w", err)
|
|
}
|
|
if strings.EqualFold(host, "localhost") {
|
|
return nil
|
|
}
|
|
ip := net.ParseIP(host)
|
|
if ip == nil || !ip.IsLoopback() {
|
|
return errors.New("调试服务只能监听本机回环地址")
|
|
}
|
|
return nil
|
|
}
|