Files
gpt-load/internal/proxy/server.go
T
tbphp 6c639478dc feat: Security-hardening Optimization (#225)
* fix: code error

* feat: opz security

* feat: Optimize batch key encryption

* feat: Optimize batch test key

* feat: Perfect encryption key

* docs: failed-to-decrypt

* feat: Security alert

* feat: Encryption migrate command

* fix: migrate command error

* feat: Adjust data length

* feat: Adjust data length

* docs: input text

* fix: Deterministic Encryption

* feat: Optimize security algorithms

* feat: Support KeyHash

* fix: Fix migration script field order issue

* fix: Fix log export key issue

* fix: code style

* fix: migrate order offset

* fix: Resolve migration script compatibility issues

* feat: Adjust Redis cleanup policy

* fix: migrate for sqlite

* fix: migrate

* fix: rm group_key_inx

* fix: rm group_key_inx

* fix: filter null hash data

* docs: log warn

* docs: update readme
2025-09-05 15:45:16 +08:00

310 lines
9.5 KiB
Go

// Package proxy provides high-performance OpenAI multi-key proxy server
package proxy
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"gpt-load/internal/channel"
"gpt-load/internal/config"
"gpt-load/internal/encryption"
app_errors "gpt-load/internal/errors"
"gpt-load/internal/keypool"
"gpt-load/internal/models"
"gpt-load/internal/response"
"gpt-load/internal/services"
"gpt-load/internal/utils"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
// ProxyServer represents the proxy server
type ProxyServer struct {
keyProvider *keypool.KeyProvider
groupManager *services.GroupManager
settingsManager *config.SystemSettingsManager
channelFactory *channel.Factory
requestLogService *services.RequestLogService
encryptionSvc encryption.Service
}
// NewProxyServer creates a new proxy server
func NewProxyServer(
keyProvider *keypool.KeyProvider,
groupManager *services.GroupManager,
settingsManager *config.SystemSettingsManager,
channelFactory *channel.Factory,
requestLogService *services.RequestLogService,
encryptionSvc encryption.Service,
) (*ProxyServer, error) {
return &ProxyServer{
keyProvider: keyProvider,
groupManager: groupManager,
settingsManager: settingsManager,
channelFactory: channelFactory,
requestLogService: requestLogService,
encryptionSvc: encryptionSvc,
}, nil
}
// HandleProxy is the main entry point for proxy requests, refactored based on the stable .bak logic.
func (ps *ProxyServer) HandleProxy(c *gin.Context) {
startTime := time.Now()
groupName := c.Param("group_name")
group, err := ps.groupManager.GetGroupByName(groupName)
if err != nil {
response.Error(c, app_errors.ParseDBError(err))
return
}
channelHandler, err := ps.channelFactory.GetChannel(group)
if err != nil {
response.Error(c, app_errors.NewAPIError(app_errors.ErrInternalServer, fmt.Sprintf("Failed to get channel for group '%s': %v", groupName, err)))
return
}
bodyBytes, err := io.ReadAll(c.Request.Body)
if err != nil {
logrus.Errorf("Failed to read request body: %v", err)
response.Error(c, app_errors.NewAPIError(app_errors.ErrBadRequest, "Failed to read request body"))
return
}
c.Request.Body.Close()
finalBodyBytes, err := ps.applyParamOverrides(bodyBytes, group)
if err != nil {
response.Error(c, app_errors.NewAPIError(app_errors.ErrInternalServer, fmt.Sprintf("Failed to apply parameter overrides: %v", err)))
return
}
isStream := channelHandler.IsStreamRequest(c, bodyBytes)
ps.executeRequestWithRetry(c, channelHandler, group, finalBodyBytes, isStream, startTime, 0)
}
// executeRequestWithRetry is the core recursive function for handling requests and retries.
func (ps *ProxyServer) executeRequestWithRetry(
c *gin.Context,
channelHandler channel.ChannelProxy,
group *models.Group,
bodyBytes []byte,
isStream bool,
startTime time.Time,
retryCount int,
) {
cfg := group.EffectiveConfig
apiKey, err := ps.keyProvider.SelectKey(group.ID)
if err != nil {
logrus.Errorf("Failed to select a key for group %s on attempt %d: %v", group.Name, retryCount+1, err)
response.Error(c, app_errors.NewAPIError(app_errors.ErrNoKeysAvailable, err.Error()))
ps.logRequest(c, group, nil, startTime, http.StatusServiceUnavailable, err, isStream, "", channelHandler, bodyBytes, models.RequestTypeFinal)
return
}
upstreamURL, err := channelHandler.BuildUpstreamURL(c.Request.URL, group)
if err != nil {
response.Error(c, app_errors.NewAPIError(app_errors.ErrInternalServer, fmt.Sprintf("Failed to build upstream URL: %v", err)))
return
}
var ctx context.Context
var cancel context.CancelFunc
if isStream {
ctx, cancel = context.WithCancel(c.Request.Context())
} else {
timeout := time.Duration(cfg.RequestTimeout) * time.Second
ctx, cancel = context.WithTimeout(c.Request.Context(), timeout)
}
defer cancel()
req, err := http.NewRequestWithContext(ctx, c.Request.Method, upstreamURL, bytes.NewReader(bodyBytes))
if err != nil {
logrus.Errorf("Failed to create upstream request: %v", err)
response.Error(c, app_errors.ErrInternalServer)
return
}
req.ContentLength = int64(len(bodyBytes))
req.Header = c.Request.Header.Clone()
// Clean up client auth key
req.Header.Del("Authorization")
req.Header.Del("X-Api-Key")
req.Header.Del("X-Goog-Api-Key")
channelHandler.ModifyRequest(req, apiKey, group)
// Apply custom header rules
if len(group.HeaderRuleList) > 0 {
headerCtx := utils.NewHeaderVariableContextFromGin(c, group, apiKey)
utils.ApplyHeaderRules(req, group.HeaderRuleList, headerCtx)
}
var client *http.Client
if isStream {
client = channelHandler.GetStreamClient()
req.Header.Set("X-Accel-Buffering", "no")
} else {
client = channelHandler.GetHTTPClient()
}
resp, err := client.Do(req)
if resp != nil {
defer resp.Body.Close()
}
// Unified error handling for retries. Exclude 404 from being a retryable error.
if err != nil || (resp != nil && resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound) {
if err != nil && app_errors.IsIgnorableError(err) {
logrus.Debugf("Client-side ignorable error for key %s, aborting retries: %v", utils.MaskAPIKey(apiKey.KeyValue), err)
ps.logRequest(c, group, apiKey, startTime, 499, err, isStream, upstreamURL, channelHandler, bodyBytes, models.RequestTypeFinal)
return
}
var statusCode int
var errorMessage string
var parsedError string
if err != nil {
statusCode = 500
errorMessage = err.Error()
parsedError = errorMessage
logrus.Debugf("Request failed (attempt %d/%d) for key %s: %v", retryCount+1, cfg.MaxRetries, utils.MaskAPIKey(apiKey.KeyValue), err)
} else {
// HTTP-level error (status >= 400)
statusCode = resp.StatusCode
errorBody, readErr := io.ReadAll(resp.Body)
if readErr != nil {
logrus.Errorf("Failed to read error body: %v", readErr)
errorBody = []byte("Failed to read error body")
}
errorBody = handleGzipCompression(resp, errorBody)
errorMessage = string(errorBody)
parsedError = app_errors.ParseUpstreamError(errorBody)
logrus.Debugf("Request failed with status %d (attempt %d/%d) for key %s. Parsed Error: %s", statusCode, retryCount+1, cfg.MaxRetries, utils.MaskAPIKey(apiKey.KeyValue), parsedError)
}
// 使用解析后的错误信息更新密钥状态
ps.keyProvider.UpdateStatus(apiKey, group, false, parsedError)
// 判断是否为最后一次尝试
isLastAttempt := retryCount >= cfg.MaxRetries
requestType := models.RequestTypeRetry
if isLastAttempt {
requestType = models.RequestTypeFinal
}
ps.logRequest(c, group, apiKey, startTime, statusCode, errors.New(parsedError), isStream, upstreamURL, channelHandler, bodyBytes, requestType)
// 如果是最后一次尝试,直接返回错误,不再递归
if isLastAttempt {
var errorJSON map[string]any
if err := json.Unmarshal([]byte(errorMessage), &errorJSON); err == nil {
c.JSON(statusCode, errorJSON)
} else {
response.Error(c, app_errors.NewAPIErrorWithUpstream(statusCode, "UPSTREAM_ERROR", errorMessage))
}
return
}
ps.executeRequestWithRetry(c, channelHandler, group, bodyBytes, isStream, startTime, retryCount+1)
return
}
// ps.keyProvider.UpdateStatus(apiKey, group, true) // 请求成功不再重置成功次数,减少IO消耗
logrus.Debugf("Request for group %s succeeded on attempt %d with key %s", group.Name, retryCount+1, utils.MaskAPIKey(apiKey.KeyValue))
for key, values := range resp.Header {
for _, value := range values {
c.Header(key, value)
}
}
c.Status(resp.StatusCode)
if isStream {
ps.handleStreamingResponse(c, resp)
} else {
ps.handleNormalResponse(c, resp)
}
ps.logRequest(c, group, apiKey, startTime, resp.StatusCode, nil, isStream, upstreamURL, channelHandler, bodyBytes, models.RequestTypeFinal)
}
// logRequest is a helper function to create and record a request log.
func (ps *ProxyServer) logRequest(
c *gin.Context,
group *models.Group,
apiKey *models.APIKey,
startTime time.Time,
statusCode int,
finalError error,
isStream bool,
upstreamAddr string,
channelHandler channel.ChannelProxy,
bodyBytes []byte,
requestType string,
) {
if ps.requestLogService == nil {
return
}
var requestBodyToLog, userAgent string
if group.EffectiveConfig.EnableRequestBodyLogging {
requestBodyToLog = utils.TruncateString(string(bodyBytes), 65000)
userAgent = c.Request.UserAgent()
}
duration := time.Since(startTime).Milliseconds()
logEntry := &models.RequestLog{
GroupID: group.ID,
GroupName: group.Name,
IsSuccess: finalError == nil && statusCode < 400,
SourceIP: c.ClientIP(),
StatusCode: statusCode,
RequestPath: utils.TruncateString(c.Request.URL.String(), 500),
Duration: duration,
UserAgent: userAgent,
RequestType: requestType,
IsStream: isStream,
UpstreamAddr: utils.TruncateString(upstreamAddr, 500),
RequestBody: requestBodyToLog,
}
if channelHandler != nil && bodyBytes != nil {
logEntry.Model = channelHandler.ExtractModel(c, bodyBytes)
}
if apiKey != nil {
// 加密密钥值用于日志存储
encryptedKeyValue, err := ps.encryptionSvc.Encrypt(apiKey.KeyValue)
if err != nil {
logrus.WithError(err).Error("Failed to encrypt key value for logging")
logEntry.KeyValue = "failed-to-encryption"
} else {
logEntry.KeyValue = encryptedKeyValue
}
// 添加 KeyHash 用于反查
logEntry.KeyHash = ps.encryptionSvc.Hash(apiKey.KeyValue)
}
if finalError != nil {
logEntry.ErrorMessage = finalError.Error()
}
if err := ps.requestLogService.Record(logEntry); err != nil {
logrus.Errorf("Failed to record request log: %v", err)
}
}