fix: safe type assertions across OAuth, model Scan, relay, and middleware

- OAuth: safe type assertions for session state and affiliate code
  (oauth.go, github.go, discord.go, linuxdo.go, oidc.go)
- Model: Scan() methods handle string values from DB drivers, not just []byte
  (channel.go ChannelInfo, task.go Properties/TaskPrivateData)
- Model: safe type assertion in CleanupChannelPollingLocks sync.Map iteration
- Relay: safe type assertions in audio_handler, AWS InvokeModel,
  ollama ConvertClaudeRequest, claude stop sequences, zhipu token cache
- Service: fix slice bounds panic in Gemini->OpenAI stop sequences conversion
- Service: safe type assertion in CleanupFileSources middleware
- Middleware: add missing c.Abort() in turnstile session save failure
- Middleware: safe type assertion in distributor channelId
- Middleware: safe int comparison in auth helper

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 16:21:34 +08:00
co-authored by Claude Opus 4.6
parent f43aa269d6
commit bda36c44be
17 changed files with 105 additions and 35 deletions
+2 -1
View File
@@ -101,7 +101,8 @@ func getDiscordUserInfoByCode(code string) (*DiscordUser, error) {
func DiscordOAuth(c *gin.Context) {
session := sessions.Default(c)
state := c.Query("state")
if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
storedState, _ := session.Get("oauth_state").(string)
if state == "" || storedState == "" || state != storedState {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "state is empty or not same",
+4 -4
View File
@@ -81,7 +81,8 @@ func getGitHubUserInfoByCode(code string) (*GitHubUser, error) {
func GitHubOAuth(c *gin.Context) {
session := sessions.Default(c)
state := c.Query("state")
if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
storedState, _ := session.Get("oauth_state").(string)
if state == "" || storedState == "" || state != storedState {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "state is empty or not same",
@@ -140,10 +141,9 @@ func GitHubOAuth(c *gin.Context) {
user.Email = githubUser.Email
user.Role = common.RoleCommonUser
user.Status = common.UserStatusEnabled
affCode := session.Get("aff")
inviterId := 0
if affCode != nil {
inviterId, _ = model.GetUserIdByAffCode(affCode.(string))
if affCodeStr, ok := session.Get("aff").(string); ok && affCodeStr != "" {
inviterId, _ = model.GetUserIdByAffCode(affCodeStr)
}
if err := user.Insert(inviterId); err != nil {
+4 -4
View File
@@ -168,7 +168,8 @@ func LinuxdoOAuth(c *gin.Context) {
}
state := c.Query("state")
if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
storedState, _ := session.Get("oauth_state").(string)
if state == "" || storedState == "" || state != storedState {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "state is empty or not same",
@@ -226,10 +227,9 @@ func LinuxdoOAuth(c *gin.Context) {
user.Role = common.RoleCommonUser
user.Status = common.UserStatusEnabled
affCode := session.Get("aff")
inviterId := 0
if affCode != nil {
inviterId, _ = model.GetUserIdByAffCode(affCode.(string))
if affCodeStr, ok := session.Get("aff").(string); ok && affCodeStr != "" {
inviterId, _ = model.GetUserIdByAffCode(affCodeStr)
}
if err := user.Insert(inviterId); err != nil {
+4 -4
View File
@@ -56,7 +56,8 @@ func HandleOAuth(c *gin.Context) {
// 1. Validate state (CSRF protection)
state := c.Query("state")
if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
storedState, _ := session.Get("oauth_state").(string)
if state == "" || storedState == "" || state != storedState {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": i18n.T(c, i18n.MsgOAuthStateInvalid),
@@ -263,10 +264,9 @@ func findOrCreateOAuthUser(c *gin.Context, provider oauth.Provider, oauthUser *o
user.Status = common.UserStatusEnabled
// Handle affiliate code
affCode := session.Get("aff")
inviterId := 0
if affCode != nil {
inviterId, _ = model.GetUserIdByAffCode(affCode.(string))
if affCodeStr, ok := session.Get("aff").(string); ok && affCodeStr != "" {
inviterId, _ = model.GetUserIdByAffCode(affCodeStr)
}
// Use transaction to ensure user creation and OAuth binding are atomic
+2 -1
View File
@@ -103,7 +103,8 @@ func getOidcUserInfoByCode(code string) (*OidcUser, error) {
func OidcAuth(c *gin.Context) {
session := sessions.Default(c)
state := c.Query("state")
if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
storedState, _ := session.Get("oauth_state").(string)
if state == "" || storedState == "" || state != storedState {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "state is empty or not same",
+2 -1
View File
@@ -112,7 +112,8 @@ func authHelper(c *gin.Context, minRole int) {
return
}
if id != apiUserId {
idVal, _ := id.(int)
if idVal != apiUserId {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": common.TranslateMessage(c, i18n.MsgAuthUserIdMismatch),
+2 -1
View File
@@ -37,7 +37,8 @@ func Distribute() func(c *gin.Context) {
return
}
if ok {
id, err := strconv.Atoi(channelId.(string))
channelIdStr, _ := channelId.(string)
id, err := strconv.Atoi(channelIdStr)
if err != nil {
abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorInvalidChannelId))
return
+1
View File
@@ -72,6 +72,7 @@ func TurnstileCheck() gin.HandlerFunc {
"message": "无法保存会话信息,请重试",
"success": false,
})
c.Abort()
return
}
}
+16 -2
View File
@@ -74,7 +74,18 @@ func (c ChannelInfo) Value() (driver.Value, error) {
// Scan implements sql.Scanner interface
func (c *ChannelInfo) Scan(value interface{}) error {
bytesValue, _ := value.([]byte)
if value == nil {
return nil
}
var bytesValue []byte
switch v := value.(type) {
case []byte:
bytesValue = v
case string:
bytesValue = []byte(v)
default:
return fmt.Errorf("cannot scan %T into ChannelInfo", value)
}
return common.Unmarshal(bytesValue, c)
}
@@ -562,7 +573,10 @@ func CleanupChannelPollingLocks() {
}
channelPollingLocks.Range(func(key, value interface{}) bool {
channelId := key.(int)
channelId, ok := key.(int)
if !ok {
return true
}
if !activeChannelSet[channelId] {
channelPollingLocks.Delete(channelId)
}
+26 -2
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"database/sql/driver"
"encoding/json"
"fmt"
"time"
"github.com/heicode/manager/common"
@@ -81,7 +82,19 @@ type Properties struct {
}
func (m *Properties) Scan(val interface{}) error {
bytesValue, _ := val.([]byte)
if val == nil {
*m = Properties{}
return nil
}
var bytesValue []byte
switch v := val.(type) {
case []byte:
bytesValue = v
case string:
bytesValue = []byte(v)
default:
return fmt.Errorf("cannot scan %T into Properties", val)
}
if len(bytesValue) == 0 {
*m = Properties{}
return nil
@@ -142,7 +155,18 @@ func GenerateTaskID() string {
}
func (p *TaskPrivateData) Scan(val interface{}) error {
bytesValue, _ := val.([]byte)
if val == nil {
return nil
}
var bytesValue []byte
switch v := val.(type) {
case []byte:
bytesValue = v
case string:
bytesValue = []byte(v)
default:
return fmt.Errorf("cannot scan %T into TaskPrivateData", val)
}
if len(bytesValue) == 0 {
return nil
}
+7 -3
View File
@@ -67,10 +67,14 @@ func AudioHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *type
service.ResetStatusCode(newAPIError, statusCodeMappingStr)
return newAPIError
}
if usage.(*dto.Usage).CompletionTokenDetails.AudioTokens > 0 || usage.(*dto.Usage).PromptTokensDetails.AudioTokens > 0 {
service.PostAudioConsumeQuota(c, info, usage.(*dto.Usage), "")
usageData, ok := usage.(*dto.Usage)
if !ok {
return types.NewError(fmt.Errorf("invalid usage type"), types.ErrorCodeBadResponseBody)
}
if usageData.CompletionTokenDetails.AudioTokens > 0 || usageData.PromptTokensDetails.AudioTokens > 0 {
service.PostAudioConsumeQuota(c, info, usageData, "")
} else {
service.PostTextConsumeQuota(c, info, usage.(*dto.Usage), nil)
service.PostTextConsumeQuota(c, info, usageData, nil)
}
return nil
+15 -3
View File
@@ -225,7 +225,11 @@ func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, a *Adaptor) (*types
ctx, cancel := newAwsInvokeContext()
defer cancel()
awsResp, err := a.AwsClient.InvokeModel(ctx, a.AwsReq.(*bedrockruntime.InvokeModelInput))
invokeInput, ok := a.AwsReq.(*bedrockruntime.InvokeModelInput)
if !ok {
return types.NewOpenAIError(errors.New("invalid AWS request type"), types.ErrorCodeAwsInvokeError, http.StatusInternalServerError), nil
}
awsResp, err := a.AwsClient.InvokeModel(ctx, invokeInput)
if err != nil {
statusCode := getAwsErrorStatusCode(err)
return types.NewOpenAIError(errors.Wrap(err, "InvokeModel"), types.ErrorCodeAwsInvokeError, statusCode), nil
@@ -255,7 +259,11 @@ func awsStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, a *Adaptor) (
ctx, cancel := newAwsInvokeContext()
defer cancel()
awsResp, err := a.AwsClient.InvokeModelWithResponseStream(ctx, a.AwsReq.(*bedrockruntime.InvokeModelWithResponseStreamInput))
streamInput, ok := a.AwsReq.(*bedrockruntime.InvokeModelWithResponseStreamInput)
if !ok {
return types.NewOpenAIError(errors.New("invalid AWS stream request type"), types.ErrorCodeAwsInvokeError, http.StatusInternalServerError), nil
}
awsResp, err := a.AwsClient.InvokeModelWithResponseStream(ctx, streamInput)
if err != nil {
statusCode := getAwsErrorStatusCode(err)
return types.NewOpenAIError(errors.Wrap(err, "InvokeModelWithResponseStream"), types.ErrorCodeAwsInvokeError, statusCode), nil
@@ -298,7 +306,11 @@ func handleNovaRequest(c *gin.Context, info *relaycommon.RelayInfo, a *Adaptor)
ctx, cancel := newAwsInvokeContext()
defer cancel()
awsResp, err := a.AwsClient.InvokeModel(ctx, a.AwsReq.(*bedrockruntime.InvokeModelInput))
novaInput, ok := a.AwsReq.(*bedrockruntime.InvokeModelInput)
if !ok {
return types.NewOpenAIError(errors.New("invalid AWS Nova request type"), types.ErrorCodeAwsInvokeError, http.StatusInternalServerError), nil
}
awsResp, err := a.AwsClient.InvokeModel(ctx, novaInput)
if err != nil {
statusCode := getAwsErrorStatusCode(err)
return types.NewOpenAIError(errors.Wrap(err, "InvokeModel"), types.ErrorCodeAwsInvokeError, statusCode), nil
+3 -1
View File
@@ -248,7 +248,9 @@ func RequestOpenAI2ClaudeMessage(c *gin.Context, textRequest dto.GeneralOpenAIRe
case []interface{}:
stopSequences := make([]string, 0)
for _, stop := range textRequest.Stop.([]interface{}) {
stopSequences = append(stopSequences, stop.(string))
if s, ok := stop.(string); ok {
stopSequences = append(stopSequences, s)
}
}
claudeRequest.StopSequences = stopSequences
}
+6 -3
View File
@@ -29,11 +29,14 @@ func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayIn
if err != nil {
return nil, err
}
openaiRequest.(*dto.GeneralOpenAIRequest).StreamOptions = &dto.StreamOptions{
oaiReq, ok := openaiRequest.(*dto.GeneralOpenAIRequest)
if !ok {
return nil, errors.New("unexpected request type from Claude conversion")
}
oaiReq.StreamOptions = &dto.StreamOptions{
IncludeUsage: true,
}
// map to ollama chat request (Claude -> OpenAI -> Ollama chat)
return openAIChatToOllamaChat(c, openaiRequest.(*dto.GeneralOpenAIRequest))
return openAIChatToOllamaChat(c, oaiReq)
}
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
+1 -1
View File
@@ -32,7 +32,7 @@ var expSeconds int64 = 24 * 3600
func getZhipuToken(apikey string) string {
data, ok := zhipuTokens.Load(apikey)
if ok {
tokenData := data.(zhipuTokenData)
tokenData, _ := data.(zhipuTokenData)
if time.Now().Before(tokenData.ExpiryTime) {
return tokenData.Token
}
+5 -1
View File
@@ -753,7 +753,11 @@ func GeminiToOpenAIRequest(geminiRequest *dto.GeminiChatRequest, info *relaycomm
}
// gemini stop sequences 最多 5 个,openai stop 最多 4 个
if len(geminiRequest.GenerationConfig.StopSequences) > 0 {
openaiRequest.Stop = geminiRequest.GenerationConfig.StopSequences[:4]
maxStop := 4
if len(geminiRequest.GenerationConfig.StopSequences) < maxStop {
maxStop = len(geminiRequest.GenerationConfig.StopSequences)
}
openaiRequest.Stop = geminiRequest.GenerationConfig.StopSequences[:maxStop]
}
if geminiRequest.GenerationConfig.CandidateCount != nil && *geminiRequest.GenerationConfig.CandidateCount > 0 {
openaiRequest.N = lo.ToPtr(*geminiRequest.GenerationConfig.CandidateCount)
+5 -3
View File
@@ -144,9 +144,11 @@ func registerSourceForCleanup(c *gin.Context, source types.FileSource) {
func CleanupFileSources(c *gin.Context) {
key := string(constant.ContextKeyFileSourcesToCleanup)
if sources, exists := c.Get(key); exists {
for _, source := range sources.([]types.FileSource) {
if cache := source.GetCache(); cache != nil {
cache.Close()
if sourcesSlice, ok := sources.([]types.FileSource); ok {
for _, source := range sourcesSlice {
if cache := source.GetCache(); cache != nil {
cache.Close()
}
}
}
c.Set(key, nil)