20 Commits

Author SHA1 Message Date
Ilya Chernishev
2e9b307194 Merge pull request #1 from shinyzero00/master
All checks were successful
build / build (push) Successful in 2m25s
refactoring pr by shinyzero00
2026-02-15 13:17:01 +03:00
Ilya Chernishev
726594a712 Change return value to nil on successful IP block 2026-02-15 13:13:26 +03:00
Ilya Chernishev
b27038a59c Execute SQL statement to create table in database 2026-02-15 13:08:40 +03:00
Ilya Chernishev
72025dab7d Remove comment about potential failure in encoding
Removed commented-out question regarding error handling.
2026-02-15 12:59:20 +03:00
Ilya Chernishev
dd131477e2 fix ST1005 2026-02-15 12:51:18 +03:00
Ilya Chernishev
670aec449a fix ST1005 staticcheck 2026-02-15 12:49:57 +03:00
zero@thinky
fc37e641be refactor(internal/config): use CutSuffix 2026-02-15 04:56:22 +03:00
zero@thinky
361de03208 refactor(cmd/fw): wtf is that error handling 2026-02-15 04:56:22 +03:00
zero@thinky
a2268fda5d fix(cmd/fw): why to fucking log when it is printed by the only caller 2026-02-15 04:56:22 +03:00
zero@thinky
9dc0b6002e refactor(internal/config): error handling 2026-02-15 04:56:22 +03:00
zero@thinky
4953be3ef6 refactor(internal/storage/RequestWriter/WriteReq): wtf is that error handling 2026-02-15 04:56:22 +03:00
zero@thinky
c386a2d6bc refactor(internal/storage/RequestWriter): deduplicate dsn 2026-02-15 04:54:38 +03:00
zero@thinky
dea03a6f70 refactor(*): what the fuck is that naming 2026-02-15 04:54:38 +03:00
zero@thinky
11f755c03c style(internal/storage/BanWriter): rm extra newline 2026-02-15 04:54:38 +03:00
zero@thinky
1c7a1c1778 refactor(internal/storage/BanWriter): deduplicate dsn 2026-02-15 04:54:38 +03:00
zero@thinky
411574cabe refactor(internal/storage): generalization and deduplication 2026-02-15 04:28:34 +03:00
d3m0k1d
820c9410a1 feat: update docs for new commands
All checks were successful
build / build (push) Successful in 2m8s
CD - BanForge Release / release (push) Successful in 3m46s
2026-02-09 22:27:28 +03:00
d3m0k1d
6f261803a7 feat: add to cli commands for open/close ports on firewall
All checks were successful
build / build (push) Successful in 2m2s
2026-02-09 21:51:31 +03:00
d3m0k1d
aacc98668f feat: add logic for PortClose and PortOpen on interfaces
All checks were successful
build / build (push) Successful in 2m4s
2026-02-09 21:31:19 +03:00
d3m0k1d
9519eedf4f feat: add new interface method to firewals
All checks were successful
build / build (push) Successful in 3m9s
2026-02-09 19:50:06 +03:00
15 changed files with 517 additions and 169 deletions

View File

