initial: ZeroTier-like P2P mesh VPN server with multi-tenant Web UI

This commit is contained in:
xieyao
2026-06-17 03:50:23 +08:00
commit ee5f036325
74 changed files with 9517 additions and 0 deletions

View 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()
}
}

View 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))
}

View 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)
}

View 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
}