fc3a66dc83
Go 控制面(SQLite + REST API)嵌入 Mihomo 数据面,Vue 3 多语言 Web 管理台。 含订阅/节点/规则/出站/监控/日志、OpenAPI、API Key、Gitea Actions CI(runs-on: ubuntu-latest) 与开源文档;镜像发布至 git.rc707blog.top Container Registry。 Co-authored-by: Cursor <cursoragent@cursor.com>
117 lines
2.8 KiB
Go
117 lines
2.8 KiB
Go
package api
|
|
|
|
import (
|
|
"math/rand"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"github.com/prism/proxy/internal/auth"
|
|
)
|
|
|
|
type loginReq struct {
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
func (s *Server) authMiddleware() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
settings, err := s.app.Store.GetSettings(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
c.Abort()
|
|
return
|
|
}
|
|
if !s.authorizeRequest(c, settings) {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
func (s *Server) authStatus(c *gin.Context) {
|
|
settings, err := s.app.Store.GetSettings(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"enabled": settings["auth_enabled"] == "true"})
|
|
}
|
|
|
|
func (s *Server) login(c *gin.Context) {
|
|
var req loginReq
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
ctx := c.Request.Context()
|
|
settings, err := s.app.Store.GetSettings(ctx)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
ip := clientIP(c)
|
|
cfg := auth.ParseLoginConfig(settings)
|
|
check, err := s.loginLimiter.Check(ctx, ip, cfg)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if !check.Allowed {
|
|
writeLoginLocked(c, check.RetryAfter)
|
|
return
|
|
}
|
|
|
|
hash := settings["admin_password"]
|
|
if !checkPassword(req.Password, hash) {
|
|
time.Sleep(loginFailureDelay())
|
|
result, err := s.loginLimiter.RecordFailure(ctx, ip, cfg)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if result.Locked {
|
|
writeLoginLocked(c, result.RetryAfter)
|
|
return
|
|
}
|
|
c.JSON(http.StatusUnauthorized, gin.H{
|
|
"error": "invalid password",
|
|
"remaining": result.Remaining,
|
|
})
|
|
return
|
|
}
|
|
if err := s.loginLimiter.RecordSuccess(ctx, ip); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
token := issueToken(hash, 24*time.Hour)
|
|
c.JSON(http.StatusOK, gin.H{"token": token})
|
|
}
|
|
|
|
func writeLoginLocked(c *gin.Context, retryAfter time.Duration) {
|
|
secs := int(retryAfter.Seconds())
|
|
if secs < 1 {
|
|
secs = 1
|
|
}
|
|
c.JSON(http.StatusTooManyRequests, gin.H{
|
|
"error": "too many login attempts",
|
|
"retry_after": secs,
|
|
})
|
|
}
|
|
|
|
func loginFailureDelay() time.Duration {
|
|
return 300*time.Millisecond + time.Duration(rand.Intn(200))*time.Millisecond
|
|
}
|
|
|
|
func checkPassword(password, stored string) bool {
|
|
if strings.HasPrefix(stored, "$2a$") {
|
|
return bcrypt.CompareHashAndPassword([]byte(stored), []byte(password)) == nil
|
|
}
|
|
return password == stored
|
|
}
|