mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-06-03 05:08:02 +02:00
Add geo IP checking
This commit is contained in:
@@ -179,6 +179,8 @@ const (
|
||||
ROUTE_V1_ALLOW_DENY = "/api/v1/allow-deny"
|
||||
ROUTE_V1_ALLOW_DENY_OVERVIEW = "/api/v1/allow-deny/overview"
|
||||
ROUTE_V1_ALLOW_DENY_ID = "/api/v1/allow-deny/:id"
|
||||
// geoip
|
||||
ROUTE_V1_GEOIP_METADATA = "/api/v1/geoip/metadata"
|
||||
// web hooks
|
||||
ROUTE_V1_WEBHOOK = "/api/v1/webhook"
|
||||
ROUTE_V1_WEBHOOK_ID = "/api/v1/webhook/:id"
|
||||
@@ -442,6 +444,8 @@ func setupRoutes(
|
||||
POST(ROUTE_V1_ALLOW_DENY, middleware.SessionHandler, controllers.AllowDeny.Create).
|
||||
PATCH(ROUTE_V1_ALLOW_DENY_ID, middleware.SessionHandler, controllers.AllowDeny.UpdateByID).
|
||||
DELETE(ROUTE_V1_ALLOW_DENY_ID, middleware.SessionHandler, controllers.AllowDeny.DeleteByID).
|
||||
// geoip
|
||||
GET(ROUTE_V1_GEOIP_METADATA, middleware.SessionHandler, controllers.GeoIP.GetMetadata).
|
||||
// web hooks
|
||||
GET(ROUTE_V1_WEBHOOK, middleware.SessionHandler, controllers.Webhook.GetAll).
|
||||
GET(ROUTE_V1_WEBHOOK_ID, middleware.SessionHandler, controllers.Webhook.GetByID).
|
||||
|
||||
@@ -29,6 +29,7 @@ type Controllers struct {
|
||||
QR *controller.QRGenerator
|
||||
APISender *controller.APISender
|
||||
AllowDeny *controller.AllowDeny
|
||||
GeoIP *controller.GeoIP
|
||||
Webhook *controller.Webhook
|
||||
Identifier *controller.Identifier
|
||||
Version *controller.Version
|
||||
@@ -184,6 +185,9 @@ func NewControllers(
|
||||
Common: common,
|
||||
IPAllowListService: services.IPAllowList,
|
||||
}
|
||||
geoIP := &controller.GeoIP{
|
||||
Common: common,
|
||||
}
|
||||
|
||||
return &Controllers{
|
||||
Asset: asset,
|
||||
@@ -207,6 +211,7 @@ func NewControllers(
|
||||
QR: qr,
|
||||
APISender: apiSender,
|
||||
AllowDeny: allowDeny,
|
||||
GeoIP: geoIP,
|
||||
Webhook: webhook,
|
||||
Identifier: identifier,
|
||||
Version: version,
|
||||
|
||||
+26
-9
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/phishingclub/phishingclub/data"
|
||||
"github.com/phishingclub/phishingclub/database"
|
||||
"github.com/phishingclub/phishingclub/errs"
|
||||
"github.com/phishingclub/phishingclub/geoip"
|
||||
"github.com/phishingclub/phishingclub/middleware"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/proxy"
|
||||
@@ -1818,6 +1819,16 @@ func (s *Server) checkIPFilter(
|
||||
// get ja4 fingerprint from context
|
||||
ja4 := middleware.GetJA4FromContext(ctx)
|
||||
|
||||
// get country code from GeoIP lookup
|
||||
var countryCode string
|
||||
if geo, err := geoip.Instance(); err == nil {
|
||||
countryCode, _ = geo.Lookup(ip)
|
||||
}
|
||||
s.logger.Debugw("checking geo ip",
|
||||
"ip", ip,
|
||||
"country", countryCode,
|
||||
)
|
||||
|
||||
allowDenyLEntries, err := s.repositories.Campaign.GetAllDenyByCampaignID(ctx, campaignID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.logger.Debugw("failed to get deny list for campaign",
|
||||
@@ -1852,14 +1863,17 @@ func (s *Server) checkIPFilter(
|
||||
return false, errs.Wrap(err)
|
||||
}
|
||||
|
||||
// for allow lists: both IP and JA4 must pass
|
||||
// for deny lists: either IP or JA4 failing blocks the request
|
||||
// check country code filter
|
||||
countryOk := allowDeny.IsCountryAllowed(countryCode)
|
||||
// for allow lists: all filters (IP, JA4, country) must pass
|
||||
// for deny lists: any filter failing blocks the request
|
||||
if isAllowListing {
|
||||
// allow list: both must be allowed
|
||||
if ipOk && ja4Ok {
|
||||
s.logger.Debugw("IP and JA4 are allow listed",
|
||||
// allow list: all must be allowed
|
||||
if ipOk && ja4Ok && countryOk {
|
||||
s.logger.Debugw("IP, JA4, and country are allow listed",
|
||||
"ip", ip,
|
||||
"ja4", ja4,
|
||||
"country", countryCode,
|
||||
"list name", allowDeny.Name.MustGet().String(),
|
||||
"list id", allowDeny.ID.MustGet().String(),
|
||||
)
|
||||
@@ -1867,13 +1881,15 @@ func (s *Server) checkIPFilter(
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// deny list: if either IP or JA4 is denied, block the request
|
||||
if !ipOk || !ja4Ok {
|
||||
s.logger.Debugw("IP or JA4 is deny listed",
|
||||
// deny list: if any filter denies, block the request
|
||||
if !ipOk || !ja4Ok || !countryOk {
|
||||
s.logger.Debugw("IP, JA4, or country is deny listed",
|
||||
"ip", ip,
|
||||
"ja4", ja4,
|
||||
"country", countryCode,
|
||||
"ipOk", ipOk,
|
||||
"ja4Ok", ja4Ok,
|
||||
"countryOk", countryOk,
|
||||
"list name", allowDeny.Name.MustGet().String(),
|
||||
"list id", allowDeny.ID.MustGet().String(),
|
||||
)
|
||||
@@ -1883,9 +1899,10 @@ func (s *Server) checkIPFilter(
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
s.logger.Debugw("IP or JA4 is not allowed",
|
||||
s.logger.Debugw("IP, JA4, or country is not allowed",
|
||||
"ip", ip,
|
||||
"ja4", ja4,
|
||||
"country", countryCode,
|
||||
)
|
||||
if denyPageID, err := campaign.DenyPageID.Get(); err == nil {
|
||||
err = s.renderDenyPage(ctx, domain, &denyPageID)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/geoip"
|
||||
)
|
||||
|
||||
// GeoIP is a controller for GeoIP-related endpoints
|
||||
type GeoIP struct {
|
||||
Common
|
||||
}
|
||||
|
||||
// GetMetadata returns the GeoIP metadata including available country codes
|
||||
func (c *GeoIP) GetMetadata(g *gin.Context) {
|
||||
_, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// get geoip instance
|
||||
geo, err := geoip.Instance()
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// get metadata
|
||||
metadata := geo.GetMetadata()
|
||||
if metadata == nil {
|
||||
c.Response.BadRequest(g)
|
||||
return
|
||||
}
|
||||
|
||||
c.Response.OK(g, metadata)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ type AllowDeny struct {
|
||||
Name string `gorm:"not null;uniqueIndex:idx_allow_denies_unique_name_and_company_id;"`
|
||||
Cidrs string `gorm:"not null;default:''"`
|
||||
JA4Fingerprints string `gorm:"not null;default:''"`
|
||||
CountryCodes string `gorm:"not null;default:''"`
|
||||
Allowed bool `gorm:"not null;"`
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/phishingclub/phishingclub/embedded"
|
||||
)
|
||||
|
||||
// countryData represents the structure from embedded geoip files
|
||||
type countryData struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
IPv4 []string `json:"ipv4"`
|
||||
IPv6 []string `json:"ipv6"`
|
||||
}
|
||||
|
||||
// metadata represents the geoip metadata file
|
||||
type Metadata struct {
|
||||
Generated string `json:"generated"`
|
||||
Source string `json:"source"`
|
||||
License string `json:"license"`
|
||||
Countries int `json:"countries"`
|
||||
CountryCodes []string `json:"country_codes"`
|
||||
}
|
||||
|
||||
// ipRange represents a single IP range with its country code
|
||||
type ipRange struct {
|
||||
network *net.IPNet
|
||||
countryCode string
|
||||
}
|
||||
|
||||
// GeoIP provides IP to country lookup functionality
|
||||
type GeoIP struct {
|
||||
ranges []ipRange
|
||||
metadata *Metadata
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
instance *GeoIP
|
||||
once sync.Once
|
||||
initErr error
|
||||
)
|
||||
|
||||
// Instance returns the singleton GeoIP instance
|
||||
func Instance() (*GeoIP, error) {
|
||||
once.Do(func() {
|
||||
instance, initErr = New()
|
||||
})
|
||||
return instance, initErr
|
||||
}
|
||||
|
||||
// New creates a new GeoIP instance by loading embedded data
|
||||
func New() (*GeoIP, error) {
|
||||
geo := &GeoIP{
|
||||
ranges: make([]ipRange, 0),
|
||||
}
|
||||
|
||||
// load metadata
|
||||
if err := geo.loadMetadata(); err != nil {
|
||||
return nil, fmt.Errorf("failed to load metadata: %w", err)
|
||||
}
|
||||
|
||||
// load all country data
|
||||
if err := geo.loadAllCountries(); err != nil {
|
||||
return nil, fmt.Errorf("failed to load countries: %w", err)
|
||||
}
|
||||
|
||||
return geo, nil
|
||||
}
|
||||
|
||||
// loadMetadata loads the metadata.json file
|
||||
func (g *GeoIP) loadMetadata() error {
|
||||
data, err := embedded.GeoIPData.ReadFile("geoip/metadata.json")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read metadata: %w", err)
|
||||
}
|
||||
|
||||
var meta Metadata
|
||||
if err := json.Unmarshal(data, &meta); err != nil {
|
||||
return fmt.Errorf("failed to parse metadata: %w", err)
|
||||
}
|
||||
|
||||
g.metadata = &meta
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadAllCountries loads all country IP ranges
|
||||
func (g *GeoIP) loadAllCountries() error {
|
||||
if g.metadata == nil {
|
||||
return fmt.Errorf("metadata not loaded")
|
||||
}
|
||||
|
||||
for _, code := range g.metadata.CountryCodes {
|
||||
if err := g.loadCountry(code); err != nil {
|
||||
// log warning but continue - some countries may not have data
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadCountry loads IP ranges for a single country
|
||||
func (g *GeoIP) loadCountry(countryCode string) error {
|
||||
filename := fmt.Sprintf("geoip/%s.json", strings.ToLower(countryCode))
|
||||
data, err := embedded.GeoIPData.ReadFile(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read %s: %w", countryCode, err)
|
||||
}
|
||||
|
||||
var country countryData
|
||||
if err := json.Unmarshal(data, &country); err != nil {
|
||||
return fmt.Errorf("failed to parse %s: %w", countryCode, err)
|
||||
}
|
||||
|
||||
// add all IPv4 ranges
|
||||
for _, cidr := range country.IPv4 {
|
||||
_, network, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
continue // skip invalid entries
|
||||
}
|
||||
g.ranges = append(g.ranges, ipRange{
|
||||
network: network,
|
||||
countryCode: country.Code,
|
||||
})
|
||||
}
|
||||
|
||||
// add all IPv6 ranges
|
||||
for _, cidr := range country.IPv6 {
|
||||
_, network, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
continue // skip invalid entries
|
||||
}
|
||||
g.ranges = append(g.ranges, ipRange{
|
||||
network: network,
|
||||
countryCode: country.Code,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lookup finds the country code for an IP address
|
||||
// returns (countryCode, found)
|
||||
func (g *GeoIP) Lookup(ipStr string) (string, bool) {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// linear search through ranges
|
||||
// for better performance, consider using a trie or radix tree
|
||||
for _, r := range g.ranges {
|
||||
if r.network.Contains(ip) {
|
||||
return r.countryCode, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// GetMetadata returns the GeoIP metadata
|
||||
func (g *GeoIP) GetMetadata() *Metadata {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
return g.metadata
|
||||
}
|
||||
|
||||
// GetCountryCodes returns a list of all available country codes
|
||||
func (g *GeoIP) GetCountryCodes() []string {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
if g.metadata == nil {
|
||||
return []string{}
|
||||
}
|
||||
return g.metadata.CountryCodes
|
||||
}
|
||||
@@ -23,6 +23,7 @@ type AllowDeny struct {
|
||||
Name nullable.Nullable[vo.String127] `json:"name"`
|
||||
Cidrs nullable.Nullable[vo.IPNetSlice] `json:"cidrs"`
|
||||
JA4Fingerprints nullable.Nullable[string] `json:"ja4Fingerprints"`
|
||||
CountryCodes nullable.Nullable[string] `json:"countryCodes"`
|
||||
Allowed nullable.Nullable[bool] `json:"allowed"`
|
||||
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
|
||||
}
|
||||
@@ -36,7 +37,7 @@ func (r *AllowDeny) Validate() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// at least one of cidrs or ja4 fingerprints must be provided
|
||||
// at least one of cidrs, ja4 fingerprints, or country codes must be provided
|
||||
hasCidrs := false
|
||||
if r.Cidrs.IsSpecified() {
|
||||
if cidrs, err := r.Cidrs.Get(); err == nil && len(cidrs) > 0 {
|
||||
@@ -51,9 +52,16 @@ func (r *AllowDeny) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if !hasCidrs && !hasJA4 {
|
||||
hasCountryCodes := false
|
||||
if r.CountryCodes.IsSpecified() {
|
||||
if codes, err := r.CountryCodes.Get(); err == nil && codes != "" {
|
||||
hasCountryCodes = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasCidrs && !hasJA4 && !hasCountryCodes {
|
||||
return errs.NewValidationError(
|
||||
errors.New("at least one of CIDRs or JA4 fingerprints must be provided"),
|
||||
errors.New("at least one of CIDRs, JA4 fingerprints, or country codes must be provided"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -93,6 +101,12 @@ func (r *AllowDeny) ToDBMap() map[string]any {
|
||||
m["ja4_fingerprints"] = ja4
|
||||
}
|
||||
}
|
||||
if r.CountryCodes.IsSpecified() {
|
||||
m["country_codes"] = ""
|
||||
if codes, err := r.CountryCodes.Get(); err == nil {
|
||||
m["country_codes"] = codes
|
||||
}
|
||||
}
|
||||
if r.Allowed.IsSpecified() {
|
||||
m["allowed"] = nil
|
||||
if allowed, err := r.Allowed.Get(); err == nil {
|
||||
@@ -294,3 +308,73 @@ func trimSpace(s string) string {
|
||||
func isSpace(b byte) bool {
|
||||
return b == ' ' || b == '\t' || b == '\n' || b == '\r'
|
||||
}
|
||||
|
||||
// IsCountryAllowed checks if a country code is allowed based on the filter rules
|
||||
func (r *AllowDeny) IsCountryAllowed(countryCode string) bool {
|
||||
if countryCode == "" {
|
||||
// if no country code available, skip country check
|
||||
return true
|
||||
}
|
||||
|
||||
isTypeAllowList := r.Allowed.MustGet()
|
||||
|
||||
// get country codes list
|
||||
countryCodesStr, err := r.CountryCodes.Get()
|
||||
if err != nil || countryCodesStr == "" {
|
||||
// if no country codes configured, skip country check
|
||||
return true
|
||||
}
|
||||
|
||||
// if country code is empty but we have country filters configured
|
||||
if countryCode == "" {
|
||||
// in allow list mode: unknown country should be denied
|
||||
// in deny list mode: unknown country should be allowed
|
||||
if isTypeAllowList {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// parse country codes (newline separated)
|
||||
codes := parseCountryCodes(countryCodesStr)
|
||||
|
||||
// check if country code matches any in the list (case-insensitive)
|
||||
isMatch := false
|
||||
countryCodeUpper := strings.ToUpper(countryCode)
|
||||
for _, code := range codes {
|
||||
if strings.ToUpper(code) == countryCodeUpper {
|
||||
isMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// if allow list and country matches
|
||||
if isTypeAllowList && isMatch {
|
||||
return true
|
||||
}
|
||||
// if deny list and country matches
|
||||
if !isTypeAllowList && isMatch {
|
||||
return false
|
||||
}
|
||||
|
||||
// If this is an allow list and country didn't match, not allowed
|
||||
if isTypeAllowList {
|
||||
return false
|
||||
}
|
||||
|
||||
// If this is a deny list and country didn't match, it is allowed
|
||||
return true
|
||||
}
|
||||
|
||||
// parseCountryCodes splits newline-separated country codes and trims whitespace
|
||||
func parseCountryCodes(input string) []string {
|
||||
var result []string
|
||||
lines := splitLines(input)
|
||||
for _, line := range lines {
|
||||
trimmed := trimSpace(line)
|
||||
if trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
+24
-11
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/phishingclub/phishingclub/cache"
|
||||
"github.com/phishingclub/phishingclub/data"
|
||||
"github.com/phishingclub/phishingclub/database"
|
||||
"github.com/phishingclub/phishingclub/geoip"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"github.com/phishingclub/phishingclub/server"
|
||||
@@ -191,7 +192,7 @@ func (m *ProxyHandler) HandleHTTPRequest(w http.ResponseWriter, req *http.Reques
|
||||
// check ip filtering for initial MITM requests (before session creation)
|
||||
// at this point, initializeRequestContext has loaded all campaign data
|
||||
if reqCtx.CampaignRecipientID != nil && reqCtx.Campaign != nil {
|
||||
blocked, resp := m.checkIPFilter(req, reqCtx)
|
||||
blocked, resp := m.checkFilter(req, reqCtx)
|
||||
if blocked {
|
||||
if resp != nil {
|
||||
return m.writeResponse(w, resp)
|
||||
@@ -437,7 +438,7 @@ func (m *ProxyHandler) processRequestWithContext(req *http.Request, reqCtx *Requ
|
||||
// check ip filtering for session-based requests (after session is resolved)
|
||||
// skip if this is initial request (already checked before session creation)
|
||||
if reqCtx.CampaignRecipientID != nil && !createSession {
|
||||
blocked, resp := m.checkIPFilter(req, reqCtx)
|
||||
blocked, resp := m.checkFilter(req, reqCtx)
|
||||
if blocked {
|
||||
return req, resp
|
||||
}
|
||||
@@ -3714,10 +3715,10 @@ func (m *ProxyHandler) registerEvasionPageVisitEventDirect(req *http.Request, re
|
||||
}
|
||||
}
|
||||
|
||||
// checkIPFilter checks if the client IP and JA4 fingerprint are allowed for proxy requests
|
||||
// checkFilter checks if the client IP, JA4 fingerprint and geo ip are allowed for proxy requests
|
||||
// JA4 fingerprint is extracted from request context (set by middleware, not from session)
|
||||
// returns (blocked, response) where blocked=true means the request should be blocked
|
||||
func (m *ProxyHandler) checkIPFilter(req *http.Request, reqCtx *RequestContext) (bool, *http.Response) {
|
||||
func (m *ProxyHandler) checkFilter(req *http.Request, reqCtx *RequestContext) (bool, *http.Response) {
|
||||
// use cached campaign info
|
||||
if reqCtx.Campaign == nil || reqCtx.CampaignID == nil {
|
||||
return false, nil // allow if we can't get campaign info
|
||||
@@ -3745,7 +3746,17 @@ func (m *ProxyHandler) checkIPFilter(req *http.Request, reqCtx *RequestContext)
|
||||
// get ja4 fingerprint from request header (set by middleware)
|
||||
ja4 := req.Header.Get(HEADER_JA4)
|
||||
|
||||
// check IP and JA4 against allow/deny lists
|
||||
// get country code from GeoIP lookup
|
||||
var countryCode string
|
||||
if geo, err := geoip.Instance(); err == nil {
|
||||
countryCode, _ = geo.Lookup(ip)
|
||||
}
|
||||
m.logger.Debugw("checking geo ip",
|
||||
"ip", ip,
|
||||
"country", countryCode,
|
||||
)
|
||||
|
||||
// check IP, JA4, and country code against allow/deny lists
|
||||
isAllowListing := false
|
||||
allowed := false // for allow lists, default is deny
|
||||
|
||||
@@ -3770,17 +3781,19 @@ func (m *ProxyHandler) checkIPFilter(req *http.Request, reqCtx *RequestContext)
|
||||
continue
|
||||
}
|
||||
|
||||
// for allow lists: both IP and JA4 must pass
|
||||
// for deny lists: either IP or JA4 failing blocks the request
|
||||
// check country code filter
|
||||
countryOk := allowDeny.IsCountryAllowed(countryCode)
|
||||
// for allow lists: all filters (IP, JA4, country) must pass
|
||||
// for deny lists: any filter failing blocks the request
|
||||
if isAllowListing {
|
||||
// allow list: both must be allowed
|
||||
if ipOk && ja4Ok {
|
||||
// allow list: all must be allowed
|
||||
if ipOk && ja4Ok && countryOk {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// deny list: if either IP or JA4 is denied, block the request
|
||||
if !ipOk || !ja4Ok {
|
||||
// deny list: if any filter denies, block the request
|
||||
if !ipOk || !ja4Ok || !countryOk {
|
||||
allowed = false
|
||||
break
|
||||
}
|
||||
|
||||
@@ -187,6 +187,7 @@ func ToAllowDeny(row *database.AllowDeny) *model.AllowDeny {
|
||||
cidrsNullable := nullable.NewNullableWithValue(cidrs)
|
||||
|
||||
ja4Fingerprints := nullable.NewNullableWithValue(row.JA4Fingerprints)
|
||||
countryCodes := nullable.NewNullableWithValue(row.CountryCodes)
|
||||
|
||||
return &model.AllowDeny{
|
||||
ID: id,
|
||||
@@ -195,6 +196,7 @@ func ToAllowDeny(row *database.AllowDeny) *model.AllowDeny {
|
||||
Name: name,
|
||||
Cidrs: cidrsNullable,
|
||||
JA4Fingerprints: ja4Fingerprints,
|
||||
CountryCodes: countryCodes,
|
||||
Allowed: nullable.NewNullableWithValue(row.Allowed),
|
||||
CompanyID: companyID,
|
||||
}
|
||||
|
||||
@@ -2658,15 +2658,17 @@ export class API {
|
||||
* @param {string} allowdeny.name
|
||||
* @param {string} allowdeny.cidrs
|
||||
* @param {string} allowdeny.ja4Fingerprints
|
||||
* @param {string} allowdeny.countryCodes
|
||||
* @param {boolean} allowdeny.allowed
|
||||
* @param {string} allowdeny.companyID
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
create: async ({ name, cidrs, ja4Fingerprints, allowed, companyID }) => {
|
||||
create: async ({ name, cidrs, ja4Fingerprints, countryCodes, allowed, companyID }) => {
|
||||
return await postJSON(this.getPath('/allow-deny'), {
|
||||
name: name,
|
||||
cidrs: cidrs,
|
||||
ja4Fingerprints: ja4Fingerprints,
|
||||
countryCodes: countryCodes,
|
||||
allowed: allowed,
|
||||
companyID: companyID
|
||||
});
|
||||
@@ -2718,14 +2720,16 @@ export class API {
|
||||
* @param {string} allowdeny.name
|
||||
* @param {string} allowdeny.cidrs
|
||||
* @param {string} allowdeny.ja4Fingerprints
|
||||
* @param {string} allowdeny.countryCodes
|
||||
* @param {string} allowdeny.companyID
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
update: async ({ id, name, cidrs, ja4Fingerprints, companyID }) => {
|
||||
update: async ({ id, name, cidrs, ja4Fingerprints, countryCodes, companyID }) => {
|
||||
return await patchJSON(this.getPath(`/allow-deny/${id}`), {
|
||||
name: name,
|
||||
cidrs: cidrs,
|
||||
ja4Fingerprints: ja4Fingerprints,
|
||||
countryCodes: countryCodes,
|
||||
companyID: companyID
|
||||
});
|
||||
},
|
||||
@@ -2741,6 +2745,20 @@ export class API {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* geoip is the API for GeoIP related operations.
|
||||
*/
|
||||
geoip = {
|
||||
/**
|
||||
* Get GeoIP metadata including available country codes.
|
||||
*
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
getMetadata: async () => {
|
||||
return await getJSON(this.getPath('/geoip/metadata'));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* webhook is the API for web hook related operations.
|
||||
*/
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
eventTypesIDToNameMap: {},
|
||||
notableEventName: ''
|
||||
};
|
||||
let allowedFilter = null;
|
||||
let campaignRecipients = [];
|
||||
let recipientEventsRecipient = {
|
||||
name: null,
|
||||
@@ -224,6 +225,9 @@
|
||||
if (t.sendStartAt === null && t.sendEndAt === null) {
|
||||
isSelfManaged = true;
|
||||
}
|
||||
if (campaign.allowDeny && campaign.allowDeny[0]) {
|
||||
allowedFilter = campaign.allowDeny[0].allowed;
|
||||
}
|
||||
} catch (e) {
|
||||
addToast('Failed to load campaign', 'Error');
|
||||
console.error('failed to load campaign', e);
|
||||
@@ -1267,7 +1271,7 @@
|
||||
>
|
||||
|
||||
<span class="text-grayblue-dark font-medium">
|
||||
{campaign?.allowDeny ? (campaign.allowDeny.allowed ? 'Allow' : 'Deny') : ''}
|
||||
{campaign?.allowDeny ? (allowedFilter ? 'Allow' : 'Deny') : ''}
|
||||
IP Filters:
|
||||
</span>
|
||||
<span class="text-pc-darkblue dark:text-white">
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
import DeleteAlert from '$lib/components/modal/DeleteAlert.svelte';
|
||||
import SelectSquare from '$lib/components/SelectSquare.svelte';
|
||||
import TableCellScope from '$lib/components/table/TableCellScope.svelte';
|
||||
import TextFieldMultiSelect from '$lib/components/TextFieldMultiSelect.svelte';
|
||||
|
||||
// services
|
||||
const appStateService = AppStateService.instance;
|
||||
@@ -44,6 +45,7 @@
|
||||
name: null,
|
||||
cidrs: null,
|
||||
ja4Fingerprints: null,
|
||||
countryCodes: [],
|
||||
allowed: null
|
||||
};
|
||||
let allowDenyList = [];
|
||||
@@ -56,6 +58,7 @@
|
||||
let isTableLoading = false;
|
||||
let modalMode = null;
|
||||
let modalText = '';
|
||||
let availableCountryCodes = [];
|
||||
|
||||
let isDeleteAlertVisible = false;
|
||||
let deleteValues = {
|
||||
@@ -74,6 +77,7 @@
|
||||
}
|
||||
refreshAllowDenies();
|
||||
tableURLParams.onChange(refreshAllowDenies);
|
||||
loadGeoIPMetadata();
|
||||
|
||||
(async () => {
|
||||
const editID = $page.url.searchParams.get('edit');
|
||||
@@ -87,6 +91,18 @@
|
||||
};
|
||||
});
|
||||
|
||||
// load geoip metadata to get country codes
|
||||
const loadGeoIPMetadata = async () => {
|
||||
try {
|
||||
const res = await api.geoip.getMetadata();
|
||||
if (res.success && res.data) {
|
||||
availableCountryCodes = res.data.country_codes || [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('failed to load geoip metadata', e);
|
||||
}
|
||||
};
|
||||
|
||||
// component logic
|
||||
const refreshAllowDenies = async () => {
|
||||
try {
|
||||
@@ -134,12 +150,13 @@
|
||||
};
|
||||
|
||||
const onClickSubmit = async () => {
|
||||
// validate that at least one of cidrs or ja4Fingerprints is provided
|
||||
// validate that at least one of cidrs, ja4Fingerprints, or countryCodes is provided
|
||||
const hasCidrs = formValues.cidrs && formValues.cidrs.trim().length > 0;
|
||||
const hasJA4 = formValues.ja4Fingerprints && formValues.ja4Fingerprints.trim().length > 0;
|
||||
const hasCountryCodes = formValues.countryCodes && formValues.countryCodes.length > 0;
|
||||
|
||||
if (!hasCidrs && !hasJA4) {
|
||||
formError = 'At least one of CIDRs or JA4 fingerprints must be provided';
|
||||
if (!hasCidrs && !hasJA4 && !hasCountryCodes) {
|
||||
formError = 'At least one of CIDRs, JA4 fingerprints, or Country Codes must be provided';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -176,6 +193,7 @@
|
||||
name: formValues.name,
|
||||
cidrs: formValues.cidrs,
|
||||
ja4Fingerprints: formValues.ja4Fingerprints || '',
|
||||
countryCodes: formValues.countryCodes.join('\n'),
|
||||
allowed: formValues.allowed,
|
||||
companyID: contextCompanyID
|
||||
});
|
||||
@@ -212,6 +230,7 @@
|
||||
name: formValues.name,
|
||||
cidrs: formValues.cidrs,
|
||||
ja4Fingerprints: formValues.ja4Fingerprints || '',
|
||||
countryCodes: formValues.countryCodes.join('\n'),
|
||||
companyID: formValues.companyID
|
||||
});
|
||||
if (res.success) {
|
||||
@@ -306,11 +325,21 @@
|
||||
};
|
||||
|
||||
const assignAllowDeny = (allowDeny) => {
|
||||
// parse country codes from newline-separated string to array
|
||||
let countryCodesArray = [];
|
||||
if (allowDeny.countryCodes) {
|
||||
countryCodesArray = allowDeny.countryCodes
|
||||
.split('\n')
|
||||
.map((code) => code.trim())
|
||||
.filter((code) => code.length > 0);
|
||||
}
|
||||
|
||||
formValues = {
|
||||
id: allowDeny.id,
|
||||
name: allowDeny.name,
|
||||
cidrs: allowDeny.cidrs,
|
||||
ja4Fingerprints: allowDeny.ja4Fingerprints || '',
|
||||
countryCodes: countryCodesArray,
|
||||
allowed: allowDeny.allowed,
|
||||
companyID: allowDeny.companyID
|
||||
};
|
||||
@@ -439,8 +468,18 @@
|
||||
toolTipText="Newlines separated JA4 fingerprints (optional)"
|
||||
>JA4 Fingerprints</TextareaField
|
||||
>
|
||||
<TextFieldMultiSelect
|
||||
id="country-codes"
|
||||
optional
|
||||
bind:value={formValues.countryCodes}
|
||||
options={availableCountryCodes}
|
||||
placeholder="Select countries..."
|
||||
toolTipText="Select country codes to filter (optional)"
|
||||
>
|
||||
Country Codes
|
||||
</TextFieldMultiSelect>
|
||||
<p style="font-size: 0.875rem; color: #666; margin-top: 0.5rem;">
|
||||
Note: At least one of CIDRs or JA4 Fingerprints must be provided.
|
||||
Note: At least one of CIDRs, JA4 Fingerprints, or Country Codes must be provided.
|
||||
</p>
|
||||
</FormColumn>
|
||||
</FormColumns>
|
||||
|
||||
Reference in New Issue
Block a user