@@ -12,55 +12,57 @@ import (
) )
var ( var (
ttl_fw string ttl_fw string
port int
protocol string
) )
var UnbanCmd = &cobra.Command{ var UnbanCmd = &cobra.Command{
Use: "unban", Use: "unban",
Short: "Unban IP", Short: "Unban IP",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 { err := func() error {
fmt.Println("IP can't be empty") if len(args) == 0 {
os.Exit(1) return fmt.Errorf("IP can't be empty")
} }
if ttl_fw == "" { if ttl_fw == "" {
ttl_fw = "1y" ttl_fw = "1y"
} }
ip := args[0] ip := args[0]
db, err := storage.NewBanWriter() db, err := storage.NewBanWriter()
if err != nil {
return err
}
cfg, err := config.LoadConfig()
if err != nil {
return err
}
fw := cfg.Firewall.Name
b := blocker.GetBlocker(fw, cfg.Firewall.Config)
if ip == "" {
return fmt.Errorf("IP can't be empty")
}
if net.ParseIP(ip) == nil {
return fmt.Errorf("invalid IP")
}
if err != nil {
return err
}
err = b.Unban(ip)
if err != nil {
return err
}
err = db.RemoveBan(ip)
if err != nil {
return err
}
fmt.Println("IP unblocked successfully!")
return nil
}()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) 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)
if ip == "" {
fmt.Println("IP can't be empty")
os.Exit(1)
}
if net.ParseIP(ip) == nil {
fmt.Println("Invalid IP")
os.Exit(1)
}
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = b.Unban(ip)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = db.RemoveBan(ip)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println("IP unblocked successfully!")
}, },
} }
@@ -68,19 +70,64 @@ var BanCmd = &cobra.Command{
Use: "ban", Use: "ban",
Short: "Ban IP", Short: "Ban IP",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 { err := func() error {
fmt.Println("IP can't be empty") if len(args) == 0 {
os.Exit(1) return fmt.Errorf("IP can't be empty")
} }
if ttl_fw == "" { if ttl_fw == "" {
ttl_fw = "1y" ttl_fw = "1y"
} }
ip := args[0] ip := args[0]
db, err := storage.NewBanWriter() db, err := storage.NewBanWriter()
if err != nil {
return err
}
cfg, err := config.LoadConfig()
if err != nil {
return err
}
fw := cfg.Firewall.Name
b := blocker.GetBlocker(fw, cfg.Firewall.Config)
if ip == "" {
return fmt.Errorf("IP can't be empty")
}
if net.ParseIP(ip) == nil {
return fmt.Errorf("invalid IP")
}
if err != nil {
return err
}
err = b.Ban(ip)
if err != nil {
return err
}
err = db.AddBan(ip, ttl_fw, "manual ban")
if err != nil {
return err
}
fmt.Println("IP blocked successfully!")
return nil
}()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
},
}
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() cfg, err := config.LoadConfig()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@@ -88,32 +135,45 @@ var BanCmd = &cobra.Command{
} }
fw := cfg.Firewall.Name fw := cfg.Firewall.Name
b := blocker.GetBlocker(fw, cfg.Firewall.Config) b := blocker.GetBlocker(fw, cfg.Firewall.Config)
if ip == "" { err = b.PortOpen(port, protocol)
fmt.Println("IP can't be empty")
os.Exit(1)
}
if net.ParseIP(ip) == nil {
fmt.Println("Invalid IP")
os.Exit(1)
}
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
err = b.Ban(ip) 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 { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
err = db.AddBan(ip, ttl_fw, "manual ban") fw := cfg.Firewall.Name
b := blocker.GetBlocker(fw, cfg.Firewall.Config)
err = b.PortClose(port, protocol)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
fmt.Println("IP blocked successfully!") fmt.Println("Port closed successfully!")
}, },
} }
func FwRegister() { func FwRegister() {
BanCmd.Flags().StringVarP(&ttl_fw, "ttl", "t", "", "ban time") 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")
} }

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

View File

