Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
820c9410a1 | ||
|
|
6f261803a7 | ||
|
|
aacc98668f | ||
|
|
9519eedf4f | ||
|
|
b8b9b227a9 | ||
|
|
08d3214f22 | ||
|
|
6ebda76738 | ||
|
|
b9754f605b | ||
|
|
be6b19426b | ||
|
|
3ebffda2c7 | ||
|
|
cadbbc9080 | ||
|
|
e907fb0b1a | ||
|
|
b0fc0646d2 | ||
|
|
c2eb02afc7 | ||
|
|
262f3daee4 | ||
|
|
fb32886d4a | ||
|
|
fb624a9147 | ||
|
|
7741e08ebc | ||
|
|
5f607d0be0 |
89
README.md
89
README.md
@@ -15,14 +15,15 @@ Log-based IPS system written in Go for Linux-based system.
|
||||
|
||||
# Overview
|
||||
BanForge is a simple IPS for replacement fail2ban in Linux system.
|
||||
The project is currently in its early stages of development.
|
||||
All release are available on my self-hosted [Gitea](https://gitea.d3m0k1d.ru/d3m0k1d/BanForge) because Github has limits for Actions.
|
||||
All release are available on my self-hosted [Gitea](https://gitea.d3m0k1d.ru/d3m0k1d/BanForge) after release v1.0.0 are available on Github release page.
|
||||
If you have any questions or suggestions, create issue on [Github](https://github.com/d3m0k1d/BanForge/issues).
|
||||
|
||||
## Roadmap
|
||||
- [x] Real-time Nginx log monitoring
|
||||
- [ ] Add support for other service
|
||||
- [ ] Add support for user service with regular expressions
|
||||
- [x] Rule system
|
||||
- [x] Nginx and Sshd support
|
||||
- [x] Working with ufw/iptables/nftables/firewalld
|
||||
- [ ] Add support for most popular web-service
|
||||
- [ ] User regexp for custom services
|
||||
- [ ] TUI interface
|
||||
|
||||
# Requirements
|
||||
@@ -31,15 +32,79 @@ If you have any questions or suggestions, create issue on [Github](https://githu
|
||||
- ufw/iptables/nftables/firewalld
|
||||
|
||||
# Installation
|
||||
Search for a release on the [Gitea](https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/releases) releases page and download it. Then create or copy a systemd unit file.
|
||||
Or clone the repo and use the Makefile.
|
||||
```
|
||||
git clone https://gitea.d3m0k1d.ru/d3m0k1d/BanForge.git
|
||||
cd BanForge
|
||||
sudo make build-daemon
|
||||
cd bin
|
||||
Search for a release on the [Gitea](https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/releases) releases page and download it.
|
||||
In release page you can find rpm, deb, apk packages, for amd or arm architecture.
|
||||
|
||||
## Installation guide for packages
|
||||
|
||||
### Debian/Ubuntu(.deb)
|
||||
```bash
|
||||
# Download the latest DEB package
|
||||
wget https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/releases/download/v0.4.0/banforge_0.4.0_linux_amd64.deb
|
||||
|
||||
# Install
|
||||
sudo dpkg -i banforge_0.4.0_linux_amd64.deb
|
||||
|
||||
# Verify installation
|
||||
sudo systemctl status banforge
|
||||
```
|
||||
|
||||
### RHEL-based(.rpm)
|
||||
```bash
|
||||
|
||||
# Download
|
||||
wget https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/releases/download/v0.4.0/banforge_0.4.0_linux_amd64.rpm
|
||||
|
||||
# Install
|
||||
sudo rpm -i banforge_0.4.0_linux_amd64.rpm
|
||||
|
||||
# Or with dnf (CentOS 8+, AlmaLinux)
|
||||
sudo dnf install banforge_0.4.0_linux_amd64.rpm
|
||||
|
||||
# Verify
|
||||
sudo systemctl status banforge
|
||||
```
|
||||
|
||||
### Alpine(.apk)
|
||||
```bash
|
||||
|
||||
# Download
|
||||
wget https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/releases/download/v0.4.0/banforge_0.4.0_linux_amd64.apk
|
||||
|
||||
# Install
|
||||
sudo apk add --allow-untrusted banforge_0.4.0_linux_amd64.apk
|
||||
|
||||
# Verify
|
||||
sudo rc-service banforge status
|
||||
```
|
||||
|
||||
### Arch Linux(.pkg.tar.zst)
|
||||
```bash
|
||||
|
||||
# Download
|
||||
wget https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/releases/download/v0.4.0/banforge_0.4.0_linux_amd64.pkg.tar.zst
|
||||
|
||||
# Install
|
||||
sudo pacman -U banforge_0.4.0_linux_amd64.pkg.tar.zst
|
||||
|
||||
# Verify
|
||||
sudo systemctl status banforge
|
||||
```
|
||||
This is examples for other versions with different architecture or new versions check release page on [Gitea](https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/releases).
|
||||
|
||||
## Installation guide for source code
|
||||
```bash
|
||||
# Download
|
||||
git clone https://github.com/d3m0k1d/BanForge.git
|
||||
cd BanForge
|
||||
make build-daemon
|
||||
cd bin
|
||||
mv banforge /usr/bin/banforge
|
||||
cd ..
|
||||
# Add init script and uses banforge init
|
||||
cd build
|
||||
./postinstall.sh
|
||||
```
|
||||
# Usage
|
||||
For first steps use this commands
|
||||
```bash
|
||||
|
||||
@@ -25,13 +25,27 @@ var DaemonCmd = &cobra.Command{
|
||||
defer stop()
|
||||
log := logger.New(false)
|
||||
log.Info("Starting BanForge daemon")
|
||||
db, err := storage.NewDB()
|
||||
reqDb_w, err := storage.NewRequestsWr()
|
||||
if err != nil {
|
||||
log.Error("Failed to create database", "error", err)
|
||||
log.Error("Failed to create request writer", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
banDb_r, err := storage.NewBanReader()
|
||||
if err != nil {
|
||||
log.Error("Failed to create ban reader", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
banDb_w, err := storage.NewBanWriter()
|
||||
if err != nil {
|
||||
log.Error("Failed to create ban writter", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() {
|
||||
err = db.Close()
|
||||
err = banDb_r.Close()
|
||||
if err != nil {
|
||||
log.Error("Failed to close database connection", "error", err)
|
||||
}
|
||||
err = banDb_w.Close()
|
||||
if err != nil {
|
||||
log.Error("Failed to close database connection", "error", err)
|
||||
}
|
||||
@@ -49,11 +63,11 @@ var DaemonCmd = &cobra.Command{
|
||||
log.Error("Failed to load rules", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
j := judge.New(db, b, resultCh, entryCh)
|
||||
j := judge.New(banDb_r, banDb_w, b, resultCh, entryCh)
|
||||
j.LoadRules(r)
|
||||
go j.UnbanChecker()
|
||||
go j.Tribunal()
|
||||
go storage.Write(db, resultCh)
|
||||
go storage.WriteReq(reqDb_w, resultCh)
|
||||
var scanners []*parser.Scanner
|
||||
|
||||
for _, svc := range cfg.Service {
|
||||
@@ -98,6 +112,11 @@ var DaemonCmd = &cobra.Command{
|
||||
ssh := parser.NewSshdParser()
|
||||
ssh.Parse(p.Events(), entryCh)
|
||||
}
|
||||
if svc.Name == "apache" {
|
||||
log.Info("Starting apache parser", "service", serviceName)
|
||||
ap := parser.NewApacheParser()
|
||||
ap.Parse(p.Events(), entryCh)
|
||||
}
|
||||
}(pars, svc.Name)
|
||||
continue
|
||||
}
|
||||
@@ -117,14 +136,18 @@ var DaemonCmd = &cobra.Command{
|
||||
if svc.Name == "nginx" {
|
||||
log.Info("Starting nginx parser", "service", serviceName)
|
||||
ng := parser.NewNginxParser()
|
||||
ng.Parse(p.Events(), resultCh)
|
||||
ng.Parse(p.Events(), entryCh)
|
||||
|
||||
}
|
||||
if svc.Name == "ssh" {
|
||||
log.Info("Starting ssh parser", "service", serviceName)
|
||||
ssh := parser.NewSshdParser()
|
||||
ssh.Parse(p.Events(), resultCh)
|
||||
|
||||
ssh.Parse(p.Events(), entryCh)
|
||||
}
|
||||
if svc.Name == "apache" {
|
||||
log.Info("Starting apache parser", "service", serviceName)
|
||||
ap := parser.NewApacheParser()
|
||||
ap.Parse(p.Events(), entryCh)
|
||||
}
|
||||
|
||||
}(pars, svc.Name)
|
||||
|
||||
@@ -12,13 +12,23 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ip string
|
||||
ttl_fw string
|
||||
port int
|
||||
protocol string
|
||||
)
|
||||
var UnbanCmd = &cobra.Command{
|
||||
Use: "unban",
|
||||
Short: "Unban IP",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
db, err := storage.NewDB()
|
||||
if len(args) == 0 {
|
||||
fmt.Println("IP can't be empty")
|
||||
os.Exit(1)
|
||||
}
|
||||
if ttl_fw == "" {
|
||||
ttl_fw = "1y"
|
||||
}
|
||||
ip := args[0]
|
||||
db, err := storage.NewBanWriter()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
@@ -60,7 +70,15 @@ var BanCmd = &cobra.Command{
|
||||
Use: "ban",
|
||||
Short: "Ban IP",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
db, err := storage.NewDB()
|
||||
if len(args) == 0 {
|
||||
fmt.Println("IP can't be empty")
|
||||
os.Exit(1)
|
||||
}
|
||||
if ttl_fw == "" {
|
||||
ttl_fw = "1y"
|
||||
}
|
||||
ip := args[0]
|
||||
db, err := storage.NewBanWriter()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
@@ -89,7 +107,7 @@ var BanCmd = &cobra.Command{
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = db.AddBan(ip, "1y")
|
||||
err = db.AddBan(ip, ttl_fw, "manual ban")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
@@ -98,7 +116,65 @@ var BanCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func FwRegister() {
|
||||
BanCmd.Flags().StringVarP(&ip, "ip", "i", "", "ip to ban")
|
||||
UnbanCmd.Flags().StringVarP(&ip, "ip", "i", "", "ip to unban")
|
||||
var PortCmd = &cobra.Command{
|
||||
Use: "port",
|
||||
Short: "Ports commands",
|
||||
}
|
||||
|
||||
var PortOpenCmd = &cobra.Command{
|
||||
Use: "open",
|
||||
Short: "Open ports on firewall",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if protocol == "" {
|
||||
fmt.Println("Protocol can't be empty")
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fw := cfg.Firewall.Name
|
||||
b := blocker.GetBlocker(fw, cfg.Firewall.Config)
|
||||
err = b.PortOpen(port, protocol)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Port opened successfully!")
|
||||
},
|
||||
}
|
||||
|
||||
var PortCloseCmd = &cobra.Command{
|
||||
Use: "close",
|
||||
Short: "Close ports on firewall",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if protocol == "" {
|
||||
fmt.Println("Protocol can't be empty")
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fw := cfg.Firewall.Name
|
||||
b := blocker.GetBlocker(fw, cfg.Firewall.Config)
|
||||
err = b.PortClose(port, protocol)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Port closed successfully!")
|
||||
},
|
||||
}
|
||||
|
||||
func FwRegister() {
|
||||
BanCmd.Flags().StringVarP(&ttl_fw, "ttl", "t", "", "ban time")
|
||||
PortCmd.AddCommand(PortOpenCmd)
|
||||
PortCmd.AddCommand(PortCloseCmd)
|
||||
PortOpenCmd.Flags().IntVarP(&port, "port", "p", 0, "port number")
|
||||
PortOpenCmd.Flags().StringVarP(&protocol, "protocol", "c", "", "protocol")
|
||||
PortCloseCmd.Flags().IntVarP(&port, "port", "p", 0, "port number")
|
||||
PortCloseCmd.Flags().StringVarP(&protocol, "protocol", "c", "", "protocol")
|
||||
}
|
||||
|
||||
@@ -82,23 +82,11 @@ var InitCmd = &cobra.Command{
|
||||
}
|
||||
fmt.Println("Firewall configured")
|
||||
|
||||
db, err := storage.NewDB()
|
||||
err = storage.CreateTables()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = db.CreateTable()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() {
|
||||
err = db.Close()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
fmt.Println("Firewall detected and configured")
|
||||
|
||||
fmt.Println("BanForge initialized successfully!")
|
||||
|
||||
@@ -13,7 +13,7 @@ var BanListCmd = &cobra.Command{
|
||||
Short: "List banned IP adresses",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var log = logger.New(false)
|
||||
d, err := storage.NewDB()
|
||||
d, err := storage.NewBanReader()
|
||||
if err != nil {
|
||||
log.Error("Failed to create database", "error", err)
|
||||
os.Exit(1)
|
||||
|
||||
17
cmd/banforge/command/version.go
Normal file
17
cmd/banforge/command/version.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var version = "0.4.3"
|
||||
|
||||
var VersionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "BanForge version",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("BanForge version:", version)
|
||||
},
|
||||
}
|
||||
@@ -13,7 +13,6 @@ var rootCmd = &cobra.Command{
|
||||
Use: "banforge",
|
||||
Short: "IPS log-based written on Golang",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
@@ -28,6 +27,8 @@ func Execute() {
|
||||
rootCmd.AddCommand(command.BanCmd)
|
||||
rootCmd.AddCommand(command.UnbanCmd)
|
||||
rootCmd.AddCommand(command.BanListCmd)
|
||||
rootCmd.AddCommand(command.VersionCmd)
|
||||
rootCmd.AddCommand(command.PortCmd)
|
||||
command.RuleRegister()
|
||||
command.FwRegister()
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
|
||||
22
docs/cli.md
22
docs/cli.md
@@ -11,6 +11,16 @@ banforge init
|
||||
**Description**
|
||||
This command creates the necessary directories and base configuration files
|
||||
required for the daemon to operate.
|
||||
|
||||
### version - Display BanForge version
|
||||
|
||||
```shell
|
||||
banforge version
|
||||
```
|
||||
|
||||
**Description**
|
||||
This command displays the current version of the BanForge software.
|
||||
|
||||
### daemon - Starts the BanForge daemon process
|
||||
|
||||
```shell
|
||||
@@ -31,6 +41,18 @@ banforge unban <ip>
|
||||
**Description**
|
||||
These commands provide an abstraction over your firewall. If you want to simplify the interface to your firewall, you can use these commands.
|
||||
|
||||
Flag -t or -ttl add bantime if not used default ban 1 year
|
||||
|
||||
### ports - Open and Close ports on firewall
|
||||
|
||||
```shell
|
||||
banforge open -port <port> -protocol <protocol>
|
||||
banforge close -port <port> -protocol <protocol>
|
||||
```
|
||||
|
||||
**Description**
|
||||
These commands provide an abstraction over your firewall. If you want to simplify the interface to your firewall, you can use these commands.
|
||||
|
||||
### list - Lists the IP addresses that are currently blocked
|
||||
```shell
|
||||
banforge list
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package blocker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
|
||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||
)
|
||||
@@ -21,14 +23,14 @@ func (f *Firewalld) Ban(ip string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command("sudo", "firewall-cmd", "--zone=drop", "--add-source", ip, "--permanent")
|
||||
cmd := exec.Command("firewall-cmd", "--zone=drop", "--add-source", ip, "--permanent")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logger.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
f.logger.Info("Add source " + ip + " " + string(output))
|
||||
output, err = exec.Command("sudo", "firewall-cmd", "--reload").CombinedOutput()
|
||||
output, err = exec.Command("firewall-cmd", "--reload").CombinedOutput()
|
||||
if err != nil {
|
||||
f.logger.Error(err.Error())
|
||||
return err
|
||||
@@ -42,14 +44,14 @@ func (f *Firewalld) Unban(ip string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command("sudo", "firewall-cmd", "--zone=drop", "--remove-source", ip, "--permanent")
|
||||
cmd := exec.Command("firewall-cmd", "--zone=drop", "--remove-source", ip, "--permanent")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logger.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
f.logger.Info("Remove source " + ip + " " + string(output))
|
||||
output, err = exec.Command("sudo", "firewall-cmd", "--reload").CombinedOutput()
|
||||
output, err = exec.Command("firewall-cmd", "--reload").CombinedOutput()
|
||||
if err != nil {
|
||||
f.logger.Error(err.Error())
|
||||
return err
|
||||
@@ -58,6 +60,66 @@ func (f *Firewalld) Unban(ip string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firewalld) PortOpen(port int, protocol string) error {
|
||||
// #nosec G204 - handle is extracted from nftables output and validated
|
||||
if port >= 0 && port <= 65535 {
|
||||
if protocol != "tcp" && protocol != "udp" {
|
||||
f.logger.Error("invalid protocol")
|
||||
return fmt.Errorf("invalid protocol")
|
||||
}
|
||||
s := strconv.Itoa(port)
|
||||
cmd := exec.Command(
|
||||
"firewall-cmd",
|
||||
"--zone=public",
|
||||
"--add-port="+s+"/"+protocol,
|
||||
"--permanent",
|
||||
)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logger.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
f.logger.Info("Add port " + s + " " + string(output))
|
||||
output, err = exec.Command("firewall-cmd", "--reload").CombinedOutput()
|
||||
if err != nil {
|
||||
f.logger.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
f.logger.Info("Reload " + string(output))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firewalld) PortClose(port int, protocol string) error {
|
||||
// #nosec G204 - handle is extracted from nftables output and validated
|
||||
if port >= 0 && port <= 65535 {
|
||||
if protocol != "tcp" && protocol != "udp" {
|
||||
f.logger.Error("invalid protocol")
|
||||
return fmt.Errorf("invalid protocol")
|
||||
}
|
||||
s := strconv.Itoa(port)
|
||||
cmd := exec.Command(
|
||||
"firewall-cmd",
|
||||
"--zone=public",
|
||||
"--remove-port="+s+"/"+protocol,
|
||||
"--permanent",
|
||||
)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logger.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
f.logger.Info("Remove port " + s + " " + string(output))
|
||||
output, err = exec.Command("firewall-cmd", "--reload").CombinedOutput()
|
||||
if err != nil {
|
||||
f.logger.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
f.logger.Info("Reload " + string(output))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firewalld) Setup(config string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ type BlockerEngine interface {
|
||||
Ban(ip string) error
|
||||
Unban(ip string) error
|
||||
Setup(config string) error
|
||||
PortOpen(port int, protocol string) error
|
||||
PortClose(port int, protocol string) error
|
||||
}
|
||||
|
||||
func GetBlocker(fw string, config string) BlockerEngine {
|
||||
|
||||
@@ -2,6 +2,7 @@ package blocker
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strconv"
|
||||
|
||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||
)
|
||||
@@ -27,7 +28,7 @@ func (f *Iptables) Ban(ip string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command("sudo", "iptables", "-A", "INPUT", "-s", ip, "-j", "DROP")
|
||||
cmd := exec.Command("iptables", "-A", "INPUT", "-s", ip, "-j", "DROP")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logger.Error("failed to ban IP",
|
||||
@@ -45,7 +46,7 @@ func (f *Iptables) Ban(ip string) error {
|
||||
return err
|
||||
}
|
||||
// #nosec G204 - f.config is validated above via validateConfigPath()
|
||||
cmd = exec.Command("sudo", "iptables-save", "-f", f.config)
|
||||
cmd = exec.Command("iptables-save", "-f", f.config)
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logger.Error("failed to save config",
|
||||
@@ -69,7 +70,7 @@ func (f *Iptables) Unban(ip string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command("sudo", "iptables", "-D", "INPUT", "-s", ip, "-j", "DROP")
|
||||
cmd := exec.Command("iptables", "-D", "INPUT", "-s", ip, "-j", "DROP")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logger.Error("failed to unban IP",
|
||||
@@ -87,7 +88,7 @@ func (f *Iptables) Unban(ip string) error {
|
||||
return err
|
||||
}
|
||||
// #nosec G204 - f.config is validated above via validateConfigPath()
|
||||
cmd = exec.Command("sudo", "iptables-save", "-f", f.config)
|
||||
cmd = exec.Command("iptables-save", "-f", f.config)
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logger.Error("failed to save config",
|
||||
@@ -102,6 +103,64 @@ func (f *Iptables) Unban(ip string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Iptables) PortOpen(port int, protocol string) error {
|
||||
if port >= 0 && port <= 65535 {
|
||||
if protocol != "tcp" && protocol != "udp" {
|
||||
f.logger.Error("invalid protocol")
|
||||
return nil
|
||||
}
|
||||
s := strconv.Itoa(port)
|
||||
// #nosec G204 - managed by system adminstartor
|
||||
cmd := exec.Command("iptables", "-A", "INPUT", "-p", protocol, "--dport", s, "-j", "ACCEPT")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logger.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
f.logger.Info("Add port " + s + " " + string(output))
|
||||
// #nosec G204 - f.config is validated above via validateConfigPath()
|
||||
cmd = exec.Command("iptables-save", "-f", f.config)
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logger.Error("failed to save config",
|
||||
"config_path", f.config,
|
||||
"error", err.Error(),
|
||||
"output", string(output))
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Iptables) PortClose(port int, protocol string) error {
|
||||
if port >= 0 && port <= 65535 {
|
||||
if protocol != "tcp" && protocol != "udp" {
|
||||
f.logger.Error("invalid protocol")
|
||||
return nil
|
||||
}
|
||||
s := strconv.Itoa(port)
|
||||
// #nosec G204 - managed by system adminstartor
|
||||
cmd := exec.Command("iptables", "-D", "INPUT", "-p", protocol, "--dport", s, "-j", "ACCEPT")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logger.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
f.logger.Info("Add port " + s + " " + string(output))
|
||||
// #nosec G204 - f.config is validated above via validateConfigPath()
|
||||
cmd = exec.Command("iptables-save", "-f", f.config)
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logger.Error("failed to save config",
|
||||
"config_path", f.config,
|
||||
"error", err.Error(),
|
||||
"output", string(output))
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Iptables) Setup(config string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package blocker
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||
@@ -26,7 +27,7 @@ func (n *Nftables) Ban(ip string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("sudo", "nft", "add", "rule", "inet", "banforge", "banned",
|
||||
cmd := exec.Command("nft", "add", "rule", "inet", "banforge", "banned",
|
||||
"ip", "saddr", ip, "drop")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
@@ -70,7 +71,7 @@ func (n *Nftables) Unban(ip string) error {
|
||||
return fmt.Errorf("no rule found for IP %s", ip)
|
||||
}
|
||||
// #nosec G204 - handle is extracted from nftables output and validated
|
||||
cmd := exec.Command("sudo", "nft", "delete", "rule", "inet", "banforge", "banned",
|
||||
cmd := exec.Command("nft", "delete", "rule", "inet", "banforge", "banned",
|
||||
"handle", handle)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
@@ -112,7 +113,7 @@ func (n *Nftables) Setup(config string) error {
|
||||
}
|
||||
}
|
||||
`
|
||||
cmd := exec.Command("sudo", "tee", config)
|
||||
cmd := exec.Command("tee", config)
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create stdin pipe: %w", err)
|
||||
@@ -135,7 +136,7 @@ func (n *Nftables) Setup(config string) error {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
}
|
||||
|
||||
cmd = exec.Command("sudo", "nft", "-f", config)
|
||||
cmd = exec.Command("nft", "-f", config)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load nftables config: %s", string(output))
|
||||
@@ -145,7 +146,7 @@ func (n *Nftables) Setup(config string) error {
|
||||
}
|
||||
|
||||
func (n *Nftables) findRuleHandle(ip string) (string, error) {
|
||||
cmd := exec.Command("sudo", "nft", "-a", "list", "chain", "inet", "banforge", "banned")
|
||||
cmd := exec.Command("nft", "-a", "list", "chain", "inet", "banforge", "banned")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to list chain rules: %w", err)
|
||||
@@ -166,19 +167,94 @@ func (n *Nftables) findRuleHandle(ip string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (n *Nftables) PortOpen(port int, protocol string) error {
|
||||
if port >= 0 && port <= 65535 {
|
||||
if protocol != "tcp" && protocol != "udp" {
|
||||
n.logger.Error("invalid protocol")
|
||||
return fmt.Errorf("invalid protocol")
|
||||
}
|
||||
s := strconv.Itoa(port)
|
||||
// #nosec G204 - managed by system adminstartor
|
||||
cmd := exec.Command(
|
||||
"nft",
|
||||
"add",
|
||||
"rule",
|
||||
"inet",
|
||||
"banforge",
|
||||
"input",
|
||||
protocol,
|
||||
"dport",
|
||||
s,
|
||||
"accept",
|
||||
)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
n.logger.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
n.logger.Info("Add port " + s + " " + string(output))
|
||||
err = saveNftablesConfig(n.config)
|
||||
if err != nil {
|
||||
n.logger.Error("failed to save config",
|
||||
"config_path", n.config,
|
||||
"error", err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Nftables) PortClose(port int, protocol string) error {
|
||||
if port >= 0 && port <= 65535 {
|
||||
if protocol != "tcp" && protocol != "udp" {
|
||||
n.logger.Error("invalid protocol")
|
||||
return fmt.Errorf("invalid protocol")
|
||||
}
|
||||
s := strconv.Itoa(port)
|
||||
// #nosec G204 - managed by system adminstartor
|
||||
cmd := exec.Command(
|
||||
"nft",
|
||||
"add",
|
||||
"rule",
|
||||
"inet",
|
||||
"banforge",
|
||||
"input",
|
||||
protocol,
|
||||
"dport",
|
||||
s,
|
||||
"drop",
|
||||
)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
n.logger.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
n.logger.Info("Add port " + s + " " + string(output))
|
||||
err = saveNftablesConfig(n.config)
|
||||
if err != nil {
|
||||
n.logger.Error("failed to save config",
|
||||
"config_path", n.config,
|
||||
"error", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveNftablesConfig(configPath string) error {
|
||||
err := validateConfigPath(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("sudo", "nft", "list", "ruleset")
|
||||
cmd := exec.Command("nft", "list", "ruleset")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get nftables ruleset: %w", err)
|
||||
}
|
||||
|
||||
cmd = exec.Command("sudo", "tee", configPath)
|
||||
cmd = exec.Command("tee", configPath)
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create stdin pipe: %w", err)
|
||||
|
||||
@@ -3,6 +3,7 @@ package blocker
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
|
||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||
)
|
||||
@@ -23,7 +24,7 @@ func (u *Ufw) Ban(ip string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("sudo", "ufw", "--force", "deny", "from", ip)
|
||||
cmd := exec.Command("ufw", "--force", "deny", "from", ip)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
u.logger.Error("failed to ban IP",
|
||||
@@ -42,7 +43,7 @@ func (u *Ufw) Unban(ip string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("sudo", "ufw", "--force", "delete", "deny", "from", ip)
|
||||
cmd := exec.Command("ufw", "--force", "delete", "deny", "from", ip)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
u.logger.Error("failed to unban IP",
|
||||
@@ -56,10 +57,48 @@ func (u *Ufw) Unban(ip string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Ufw) PortOpen(port int, protocol string) error {
|
||||
if port >= 0 && port <= 65535 {
|
||||
if protocol != "tcp" && protocol != "udp" {
|
||||
u.logger.Error("invalid protocol")
|
||||
return fmt.Errorf("invalid protocol")
|
||||
}
|
||||
s := strconv.Itoa(port)
|
||||
// #nosec G204 - managed by system adminstartor
|
||||
cmd := exec.Command("ufw", "allow", s+"/"+protocol)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
u.logger.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
u.logger.Info("Add port " + s + " " + string(output))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Ufw) PortClose(port int, protocol string) error {
|
||||
if port >= 0 && port <= 65535 {
|
||||
if protocol != "tcp" && protocol != "udp" {
|
||||
u.logger.Error("invalid protocol")
|
||||
return nil
|
||||
}
|
||||
s := strconv.Itoa(port)
|
||||
// #nosec G204 - managed by system adminstartor
|
||||
cmd := exec.Command("ufw", "deny", s+"/"+protocol)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
u.logger.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
u.logger.Info("Add port " + s + " " + string(output))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Ufw) Setup(config string) error {
|
||||
if config != "" {
|
||||
fmt.Printf("Ufw dont support config file\n")
|
||||
cmd := exec.Command("sudo", "ufw", "enable")
|
||||
cmd := exec.Command("ufw", "enable")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
u.logger.Error("failed to enable ufw",
|
||||
@@ -69,7 +108,7 @@ func (u *Ufw) Setup(config string) error {
|
||||
}
|
||||
}
|
||||
if config == "" {
|
||||
cmd := exec.Command("sudo", "ufw", "enable")
|
||||
cmd := exec.Command("ufw", "enable")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
u.logger.Error("failed to enable ufw",
|
||||
|
||||
@@ -12,7 +12,8 @@ import (
|
||||
)
|
||||
|
||||
type Judge struct {
|
||||
db *storage.DB
|
||||
db_r *storage.BanReader
|
||||
db_w *storage.BanWriter
|
||||
logger *logger.Logger
|
||||
Blocker blocker.BlockerEngine
|
||||
rulesByService map[string][]config.Rule
|
||||
@@ -21,13 +22,15 @@ type Judge struct {
|
||||
}
|
||||
|
||||
func New(
|
||||
db *storage.DB,
|
||||
db_r *storage.BanReader,
|
||||
db_w *storage.BanWriter,
|
||||
b blocker.BlockerEngine,
|
||||
resultCh chan *storage.LogEntry,
|
||||
entryCh chan *storage.LogEntry,
|
||||
) *Judge {
|
||||
return &Judge{
|
||||
db: db,
|
||||
db_w: db_w,
|
||||
db_r: db_r,
|
||||
logger: logger.New(false),
|
||||
rulesByService: make(map[string][]config.Rule),
|
||||
Blocker: b,
|
||||
@@ -85,7 +88,7 @@ func (j *Judge) Tribunal() {
|
||||
ruleMatched = true
|
||||
j.logger.Info("Rule matched", "rule", rule.Name, "ip", entry.IP)
|
||||
|
||||
banned, err := j.db.IsBanned(entry.IP)
|
||||
banned, err := j.db_r.IsBanned(entry.IP)
|
||||
if err != nil {
|
||||
j.logger.Error("Failed to check ban status", "ip", entry.IP, "error", err)
|
||||
break
|
||||
@@ -97,7 +100,7 @@ func (j *Judge) Tribunal() {
|
||||
break
|
||||
}
|
||||
|
||||
err = j.db.AddBan(entry.IP, rule.BanTime)
|
||||
err = j.db_w.AddBan(entry.IP, rule.BanTime, rule.Name)
|
||||
if err != nil {
|
||||
j.logger.Error(
|
||||
"Failed to add ban to database",
|
||||
@@ -142,22 +145,16 @@ func (j *Judge) UnbanChecker() {
|
||||
defer tick.Stop()
|
||||
|
||||
for range tick.C {
|
||||
ips, err := j.db.CheckExpiredBans()
|
||||
ips, err := j.db_w.RemoveExpiredBans()
|
||||
if err != nil {
|
||||
j.logger.Error(fmt.Sprintf("Failed to check expired bans: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
err = j.db.RemoveBan(ip)
|
||||
if err != nil {
|
||||
j.logger.Error(fmt.Sprintf("Failed to remove ban: %v", err))
|
||||
}
|
||||
if err := j.Blocker.Unban(ip); err != nil {
|
||||
j.logger.Error(fmt.Sprintf("Failed to unban IP %s: %v", ip, err))
|
||||
continue
|
||||
j.logger.Error(fmt.Sprintf("Failed to unban IP at firewall: %v", err))
|
||||
}
|
||||
j.logger.Info(fmt.Sprintf("IP unbanned: %s", ip))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
61
internal/parser/ApacheParser.go
Normal file
61
internal/parser/ApacheParser.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||
"github.com/d3m0k1d/BanForge/internal/storage"
|
||||
)
|
||||
|
||||
type ApacheParser struct {
|
||||
pattern *regexp.Regexp
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
func NewApacheParser() *ApacheParser {
|
||||
pattern := regexp.MustCompile(
|
||||
`^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+-\s+-\s+\[(.*?)\]\s+"(\w+)\s+(.*?)\s+HTTP/[\d.]+"\s+(\d+)\s+(\d+|-)\s+"(.*?)"\s+"(.*?)"`,
|
||||
)
|
||||
// Groups:
|
||||
// 1: IP
|
||||
// 2: Timestamp
|
||||
// 3: Method (GET, POST, etc.)
|
||||
// 4: Path
|
||||
// 5: Status Code (200, 404, 403...)
|
||||
// 6: Response Size
|
||||
// 7: Referer
|
||||
// 8: User-Agent
|
||||
|
||||
return &ApacheParser{
|
||||
pattern: pattern,
|
||||
logger: logger.New(false),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ApacheParser) Parse(eventCh <-chan Event, resultCh chan<- *storage.LogEntry) {
|
||||
// Group 1: IP, Group 2: Timestamp, Group 3: Method, Group 4: Path, Group 5: Status
|
||||
for event := range eventCh {
|
||||
matches := p.pattern.FindStringSubmatch(event.Data)
|
||||
if matches == nil {
|
||||
continue
|
||||
}
|
||||
path := matches[4]
|
||||
status := matches[5]
|
||||
method := matches[3]
|
||||
|
||||
resultCh <- &storage.LogEntry{
|
||||
Service: "apache",
|
||||
IP: matches[1],
|
||||
Path: path,
|
||||
Status: status,
|
||||
Method: method,
|
||||
}
|
||||
p.logger.Info(
|
||||
"Parsed apache log entry",
|
||||
"ip", matches[1],
|
||||
"path", path,
|
||||
"status", status,
|
||||
"method", method,
|
||||
)
|
||||
}
|
||||
}
|
||||
214
internal/storage/ban_db.go
Normal file
214
internal/storage/ban_db.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/d3m0k1d/BanForge/internal/config"
|
||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// Writer block
|
||||
type BanWriter struct {
|
||||
logger *logger.Logger
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewBanWriter() (*BanWriter, error) {
|
||||
db, err := sql.Open(
|
||||
"sqlite",
|
||||
"/var/lib/banforge/bans.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(30000)&_pragma=synchronous(NORMAL)",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &BanWriter{
|
||||
logger: logger.New(false),
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *BanWriter) CreateTable() error {
|
||||
_, err := d.db.Exec(CreateBansTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.logger.Info("Created tables")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *BanWriter) AddBan(ip string, ttl string, reason string) error {
|
||||
duration, err := config.ParseDurationWithYears(ttl)
|
||||
if err != nil {
|
||||
d.logger.Error("Invalid duration format", "ttl", ttl, "error", err)
|
||||
return fmt.Errorf("invalid duration: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
expiredAt := now.Add(duration)
|
||||
|
||||
_, err = d.db.Exec(
|
||||
"INSERT INTO bans (ip, reason, banned_at, expired_at) VALUES (?, ?, ?, ?)",
|
||||
ip,
|
||||
reason,
|
||||
now.Format(time.RFC3339),
|
||||
expiredAt.Format(time.RFC3339),
|
||||
)
|
||||
if err != nil {
|
||||
d.logger.Error("Failed to add ban", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *BanWriter) RemoveBan(ip string) error {
|
||||
_, err := d.db.Exec("DELETE FROM bans WHERE ip = ?", ip)
|
||||
if err != nil {
|
||||
d.logger.Error("Failed to remove ban", "error", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *BanWriter) RemoveExpiredBans() ([]string, error) {
|
||||
var ips []string
|
||||
now := time.Now().Format(time.RFC3339)
|
||||
|
||||
rows, err := w.db.Query(
|
||||
"SELECT ip FROM bans WHERE expired_at < ?",
|
||||
now,
|
||||
)
|
||||
if err != nil {
|
||||
w.logger.Error("Failed to get expired bans", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
w.logger.Error("Failed to close rows", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
for rows.Next() {
|
||||
var ip string
|
||||
err := rows.Scan(&ip)
|
||||
if err != nil {
|
||||
w.logger.Error("Failed to scan ban", "error", err)
|
||||
continue
|
||||
}
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := w.db.Exec(
|
||||
"DELETE FROM bans WHERE expired_at < ?",
|
||||
now,
|
||||
)
|
||||
if err != nil {
|
||||
w.logger.Error("Failed to remove expired bans", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rowsAffected > 0 {
|
||||
w.logger.Info("Removed expired bans", "count", rowsAffected, "ips", len(ips))
|
||||
}
|
||||
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
func (d *BanWriter) Close() error {
|
||||
d.logger.Info("Closing database connection")
|
||||
err := d.db.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reader block
|
||||
|
||||
type BanReader struct {
|
||||
logger *logger.Logger
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewBanReader() (*BanReader, error) {
|
||||
db, err := sql.Open("sqlite",
|
||||
"/var/lib/banforge/bans.db?"+
|
||||
"mode=ro&"+
|
||||
"_pragma=journal_mode(WAL)&"+
|
||||
"_pragma=mmap_size(268435456)&"+
|
||||
"_pragma=cache_size(-2000)&"+
|
||||
"_pragma=query_only(1)")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &BanReader{
|
||||
logger: logger.New(false),
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *BanReader) IsBanned(ip string) (bool, error) {
|
||||
var bannedIP string
|
||||
err := d.db.QueryRow("SELECT ip FROM bans WHERE ip = ? ", ip).Scan(&bannedIP)
|
||||
if err == sql.ErrNoRows {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check ban status: %w", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (d *BanReader) BanList() error {
|
||||
|
||||
var count int
|
||||
t := table.NewWriter()
|
||||
t.SetOutputMirror(os.Stdout)
|
||||
t.SetStyle(table.StyleBold)
|
||||
t.AppendHeader(table.Row{"№", "IP", "Banned At", "Reason", "Expires At"})
|
||||
rows, err := d.db.Query("SELECT ip, banned_at, reason, expired_at FROM bans")
|
||||
if err != nil {
|
||||
d.logger.Error("Failed to get ban list", "error", err)
|
||||
return err
|
||||
}
|
||||
for rows.Next() {
|
||||
count++
|
||||
var ip string
|
||||
var bannedAt string
|
||||
var reason string
|
||||
var expiredAt string
|
||||
err := rows.Scan(&ip, &bannedAt, &reason, &expiredAt)
|
||||
if err != nil {
|
||||
d.logger.Error("Failed to get ban list", "error", err)
|
||||
return err
|
||||
}
|
||||
t.AppendRow(table.Row{count, ip, bannedAt, reason, expiredAt})
|
||||
|
||||
}
|
||||
t.Render()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *BanReader) Close() error {
|
||||
d.logger.Info("Closing database connection")
|
||||
err := d.db.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
380
internal/storage/ban_db_test.go
Normal file
380
internal/storage/ban_db_test.go
Normal file
@@ -0,0 +1,380 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBanWriter_AddBan(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "bans_test.db")
|
||||
|
||||
writer, err := NewBanWriterWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BanWriter: %v", err)
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
err = writer.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
|
||||
ip := "192.168.1.1"
|
||||
ttl := "1h"
|
||||
|
||||
err = writer.AddBan(ip, ttl, "test")
|
||||
if err != nil {
|
||||
t.Errorf("AddBan failed: %v", err)
|
||||
}
|
||||
|
||||
reader, err := NewBanReaderWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BanReader: %v", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
isBanned, err := reader.IsBanned(ip)
|
||||
if err != nil {
|
||||
t.Errorf("IsBanned failed: %v", err)
|
||||
}
|
||||
if !isBanned {
|
||||
t.Error("Expected IP to be banned, but it's not")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBanWriter_RemoveBan(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "bans_test.db")
|
||||
|
||||
writer, err := NewBanWriterWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BanWriter: %v", err)
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
err = writer.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
|
||||
ip := "192.168.1.2"
|
||||
err = writer.AddBan(ip, "1h", "test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add ban: %v", err)
|
||||
}
|
||||
|
||||
reader, err := NewBanReaderWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BanReader: %v", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
isBanned, err := reader.IsBanned(ip)
|
||||
if err != nil {
|
||||
t.Fatalf("IsBanned failed: %v", err)
|
||||
}
|
||||
if !isBanned {
|
||||
t.Fatal("Expected IP to be banned before removal")
|
||||
}
|
||||
|
||||
err = writer.RemoveBan(ip)
|
||||
if err != nil {
|
||||
t.Errorf("RemoveBan failed: %v", err)
|
||||
}
|
||||
|
||||
isBanned, err = reader.IsBanned(ip)
|
||||
if err != nil {
|
||||
t.Errorf("IsBanned failed after removal: %v", err)
|
||||
}
|
||||
if isBanned {
|
||||
t.Error("Expected IP to be unbanned after removal, but it's still banned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBanWriter_RemoveExpiredBans(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "bans_test.db")
|
||||
|
||||
writer, err := NewBanWriterWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BanWriter: %v", err)
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
err = writer.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
|
||||
expiredIP := "192.168.1.3"
|
||||
err = writer.AddBan(expiredIP, "-1h", "tes")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add expired ban: %v", err)
|
||||
}
|
||||
|
||||
activeIP := "192.168.1.4"
|
||||
err = writer.AddBan(activeIP, "1h", "test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add active ban: %v", err)
|
||||
}
|
||||
|
||||
removedIPs, err := writer.RemoveExpiredBans()
|
||||
if err != nil {
|
||||
t.Errorf("RemoveExpiredBans failed: %v", err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, ip := range removedIPs {
|
||||
if ip == expiredIP {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Expected expired IP to be in removed list")
|
||||
}
|
||||
|
||||
if len(removedIPs) != 1 {
|
||||
t.Errorf("Expected 1 removed IP, got %d", len(removedIPs))
|
||||
}
|
||||
|
||||
reader, err := NewBanReaderWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BanReader: %v", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
isExpiredBanned, err := reader.IsBanned(expiredIP)
|
||||
if err != nil {
|
||||
t.Errorf("IsBanned failed for expired IP: %v", err)
|
||||
}
|
||||
if isExpiredBanned {
|
||||
t.Error("Expected expired IP to be unbanned, but it's still banned")
|
||||
}
|
||||
|
||||
isActiveBanned, err := reader.IsBanned(activeIP)
|
||||
if err != nil {
|
||||
t.Errorf("IsBanned failed for active IP: %v", err)
|
||||
}
|
||||
if !isActiveBanned {
|
||||
t.Error("Expected active IP to still be banned, but it's not")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBanReader_IsBanned(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "bans_test.db")
|
||||
|
||||
writer, err := NewBanWriterWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BanWriter: %v", err)
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
err = writer.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
|
||||
ip := "192.168.1.5"
|
||||
err = writer.AddBan(ip, "1h", "test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add ban: %v", err)
|
||||
}
|
||||
|
||||
reader, err := NewBanReaderWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BanReader: %v", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
isBanned, err := reader.IsBanned(ip)
|
||||
if err != nil {
|
||||
t.Errorf("IsBanned failed for banned IP: %v", err)
|
||||
}
|
||||
if !isBanned {
|
||||
t.Error("Expected IP to be banned")
|
||||
}
|
||||
|
||||
isBanned, err = reader.IsBanned("192.168.1.6")
|
||||
if err != nil {
|
||||
t.Errorf("IsBanned failed for non-banned IP: %v", err)
|
||||
}
|
||||
if isBanned {
|
||||
t.Error("Expected IP to not be banned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBanWriter_Close(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "bans_test.db")
|
||||
|
||||
writer, err := NewBanWriterWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BanWriter: %v", err)
|
||||
}
|
||||
|
||||
err = writer.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close failed: %v", err)
|
||||
}
|
||||
|
||||
_, err = writer.db.Exec("SELECT 1")
|
||||
if err == nil {
|
||||
t.Error("Expected error when using closed connection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBanReader_Close(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "bans_test.db")
|
||||
|
||||
writer, err := NewBanWriterWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BanWriter: %v", err)
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
err = writer.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
|
||||
reader, err := NewBanReaderWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BanReader: %v", err)
|
||||
}
|
||||
|
||||
err = reader.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close failed: %v", err)
|
||||
}
|
||||
|
||||
_, err = reader.db.Query("SELECT 1")
|
||||
if err == nil {
|
||||
t.Error("Expected error when using closed connection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBanWriter_AddBan_InvalidDuration(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "bans_test.db")
|
||||
|
||||
writer, err := NewBanWriterWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BanWriter: %v", err)
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
err = writer.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
|
||||
err = writer.AddBan("192.168.1.7", "invalid_duration", "test")
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid duration")
|
||||
} else if err.Error() == "" || err.Error() == "<nil>" {
|
||||
t.Error("Expected meaningful error message for invalid duration")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleBans(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "bans_test.db")
|
||||
|
||||
writer, err := NewBanWriterWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BanWriter: %v", err)
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
err = writer.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
|
||||
ips := []string{"192.168.1.8", "192.168.1.9", "192.168.1.10"}
|
||||
|
||||
for _, ip := range ips {
|
||||
err := writer.AddBan(ip, "1h", "test")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to add ban for IP %s: %v", ip, err)
|
||||
}
|
||||
}
|
||||
|
||||
reader, err := NewBanReaderWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BanReader: %v", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
for _, ip := range ips {
|
||||
isBanned, err := reader.IsBanned(ip)
|
||||
if err != nil {
|
||||
t.Errorf("IsBanned failed for IP %s: %v", ip, err)
|
||||
continue
|
||||
}
|
||||
if !isBanned {
|
||||
t.Errorf("Expected IP %s to be banned", ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveNonExistentBan(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "bans_test.db")
|
||||
|
||||
writer, err := NewBanWriterWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BanWriter: %v", err)
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
err = writer.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
|
||||
err = writer.RemoveBan("192.168.1.11")
|
||||
if err != nil {
|
||||
t.Errorf("RemoveBan should not return error for non-existent ban: %v", err)
|
||||
}
|
||||
}
|
||||
func NewBanWriterWithDBPath(dbPath string) (*BanWriter, error) {
|
||||
db, err := sql.Open("sqlite", dbPath+"?_pragma=journal_mode(WAL)&_pragma=busy_timeout(30000)&_pragma=synchronous(NORMAL)")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &BanWriter{
|
||||
logger: logger.New(false),
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewBanReaderWithDBPath(dbPath string) (*BanReader, error) {
|
||||
db, err := sql.Open("sqlite",
|
||||
dbPath+"?"+
|
||||
"mode=ro&"+
|
||||
"_pragma=journal_mode(WAL)&"+
|
||||
"_pragma=mmap_size(268435456)&"+
|
||||
"_pragma=cache_size(-2000)&"+
|
||||
"_pragma=query_only(1)")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &BanReader{
|
||||
logger: logger.New(false),
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
@@ -3,157 +3,54 @@ package storage
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/d3m0k1d/BanForge/internal/config"
|
||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
logger *logger.Logger
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewDB() (*DB, error) {
|
||||
db, err := sql.Open(
|
||||
"sqlite",
|
||||
"/var/lib/banforge/storage.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(30000)&_pragma=synchronous(NORMAL)",
|
||||
)
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(1)
|
||||
db.SetConnMaxLifetime(0)
|
||||
func CreateTables() error {
|
||||
// Requests DB
|
||||
db_r, err := sql.Open("sqlite",
|
||||
"/var/lib/banforge/requests.db?"+
|
||||
"mode=rwc&"+
|
||||
"_pragma=journal_mode(WAL)&"+
|
||||
"_pragma=busy_timeout(30000)&"+
|
||||
"_pragma=synchronous(NORMAL)")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return fmt.Errorf("failed to open requests db: %w", err)
|
||||
}
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DB{
|
||||
logger: logger.New(false),
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DB) Close() error {
|
||||
d.logger.Info("Closing database connection")
|
||||
err := d.db.Close()
|
||||
defer func() {
|
||||
err = db_r.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
fmt.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = db_r.Exec(CreateRequestsTable)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create requests table: %w", err)
|
||||
}
|
||||
|
||||
// Bans DB
|
||||
db_b, err := sql.Open("sqlite",
|
||||
"/var/lib/banforge/bans.db?"+
|
||||
"mode=rwc&"+
|
||||
"_pragma=journal_mode(WAL)&"+
|
||||
"_pragma=busy_timeout(30000)&"+
|
||||
"_pragma=synchronous(FULL)")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open bans db: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
err = db_b.Close()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = db_b.Exec(CreateBansTable)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create bans table: %w", err)
|
||||
}
|
||||
fmt.Println("Tables created successfully!")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) CreateTable() error {
|
||||
_, err := d.db.Exec(CreateTables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.logger.Info("Created tables")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) IsBanned(ip string) (bool, error) {
|
||||
var bannedIP string
|
||||
err := d.db.QueryRow("SELECT ip FROM bans WHERE ip = ? ", ip).Scan(&bannedIP)
|
||||
if err == sql.ErrNoRows {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check ban status: %w", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (d *DB) AddBan(ip string, ttl string) error {
|
||||
duration, err := config.ParseDurationWithYears(ttl)
|
||||
if err != nil {
|
||||
d.logger.Error("Invalid duration format", "ttl", ttl, "error", err)
|
||||
return fmt.Errorf("invalid duration: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
expiredAt := now.Add(duration)
|
||||
|
||||
_, err = d.db.Exec(
|
||||
"INSERT INTO bans (ip, reason, banned_at, expired_at) VALUES (?, ?, ?, ?)",
|
||||
ip,
|
||||
"1",
|
||||
now.Format(time.RFC3339),
|
||||
expiredAt.Format(time.RFC3339),
|
||||
)
|
||||
if err != nil {
|
||||
d.logger.Error("Failed to add ban", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) RemoveBan(ip string) error {
|
||||
_, err := d.db.Exec("DELETE FROM bans WHERE ip = ?", ip)
|
||||
if err != nil {
|
||||
d.logger.Error("Failed to remove ban", "error", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) BanList() error {
|
||||
|
||||
var count int
|
||||
t := table.NewWriter()
|
||||
t.SetOutputMirror(os.Stdout)
|
||||
t.SetStyle(table.StyleBold)
|
||||
t.AppendHeader(table.Row{"№", "IP", "Banned At"})
|
||||
rows, err := d.db.Query("SELECT ip, banned_at FROM bans")
|
||||
if err != nil {
|
||||
d.logger.Error("Failed to get ban list", "error", err)
|
||||
return err
|
||||
}
|
||||
for rows.Next() {
|
||||
count++
|
||||
var ip string
|
||||
var bannedAt string
|
||||
err := rows.Scan(&ip, &bannedAt)
|
||||
if err != nil {
|
||||
d.logger.Error("Failed to get ban list", "error", err)
|
||||
return err
|
||||
}
|
||||
t.AppendRow(table.Row{count, ip, bannedAt})
|
||||
|
||||
}
|
||||
t.Render()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) CheckExpiredBans() ([]string, error) {
|
||||
var ips []string
|
||||
rows, err := d.db.Query(
|
||||
"SELECT ip FROM bans WHERE expired_at < ?",
|
||||
time.Now().Format(time.RFC3339),
|
||||
)
|
||||
if err != nil {
|
||||
d.logger.Error("Failed to get ban list", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var ip string
|
||||
r, err := d.db.Exec("DELETE FROM bans WHERE ip = ?", ip)
|
||||
if err != nil {
|
||||
d.logger.Error("Failed to get ban list", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
d.logger.Info("Ban removed", "ip", ip, "rows", r)
|
||||
err = rows.Scan(&ip)
|
||||
if err != nil {
|
||||
d.logger.Error("Failed to get ban list", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||
_ "modernc.org/sqlite"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func createTestDB(t *testing.T) *sql.DB {
|
||||
tmpDir, err := os.MkdirTemp("", "banforge-test-*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
filePath := filepath.Join(tmpDir, "test.db")
|
||||
db, err := sql.Open("sqlite", filePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
db.Close()
|
||||
os.RemoveAll(tmpDir)
|
||||
})
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func createTestDBStruct(t *testing.T) *DB {
|
||||
tmpDir, err := os.MkdirTemp("", "banforge-test-*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
filePath := filepath.Join(tmpDir, "test.db")
|
||||
sqlDB, err := sql.Open("sqlite", filePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
sqlDB.Close()
|
||||
os.RemoveAll(tmpDir)
|
||||
})
|
||||
|
||||
return &DB{
|
||||
logger: logger.New(false),
|
||||
db: sqlDB,
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTable(t *testing.T) {
|
||||
d := createTestDBStruct(t)
|
||||
|
||||
err := d.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rows, err := d.db.Query("SELECT 1 FROM requests LIMIT 1")
|
||||
if err != nil {
|
||||
t.Fatal("requests table should exist:", err)
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
rows, err = d.db.Query("SELECT 1 FROM bans LIMIT 1")
|
||||
if err != nil {
|
||||
t.Fatal("bans table should exist:", err)
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
|
||||
func TestIsBanned(t *testing.T) {
|
||||
d := createTestDBStruct(t)
|
||||
|
||||
err := d.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = d.db.Exec("INSERT INTO bans (ip, banned_at) VALUES (?, ?)", "127.0.0.1", time.Now().Format(time.RFC3339))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
isBanned, err := d.IsBanned("127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !isBanned {
|
||||
t.Fatal("should be banned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddBan(t *testing.T) {
|
||||
d := createTestDBStruct(t)
|
||||
|
||||
err := d.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = d.AddBan("127.0.0.1", "7h")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var ip string
|
||||
err = d.db.QueryRow("SELECT ip FROM bans WHERE ip = ?", "127.0.0.1").Scan(&ip)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if ip != "127.0.0.1" {
|
||||
t.Fatal("ip should be 127.0.0.1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBanList(t *testing.T) {
|
||||
d := createTestDBStruct(t)
|
||||
|
||||
err := d.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = d.db.Exec("INSERT INTO bans (ip, banned_at) VALUES (?, ?)", "127.0.0.1", time.Now().Format(time.RFC3339))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = d.BanList()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
d := createTestDBStruct(t)
|
||||
|
||||
err := d.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package storage
|
||||
|
||||
const CreateTables = `
|
||||
|
||||
const CreateRequestsTable = `
|
||||
CREATE TABLE IF NOT EXISTS requests (
|
||||
id INTEGER PRIMARY KEY,
|
||||
service TEXT NOT NULL,
|
||||
@@ -12,6 +11,14 @@ CREATE TABLE IF NOT EXISTS requests (
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_requests_service ON requests(service);
|
||||
CREATE INDEX IF NOT EXISTS idx_requests_ip ON requests(ip);
|
||||
CREATE INDEX IF NOT EXISTS idx_requests_status ON requests(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_requests_created_at ON requests(created_at);
|
||||
`
|
||||
|
||||
// Миграция для bans.db
|
||||
const CreateBansTable = `
|
||||
CREATE TABLE IF NOT EXISTS bans (
|
||||
id INTEGER PRIMARY KEY,
|
||||
ip TEXT UNIQUE NOT NULL,
|
||||
@@ -20,9 +27,5 @@ CREATE TABLE IF NOT EXISTS bans (
|
||||
expired_at DATETIME
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_service ON requests(service);
|
||||
CREATE INDEX IF NOT EXISTS idx_ip ON requests(ip);
|
||||
CREATE INDEX IF NOT EXISTS idx_status ON requests(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_created_at ON requests(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_ban_ip ON bans(ip);
|
||||
`
|
||||
CREATE INDEX IF NOT EXISTS idx_bans_ip ON bans(ip);
|
||||
`
|
||||
|
||||
30
internal/storage/requests_db.go
Normal file
30
internal/storage/requests_db.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
type Request_Writer struct {
|
||||
logger *logger.Logger
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewRequestsWr() (*Request_Writer, error) {
|
||||
db, err := sql.Open(
|
||||
"sqlite",
|
||||
"/var/lib/banforge/requests.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(30000)&_pragma=synchronous(NORMAL)",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(1)
|
||||
db.SetConnMaxLifetime(0)
|
||||
return &Request_Writer{
|
||||
logger: logger.New(false),
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func Write(db *DB, resultCh <-chan *LogEntry) {
|
||||
func WriteReq(db *Request_Writer, resultCh <-chan *LogEntry) {
|
||||
db.logger.Info("Starting log writer")
|
||||
const batchSize = 100
|
||||
const flushInterval = 1 * time.Second
|
||||
@@ -28,17 +28,15 @@ func Write(db *DB, resultCh <-chan *LogEntry) {
|
||||
"INSERT INTO requests (service, ip, path, method, status, created_at) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
if err != nil {
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
db.logger.Error("Failed to rollback transaction", "error", err)
|
||||
}
|
||||
db.logger.Error("Failed to prepare statement", "error", err)
|
||||
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
||||
db.logger.Error("Failed to rollback transaction", "error", rollbackErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
err := stmt.Close()
|
||||
if err != nil {
|
||||
db.logger.Error("Failed to close statement", "error", err)
|
||||
if closeErr := stmt.Close(); closeErr != nil {
|
||||
db.logger.Error("Failed to close statement", "error", closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -58,10 +56,10 @@ func Write(db *DB, resultCh <-chan *LogEntry) {
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
db.logger.Error("Failed to commit transaction", "error", err)
|
||||
} else {
|
||||
db.logger.Debug("Flushed batch", "count", len(batch))
|
||||
return
|
||||
}
|
||||
|
||||
db.logger.Debug("Flushed batch", "count", len(batch))
|
||||
batch = batch[:0]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,40 +1,319 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||
_ "modernc.org/sqlite"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
var ip string
|
||||
d := createTestDBStruct(t)
|
||||
func TestWrite_BatchInsert(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "requests_test.db")
|
||||
|
||||
err := d.CreateTable()
|
||||
writer, err := NewRequestWriterWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Fatalf("Failed to create RequestWriter: %v", err)
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
err = writer.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
|
||||
resultCh := make(chan *LogEntry, 100) // ← Добавь буфер
|
||||
resultCh := make(chan *LogEntry, 100)
|
||||
|
||||
go Write(d, resultCh)
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
WriteReq(writer, resultCh)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
resultCh <- &LogEntry{
|
||||
Service: "test",
|
||||
IP: "127.0.0.1",
|
||||
Path: "/test",
|
||||
entries := []*LogEntry{
|
||||
{Service: "service1", IP: "192.168.1.1", Path: "/path1", Method: "GET", Status: "200"},
|
||||
{Service: "service2", IP: "192.168.1.2", Path: "/path2", Method: "POST", Status: "404"},
|
||||
{Service: "service3", IP: "192.168.1.3", Path: "/path3", Method: "PUT", Status: "500"},
|
||||
{Service: "service4", IP: "192.168.1.4", Path: "/path4", Method: "DELETE", Status: "200"},
|
||||
{Service: "service5", IP: "192.168.1.5", Path: "/path5", Method: "GET", Status: "301"},
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
resultCh <- entry
|
||||
}
|
||||
|
||||
close(resultCh)
|
||||
<-done
|
||||
|
||||
count, err := writer.GetRequestCount()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get request count: %v", err)
|
||||
}
|
||||
|
||||
if count != len(entries) {
|
||||
t.Errorf("Expected %d entries, got %d", len(entries), count)
|
||||
}
|
||||
rows, err := writer.db.Query("SELECT service, ip, path, method, status FROM requests ORDER BY id")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to query requests: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
i := 0
|
||||
for rows.Next() {
|
||||
var service, ip, path, method, status string
|
||||
err := rows.Scan(&service, &ip, &path, &method, &status)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to scan row: %v", err)
|
||||
}
|
||||
|
||||
if i >= len(entries) {
|
||||
t.Fatal("More rows returned than expected")
|
||||
}
|
||||
|
||||
expected := entries[i]
|
||||
if service != expected.Service {
|
||||
t.Errorf("Expected service %s, got %s", expected.Service, service)
|
||||
}
|
||||
if ip != expected.IP {
|
||||
t.Errorf("Expected IP %s, got %s", expected.IP, ip)
|
||||
}
|
||||
if path != expected.Path {
|
||||
t.Errorf("Expected path %s, got %s", expected.Path, path)
|
||||
}
|
||||
if method != expected.Method {
|
||||
t.Errorf("Expected method %s, got %s", expected.Method, method)
|
||||
}
|
||||
if status != expected.Status {
|
||||
t.Errorf("Expected status %s, got %s", expected.Status, status)
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
if i != len(entries) {
|
||||
t.Errorf("Expected to read %d entries, got %d", len(entries), i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrite_BatchSizeTrigger(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "requests_test.db")
|
||||
|
||||
writer, err := NewRequestWriterWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create RequestWriter: %v", err)
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
err = writer.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
resultCh := make(chan *LogEntry, 100)
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
WriteReq(writer, resultCh)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
batchSize := 100
|
||||
entries := make([]*LogEntry, batchSize)
|
||||
for i := 0; i < batchSize; i++ {
|
||||
entries[i] = &LogEntry{
|
||||
Service: "service" + string(rune(i+'0')),
|
||||
IP: "192.168.1." + string(rune(i+'0')),
|
||||
Path: "/path" + string(rune(i+'0')),
|
||||
Method: "GET",
|
||||
Status: "200",
|
||||
}
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
resultCh <- entry
|
||||
}
|
||||
|
||||
close(resultCh)
|
||||
<-done
|
||||
|
||||
count, err := writer.GetRequestCount()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get request count: %v", err)
|
||||
}
|
||||
|
||||
if count != batchSize {
|
||||
t.Errorf("Expected %d entries, got %d", batchSize, count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrite_FlushInterval(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "requests_test.db")
|
||||
|
||||
writer, err := NewRequestWriterWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create RequestWriter: %v", err)
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
err = writer.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
|
||||
resultCh := make(chan *LogEntry, 100)
|
||||
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
WriteReq(writer, resultCh)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
entries := []*LogEntry{
|
||||
{Service: "service1", IP: "192.168.1.1", Path: "/path1", Method: "GET", Status: "200"},
|
||||
{Service: "service2", IP: "192.168.1.2", Path: "/path2", Method: "POST", Status: "404"},
|
||||
{Service: "service3", IP: "192.168.1.3", Path: "/path3", Method: "PUT", Status: "500"},
|
||||
{Service: "service4", IP: "192.168.1.4", Path: "/path4", Method: "DELETE", Status: "200"},
|
||||
{Service: "service5", IP: "192.168.1.5", Path: "/path5", Method: "GET", Status: "301"},
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
resultCh <- entry
|
||||
}
|
||||
time.Sleep(1500 * time.Millisecond)
|
||||
|
||||
close(resultCh)
|
||||
<-done
|
||||
|
||||
count, err := writer.GetRequestCount()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get request count: %v", err)
|
||||
}
|
||||
|
||||
if count != len(entries) {
|
||||
t.Errorf("Expected %d entries, got %d", len(entries), count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrite_EmptyBatch(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "requests_test.db")
|
||||
|
||||
writer, err := NewRequestWriterWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create RequestWriter: %v", err)
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
err = writer.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
|
||||
resultCh := make(chan *LogEntry, 100)
|
||||
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
WriteReq(writer, resultCh)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
close(resultCh)
|
||||
<-done
|
||||
count, err := writer.GetRequestCount()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get request count: %v", err)
|
||||
}
|
||||
|
||||
if count != 0 {
|
||||
t.Errorf("Expected 0 entries for empty batch, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrite_ChannelClosed(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "requests_test.db")
|
||||
|
||||
writer, err := NewRequestWriterWithDBPath(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create RequestWriter: %v", err)
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
err = writer.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
resultCh := make(chan *LogEntry, 100)
|
||||
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
WriteReq(writer, resultCh)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
entries := []*LogEntry{
|
||||
{Service: "service1", IP: "192.168.1.1", Path: "/path1", Method: "GET", Status: "200"},
|
||||
{Service: "service2", IP: "192.168.1.2", Path: "/path2", Method: "POST", Status: "404"},
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
resultCh <- entry
|
||||
}
|
||||
|
||||
close(resultCh)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
<-done
|
||||
|
||||
err = d.db.QueryRow("SELECT ip FROM requests LIMIT 1").Scan(&ip)
|
||||
count, err := writer.GetRequestCount()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Fatalf("Failed to get request count: %v", err)
|
||||
}
|
||||
if ip != "127.0.0.1" {
|
||||
t.Fatal("ip should be 127.0.0.1")
|
||||
|
||||
if count != len(entries) {
|
||||
t.Errorf("Expected %d entries, got %d", len(entries), count)
|
||||
}
|
||||
}
|
||||
|
||||
func NewRequestWriterWithDBPath(dbPath string) (*Request_Writer, error) {
|
||||
db, err := sql.Open("sqlite", dbPath+"?_pragma=journal_mode(WAL)&_pragma=busy_timeout(30000)&_pragma=synchronous(NORMAL)")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(1)
|
||||
db.SetConnMaxLifetime(0)
|
||||
return &Request_Writer{
|
||||
logger: logger.New(false),
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *Request_Writer) CreateTable() error {
|
||||
_, err := w.db.Exec(CreateRequestsTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.logger.Info("Created requests table")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Request_Writer) Close() error {
|
||||
w.logger.Info("Closing request database connection")
|
||||
err := w.db.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Request_Writer) GetRequestCount() (int, error) {
|
||||
var count int
|
||||
err := w.db.QueryRow("SELECT COUNT(*) FROM requests").Scan(&count)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user