OpenAI-compatible AI gateway with Vue admin UI, multi-provider egress, ingress key governance, monitoring, and security controls. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rose_cat707/luminary/internal/model"
|
||||
"github.com/rose_cat707/luminary/internal/store/cache"
|
||||
)
|
||||
|
||||
func IPFilter(c *cache.Store, scope model.IPRuleScope) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
rules := c.ListIPRules()
|
||||
if len(rules) == 0 {
|
||||
ctx.Next()
|
||||
return
|
||||
}
|
||||
clientIP := clientIP(ctx)
|
||||
ip := net.ParseIP(clientIP)
|
||||
if ip == nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "invalid client ip"})
|
||||
return
|
||||
}
|
||||
var allowRules, denyRules []model.IPRule
|
||||
for _, r := range rules {
|
||||
if !r.Enabled {
|
||||
continue
|
||||
}
|
||||
if r.Scope != scope && r.Scope != model.IPScopeAll {
|
||||
continue
|
||||
}
|
||||
if r.Type == model.IPRuleAllow {
|
||||
allowRules = append(allowRules, r)
|
||||
} else {
|
||||
denyRules = append(denyRules, r)
|
||||
}
|
||||
}
|
||||
for _, r := range denyRules {
|
||||
if ipMatchesRule(r.CIDR, ip) {
|
||||
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "ip denied"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(allowRules) > 0 {
|
||||
allowed := false
|
||||
for _, r := range allowRules {
|
||||
if ipMatchesRule(r.CIDR, ip) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "ip not allowed"})
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func cidrContains(cidr string, ip net.IP) bool {
|
||||
if !strings.Contains(cidr, "/") {
|
||||
return ip.String() == cidr
|
||||
}
|
||||
_, network, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return network.Contains(ip)
|
||||
}
|
||||
|
||||
// ipMatchesRule checks CIDR/IP match, treating IPv4/IPv6 loopback as equivalent.
|
||||
func ipMatchesRule(cidr string, ip net.IP) bool {
|
||||
if cidrContains(cidr, ip) {
|
||||
return true
|
||||
}
|
||||
if !ip.IsLoopback() {
|
||||
return false
|
||||
}
|
||||
alt := loopbackAlt(ip)
|
||||
return alt != nil && cidrContains(cidr, alt)
|
||||
}
|
||||
|
||||
func loopbackAlt(ip net.IP) net.IP {
|
||||
if v4 := ip.To4(); v4 != nil && v4.IsLoopback() {
|
||||
return net.ParseIP("::1")
|
||||
}
|
||||
if ip.Equal(net.ParseIP("::1")) {
|
||||
return net.ParseIP("127.0.0.1")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func clientIP(ctx *gin.Context) string {
|
||||
return ClientIP(ctx)
|
||||
}
|
||||
|
||||
const sessionCookie = "luminary_session"
|
||||
|
||||
type SessionValidator func(token string) (bool, time.Time)
|
||||
|
||||
func AdminAuth(validate SessionValidator) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
token := extractToken(ctx)
|
||||
if token == "" {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
ok, expires := validate(token)
|
||||
if !ok || time.Now().After(expires) {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "session expired"})
|
||||
return
|
||||
}
|
||||
ctx.Set("session_token", token)
|
||||
ctx.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func extractToken(ctx *gin.Context) string {
|
||||
if auth := ctx.GetHeader("Authorization"); strings.HasPrefix(auth, "Bearer ") {
|
||||
return strings.TrimPrefix(auth, "Bearer ")
|
||||
}
|
||||
if cookie, err := ctx.Cookie(sessionCookie); err == nil {
|
||||
return cookie
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func SessionCookieName() string { return sessionCookie }
|
||||
|
||||
func IngressAuth(gw IngressKeyResolver) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
key := extractIngressKey(ctx)
|
||||
if key == "" {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing api key"})
|
||||
return
|
||||
}
|
||||
ingress, err := gw.ResolveIngressKey(key)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid api key"})
|
||||
return
|
||||
}
|
||||
ctx.Set("ingress_key", ingress)
|
||||
ctx.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func extractIngressKey(ctx *gin.Context) string {
|
||||
if auth := ctx.GetHeader("Authorization"); strings.HasPrefix(auth, "Bearer ") {
|
||||
return strings.TrimPrefix(auth, "Bearer ")
|
||||
}
|
||||
return ctx.GetHeader("x-api-key")
|
||||
}
|
||||
|
||||
type IngressKeyResolver interface {
|
||||
ResolveIngressKey(key string) (*model.IngressKey, error)
|
||||
}
|
||||
|
||||
func Logger() gin.HandlerFunc {
|
||||
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||
return param.TimeStamp.Format(time.RFC3339) + " " + param.Method + " " + param.Path + " " + param.ClientIP + "\n"
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user