Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions common/quota_math.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package common

import "math"

// QuotaFromFloat converts a computed quota value to int with saturation.
// Quota products can include user-controlled multipliers such as image count,
// video seconds, or resolution ratios; oversized products must never wrap into
// a negative charge. The bound is int32 because quota columns are int fields
// used as 32-bit database integers in supported deployments.
func QuotaFromFloat(value float64) int {
if math.IsNaN(value) {
return 0
}
if value >= math.MaxInt32 {
return math.MaxInt32
}
if value <= math.MinInt32 {
return math.MinInt32
}
return int(value)
}
18 changes: 18 additions & 0 deletions common/quota_math_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package common

import (
"math"
"testing"

"github.com/stretchr/testify/assert"
)

func TestQuotaFromFloat(t *testing.T) {
assert.Equal(t, 42, QuotaFromFloat(42.4))
assert.Equal(t, -42, QuotaFromFloat(-42.4))
assert.Equal(t, math.MaxInt32, QuotaFromFloat(2000*1.8446744073686647e19))
assert.Equal(t, math.MinInt32, QuotaFromFloat(-2000*1.8446744073686647e19))
assert.Equal(t, math.MaxInt32, QuotaFromFloat(math.Inf(1)))
assert.Equal(t, math.MinInt32, QuotaFromFloat(math.Inf(-1)))
assert.Equal(t, 0, QuotaFromFloat(math.NaN()))
}
13 changes: 13 additions & 0 deletions common/ssrf_protection.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@ func (p *SSRFProtection) validateHostAndPort(host string, port int) error {
return nil
}

func (p *SSRFProtection) ValidateNetworkTarget(host string, port int) error {
return p.validateHostAndPort(host, port)
}

