initial: ZeroTier-like P2P mesh VPN server with multi-tenant Web UI
This commit is contained in:
29
internal/middleware/cors.go
Normal file
29
internal/middleware/cors.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func CORS(allowedOrigins []string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
origin := c.GetHeader("Origin")
|
||||
for _, allowed := range allowedOrigins {
|
||||
if allowed == "*" || allowed == origin {
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
break
|
||||
}
|
||||
}
|
||||
if origin == "" {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
}
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
58
internal/middleware/jwt.go
Normal file
58
internal/middleware/jwt.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type JWTClaims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func JWTAuth(secret string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
auth := c.GetHeader("Authorization")
|
||||
if auth == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing authorization"})
|
||||
return
|
||||
}
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization format"})
|
||||
return
|
||||
}
|
||||
claims := &JWTClaims{}
|
||||
token, err := jwt.ParseWithClaims(parts[1], claims, func(t *jwt.Token) (interface{}, error) {
|
||||
return []byte(secret), nil
|
||||
})
|
||||
if err != nil || !token.Valid {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
|
||||
return
|
||||
}
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateToken(secret string, userID uint, username, role string, expireHours int) (string, error) {
|
||||
claims := JWTClaims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expireHours) * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(secret))
|
||||
}
|
||||
97
internal/middleware/logger.go
Normal file
97
internal/middleware/logger.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"zeromesh/internal/model"
|
||||
)
|
||||
|
||||
type LogWriter interface {
|
||||
InsertApiLog(*model.ApiLog) error
|
||||
InsertErrorLog(*model.ErrorLog) error
|
||||
}
|
||||
|
||||
type RequestLogger struct {
|
||||
writer LogWriter
|
||||
}
|
||||
|
||||
func NewRequestLogger(writer LogWriter) *RequestLogger {
|
||||
return &RequestLogger{writer: writer}
|
||||
}
|
||||
|
||||
func (l *RequestLogger) Handler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
|
||||
if strings.HasPrefix(path, "/api/v1/log") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
body := ""
|
||||
if c.Request.Body != nil {
|
||||
b, _ := io.ReadAll(c.Request.Body)
|
||||
body = string(b)
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(b))
|
||||
}
|
||||
|
||||
blw := &bodyLogWriter{ResponseWriter: c.Writer, buf: &bytes.Buffer{}}
|
||||
c.Writer = blw
|
||||
|
||||
c.Next()
|
||||
|
||||
latencyMs := time.Since(start).Milliseconds()
|
||||
ip := c.ClientIP()
|
||||
method := c.Request.Method
|
||||
query := c.Request.URL.RawQuery
|
||||
ua := c.Request.UserAgent()
|
||||
status := c.Writer.Status()
|
||||
|
||||
if status >= 400 {
|
||||
el := &model.ErrorLog{
|
||||
IP: ip,
|
||||
Method: method,
|
||||
Path: path,
|
||||
StatusCode: status,
|
||||
Query: query,
|
||||
UserAgent: ua,
|
||||
RequestBody: body,
|
||||
ResponseBody: blw.buf.String(),
|
||||
LatencyMs: latencyMs,
|
||||
}
|
||||
if err := l.writer.InsertErrorLog(el); err != nil {
|
||||
c.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
al := &model.ApiLog{
|
||||
IP: ip,
|
||||
Method: method,
|
||||
Path: path,
|
||||
StatusCode: status,
|
||||
Query: query,
|
||||
UserAgent: ua,
|
||||
LatencyMs: latencyMs,
|
||||
}
|
||||
if err := l.writer.InsertApiLog(al); err != nil {
|
||||
c.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type bodyLogWriter struct {
|
||||
gin.ResponseWriter
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *bodyLogWriter) Write(b []byte) (int, error) {
|
||||
w.buf.Write(b)
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
64
internal/middleware/ratelimit.go
Normal file
64
internal/middleware/ratelimit.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type RateLimiter struct {
|
||||
mu sync.Mutex
|
||||
clients map[string]*clientBuckets
|
||||
rate int
|
||||
burst int
|
||||
}
|
||||
|
||||
type clientBuckets struct {
|
||||
tokens int
|
||||
lastFill time.Time
|
||||
}
|
||||
|
||||
func NewRateLimiter(rate, burst int) *RateLimiter {
|
||||
return &RateLimiter{
|
||||
clients: make(map[string]*clientBuckets),
|
||||
rate: rate,
|
||||
burst: burst,
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *RateLimiter) Handler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !rl.allow(c.ClientIP()) {
|
||||
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *RateLimiter) allow(key string) bool {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
|
||||
b, ok := rl.clients[key]
|
||||
if !ok {
|
||||
b = &clientBuckets{tokens: rl.burst, lastFill: time.Now()}
|
||||
rl.clients[key] = b
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
elapsed := now.Sub(b.lastFill)
|
||||
b.lastFill = now
|
||||
b.tokens += int(elapsed.Seconds()) * rl.rate
|
||||
if b.tokens > rl.burst {
|
||||
b.tokens = rl.burst
|
||||
}
|
||||
|
||||
if b.tokens > 0 {
|
||||
b.tokens--
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user