Initial open source release

This commit is contained in:
Ronni Skansing
2025-08-21 16:14:09 +02:00
commit 11cf01f08e
488 changed files with 97180 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
package middleware
import (
"net"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/phishingclub/phishingclub/config"
"go.uber.org/zap"
)
func NewAllowIPMiddleware(conf *config.Config, logger *zap.SugaredLogger) gin.HandlerFunc {
return func(c *gin.Context) {
// If no IP restrictions are configured, allow all
if len(conf.IPSecurity.AdminAllowed) == 0 {
c.Next()
return
}
c.RemoteIP()
clientIP := c.ClientIP()
allowed := false
for _, allowedIP := range conf.IPSecurity.AdminAllowed {
// check if the allowed entry is a CIDR
if strings.Contains(allowedIP, "/") {
_, ipNet, err := net.ParseCIDR(allowedIP)
if err != nil {
logger.Errorw("Invalid CIDR in allowed IPs",
"cidr", allowedIP,
"error", err)
continue
}
ip := net.ParseIP(clientIP)
if ipNet.Contains(ip) {
allowed = true
break
}
} else {
// Direct IP comparison
if clientIP == allowedIP {
allowed = true
break
}
}
}
if !allowed {
logger.Infow("blocked unauthorized IP access attempt",
"ip", clientIP)
c.AbortWithStatus(http.StatusForbidden)
return
}
c.Next()
}
}
+83
View File
@@ -0,0 +1,83 @@
package middleware
import (
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/time/rate"
)
// NewIPRateLimiterMiddleware creates a middleware that limits the number of requests per IP
// limit is the number of requests per second
// burst is the maximum burst size, the maximum number of requests that can be made in a burst without being limited
func NewIPRateLimiterMiddleware(limit float64, burst int) gin.HandlerFunc {
ipLimiter := NewKeyRateLimiter(rate.Limit(limit), burst, 10*time.Minute)
return func(c *gin.Context) {
limiter := ipLimiter.GetLimiter(c.ClientIP())
if !limiter.Allow() {
c.AbortWithStatus(http.StatusTooManyRequests)
return
}
c.Next()
}
}
//const cleanupInterval = 1 * time.Minute
//const entryExpiration = 10 * time.Minute
// KeyRateLimiter is a rate limiter for key such as username, email or IP
type KeyRateLimiter struct {
// ips is a map of key to rate limit
key sync.Map
// limiter is the rate limit, e.g. 1 request per seconds
limiter rate.Limit
// burst is the maximum burst size, the maximum number of requests that can be made in a burst without being limited
burst int
// cleanupInterval is the interval at which the expired keys are cleaned up
cleanupInterval time.Duration
}
// NewKeyRateLimiter creates a new key rate limiter
// limiter is the rate limit, e.g. 1 request per seconds
// burst is the maximum burst size, the maximum number of requests that can be made in a burst without being limited
func NewKeyRateLimiter(
limiter rate.Limit,
burst int,
cleanupInterval time.Duration,
) *KeyRateLimiter {
rl := &KeyRateLimiter{
limiter: limiter,
burst: burst,
}
go rl.cleanup()
return rl
}
// cleanup cleans up the expired keys, this is to avoid
// memory leaking through the sync.Map when the key is not used anymore
func (r *KeyRateLimiter) cleanup() {
for range time.Tick(r.cleanupInterval) {
now := time.Now()
r.key.Range(func(key, value interface{}) bool {
expirationTime := value.(time.Time)
if now.After(expirationTime) {
r.key.Delete(key)
}
return true
})
}
}
// GetLimiter gets the limiter for an key or creates one if it does not exist
func (r *KeyRateLimiter) GetLimiter(key string) *rate.Limiter {
value, exists := r.key.Load(key)
if exists {
return value.(*rate.Limiter)
}
limiter := rate.NewLimiter(r.limiter, r.burst)
r.key.Store(key, limiter)
return limiter
}
+114
View File
@@ -0,0 +1,114 @@
package middleware
import (
"crypto/sha256"
"crypto/subtle"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/phishingclub/phishingclub/api"
"github.com/phishingclub/phishingclub/controller"
"github.com/phishingclub/phishingclub/data"
"github.com/phishingclub/phishingclub/model"
"github.com/phishingclub/phishingclub/service"
"go.uber.org/zap"
)
// NewSessionHandler creates a middleware that authenticates the user
// by checking it has a session, and if it does, it extends the session and puts
// the user and the session in the gin context.
// if the user does not have a session or must renew password, it returns an unauthorized response.
// if the request contains a valid user API key, the entire session handling is skipped
func NewSessionHandler(
sessionService *service.Session,
userService *service.User,
responseHandler api.JSONResponseHandler,
logger *zap.SugaredLogger,
) gin.HandlerFunc {
return func(c *gin.Context) {
isValidAPISession := handleAPISession(c, userService, logger)
if isValidAPISession {
return
}
s, err := sessionService.GetAndExtendSession(c)
if err != nil {
// errors are logged in service
_ = err
responseHandler.Unauthorized(c)
return
}
user := s.User
if user == nil {
logger.Error("user not found in session")
responseHandler.Unauthorized(c)
return
}
controller.SetSessionInGinContext(c, s)
c.Next()
}
}
// handleAPISession handles if there is a API token in the request header
// returns true if this was a valid API session request
func handleAPISession(
c *gin.Context,
userService *service.User,
logger *zap.SugaredLogger,
) bool {
if headerAPIKey := c.Request.Header.Get(data.APIHeaderKey); len(headerAPIKey) > 0 {
// to check API apiUsers in constant time, we have to retrieve them all
// hash them all and constant time check.
apiUsers, err := userService.GetAllAPIKeysSHA256(c)
if err != nil {
logger.Error("failed to get all api key hashes")
// responseHandler.BadRequest(c)
return false
}
incomingHash := sha256.Sum256([]byte(headerAPIKey))
found := false
// Must check ALL keys in constant time
var rApiUser *model.APIUser
for _, apiUser := range apiUsers {
if subtle.ConstantTimeCompare(incomingHash[:], apiUser.APIKeyHash[:]) == 1 {
found = true
rApiUser = apiUser
break
}
}
if !found {
logger.Debug("API key not found")
// responseHandler.Unauthorized(c)
return false
}
// get user
systemService, err := model.NewSystemSession()
if err != nil {
logger.Error("failed to get system user")
return false
}
user, err := userService.GetByID(c, systemService, rApiUser.ID)
if err != nil {
logger.Error("failed to get user from API token")
return false
}
now := time.Now()
t := now.Add(time.Duration(1 * time.Minute)).UTC()
expiresAt := &t
maxAgeAt := &t
sid := uuid.MustParse(data.APISessionID)
session := &model.Session{
ID: &sid,
ExpiresAt: expiresAt,
MaxAgeAt: maxAgeAt,
IP: c.ClientIP(),
User: user,
IsUserLoaded: true,
IsAPITokenRequest: true,
}
controller.SetSessionInGinContext(c, session)
c.Next()
return true
}
return false
}