| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418 |
- package service
- import (
- "context"
- "crypto/sha256"
- "encoding/hex"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "os/exec"
- "strconv"
- "strings"
- "sync"
- "sync/atomic"
- "time"
- pluginv1 "github.com/Wei-Shaw/sub2api/pkg/pluginapi/v1"
- hclog "github.com/hashicorp/go-hclog"
- hcplugin "github.com/hashicorp/go-plugin"
- )
- type pluginRuntime struct {
- installation *PluginInstallation
- client *hcplugin.Client
- api pluginv1.TransportPluginClient
- inFlight atomic.Int64
- draining atomic.Bool
- done chan struct{}
- doneOnce sync.Once
- }
- func startPluginRuntime(ctx context.Context, installation *PluginInstallation, startTimeout time.Duration, socketDir string) (*pluginRuntime, error) {
- if installation == nil {
- return nil, errors.New("插件安装记录为空")
- }
- checksum, err := hex.DecodeString(installation.BinarySHA256)
- if err != nil || len(checksum) != sha256.Size {
- return nil, errors.New("插件二进制哈希无效")
- }
- cmd := exec.CommandContext(context.WithoutCancel(ctx), installation.BinaryPath)
- client := hcplugin.NewClient(&hcplugin.ClientConfig{
- HandshakeConfig: pluginv1.HandshakeConfig,
- Plugins: pluginv1.ClientPluginMap(),
- Cmd: cmd,
- AllowedProtocols: []hcplugin.Protocol{hcplugin.ProtocolGRPC},
- StartTimeout: startTimeout,
- SecureConfig: &hcplugin.SecureConfig{
- Checksum: checksum,
- Hash: sha256.New(),
- },
- Logger: hclog.NewNullLogger(),
- SyncStdout: io.Discard,
- SyncStderr: io.Discard,
- UnixSocketConfig: &hcplugin.UnixSocketConfig{TempDir: socketDir},
- SkipHostEnv: true,
- })
- rpcClient, err := client.Client()
- if err != nil {
- client.Kill()
- return nil, fmt.Errorf("启动插件进程: %w", err)
- }
- dispensed, err := rpcClient.Dispense(pluginv1.TransportPluginName)
- if err != nil {
- client.Kill()
- return nil, fmt.Errorf("获取插件传输能力: %w", err)
- }
- api, ok := dispensed.(pluginv1.TransportPluginClient)
- if !ok {
- client.Kill()
- return nil, errors.New("插件未实现传输 gRPC 客户端")
- }
- runtime := &pluginRuntime{
- installation: installation,
- client: client,
- api: api,
- done: make(chan struct{}),
- }
- infoCtx, cancel := context.WithTimeout(ctx, startTimeout)
- defer cancel()
- info, err := api.GetInfo(infoCtx, &pluginv1.GetInfoRequest{})
- if err != nil {
- runtime.kill()
- return nil, fmt.Errorf("读取插件信息: %w", err)
- }
- if info.PluginId != installation.PluginKey || info.PluginVersion != installation.Version ||
- info.ProtocolVersion != pluginv1.ProtocolVersion || info.TransportApiVersion != pluginv1.TransportAPIVersion {
- runtime.kill()
- return nil, errors.New("插件运行时信息与已校验清单不一致")
- }
- health, err := api.Health(infoCtx, &pluginv1.HealthRequest{})
- if err != nil || !health.Healthy {
- runtime.kill()
- if err != nil {
- return nil, fmt.Errorf("插件健康检查失败: %w", err)
- }
- return nil, fmt.Errorf("插件不健康: %s", health.Message)
- }
- return runtime, nil
- }
- func (r *pluginRuntime) validateAndApplyConfig(ctx context.Context, configJSON []byte) error {
- _, err := r.validateAndApplyNormalizedConfig(ctx, configJSON)
- return err
- }
- func (r *pluginRuntime) validateAndApplyNormalizedConfig(ctx context.Context, configJSON []byte) ([]byte, error) {
- validation, err := r.api.ValidateConfig(ctx, &pluginv1.ValidateConfigRequest{ConfigJson: configJSON})
- if err != nil {
- return nil, fmt.Errorf("插件配置校验失败: %w", err)
- }
- if !validation.Valid {
- return nil, fmt.Errorf("插件配置无效: %s", validation.Message)
- }
- if len(validation.NormalizedConfigJson) > 0 {
- configJSON = validation.NormalizedConfigJson
- }
- if len(configJSON) == 0 || len(configJSON) > pluginConfigMaxBytes || !json.Valid(configJSON) {
- return nil, errors.New("插件返回的规范化配置不是有效且大小受限的 JSON")
- }
- var normalized any
- if err := json.Unmarshal(configJSON, &normalized); err != nil {
- return nil, fmt.Errorf("解析插件规范化配置: %w", err)
- }
- if normalized == nil {
- return nil, errors.New("插件返回的规范化配置根节点必须是对象")
- }
- if _, ok := normalized.(map[string]any); !ok {
- return nil, errors.New("插件返回的规范化配置根节点必须是对象")
- }
- configJSON, err = json.Marshal(normalized)
- if err != nil {
- return nil, fmt.Errorf("序列化插件规范化配置: %w", err)
- }
- applied, err := r.api.ApplyConfig(ctx, &pluginv1.ApplyConfigRequest{ConfigJson: configJSON})
- if err != nil {
- return nil, fmt.Errorf("应用插件配置失败: %w", err)
- }
- if !applied.Applied {
- return nil, fmt.Errorf("插件拒绝应用配置: %s", applied.Message)
- }
- return configJSON, nil
- }
- func (r *pluginRuntime) checkHealth(ctx context.Context) error {
- if r == nil || r.api == nil || r.client == nil || r.client.Exited() {
- return errors.New("插件进程已退出")
- }
- health, err := r.api.Health(ctx, &pluginv1.HealthRequest{})
- if err != nil {
- return fmt.Errorf("插件健康检查失败: %w", err)
- }
- if health == nil || !health.Healthy {
- message := "插件报告不健康"
- if health != nil && strings.TrimSpace(health.Message) != "" {
- message = "插件不健康: " + health.Message
- }
- return errors.New(message)
- }
- return nil
- }
- func (r *pluginRuntime) beginRequest() bool {
- if r == nil || r.draining.Load() {
- return false
- }
- r.inFlight.Add(1)
- if r.draining.Load() {
- r.finishRequest()
- return false
- }
- return true
- }
- func (r *pluginRuntime) finishRequest() {
- if r.inFlight.Add(-1) == 0 && r.draining.Load() {
- r.doneOnce.Do(func() { close(r.done) })
- }
- }
- func (r *pluginRuntime) drain(timeout time.Duration) {
- if r == nil {
- return
- }
- r.draining.Store(true)
- if r.inFlight.Load() == 0 {
- r.doneOnce.Do(func() { close(r.done) })
- }
- timer := time.NewTimer(timeout)
- defer timer.Stop()
- select {
- case <-r.done:
- case <-timer.C:
- }
- r.kill()
- }
- func (r *pluginRuntime) kill() {
- if r != nil && r.client != nil {
- r.client.Kill()
- }
- }
- func (r *pluginRuntime) roundTrip(ctx context.Context, request *http.Request, proxyURL string, account *Account) (*http.Response, error) {
- if request == nil || request.URL == nil || account == nil {
- return nil, errors.New("插件出站请求参数不完整")
- }
- streamCtx, cancel := context.WithCancel(ctx)
- stream, err := r.api.Forward(streamCtx)
- if err != nil {
- cancel()
- return nil, normalizePluginRPCError(ctx, "创建插件转发流", err, false)
- }
- requestID := strconv.FormatInt(time.Now().UnixNano(), 36) + "-" + strconv.FormatInt(account.ID, 36)
- if err := stream.Send(&pluginv1.ForwardRequest{Frame: &pluginv1.ForwardRequest_Start{Start: &pluginv1.ForwardRequestStart{
- RequestId: requestID,
- Method: request.Method,
- Url: request.URL.String(),
- Host: request.Host,
- Headers: headersToPlugin(request.Header),
- ProxyUrl: proxyURL,
- AccountId: account.ID,
- AccountConcurrency: int32(account.Concurrency),
- Platform: account.Platform,
- AccountType: account.Type,
- ContentLength: request.ContentLength,
- HasBody: request.Body != nil && request.Body != http.NoBody,
- }}}); err != nil {
- cancel()
- // gRPC Send 返回错误时无法证明服务端没有收到元数据,必须禁止自动重放。
- return nil, normalizePluginRPCError(ctx, "发送插件请求元数据", err, true)
- }
- sendErr := make(chan error, 1)
- go func() {
- err := sendPluginRequestBody(stream, request.Body)
- sendErr <- err
- if err != nil {
- cancel()
- }
- }()
- first, err := stream.Recv()
- if err != nil {
- cancel()
- select {
- case bodyErr := <-sendErr:
- if bodyErr != nil {
- return nil, normalizePluginRPCError(ctx, "发送插件请求体", bodyErr, true)
- }
- default:
- }
- return nil, normalizePluginRPCError(ctx, "接收插件响应头", err, true)
- }
- if frameError := first.GetError(); frameError != nil {
- cancel()
- return nil, &PluginTransportError{Code: frameError.Code, Message: frameError.Message, RequestSent: frameError.RequestSent}
- }
- start := first.GetStart()
- if start == nil || start.StatusCode < 100 || start.StatusCode > 599 {
- cancel()
- return nil, &PluginTransportError{
- Code: "PLUGIN_INVALID_RESPONSE",
- Message: "插件未返回有效的 HTTP 响应头",
- RequestSent: true,
- }
- }
- pipeReader, pipeWriter := io.Pipe()
- body := &pluginResponseBody{
- reader: pipeReader,
- cancel: cancel,
- done: r.finishRequest,
- }
- go receivePluginResponseBody(stream, pipeWriter, sendErr)
- return &http.Response{
- Status: start.Status,
- StatusCode: int(start.StatusCode),
- Proto: start.Protocol,
- ProtoMajor: int(start.ProtocolMajor),
- ProtoMinor: int(start.ProtocolMinor),
- Header: headersFromPlugin(start.Headers),
- Body: body,
- ContentLength: start.ContentLength,
- Request: request,
- }, nil
- }
- type PluginTransportError struct {
- Code string
- Message string
- RequestSent bool
- }
- func (e *PluginTransportError) Error() string {
- if e == nil {
- return "插件传输失败"
- }
- code := strings.Map(func(value rune) rune {
- if value >= 'a' && value <= 'z' || value >= 'A' && value <= 'Z' || value >= '0' && value <= '9' || value == '_' || value == '-' || value == '.' {
- return value
- }
- return -1
- }, e.Code)
- if len(code) > 64 {
- code = code[:64]
- }
- return fmt.Sprintf("插件传输失败 [%s]: %s", code, sanitizeUpstreamErrorMessage(e.Message))
- }
- func normalizePluginRPCError(ctx context.Context, operation string, err error, requestMayHaveBeenSent bool) error {
- if ctx != nil && ctx.Err() != nil {
- return ctx.Err()
- }
- return &PluginTransportError{
- Code: "PLUGIN_RPC_ERROR",
- Message: fmt.Sprintf("%s: %v", operation, err),
- RequestSent: requestMayHaveBeenSent,
- }
- }
- func sendPluginRequestBody(stream pluginv1.TransportPlugin_ForwardClient, body io.ReadCloser) error {
- if body != nil {
- defer func() { _ = body.Close() }()
- buffer := make([]byte, 32*1024)
- for {
- read, err := body.Read(buffer)
- if read > 0 {
- chunk := append([]byte(nil), buffer[:read]...)
- if sendErr := stream.Send(&pluginv1.ForwardRequest{Frame: &pluginv1.ForwardRequest_BodyChunk{BodyChunk: chunk}}); sendErr != nil {
- return sendErr
- }
- }
- if err == io.EOF {
- break
- }
- if err != nil {
- return err
- }
- }
- }
- if err := stream.Send(&pluginv1.ForwardRequest{Frame: &pluginv1.ForwardRequest_BodyEnd{BodyEnd: true}}); err != nil {
- return err
- }
- return stream.CloseSend()
- }
- func receivePluginResponseBody(stream pluginv1.TransportPlugin_ForwardClient, writer *io.PipeWriter, sendErr <-chan error) {
- defer func() { _ = writer.Close() }()
- for {
- frame, err := stream.Recv()
- if err == io.EOF {
- return
- }
- if err != nil {
- _ = writer.CloseWithError(normalizePluginRPCError(stream.Context(), "接收插件响应体", err, true))
- return
- }
- if chunk := frame.GetBodyChunk(); len(chunk) > 0 {
- if _, err := writer.Write(chunk); err != nil {
- return
- }
- continue
- }
- if frame.GetEnd() != nil {
- select {
- case err := <-sendErr:
- if err != nil {
- _ = writer.CloseWithError(normalizePluginRPCError(stream.Context(), "发送插件请求体", err, true))
- }
- default:
- }
- return
- }
- if frameError := frame.GetError(); frameError != nil {
- _ = writer.CloseWithError(&PluginTransportError{Code: frameError.Code, Message: frameError.Message, RequestSent: frameError.RequestSent})
- return
- }
- }
- }
- type pluginResponseBody struct {
- reader *io.PipeReader
- cancel context.CancelFunc
- done func()
- once sync.Once
- }
- func (b *pluginResponseBody) Read(data []byte) (int, error) {
- return b.reader.Read(data)
- }
- func (b *pluginResponseBody) Close() error {
- var err error
- b.once.Do(func() {
- b.cancel()
- err = b.reader.Close()
- b.done()
- })
- return err
- }
- func headersToPlugin(headers http.Header) map[string]*pluginv1.HeaderValues {
- out := make(map[string]*pluginv1.HeaderValues, len(headers))
- for key, values := range headers {
- out[key] = &pluginv1.HeaderValues{Values: append([]string(nil), values...)}
- }
- return out
- }
- func headersFromPlugin(headers map[string]*pluginv1.HeaderValues) http.Header {
- out := make(http.Header, len(headers))
- for key, values := range headers {
- if values != nil {
- out[key] = append([]string(nil), values.Values...)
- }
- }
- return out
- }
|