@@ -13,7 +13,6 @@ var rootCmd = &cobra.Command{
Use: "banforge", Use: "banforge",
Short: "IPS log-based written on Golang", Short: "IPS log-based written on Golang",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
}, },
} }
@@ -28,6 +27,8 @@ func Execute() {
rootCmd.AddCommand(command.BanCmd) rootCmd.AddCommand(command.BanCmd)
rootCmd.AddCommand(command.UnbanCmd) rootCmd.AddCommand(command.UnbanCmd)
rootCmd.AddCommand(command.BanListCmd) rootCmd.AddCommand(command.BanListCmd)
rootCmd.AddCommand(command.VersionCmd)
rootCmd.AddCommand(command.PortCmd)
command.RuleRegister() command.RuleRegister()
command.FwRegister() command.FwRegister()
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {

View File

@@ -11,6 +11,16 @@ banforge init
**Description** **Description**
This command creates the necessary directories and base configuration files This command creates the necessary directories and base configuration files
required for the daemon to operate. 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 ### daemon - Starts the BanForge daemon process
```shell ```shell
@@ -32,6 +42,17 @@ banforge unban <ip>
These commands provide an abstraction over your firewall. If you want to simplify the interface to your firewall, you can use these commands. 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 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 ### list - Lists the IP addresses that are currently blocked
```shell ```shell
banforge list banforge list

View File

@@ -1,7 +1,9 @@
package blocker package blocker
import ( import (
"fmt"
"os/exec" "os/exec"
"strconv"
"github.com/d3m0k1d/BanForge/internal/logger" "github.com/d3m0k1d/BanForge/internal/logger"
) )
@@ -58,6 +60,63 @@ func (f *Firewalld) Unban(ip string) error {
return nil 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" {
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 {
return err
}
f.logger.Info("Remove port " + s + " " + string(output))
output, err = exec.Command("firewall-cmd", "--reload").CombinedOutput()
if err != nil {
return err
}
f.logger.Info("Reload " + string(output))
}
return nil
}
func (f *Firewalld) Setup(config string) error { func (f *Firewalld) Setup(config string) error {
return nil return nil
} }

View File

@@ -10,6 +10,8 @@ type BlockerEngine interface {
Ban(ip string) error Ban(ip string) error
Unban(ip string) error Unban(ip string) error
Setup(config 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 { func GetBlocker(fw string, config string) BlockerEngine {

View File

@@ -2,6 +2,7 @@ package blocker
import ( import (
"os/exec" "os/exec"
"strconv"
"github.com/d3m0k1d/BanForge/internal/logger" "github.com/d3m0k1d/BanForge/internal/logger"
) )
@@ -102,6 +103,64 @@ func (f *Iptables) Unban(ip string) error {
return nil 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 { func (f *Iptables) Setup(config string) error {
return nil return nil
} }

View File

@@ -3,6 +3,7 @@ package blocker
import ( import (
"fmt" "fmt"
"os/exec" "os/exec"
"strconv"
"strings" "strings"
"github.com/d3m0k1d/BanForge/internal/logger" "github.com/d3m0k1d/BanForge/internal/logger"
@@ -166,6 +167,81 @@ func (n *Nftables) findRuleHandle(ip string) (string, error) {
return "", nil 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 { func saveNftablesConfig(configPath string) error {
err := validateConfigPath(configPath) err := validateConfigPath(configPath)
if err != nil { if err != nil {

View File

@@ -3,6 +3,7 @@ package blocker
import ( import (
"fmt" "fmt"
"os/exec" "os/exec"
"strconv"
"github.com/d3m0k1d/BanForge/internal/logger" "github.com/d3m0k1d/BanForge/internal/logger"
) )
@@ -56,6 +57,44 @@ func (u *Ufw) Unban(ip string) error {
return nil 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 { func (u *Ufw) Setup(config string) error {
if config != "" { if config != "" {
fmt.Printf("Ufw dont support config file\n") fmt.Printf("Ufw dont support config file\n")

View File

@@ -1,6 +1,7 @@
package config package config
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"strconv" "strconv"
@@ -57,13 +58,9 @@ func NewRule(
return err return err
} }
defer func() { defer func() {
err = file.Close() err = errors.Join(err, file.Close())
if err != nil {
fmt.Println(err)
}
}() }()
cfg := Rules{Rules: r} cfg := Rules{Rules: r}
err = toml.NewEncoder(file).Encode(cfg) err = toml.NewEncoder(file).Encode(cfg)
if err != nil { if err != nil {
return err return err
@@ -126,24 +123,24 @@ func EditRule(Name string, ServiceName string, Path string, Status string, Metho
} }
func ParseDurationWithYears(s string) (time.Duration, error) { func ParseDurationWithYears(s string) (time.Duration, error) {
if strings.HasSuffix(s, "y") { if ss, ok := strings.CutSuffix(s, "y"); ok {
years, err := strconv.Atoi(strings.TrimSuffix(s, "y")) years, err := strconv.Atoi(ss)
if err != nil { if err != nil {
return 0, err return 0, err
} }
return time.Duration(years) * 365 * 24 * time.Hour, nil return time.Duration(years) * 365 * 24 * time.Hour, nil
} }
if strings.HasSuffix(s, "M") { if ss, ok := strings.CutSuffix(s, "M"); ok {
months, err := strconv.Atoi(strings.TrimSuffix(s, "M")) months, err := strconv.Atoi(ss)
if err != nil { if err != nil {
return 0, err return 0, err
} }
return time.Duration(months) * 30 * 24 * time.Hour, nil return time.Duration(months) * 30 * 24 * time.Hour, nil
} }
if strings.HasSuffix(s, "d") { if ss, ok := strings.CutSuffix(s, "d"); ok {
days, err := strconv.Atoi(strings.TrimSuffix(s, "d")) days, err := strconv.Atoi(ss)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@@ -21,7 +21,7 @@ type BanWriter struct {
func NewBanWriter() (*BanWriter, error) { func NewBanWriter() (*BanWriter, error) {
db, err := sql.Open( db, err := sql.Open(
"sqlite", "sqlite",
"/var/lib/banforge/bans.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(30000)&_pragma=synchronous(NORMAL)", buildSqliteDsn(banDBPath, pragmas),
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -175,7 +175,6 @@ func (d *BanReader) IsBanned(ip string) (bool, error) {
} }
func (d *BanReader) BanList() error { func (d *BanReader) BanList() error {
var count int var count int
t := table.NewWriter() t := table.NewWriter()
t.SetOutputMirror(os.Stdout) t.SetOutputMirror(os.Stdout)

View File

@@ -2,55 +2,60 @@ package storage
import ( import (
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"strings"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
func CreateTables() error { const (
// Requests DB DBDir = "/var/lib/banforge/"
db_r, err := sql.Open("sqlite", ReqDBPath = DBDir + "requests.db"
"/var/lib/banforge/requests.db?"+ banDBPath = DBDir + "bans.db"
"mode=rwc&"+ )
"_pragma=journal_mode(WAL)&"+
"_pragma=busy_timeout(30000)&"+
"_pragma=synchronous(NORMAL)")
if err != nil {
return fmt.Errorf("failed to open requests db: %w", err)
}
defer func() {
err = db_r.Close()
if err != nil {
fmt.Println(err)
}
}()
_, err = db_r.Exec(CreateRequestsTable) var pragmas = map[string]string{
if err != nil { `journal_mode`: `wal`,
return fmt.Errorf("failed to create requests table: %w", err) `synchronous`: `normal`,
} `busy_timeout`: `30000`,
// also consider these
// Bans DB // `temp_store`: `memory`,
db_b, err := sql.Open("sqlite", // `cache_size`: `1000000000`,
"/var/lib/banforge/bans.db?"+ }
"mode=rwc&"+
"_pragma=journal_mode(WAL)&"+ func buildSqliteDsn(path string, pragmas map[string]string) string {
"_pragma=busy_timeout(30000)&"+ pragmastrs := make([]string, len(pragmas))
"_pragma=synchronous(FULL)") i := 0
if err != nil { for k, v := range pragmas {
return fmt.Errorf("failed to open bans db: %w", err) pragmastrs[i] = (fmt.Sprintf(`pragma=%s(%s)`, k, v))
} i++
defer func() { }
err = db_b.Close() return path + "?" + "mode=rwc&" + strings.Join(pragmastrs, "&")
if err != nil { }
fmt.Println(err)
} func initDB(dsn, sqlstr string) (err error) {
}() db, err := sql.Open("sqlite", dsn)
if err != nil {
_, err = db_b.Exec(CreateBansTable) return fmt.Errorf("failed to open %q: %w", dsn, err)
if err != nil { }
return fmt.Errorf("failed to create bans table: %w", err) defer func() {
} closeErr := db.Close()
fmt.Println("Tables created successfully!") if closeErr != nil {
return nil err = errors.Join(err, fmt.Errorf("failed to close %q: %w", dsn, closeErr))
}
}()
_, err = db.Exec(sqlstr)
if err != nil {
return fmt.Errorf("failed to create table: %w", err)
}
return err
}
func CreateTables() (err error) {
// Requests DB
err1 := initDB(buildSqliteDsn(ReqDBPath, pragmas), CreateRequestsTable)
err2 := initDB(buildSqliteDsn(banDBPath, pragmas), CreateBansTable)
return errors.Join(err1, err2)
} }

View File

@@ -7,15 +7,15 @@ import (
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
type Request_Writer struct { type RequestWriter struct {
logger *logger.Logger logger *logger.Logger
db *sql.DB db *sql.DB
} }
func NewRequestsWr() (*Request_Writer, error) { func NewRequestsWr() (*RequestWriter, error) {
db, err := sql.Open( db, err := sql.Open(
"sqlite", "sqlite",
"/var/lib/banforge/requests.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(30000)&_pragma=synchronous(NORMAL)", buildSqliteDsn(ReqDBPath, pragmas),
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -23,7 +23,7 @@ func NewRequestsWr() (*Request_Writer, error) {
db.SetMaxOpenConns(1) db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1) db.SetMaxIdleConns(1)
db.SetConnMaxLifetime(0) db.SetConnMaxLifetime(0)
return &Request_Writer{ return &RequestWriter{
logger: logger.New(false), logger: logger.New(false),
db: db, db: db,
}, nil }, nil

View File

@@ -1,10 +1,13 @@
package storage package storage
import ( import (
"database/sql"
"errors"
"fmt"
"time" "time"
) )
func WriteReq(db *Request_Writer, resultCh <-chan *LogEntry) { func WriteReq(db *RequestWriter, resultCh <-chan *LogEntry) {
db.logger.Info("Starting log writer") db.logger.Info("Starting log writer")
const batchSize = 100 const batchSize = 100
const flushInterval = 1 * time.Second const flushInterval = 1 * time.Second
@@ -14,53 +17,63 @@ func WriteReq(db *Request_Writer, resultCh <-chan *LogEntry) {
defer ticker.Stop() defer ticker.Stop()
flush := func() { flush := func() {
if len(batch) == 0 { defer db.logger.Debug("Flushed batch", "count", len(batch))
return err := func() (err error) {
} if len(batch) == 0 {
return nil
tx, err := db.db.Begin()
if err != nil {
db.logger.Error("Failed to begin transaction", "error", err)
return
}
stmt, err := tx.Prepare(
"INSERT INTO requests (service, ip, path, method, status, created_at) VALUES (?, ?, ?, ?, ?, ?)",
)
if err != nil {
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() {
if closeErr := stmt.Close(); closeErr != nil {
db.logger.Error("Failed to close statement", "error", closeErr)
}
}()
for _, entry := range batch { tx, err := db.db.Begin()
_, err := stmt.Exec( if err != nil {
entry.Service, return fmt.Errorf("failed to begin transaction: %w", err)
entry.IP, }
entry.Path, defer func() {
entry.Method, if rollbackErr := tx.Rollback(); rollbackErr != nil &&
entry.Status, !errors.Is(rollbackErr, sql.ErrTxDone) {
time.Now().Format(time.RFC3339), err = errors.Join(
err,
fmt.Errorf("failed to rollback transaction: %w", rollbackErr),
)
}
}()
stmt, err := tx.Prepare(
"INSERT INTO requests (service, ip, path, method, status, created_at) VALUES (?, ?, ?, ?, ?, ?)",
) )
if err != nil { if err != nil {
db.logger.Error("Failed to insert entry", "error", err) err = fmt.Errorf("failed to prepare statement: %w", err)
return err
} }
} defer func() {
if closeErr := stmt.Close(); closeErr != nil {
err = errors.Join(err, fmt.Errorf("failed to close statement: %w", closeErr))
}
}()
if err := tx.Commit(); err != nil { for _, entry := range batch {
db.logger.Error("Failed to commit transaction", "error", err) _, err := stmt.Exec(
return entry.Service,
} entry.IP,
entry.Path,
entry.Method,
entry.Status,
time.Now().Format(time.RFC3339),
)
if err != nil {
db.logger.Error(fmt.Errorf("failed to insert entry: %w", err).Error())
}
}
db.logger.Debug("Flushed batch", "count", len(batch)) if err := tx.Commit(); err != nil {
batch = batch[:0] return fmt.Errorf("failed to commit transaction: %w", err)
}
batch = batch[:0]
return err
}()
if err != nil {
db.logger.Error(err.Error())
}
} }
for { for {

View File

@@ -277,7 +277,7 @@ func TestWrite_ChannelClosed(t *testing.T) {
} }
} }
func NewRequestWriterWithDBPath(dbPath string) (*Request_Writer, error) { func NewRequestWriterWithDBPath(dbPath string) (*RequestWriter, error) {
db, err := sql.Open("sqlite", dbPath+"?_pragma=journal_mode(WAL)&_pragma=busy_timeout(30000)&_pragma=synchronous(NORMAL)") db, err := sql.Open("sqlite", dbPath+"?_pragma=journal_mode(WAL)&_pragma=busy_timeout(30000)&_pragma=synchronous(NORMAL)")
if err != nil { if err != nil {
return nil, err return nil, err
@@ -285,13 +285,13 @@ func NewRequestWriterWithDBPath(dbPath string) (*Request_Writer, error) {
db.SetMaxOpenConns(1) db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1) db.SetMaxIdleConns(1)
db.SetConnMaxLifetime(0) db.SetConnMaxLifetime(0)
return &Request_Writer{ return &RequestWriter{
logger: logger.New(false), logger: logger.New(false),
db: db, db: db,
}, nil }, nil
} }
func (w *Request_Writer) CreateTable() error { func (w *RequestWriter) CreateTable() error {
_, err := w.db.Exec(CreateRequestsTable) _, err := w.db.Exec(CreateRequestsTable)
if err != nil { if err != nil {
return err return err
@@ -300,7 +300,7 @@ func (w *Request_Writer) CreateTable() error {
return nil return nil
} }
func (w *Request_Writer) Close() error { func (w *RequestWriter) Close() error {
w.logger.Info("Closing request database connection") w.logger.Info("Closing request database connection")
err := w.db.Close() err := w.db.Close()
if err != nil { if err != nil {
@@ -309,7 +309,7 @@ func (w *Request_Writer) Close() error {
return nil return nil
} }
func (w *Request_Writer) GetRequestCount() (int, error) { func (w *RequestWriter) GetRequestCount() (int, error) {
var count int var count int
err := w.db.QueryRow("SELECT COUNT(*) FROM requests").Scan(&count) err := w.db.QueryRow("SELECT COUNT(*) FROM requests").Scan(&count)
if err != nil { if err != nil {