mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-17 05:53:15 +02:00
Add sections header to the validation.js and four more go-files
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// LEGACY, WILL BE REMOVED IN FUTURE VERSIONS.
|
||||
package fail2ban
|
||||
|
||||
import (
|
||||
@@ -24,13 +25,13 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// Typical fail2ban log line:
|
||||
// 2023-01-20 10:15:30,123 fail2ban.actions [1234]: NOTICE [sshd] Ban 192.168.0.101
|
||||
logRegex = regexp.MustCompile(`^(\S+\s+\S+) fail2ban\.actions.*?\[\d+\]: NOTICE\s+\[(\S+)\]\s+Ban\s+(\S+)`)
|
||||
)
|
||||
// =========================================================================
|
||||
// Types
|
||||
// =========================================================================
|
||||
|
||||
// BanEvent holds details about a ban
|
||||
var logRegex = regexp.MustCompile(`^(\S+\s+\S+) fail2ban\.actions.*?\[\d+\]: NOTICE\s+\[(\S+)\]\s+Ban\s+(\S+)`)
|
||||
|
||||
// This is a single ban event from the fail2ban log. REMOVE THIS TYPE.
|
||||
type BanEvent struct {
|
||||
Time time.Time
|
||||
Jail string
|
||||
@@ -38,7 +39,11 @@ type BanEvent struct {
|
||||
LogLine string
|
||||
}
|
||||
|
||||
// ParseBanLog returns a map[jailName]BanEvents and also the last 5 ban events overall.
|
||||
// =========================================================================
|
||||
// Log Parsing
|
||||
// =========================================================================
|
||||
|
||||
// ParseBanLog reads the fail2ban log and returns events grouped by jail.
|
||||
func ParseBanLog(logPath string) (map[string][]BanEvent, error) {
|
||||
file, err := os.Open(logPath)
|
||||
if err != nil {
|
||||
@@ -54,17 +59,12 @@ func ParseBanLog(logPath string) (map[string][]BanEvent, error) {
|
||||
|
||||
matches := logRegex.FindStringSubmatch(line)
|
||||
if len(matches) == 4 {
|
||||
// matches[1] -> "2023-01-20 10:15:30,123"
|
||||
// matches[2] -> jail name, e.g. "sshd"
|
||||
// matches[3] -> IP, e.g. "192.168.0.101"
|
||||
timestampStr := matches[1]
|
||||
jail := matches[2]
|
||||
ip := matches[3]
|
||||
|
||||
// parse "2023-01-20 10:15:30,123" -> time.Time
|
||||
parsedTime, err := time.Parse("2006-01-02 15:04:05,000", timestampStr)
|
||||
if err != nil {
|
||||
// If parse fails, skip or set parsedTime=zero
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,11 @@ import (
|
||||
"github.com/swissmakers/fail2ban-ui/internal/config"
|
||||
)
|
||||
|
||||
// Connector describes a communication backend for a Fail2ban server.
|
||||
// =========================================================================
|
||||
// Connector Interface
|
||||
// =========================================================================
|
||||
|
||||
// Connector is the communication backend for a Fail2ban server.
|
||||
type Connector interface {
|
||||
ID() string
|
||||
Server() config.Fail2banServer
|
||||
@@ -19,7 +23,7 @@ type Connector interface {
|
||||
BanIP(ctx context.Context, jail, ip string) error
|
||||
Reload(ctx context.Context) error
|
||||
Restart(ctx context.Context) error
|
||||
GetFilterConfig(ctx context.Context, jail string) (string, string, error) // Returns (config, filePath, error)
|
||||
GetFilterConfig(ctx context.Context, jail string) (string, string, error)
|
||||
SetFilterConfig(ctx context.Context, jail, content string) error
|
||||
FetchBanEvents(ctx context.Context, limit int) ([]BanEvent, error)
|
||||
|
||||
@@ -32,7 +36,7 @@ type Connector interface {
|
||||
TestFilter(ctx context.Context, filterName string, logLines []string, filterContent string) (output string, filterPath string, err error)
|
||||
|
||||
// Jail configuration operations
|
||||
GetJailConfig(ctx context.Context, jail string) (string, string, error) // Returns (config, filePath, error)
|
||||
GetJailConfig(ctx context.Context, jail string) (string, string, error)
|
||||
SetJailConfig(ctx context.Context, jail, content string) error
|
||||
TestLogpath(ctx context.Context, logpath string) ([]string, error)
|
||||
TestLogpathWithResolution(ctx context.Context, logpath string) (originalPath, resolvedPath string, files []string, err error)
|
||||
@@ -54,7 +58,11 @@ type Connector interface {
|
||||
DeleteFilter(ctx context.Context, filterName string) error
|
||||
}
|
||||
|
||||
// Manager orchestrates all connectors for configured Fail2ban servers.
|
||||
// =========================================================================
|
||||
// Manager
|
||||
// =========================================================================
|
||||
|
||||
// Manager holds connectors for all configured Fail2ban servers.
|
||||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
connectors map[string]Connector
|
||||
@@ -65,7 +73,6 @@ var (
|
||||
managerInst *Manager
|
||||
)
|
||||
|
||||
// GetManager returns the singleton connector manager.
|
||||
func GetManager() *Manager {
|
||||
managerOnce.Do(func() {
|
||||
managerInst = &Manager{
|
||||
@@ -75,7 +82,6 @@ func GetManager() *Manager {
|
||||
return managerInst
|
||||
}
|
||||
|
||||
// ReloadFromSettings rebuilds connectors using the provided settings.
|
||||
func (m *Manager) ReloadFromSettings(settings config.AppSettings) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
@@ -96,7 +102,7 @@ func (m *Manager) ReloadFromSettings(settings config.AppSettings) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Connector returns the connector for the specified server ID.
|
||||
// Returns the connector for the specified server ID.
|
||||
func (m *Manager) Connector(serverID string) (Connector, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
@@ -111,7 +117,7 @@ func (m *Manager) Connector(serverID string) (Connector, error) {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// DefaultConnector returns the default connector as defined in settings.
|
||||
// Returns the default connector as defined in settings.
|
||||
func (m *Manager) DefaultConnector() (Connector, error) {
|
||||
server := config.GetDefaultServer()
|
||||
if server.ID == "" {
|
||||
@@ -120,7 +126,7 @@ func (m *Manager) DefaultConnector() (Connector, error) {
|
||||
return m.Connector(server.ID)
|
||||
}
|
||||
|
||||
// Connectors returns all connectors.
|
||||
// Returns all connectors.
|
||||
func (m *Manager) Connectors() []Connector {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
@@ -131,7 +137,11 @@ func (m *Manager) Connectors() []Connector {
|
||||
return result
|
||||
}
|
||||
|
||||
// UpdateActionFiles updates action files for all active remote connectors (SSH and Agent).
|
||||
// =========================================================================
|
||||
// Action File Management
|
||||
// =========================================================================
|
||||
|
||||
// Updates action files for all active remote connectors (SSH and Agent).
|
||||
func (m *Manager) UpdateActionFiles(ctx context.Context) error {
|
||||
m.mu.RLock()
|
||||
connectors := make([]Connector, 0, len(m.connectors))
|
||||
@@ -154,7 +164,7 @@ func (m *Manager) UpdateActionFiles(ctx context.Context) error {
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// UpdateActionFileForServer updates the action file for a specific server by ID.
|
||||
// Updates the action file for a single server.
|
||||
func (m *Manager) UpdateActionFileForServer(ctx context.Context, serverID string) error {
|
||||
m.mu.RLock()
|
||||
conn, ok := m.connectors[serverID]
|
||||
@@ -165,7 +175,6 @@ func (m *Manager) UpdateActionFileForServer(ctx context.Context, serverID string
|
||||
return updateConnectorAction(ctx, conn)
|
||||
}
|
||||
|
||||
// updateConnectorAction updates the action file for a specific connector.
|
||||
func updateConnectorAction(ctx context.Context, conn Connector) error {
|
||||
switch c := conn.(type) {
|
||||
case *SSHConnector:
|
||||
@@ -173,15 +182,18 @@ func updateConnectorAction(ctx context.Context, conn Connector) error {
|
||||
case *AgentConnector:
|
||||
return c.ensureAction(ctx)
|
||||
default:
|
||||
return nil // Local connectors are handled separately
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Connector Factory
|
||||
// =========================================================================
|
||||
|
||||
func newConnectorForServer(server config.Fail2banServer) (Connector, error) {
|
||||
switch server.Type {
|
||||
case "local":
|
||||
// Run migration FIRST before ensuring structure — but only when
|
||||
// the experimental JAIL_AUTOMIGRATION=true env var is set.
|
||||
// Run migration before ensuring structure, but only when the experimental JAIL_AUTOMIGRATION=true env var is set.
|
||||
if isJailAutoMigrationEnabled() {
|
||||
config.DebugLog("JAIL_AUTOMIGRATION=true: running experimental jail.local → jail.d/ migration for local server %s", server.Name)
|
||||
if err := MigrateJailsFromJailLocal(); err != nil {
|
||||
|
||||
@@ -27,13 +27,12 @@ import (
|
||||
"github.com/swissmakers/fail2ban-ui/internal/config"
|
||||
)
|
||||
|
||||
var (
|
||||
// Variable pattern: %(variable_name)s
|
||||
variablePattern = regexp.MustCompile(`%\(([^)]+)\)s`)
|
||||
)
|
||||
// =========================================================================
|
||||
// Pattern Matching
|
||||
// =========================================================================
|
||||
|
||||
var variablePattern = regexp.MustCompile(`%\(([^)]+)\)s`)
|
||||
|
||||
// extractVariablesFromString extracts all variable names from a string.
|
||||
// Returns a list of variable names found in the pattern %(name)s.
|
||||
func extractVariablesFromString(s string) []string {
|
||||
matches := variablePattern.FindAllStringSubmatch(s, -1)
|
||||
if len(matches) == 0 {
|
||||
@@ -49,8 +48,10 @@ func extractVariablesFromString(s string) []string {
|
||||
return variables
|
||||
}
|
||||
|
||||
// searchVariableInFile searches for a variable definition in a single file.
|
||||
// Returns the value if found, empty string if not found, and error on file read error.
|
||||
// =========================================================================
|
||||
// Variable Lookup
|
||||
// =========================================================================
|
||||
|
||||
func searchVariableInFile(filePath, varName string) (string, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
@@ -162,19 +163,16 @@ func searchVariableInFile(filePath, varName string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// findVariableDefinition searches for a variable definition in all .local files first,
|
||||
// then .conf files under /etc/fail2ban/ and subdirectories.
|
||||
// Searches for a variable definition in all .local files first, then .conf files under /etc/fail2ban/ and subdirectories.
|
||||
// Returns the FIRST value found (prioritizing .local over .conf).
|
||||
func findVariableDefinition(varName string) (string, error) {
|
||||
fail2banPath := "/etc/fail2ban"
|
||||
|
||||
config.DebugLog("findVariableDefinition: searching for variable '%s'", varName)
|
||||
|
||||
if _, err := os.Stat(fail2banPath); os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("variable '%s' not found: /etc/fail2ban directory does not exist", varName)
|
||||
}
|
||||
|
||||
// First pass: search .local files (higher priority)
|
||||
var foundValue string
|
||||
err := filepath.Walk(fail2banPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
@@ -187,14 +185,13 @@ func findVariableDefinition(varName string) (string, error) {
|
||||
|
||||
value, err := searchVariableInFile(path, varName)
|
||||
if err != nil {
|
||||
return nil // Skip files we can't read
|
||||
return nil
|
||||
}
|
||||
|
||||
if value != "" {
|
||||
foundValue = value
|
||||
return filepath.SkipAll // Stop walking when found
|
||||
return filepath.SkipAll
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -207,7 +204,6 @@ func findVariableDefinition(varName string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Second pass: search .conf files (only if not found in .local)
|
||||
config.DebugLog("findVariableDefinition: variable '%s' not found in .local files, searching .conf files", varName)
|
||||
err = filepath.Walk(fail2banPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
@@ -225,9 +221,8 @@ func findVariableDefinition(varName string) (string, error) {
|
||||
|
||||
if value != "" {
|
||||
foundValue = value
|
||||
return filepath.SkipAll // Stop walking when found
|
||||
return filepath.SkipAll
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -244,9 +239,10 @@ func findVariableDefinition(varName string) (string, error) {
|
||||
return "", fmt.Errorf("variable '%s' not found in Fail2Ban configuration files", varName)
|
||||
}
|
||||
|
||||
// resolveVariableRecursive resolves a variable recursively, handling nested variables.
|
||||
// visited map tracks visited variables to detect circular references.
|
||||
// This function fully resolves all nested variables until no variables remain.
|
||||
// =========================================================================
|
||||
// Resolution
|
||||
// =========================================================================
|
||||
|
||||
func resolveVariableRecursive(varName string, visited map[string]bool) (string, error) {
|
||||
if visited[varName] {
|
||||
return "", fmt.Errorf("circular reference detected for variable '%s'", varName)
|
||||
@@ -260,7 +256,6 @@ func resolveVariableRecursive(varName string, visited map[string]bool) (string,
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Keep resolving until no more variables are found
|
||||
resolved := value
|
||||
maxIterations := 10
|
||||
iteration := 0
|
||||
@@ -268,16 +263,12 @@ func resolveVariableRecursive(varName string, visited map[string]bool) (string,
|
||||
for iteration < maxIterations {
|
||||
variables := extractVariablesFromString(resolved)
|
||||
if len(variables) == 0 {
|
||||
// No more variables, fully resolved
|
||||
config.DebugLog("resolveVariableRecursive: '%s' fully resolved to '%s'", varName, resolved)
|
||||
break
|
||||
}
|
||||
|
||||
config.DebugLog("resolveVariableRecursive: iteration %d for '%s', found %d variables in '%s': %v", iteration+1, varName, len(variables), resolved, variables)
|
||||
|
||||
// Resolve all nested variables
|
||||
for _, nestedVar := range variables {
|
||||
// Check for circular reference
|
||||
if visited[nestedVar] {
|
||||
return "", fmt.Errorf("circular reference detected: '%s' -> '%s'", varName, nestedVar)
|
||||
}
|
||||
@@ -289,70 +280,50 @@ func resolveVariableRecursive(varName string, visited map[string]bool) (string,
|
||||
}
|
||||
|
||||
config.DebugLog("resolveVariableRecursive: resolved '%s' to '%s' for '%s'", nestedVar, nestedValue, varName)
|
||||
|
||||
// Replace ALL occurrences of the nested variable
|
||||
// Pattern: %(varName)s - need to escape parentheses for regex
|
||||
// The pattern %(varName)s needs to be escaped as %\(varName\)s in regex
|
||||
pattern := fmt.Sprintf("%%\\(%s\\)s", regexp.QuoteMeta(nestedVar))
|
||||
re := regexp.MustCompile(pattern)
|
||||
beforeReplace := resolved
|
||||
resolved = re.ReplaceAllString(resolved, nestedValue)
|
||||
config.DebugLog("resolveVariableRecursive: replaced pattern '%s' in '%s' with '%s', result: '%s'", pattern, beforeReplace, nestedValue, resolved)
|
||||
|
||||
// Verify the replacement actually happened
|
||||
if beforeReplace == resolved {
|
||||
config.DebugLog("resolveVariableRecursive: WARNING - replacement did not change string! Pattern: '%s', Before: '%s', After: '%s'", pattern, beforeReplace, resolved)
|
||||
// If replacement didn't work, this is a critical error
|
||||
return "", fmt.Errorf("failed to replace variable '%s' in '%s': pattern '%s' did not match", nestedVar, beforeReplace, pattern)
|
||||
}
|
||||
}
|
||||
|
||||
// After replacing all variables in this iteration, check if we're done
|
||||
// Verify no variables remain before continuing
|
||||
remainingVars := extractVariablesFromString(resolved)
|
||||
if len(remainingVars) == 0 {
|
||||
// No more variables, fully resolved
|
||||
config.DebugLog("resolveVariableRecursive: '%s' fully resolved to '%s' after replacements", varName, resolved)
|
||||
break
|
||||
}
|
||||
|
||||
// If we still have variables after replacement, continue to next iteration
|
||||
// But check if we made progress (resolved should be different from before)
|
||||
iteration++
|
||||
}
|
||||
|
||||
if iteration >= maxIterations {
|
||||
return "", fmt.Errorf("maximum resolution iterations reached for variable '%s', possible circular reference. Last resolved value: '%s'", varName, resolved)
|
||||
}
|
||||
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
// ResolveLogpathVariables resolves all variables in a logpath string.
|
||||
// Returns the fully resolved path. If no variables are present, returns the original path.
|
||||
// Keeps resolving until no more variables are found (handles nested variables).
|
||||
// Expands %(var)s patterns in logpath using fail2ban config.
|
||||
func ResolveLogpathVariables(logpath string) (string, error) {
|
||||
if logpath == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
logpath = strings.TrimSpace(logpath)
|
||||
|
||||
// Keep resolving until no more variables are found
|
||||
resolved := logpath
|
||||
maxIterations := 10 // Prevent infinite loops
|
||||
maxIterations := 10
|
||||
iteration := 0
|
||||
|
||||
for iteration < maxIterations {
|
||||
variables := extractVariablesFromString(resolved)
|
||||
if len(variables) == 0 {
|
||||
// No more variables, we're done
|
||||
break
|
||||
}
|
||||
|
||||
config.DebugLog("ResolveLogpathVariables: iteration %d, found %d variables in '%s'", iteration+1, len(variables), resolved)
|
||||
|
||||
// Resolve all variables found in the current string
|
||||
visited := make(map[string]bool)
|
||||
for _, varName := range variables {
|
||||
config.DebugLog("ResolveLogpathVariables: resolving variable '%s' from string '%s'", varName, resolved)
|
||||
@@ -363,9 +334,6 @@ func ResolveLogpathVariables(logpath string) (string, error) {
|
||||
|
||||
config.DebugLog("ResolveLogpathVariables: resolved variable '%s' to '%s'", varName, varValue)
|
||||
|
||||
// Replace ALL occurrences of the variable in the resolved string
|
||||
// Pattern: %(varName)s - need to escape parentheses for regex
|
||||
// The pattern %(varName)s needs to be escaped as %\(varName\)s in regex
|
||||
pattern := fmt.Sprintf("%%\\(%s\\)s", regexp.QuoteMeta(varName))
|
||||
re := regexp.MustCompile(pattern)
|
||||
beforeReplace := resolved
|
||||
|
||||
Reference in New Issue
Block a user