func (p *SSRFProtection) validateResolvedIP(host string, ip net.IP) error {
if !p.IsIPAccessAllowed(ip) {
if isPrivateIP(ip) && !p.AllowPrivateIp {
Expand All @@ -292,6 +296,10 @@ func (p *SSRFProtection) validateResolvedIP(host string, ip net.IP) error {
return nil
}

func (p *SSRFProtection) ValidateResolvedIP(host string, ip net.IP) error {
return p.validateResolvedIP(host, ip)
}

func (p *SSRFProtection) resolveValidatedIPs(ctx context.Context, host string) ([]net.IP, error) {
ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
if err != nil {
Expand Down Expand Up @@ -414,6 +422,11 @@ func NewSSRFProtectionWithFetchSetting(enableSSRFProtection, allowPrivateIp bool
}, true, nil
}

func NewSSRFProtectionFromFetchSetting(allowPrivateIp bool, domainFilterMode bool, ipFilterMode bool, domainList, ipList, allowedPorts []string, applyIPFilterForDomain bool) (*SSRFProtection, error) {
protection, _, err := NewSSRFProtectionWithFetchSetting(true, allowPrivateIp, domainFilterMode, ipFilterMode, domainList, ipList, allowedPorts, applyIPFilterForDomain)
return protection, err
}

// ValidateURLWithFetchSetting 使用FetchSetting配置验证URL
func ValidateURLWithFetchSetting(urlStr string, enableSSRFProtection, allowPrivateIp bool, domainFilterMode bool, ipFilterMode bool, domainList, ipList, allowedPorts []string, applyIPFilterForDomain bool) error {
protection, enabled, err := NewSSRFProtectionWithFetchSetting(enableSSRFProtection, allowPrivateIp, domainFilterMode, ipFilterMode, domainList, ipList, allowedPorts, applyIPFilterForDomain)
Expand Down
2 changes: 1 addition & 1 deletion controller/discord.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ func DiscordBind(c *gin.Context) {
return
}
user.DiscordId = discordUser.UID
err = user.Update(false)
err = user.UpdateFields(false, model.UserUpdateFieldDiscordId)
if err != nil {
common.ApiError(c, err)
return
Expand Down
2 changes: 1 addition & 1 deletion controller/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ func GitHubBind(c *gin.Context) {
return
}
user.GitHubId = githubUser.Login
err = user.Update(false)
err = user.UpdateFields(false, model.UserUpdateFieldGitHubId)
if err != nil {
common.ApiError(c, err)
return
Expand Down
2 changes: 1 addition & 1 deletion controller/linuxdo.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func LinuxDoBind(c *gin.Context) {
}

user.LinuxDOId = strconv.Itoa(linuxdoUser.Id)
err = user.Update(false)
err = user.UpdateFields(false, model.UserUpdateFieldLinuxDOId)
if err != nil {
common.ApiError(c, err)
return
Expand Down
63 changes: 59 additions & 4 deletions controller/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,23 @@ func GetAllLogs(c *gin.Context) {
group := c.Query("group")
requestId := c.Query("request_id")
upstreamRequestId := c.Query("upstream_request_id")
logs, total, err := model.GetAllLogs(logType, logFilter, startTimestamp, endTimestamp, modelName, username, tokenName, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), channel, group, requestId, upstreamRequestId)
quotaFilter := c.Query("quota_filter")
logs, total, err := model.GetAllLogs(model.LogQueryParams{
LogType: logType,
LogFilter: logFilter,
StartTimestamp: startTimestamp,
EndTimestamp: endTimestamp,
ModelName: modelName,
Username: username,
TokenName: tokenName,
StartIdx: pageInfo.GetStartIdx(),
Num: pageInfo.GetPageSize(),
Channel: channel,
Group: group,
RequestId: requestId,
UpstreamRequestId: upstreamRequestId,
QuotaFilter: quotaFilter,
})
if err != nil {
common.ApiError(c, err)
return
Expand Down Expand Up @@ -77,7 +93,22 @@ func GetUserLogs(c *gin.Context) {
group := c.Query("group")
requestId := c.Query("request_id")
upstreamRequestId := c.Query("upstream_request_id")
logs, total, err := model.GetUserLogs(userId, logType, logFilter, startTimestamp, endTimestamp, modelName, tokenName, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), group, requestId, upstreamRequestId)
quotaFilter := c.Query("quota_filter")
logs, total, err := model.GetUserLogs(model.LogQueryParams{
UserId: userId,
LogType: logType,
LogFilter: logFilter,
StartTimestamp: startTimestamp,
EndTimestamp: endTimestamp,
ModelName: modelName,
TokenName: tokenName,
StartIdx: pageInfo.GetStartIdx(),
Num: pageInfo.GetPageSize(),
Group: group,
RequestId: requestId,
UpstreamRequestId: upstreamRequestId,
QuotaFilter: quotaFilter,
})
if err != nil {
common.ApiError(c, err)
return
Expand Down Expand Up @@ -165,7 +196,19 @@ func GetLogsStat(c *gin.Context) {
modelName := c.Query("model_name")
channel, _ := strconv.Atoi(c.Query("channel"))
group := c.Query("group")
stat, err := model.SumUsedQuota(logType, logFilter, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group)
quotaFilter := c.Query("quota_filter")
stat, err := model.SumUsedQuota(model.LogQueryParams{
LogType: logType,
LogFilter: logFilter,
StartTimestamp: startTimestamp,
EndTimestamp: endTimestamp,
ModelName: modelName,
Username: username,
TokenName: tokenName,
Channel: channel,
Group: group,
QuotaFilter: quotaFilter,
})
if err != nil {
common.ApiError(c, err)
return
Expand Down Expand Up @@ -193,7 +236,19 @@ func GetLogsSelfStat(c *gin.Context) {
modelName := c.Query("model_name")
channel, _ := strconv.Atoi(c.Query("channel"))
group := c.Query("group")
quotaNum, err := model.SumUsedQuota(logType, logFilter, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group)
quotaFilter := c.Query("quota_filter")
quotaNum, err := model.SumUsedQuota(model.LogQueryParams{
LogType: logType,
LogFilter: logFilter,
StartTimestamp: startTimestamp,
EndTimestamp: endTimestamp,
ModelName: modelName,
Username: username,
TokenName: tokenName,
Channel: channel,
Group: group,
QuotaFilter: quotaFilter,
})
if err != nil {
common.ApiError(c, err)
return
Expand Down
2 changes: 2 additions & 0 deletions controller/midjourney.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ func GetAllMidjourney(c *gin.Context) {
MjID: c.Query("mj_id"),
StartTimestamp: c.Query("start_timestamp"),
EndTimestamp: c.Query("end_timestamp"),
QuotaFilter: c.Query("quota_filter"),
}

items := model.GetAllTasks(pageInfo.GetStartIdx(), pageInfo.GetPageSize(), queryParams)
Expand All @@ -288,6 +289,7 @@ func GetUserMidjourney(c *gin.Context) {
MjID: c.Query("mj_id"),
StartTimestamp: c.Query("start_timestamp"),
EndTimestamp: c.Query("end_timestamp"),
QuotaFilter: c.Query("quota_filter"),
}

items := model.GetAllUserTask(userId, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), queryParams)
Expand Down
49 changes: 23 additions & 26 deletions controller/misc.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package controller

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"

"github.com/MAX-API-Next/MAX-API/common"
"github.com/MAX-API-Next/MAX-API/constant"
"github.com/MAX-API-Next/MAX-API/i18n"
"github.com/MAX-API-Next/MAX-API/logger"
"github.com/MAX-API-Next/MAX-API/middleware"
"github.com/MAX-API-Next/MAX-API/model"
Expand Down Expand Up @@ -238,12 +239,9 @@ func GetHomePageContent(c *gin.Context) {
}

func SendEmailVerification(c *gin.Context) {
email := c.Query("email")
email := model.NormalizeEmail(c.Query("email"))
if err := common.Validate.Var(email, "required,email"); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的参数",
})
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
}
parts := strings.Split(email, "@")
Expand Down Expand Up @@ -284,10 +282,7 @@ func SendEmailVerification(c *gin.Context) {
}

if model.IsEmailAlreadyTaken(email) {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "邮箱地址已被占用",
})
common.ApiErrorI18n(c, i18n.MsgUserEmailAlreadyTaken)
return
}
code := common.GenerateVerificationCode(6)
Expand All @@ -309,15 +304,12 @@ func SendEmailVerification(c *gin.Context) {
}

func SendPasswordResetEmail(c *gin.Context) {
email := c.Query("email")
email := model.NormalizeEmail(c.Query("email"))
if err := common.Validate.Var(email, "required,email"); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的参数",
})
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
}
if model.IsEmailAlreadyTaken(email) {
if _, err := model.GetUniqueUserByEmail(email); err == nil {
code := common.GenerateVerificationCode(0)
common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose)
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", system_setting.ServerAddress, email, code)
Expand All @@ -328,8 +320,10 @@ func SendPasswordResetEmail(c *gin.Context) {
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, link, common.VerificationValidMinutes)
err := common.SendEmail(subject, email, content)
if err != nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("failed to send password reset email to %s: %s", email, err.Error()))
logger.LogError(c.Request.Context(), fmt.Sprintf("failed to send password reset email to %s: %s", common.MaskEmail(email), err.Error()))
}
} else if !errors.Is(err, model.ErrEmailNotFound) {
logger.LogWarn(c.Request.Context(), fmt.Sprintf("skip password reset email for %s: %s", common.MaskEmail(email), err.Error()))
}
c.JSON(http.StatusOK, gin.H{
"success": true,
Expand All @@ -344,24 +338,27 @@ type PasswordResetRequest struct {

func ResetPassword(c *gin.Context) {
var req PasswordResetRequest
err := json.NewDecoder(c.Request.Body).Decode(&req)
err := common.DecodeJson(c.Request.Body, &req)
if err != nil {
common.ApiError(c, err)
return
}
req.Email = model.NormalizeEmail(req.Email)
if req.Email == "" || req.Token == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的参数",
})
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
}
if !common.VerifyCodeWithKey(req.Email, req.Token, common.PasswordResetPurpose) {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "重置链接非法或已过期",
})
common.ApiErrorI18n(c, i18n.MsgUserPasswordResetLinkInvalid)
return
}
password := common.GenerateVerificationCode(12)
err = model.ResetUserPasswordByEmail(req.Email, password)
if err != nil {
if errors.Is(err, model.ErrEmailNotFound) || errors.Is(err, model.ErrEmailAmbiguous) {
common.ApiErrorI18n(c, i18n.MsgUserPasswordResetLinkInvalid)
return
}
common.ApiError(c, err)
return
}
Expand Down
Loading
Loading