mirror of
https://github.com/lukaszraczylo/lolcathost.git
synced 2026-07-07 04:24:39 +00:00
gosec govulncheck runs (#1)
* gosec govulncheck runs
* Fix flaky TestRateLimiter_Matrix test
The test was failing due to two issues:
1. Test name generation used invalid character conversion (string(rune('0'+limit)))
which produced non-printable characters for limits >= 10
2. Using 10ms windows with 100 requests caused race conditions - early requests
would expire before all 100 were made, allowing the 101st request
Changed to use struct-based test cases with proper fmt.Sprintf naming and
a consistent 1-second window that won't expire during rapid test execution.
This commit is contained in:
@@ -14,7 +14,6 @@ jobs:
|
|||||||
autoupdate:
|
autoupdate:
|
||||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-autoupdate.yaml@main
|
uses: lukaszraczylo/shared-actions/.github/workflows/go-autoupdate.yaml@main
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: ">=1.24"
|
||||||
release-workflow: "release.yml"
|
release-workflow: "release.yml"
|
||||||
secrets:
|
secrets: inherit
|
||||||
pat-token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
|
||||||
|
|||||||
@@ -9,8 +9,14 @@ on:
|
|||||||
- "**"
|
- "**"
|
||||||
- "!main"
|
- "!main"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
actions: write
|
||||||
|
pull-requests: write
|
||||||
|
security-events: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pr-checks:
|
pr-checks:
|
||||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-pr.yaml@main
|
uses: lukaszraczylo/shared-actions/.github/workflows/go-pr.yaml@main
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: ">=1.24"
|
||||||
|
|||||||
@@ -17,5 +17,5 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
|
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: ">=1.24"
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ func runList() {
|
|||||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", status, e.Domain, e.IP, e.Alias, e.Group)
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", status, e.Domain, e.IP, e.Alias, e.Group)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Flush()
|
_ = w.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
func runOn(alias string) {
|
func runOn(alias string) {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func (c *Client) Connect() error {
|
|||||||
|
|
||||||
// Close existing connection if any
|
// Close existing connection if any
|
||||||
if c.conn != nil {
|
if c.conn != nil {
|
||||||
c.conn.Close()
|
_ = c.conn.Close()
|
||||||
c.conn = nil
|
c.conn = nil
|
||||||
c.reader = nil
|
c.reader = nil
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ func (c *Client) send(req *protocol.Request) (*protocol.Response, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set deadline
|
// Set deadline
|
||||||
c.conn.SetDeadline(time.Now().Add(c.timeout))
|
_ = c.conn.SetDeadline(time.Now().Add(c.timeout))
|
||||||
|
|
||||||
// Send request
|
// Send request
|
||||||
data, err := json.Marshal(req)
|
data, err := json.Marshal(req)
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ func (m *Manager) watchLoop() {
|
|||||||
func (m *Manager) Stop() {
|
func (m *Manager) Stop() {
|
||||||
close(m.stopCh)
|
close(m.stopCh)
|
||||||
if m.watcher != nil {
|
if m.watcher != nil {
|
||||||
m.watcher.Close()
|
_ = m.watcher.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,76 +338,6 @@ func (c *Config) DeleteHost(alias string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateHost updates an existing host by alias.
|
|
||||||
func (c *Config) UpdateHost(oldAlias, domain, ip, newAlias, groupName string) error {
|
|
||||||
// Find the host
|
|
||||||
var foundGroup int = -1
|
|
||||||
var foundHost int = -1
|
|
||||||
for i := range c.Groups {
|
|
||||||
for j := range c.Groups[i].Hosts {
|
|
||||||
if c.Groups[i].Hosts[j].Alias == oldAlias {
|
|
||||||
foundGroup = i
|
|
||||||
foundHost = j
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if foundHost >= 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if foundHost < 0 {
|
|
||||||
return fmt.Errorf("alias not found: %s", oldAlias)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicate alias if alias is changing
|
|
||||||
if oldAlias != newAlias {
|
|
||||||
if existing, _ := c.FindHostByAlias(newAlias); existing != nil {
|
|
||||||
return fmt.Errorf("alias already exists: %s", newAlias)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current enabled state
|
|
||||||
enabled := c.Groups[foundGroup].Hosts[foundHost].Enabled
|
|
||||||
|
|
||||||
// If group is changing, move to new group
|
|
||||||
if c.Groups[foundGroup].Name != groupName {
|
|
||||||
// Remove from old group
|
|
||||||
c.Groups[foundGroup].Hosts = append(c.Groups[foundGroup].Hosts[:foundHost], c.Groups[foundGroup].Hosts[foundHost+1:]...)
|
|
||||||
|
|
||||||
// Add to new group
|
|
||||||
host := Host{
|
|
||||||
Domain: domain,
|
|
||||||
IP: ip,
|
|
||||||
Alias: newAlias,
|
|
||||||
Enabled: enabled,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find or create target group
|
|
||||||
found := false
|
|
||||||
for i := range c.Groups {
|
|
||||||
if c.Groups[i].Name == groupName {
|
|
||||||
c.Groups[i].Hosts = append(c.Groups[i].Hosts, host)
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
c.Groups = append(c.Groups, Group{
|
|
||||||
Name: groupName,
|
|
||||||
Hosts: []Host{host},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Update in place
|
|
||||||
c.Groups[foundGroup].Hosts[foundHost].Domain = domain
|
|
||||||
c.Groups[foundGroup].Hosts[foundHost].IP = ip
|
|
||||||
c.Groups[foundGroup].Hosts[foundHost].Alias = newAlias
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApplyPreset applies a preset to the configuration.
|
// ApplyPreset applies a preset to the configuration.
|
||||||
func (c *Config) ApplyPreset(name string) error {
|
func (c *Config) ApplyPreset(name string) error {
|
||||||
preset := c.FindPreset(name)
|
preset := c.FindPreset(name)
|
||||||
@@ -482,6 +412,7 @@ func (m *Manager) Save() error {
|
|||||||
return fmt.Errorf("failed to marshal config: %w", err)
|
return fmt.Errorf("failed to marshal config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #nosec G306 -- config file should be world-readable
|
||||||
if err := os.WriteFile(m.path, data, 0644); err != nil {
|
if err := os.WriteFile(m.path, data, 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write config: %w", err)
|
return fmt.Errorf("failed to write config: %w", err)
|
||||||
}
|
}
|
||||||
@@ -492,6 +423,7 @@ func (m *Manager) Save() error {
|
|||||||
// CreateDefault creates a default configuration file.
|
// CreateDefault creates a default configuration file.
|
||||||
func CreateDefault(path string) error {
|
func CreateDefault(path string) error {
|
||||||
dir := filepath.Dir(path)
|
dir := filepath.Dir(path)
|
||||||
|
// #nosec G301 -- config directory should be world-readable
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create config directory: %w", err)
|
return fmt.Errorf("failed to create config directory: %w", err)
|
||||||
}
|
}
|
||||||
@@ -533,6 +465,7 @@ func CreateDefault(path string) error {
|
|||||||
return fmt.Errorf("failed to marshal default config: %w", err)
|
return fmt.Errorf("failed to marshal default config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #nosec G306 -- config file should be world-readable
|
||||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write default config: %w", err)
|
return fmt.Errorf("failed to write default config: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -432,77 +432,6 @@ func TestConfig_DeleteHost(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_UpdateHost(t *testing.T) {
|
|
||||||
t.Run("update in same group", func(t *testing.T) {
|
|
||||||
cfg := &Config{
|
|
||||||
Groups: []Group{
|
|
||||||
{Name: "dev", Hosts: []Host{{Domain: "old.com", IP: "127.0.0.1", Alias: "test"}}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := cfg.UpdateHost("test", "new.com", "192.168.1.1", "test", "dev")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "new.com", cfg.Groups[0].Hosts[0].Domain)
|
|
||||||
assert.Equal(t, "192.168.1.1", cfg.Groups[0].Hosts[0].IP)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("move to different group", func(t *testing.T) {
|
|
||||||
cfg := &Config{
|
|
||||||
Groups: []Group{
|
|
||||||
{Name: "source", Hosts: []Host{{Domain: "a.com", IP: "127.0.0.1", Alias: "test"}}},
|
|
||||||
{Name: "target", Hosts: []Host{}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := cfg.UpdateHost("test", "a.com", "127.0.0.1", "test", "target")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Len(t, cfg.Groups[0].Hosts, 0)
|
|
||||||
assert.Len(t, cfg.Groups[1].Hosts, 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("move to new group", func(t *testing.T) {
|
|
||||||
cfg := &Config{
|
|
||||||
Groups: []Group{
|
|
||||||
{Name: "source", Hosts: []Host{{Domain: "a.com", IP: "127.0.0.1", Alias: "test"}}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := cfg.UpdateHost("test", "a.com", "127.0.0.1", "test", "newgroup")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Len(t, cfg.Groups, 2)
|
|
||||||
assert.Equal(t, "newgroup", cfg.Groups[1].Name)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("change alias", func(t *testing.T) {
|
|
||||||
cfg := &Config{
|
|
||||||
Groups: []Group{
|
|
||||||
{Name: "dev", Hosts: []Host{{Domain: "a.com", IP: "127.0.0.1", Alias: "old"}}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := cfg.UpdateHost("old", "a.com", "127.0.0.1", "new", "dev")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "new", cfg.Groups[0].Hosts[0].Alias)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("alias conflict error", func(t *testing.T) {
|
|
||||||
cfg := &Config{
|
|
||||||
Groups: []Group{
|
|
||||||
{Name: "dev", Hosts: []Host{
|
|
||||||
{Domain: "a.com", Alias: "a"},
|
|
||||||
{Domain: "b.com", Alias: "b"},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := cfg.UpdateHost("a", "a.com", "127.0.0.1", "b", "dev")
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "alias already exists")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("host not found error", func(t *testing.T) {
|
|
||||||
cfg := &Config{Groups: []Group{}}
|
|
||||||
err := cfg.UpdateHost("nonexistent", "a.com", "127.0.0.1", "new", "dev")
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "alias not found")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_AddPreset(t *testing.T) {
|
func TestConfig_AddPreset(t *testing.T) {
|
||||||
t.Run("add new preset", func(t *testing.T) {
|
t.Run("add new preset", func(t *testing.T) {
|
||||||
cfg := &Config{Presets: []Preset{}}
|
cfg := &Config{Presets: []Preset{}}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func New(configPath string) (*Daemon, error) {
|
|||||||
cfg.EnsureDefaultGroup()
|
cfg.EnsureDefaultGroup()
|
||||||
// Save if we added a default group
|
// Save if we added a default group
|
||||||
if len(cfg.Groups) == 1 && cfg.Groups[0].Name == "default" && len(cfg.Groups[0].Hosts) == 0 {
|
if len(cfg.Groups) == 1 && cfg.Groups[0].Name == "default" && len(cfg.Groups[0].Hosts) == 0 {
|
||||||
cfgManager.Save()
|
_ = cfgManager.Save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -178,13 +178,14 @@ func (m *HostsManager) buildManagedSection(entries []HostEntry) string {
|
|||||||
func (m *HostsManager) writeAtomic(content string) error {
|
func (m *HostsManager) writeAtomic(content string) error {
|
||||||
// Write to temp file first
|
// Write to temp file first
|
||||||
tmpFile := m.hostsPath + ".tmp"
|
tmpFile := m.hostsPath + ".tmp"
|
||||||
|
// #nosec G306 -- hosts file must be world-readable
|
||||||
if err := os.WriteFile(tmpFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(tmpFile, []byte(content), 0644); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rename atomically
|
// Rename atomically
|
||||||
if err := os.Rename(tmpFile, m.hostsPath); err != nil {
|
if err := os.Rename(tmpFile, m.hostsPath); err != nil {
|
||||||
os.Remove(tmpFile)
|
_ = os.Remove(tmpFile)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +194,7 @@ func (m *HostsManager) writeAtomic(content string) error {
|
|||||||
|
|
||||||
// CreateBackup creates a backup of the current hosts file.
|
// CreateBackup creates a backup of the current hosts file.
|
||||||
func (m *HostsManager) CreateBackup() error {
|
func (m *HostsManager) CreateBackup() error {
|
||||||
|
// #nosec G301 -- backup directory should be world-readable for recovery
|
||||||
if err := os.MkdirAll(m.backupDir, 0755); err != nil {
|
if err := os.MkdirAll(m.backupDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create backup directory: %w", err)
|
return fmt.Errorf("failed to create backup directory: %w", err)
|
||||||
}
|
}
|
||||||
@@ -205,6 +207,7 @@ func (m *HostsManager) CreateBackup() error {
|
|||||||
timestamp := time.Now().Format("20060102-150405")
|
timestamp := time.Now().Format("20060102-150405")
|
||||||
backupPath := filepath.Join(m.backupDir, fmt.Sprintf("hosts.%s.bak", timestamp))
|
backupPath := filepath.Join(m.backupDir, fmt.Sprintf("hosts.%s.bak", timestamp))
|
||||||
|
|
||||||
|
// #nosec G306 -- backup files should be world-readable for recovery
|
||||||
if err := os.WriteFile(backupPath, content, 0644); err != nil {
|
if err := os.WriteFile(backupPath, content, 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write backup: %w", err)
|
return fmt.Errorf("failed to write backup: %w", err)
|
||||||
}
|
}
|
||||||
@@ -243,7 +246,7 @@ func (m *HostsManager) cleanupBackups() error {
|
|||||||
// Remove oldest backups
|
// Remove oldest backups
|
||||||
for i := MaxBackups; i < len(backups); i++ {
|
for i := MaxBackups; i < len(backups); i++ {
|
||||||
path := filepath.Join(m.backupDir, backups[i].Name())
|
path := filepath.Join(m.backupDir, backups[i].Name())
|
||||||
os.Remove(path)
|
_ = os.Remove(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -301,6 +304,7 @@ func (m *HostsManager) GetBackupContent(name string) (string, error) {
|
|||||||
return "", fmt.Errorf("invalid backup name")
|
return "", fmt.Errorf("invalid backup name")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #nosec G304 -- backupPath is validated above: filepath.Base(name) == name and prefix/suffix checks
|
||||||
content, err := os.ReadFile(backupPath)
|
content, err := os.ReadFile(backupPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to read backup: %w", err)
|
return "", fmt.Errorf("failed to read backup: %w", err)
|
||||||
@@ -318,6 +322,7 @@ func (m *HostsManager) RestoreBackup(name string) error {
|
|||||||
return fmt.Errorf("invalid backup name")
|
return fmt.Errorf("invalid backup name")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #nosec G304 -- backupPath is validated above: filepath.Base(name) == name and prefix/suffix checks
|
||||||
content, err := os.ReadFile(backupPath)
|
content, err := os.ReadFile(backupPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read backup: %w", err)
|
return fmt.Errorf("failed to read backup: %w", err)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func (s *Server) getPeerCredentials(conn net.Conn) *PeerCredentials {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var creds *PeerCredentials
|
var creds *PeerCredentials
|
||||||
rawConn.Control(func(fd uintptr) {
|
_ = rawConn.Control(func(fd uintptr) {
|
||||||
xucred, err := unix.GetsockoptXucred(int(fd), unix.SOL_LOCAL, unix.LOCAL_PEERCRED)
|
xucred, err := unix.GetsockoptXucred(int(fd), unix.SOL_LOCAL, unix.LOCAL_PEERCRED)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -33,6 +33,7 @@ func (s *Server) getPeerCredentials(conn net.Conn) *PeerCredentials {
|
|||||||
// Get PID separately using LOCAL_PEERPID
|
// Get PID separately using LOCAL_PEERPID
|
||||||
var pid int32
|
var pid int32
|
||||||
pidLen := uint32(unsafe.Sizeof(pid))
|
pidLen := uint32(unsafe.Sizeof(pid))
|
||||||
|
// #nosec G103 -- unsafe required for low-level syscall to get peer PID
|
||||||
_, _, errno := syscall.Syscall6(
|
_, _, errno := syscall.Syscall6(
|
||||||
syscall.SYS_GETSOCKOPT,
|
syscall.SYS_GETSOCKOPT,
|
||||||
fd,
|
fd,
|
||||||
|
|||||||
@@ -114,10 +114,12 @@ type AuditEntry struct {
|
|||||||
func NewAuditLogger(path string) (*AuditLogger, error) {
|
func NewAuditLogger(path string) (*AuditLogger, error) {
|
||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
dir := path[:len(path)-len("/audit.log")]
|
dir := path[:len(path)-len("/audit.log")]
|
||||||
|
// #nosec G301 -- log directory should be world-readable
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
return nil, fmt.Errorf("failed to create log directory: %w", err)
|
return nil, fmt.Errorf("failed to create log directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #nosec G304,G302 -- path is from constant AuditLogPath; audit log should be world-readable
|
||||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open audit log: %w", err)
|
return nil, fmt.Errorf("failed to open audit log: %w", err)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package daemon
|
package daemon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -145,26 +146,31 @@ func TestPeerCredentials(t *testing.T) {
|
|||||||
|
|
||||||
// Matrix test for rate limiting
|
// Matrix test for rate limiting
|
||||||
func TestRateLimiter_Matrix(t *testing.T) {
|
func TestRateLimiter_Matrix(t *testing.T) {
|
||||||
limits := []int{1, 5, 10, 100}
|
testCases := []struct {
|
||||||
windows := []time.Duration{10 * time.Millisecond, 100 * time.Millisecond, time.Second}
|
limit int
|
||||||
|
window time.Duration
|
||||||
|
}{
|
||||||
|
{1, time.Second},
|
||||||
|
{5, time.Second},
|
||||||
|
{10, time.Second},
|
||||||
|
{100, time.Second},
|
||||||
|
}
|
||||||
|
|
||||||
for _, limit := range limits {
|
for _, tc := range testCases {
|
||||||
for _, window := range windows {
|
t.Run(
|
||||||
t.Run(
|
fmt.Sprintf("limit=%d_window=%s", tc.limit, tc.window),
|
||||||
"limit="+string(rune('0'+limit))+"_window="+window.String(),
|
func(t *testing.T) {
|
||||||
func(t *testing.T) {
|
rl := NewRateLimiter(tc.limit, tc.window)
|
||||||
rl := NewRateLimiter(limit, window)
|
|
||||||
|
|
||||||
// Should allow exactly 'limit' requests
|
// Should allow exactly 'limit' requests
|
||||||
for i := 0; i < limit; i++ {
|
for i := 0; i < tc.limit; i++ {
|
||||||
assert.True(t, rl.Allow(1))
|
assert.True(t, rl.Allow(1), "request %d should be allowed", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next should be blocked
|
// Next should be blocked
|
||||||
assert.False(t, rl.Allow(1))
|
assert.False(t, rl.Allow(1), "request after limit should be blocked")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func NewServer(socketPath string, cfgManager *config.Manager) *Server {
|
|||||||
// Start starts the server.
|
// Start starts the server.
|
||||||
func (s *Server) Start() error {
|
func (s *Server) Start() error {
|
||||||
// Remove existing socket
|
// Remove existing socket
|
||||||
os.Remove(s.socketPath)
|
_ = os.Remove(s.socketPath)
|
||||||
|
|
||||||
listener, err := net.Listen("unix", s.socketPath)
|
listener, err := net.Listen("unix", s.socketPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -56,14 +56,15 @@ func (s *Server) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set socket permissions: 0660 root:lolcathost
|
// Set socket permissions: 0660 root:lolcathost
|
||||||
|
// #nosec G302 -- socket must be group-accessible for lolcathost group members
|
||||||
if err := os.Chmod(s.socketPath, 0660); err != nil {
|
if err := os.Chmod(s.socketPath, 0660); err != nil {
|
||||||
listener.Close()
|
_ = listener.Close()
|
||||||
return fmt.Errorf("failed to set socket permissions: %w", err)
|
return fmt.Errorf("failed to set socket permissions: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set socket group to lolcathost (GID 850)
|
// Set socket group to lolcathost (GID 850)
|
||||||
if err := os.Chown(s.socketPath, 0, 850); err != nil {
|
if err := os.Chown(s.socketPath, 0, 850); err != nil {
|
||||||
listener.Close()
|
_ = listener.Close()
|
||||||
return fmt.Errorf("failed to set socket ownership: %w", err)
|
return fmt.Errorf("failed to set socket ownership: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,13 +95,13 @@ func (s *Server) Stop() error {
|
|||||||
close(s.stopCh)
|
close(s.stopCh)
|
||||||
|
|
||||||
if s.listener != nil {
|
if s.listener != nil {
|
||||||
s.listener.Close()
|
_ = s.listener.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Remove(s.socketPath)
|
_ = os.Remove(s.socketPath)
|
||||||
|
|
||||||
if s.auditLogger != nil {
|
if s.auditLogger != nil {
|
||||||
s.auditLogger.Close()
|
_ = s.auditLogger.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -200,7 +201,7 @@ func (s *Server) isAuthorized(creds *PeerCredentials) bool {
|
|||||||
func (s *Server) writeResponse(conn net.Conn, resp *protocol.Response) {
|
func (s *Server) writeResponse(conn net.Conn, resp *protocol.Response) {
|
||||||
data, _ := json.Marshal(resp)
|
data, _ := json.Marshal(resp)
|
||||||
data = append(data, '\n')
|
data = append(data, '\n')
|
||||||
conn.Write(data)
|
_, _ = conn.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleRequest(req *protocol.Request, creds *PeerCredentials) *protocol.Response {
|
func (s *Server) handleRequest(req *protocol.Request, creds *PeerCredentials) *protocol.Response {
|
||||||
@@ -492,7 +493,7 @@ func (s *Server) handleRollback(req *protocol.Request) *protocol.Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Flush DNS after restore
|
// Flush DNS after restore
|
||||||
s.flusher.Flush()
|
_ = s.flusher.Flush()
|
||||||
|
|
||||||
resp, _ := protocol.NewOKResponse(map[string]string{"restored": payload.BackupName})
|
resp, _ := protocol.NewOKResponse(map[string]string{"restored": payload.BackupName})
|
||||||
return resp
|
return resp
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ func (i *Installer) Uninstall() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove socket
|
// Remove socket
|
||||||
os.Remove(SocketPath)
|
_ = os.Remove(SocketPath)
|
||||||
|
|
||||||
// Note: We don't remove the group, logs, or backups
|
// Note: We don't remove the group, logs, or backups
|
||||||
// The user may want to keep these
|
// The user may want to keep these
|
||||||
@@ -225,6 +225,7 @@ func (i *Installer) createGroupDarwin() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, args := range cmds {
|
for _, args := range cmds {
|
||||||
|
// #nosec G204 -- args are hardcoded dscl commands with the constant GroupName
|
||||||
if err := exec.Command(args[0], args[1:]...).Run(); err != nil {
|
if err := exec.Command(args[0], args[1:]...).Run(); err != nil {
|
||||||
return fmt.Errorf("command %v failed: %w", args, err)
|
return fmt.Errorf("command %v failed: %w", args, err)
|
||||||
}
|
}
|
||||||
@@ -315,6 +316,7 @@ func (i *Installer) createDirectories() error {
|
|||||||
|
|
||||||
for _, dir := range dirs {
|
for _, dir := range dirs {
|
||||||
i.log(" Creating directory '%s'...", dir)
|
i.log(" Creating directory '%s'...", dir)
|
||||||
|
// #nosec G301 -- system directories should be world-readable
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create %s: %w", dir, err)
|
return fmt.Errorf("failed to create %s: %w", dir, err)
|
||||||
}
|
}
|
||||||
@@ -340,21 +342,23 @@ func (i *Installer) installLaunchDaemon() error {
|
|||||||
|
|
||||||
// Unload if already loaded (do this before writing plist)
|
// Unload if already loaded (do this before writing plist)
|
||||||
i.log(" Stopping existing daemon if running...")
|
i.log(" Stopping existing daemon if running...")
|
||||||
exec.Command("launchctl", "bootout", "system/com.lolcathost.daemon").Run()
|
_ = exec.Command("launchctl", "bootout", "system/com.lolcathost.daemon").Run()
|
||||||
|
|
||||||
// Give launchd time to fully unload the service
|
// Give launchd time to fully unload the service
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
// Remove old plist to ensure clean state
|
// Remove old plist to ensure clean state
|
||||||
os.Remove(plistPath)
|
_ = os.Remove(plistPath)
|
||||||
|
|
||||||
i.log(" Writing LaunchDaemon plist...")
|
i.log(" Writing LaunchDaemon plist...")
|
||||||
|
// #nosec G306 -- plist files are world-readable by convention
|
||||||
if err := os.WriteFile(plistPath, []byte(plistContent), 0644); err != nil {
|
if err := os.WriteFile(plistPath, []byte(plistContent), 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write plist: %w", err)
|
return fmt.Errorf("failed to write plist: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bootstrap the daemon
|
// Bootstrap the daemon
|
||||||
i.log(" Starting daemon...")
|
i.log(" Starting daemon...")
|
||||||
|
// #nosec G204 -- plistPath is constructed from constant LaunchDaemonDir
|
||||||
cmd := exec.Command("launchctl", "bootstrap", "system", plistPath)
|
cmd := exec.Command("launchctl", "bootstrap", "system", plistPath)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -376,10 +380,10 @@ func (i *Installer) uninstallLaunchDaemon() {
|
|||||||
plistPath := filepath.Join(LaunchDaemonDir, "com.lolcathost.daemon.plist")
|
plistPath := filepath.Join(LaunchDaemonDir, "com.lolcathost.daemon.plist")
|
||||||
|
|
||||||
i.log(" Stopping daemon...")
|
i.log(" Stopping daemon...")
|
||||||
exec.Command("launchctl", "bootout", "system/com.lolcathost.daemon").Run()
|
_ = exec.Command("launchctl", "bootout", "system/com.lolcathost.daemon").Run()
|
||||||
|
|
||||||
i.log(" Removing LaunchDaemon plist...")
|
i.log(" Removing LaunchDaemon plist...")
|
||||||
os.Remove(plistPath)
|
_ = os.Remove(plistPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Installer) installSystemdService() error {
|
func (i *Installer) installSystemdService() error {
|
||||||
@@ -387,6 +391,7 @@ func (i *Installer) installSystemdService() error {
|
|||||||
unitContent := fmt.Sprintf(SystemdUnit, i.binaryPath)
|
unitContent := fmt.Sprintf(SystemdUnit, i.binaryPath)
|
||||||
|
|
||||||
i.log(" Writing systemd unit...")
|
i.log(" Writing systemd unit...")
|
||||||
|
// #nosec G306 -- systemd unit files are world-readable by convention
|
||||||
if err := os.WriteFile(unitPath, []byte(unitContent), 0644); err != nil {
|
if err := os.WriteFile(unitPath, []byte(unitContent), 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write unit file: %w", err)
|
return fmt.Errorf("failed to write unit file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -408,12 +413,12 @@ func (i *Installer) installSystemdService() error {
|
|||||||
|
|
||||||
func (i *Installer) uninstallSystemdService() {
|
func (i *Installer) uninstallSystemdService() {
|
||||||
i.log(" Stopping and disabling service...")
|
i.log(" Stopping and disabling service...")
|
||||||
exec.Command("systemctl", "disable", "--now", "lolcathost.service").Run()
|
_ = exec.Command("systemctl", "disable", "--now", "lolcathost.service").Run()
|
||||||
|
|
||||||
i.log(" Removing systemd unit...")
|
i.log(" Removing systemd unit...")
|
||||||
os.Remove(filepath.Join(SystemdDir, "lolcathost.service"))
|
_ = os.Remove(filepath.Join(SystemdDir, "lolcathost.service"))
|
||||||
|
|
||||||
exec.Command("systemctl", "daemon-reload").Run()
|
_ = exec.Command("systemctl", "daemon-reload").Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Installer) createDefaultConfig() error {
|
func (i *Installer) createDefaultConfig() error {
|
||||||
@@ -430,6 +435,7 @@ func (i *Installer) createDefaultConfig() error {
|
|||||||
|
|
||||||
// Create config directory
|
// Create config directory
|
||||||
configDir := filepath.Dir(configPath)
|
configDir := filepath.Dir(configPath)
|
||||||
|
// #nosec G301 -- config directory should be world-readable
|
||||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create config directory: %w", err)
|
return fmt.Errorf("failed to create config directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-8
@@ -354,7 +354,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.setError(fmt.Sprintf("Refresh failed: %v", msg.err))
|
m.setError(fmt.Sprintf("Refresh failed: %v", msg.err))
|
||||||
// Mark as disconnected to trigger reconnect
|
// Mark as disconnected to trigger reconnect
|
||||||
m.connected = false
|
m.connected = false
|
||||||
m.client.Close()
|
_ = m.client.Close()
|
||||||
} else {
|
} else {
|
||||||
// Always update the list, even if entries is nil/empty
|
// Always update the list, even if entries is nil/empty
|
||||||
m.list.SetItems(msg.entries)
|
m.list.SetItems(msg.entries)
|
||||||
@@ -603,7 +603,7 @@ func (m *Model) handleFormKey(msg tea.KeyMsg) tea.Cmd {
|
|||||||
oldAlias := m.form.EditAlias()
|
oldAlias := m.form.EditAlias()
|
||||||
return tea.Sequence(
|
return tea.Sequence(
|
||||||
func() tea.Msg {
|
func() tea.Msg {
|
||||||
m.client.Delete(oldAlias)
|
_ = m.client.Delete(oldAlias)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
m.addHost(domain, ip, "", group), // Empty alias = auto-generate
|
m.addHost(domain, ip, "", group), // Empty alias = auto-generate
|
||||||
@@ -678,7 +678,7 @@ func (m *Model) handlePresetFormKey(msg tea.KeyMsg) tea.Cmd {
|
|||||||
oldName := m.presetPicker.EditName()
|
oldName := m.presetPicker.EditName()
|
||||||
return tea.Sequence(
|
return tea.Sequence(
|
||||||
func() tea.Msg {
|
func() tea.Msg {
|
||||||
m.client.DeletePreset(oldName)
|
_ = m.client.DeletePreset(oldName)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
m.addPreset(name, enable, disable),
|
m.addPreset(name, enable, disable),
|
||||||
@@ -1136,11 +1136,6 @@ func (m *Model) confirmDeleteView() string {
|
|||||||
return dialogStyle.Render(sb.String())
|
return dialogStyle.Render(sb.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run starts the TUI application.
|
|
||||||
func Run(socketPath string) error {
|
|
||||||
return RunWithVersion(socketPath, "dev", "", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunWithVersion starts the TUI application with version info for update checking.
|
// RunWithVersion starts the TUI application with version info for update checking.
|
||||||
func RunWithVersion(socketPath, version, githubOwner, githubRepo string) error {
|
func RunWithVersion(socketPath, version, githubOwner, githubRepo string) error {
|
||||||
m := NewModel(socketPath)
|
m := NewModel(socketPath)
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ func (c *Checker) CheckForUpdate(ctx context.Context) *UpdateInfo {
|
|||||||
|
|
||||||
// fetchLatestRelease fetches the latest release info from GitHub API
|
// fetchLatestRelease fetches the latest release info from GitHub API
|
||||||
func (c *Checker) fetchLatestRelease(ctx context.Context) (*ReleaseInfo, error) {
|
func (c *Checker) fetchLatestRelease(ctx context.Context) (*ReleaseInfo, error) {
|
||||||
|
// #nosec G107 -- URL is constructed from hardcoded constant and validated owner/repo
|
||||||
url := fmt.Sprintf(githubReleasesURL, c.owner, c.repo)
|
url := fmt.Sprintf(githubReleasesURL, c.owner, c.repo)
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
@@ -145,15 +146,9 @@ func parseVersion(v string) []int {
|
|||||||
|
|
||||||
for _, p := range parts {
|
for _, p := range parts {
|
||||||
var num int
|
var num int
|
||||||
fmt.Sscanf(p, "%d", &num)
|
_, _ = fmt.Sscanf(p, "%d", &num)
|
||||||
result = append(result, num)
|
result = append(result, num)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatUpdateMessage formats a user-friendly update notification
|
|
||||||
func (u *UpdateInfo) FormatUpdateMessage() string {
|
|
||||||
return fmt.Sprintf("New version available: %s (current: %s) - %s",
|
|
||||||
u.LatestVersion, u.CurrentVersion, u.ReleaseURL)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -76,19 +76,6 @@ func TestIsNewerVersion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateInfo_FormatUpdateMessage(t *testing.T) {
|
|
||||||
info := &UpdateInfo{
|
|
||||||
CurrentVersion: "1.0.0",
|
|
||||||
LatestVersion: "1.1.0",
|
|
||||||
ReleaseURL: "https://github.com/lukaszraczylo/lolcathost/releases/tag/v1.1.0",
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := info.FormatUpdateMessage()
|
|
||||||
assert.Contains(t, msg, "1.0.0")
|
|
||||||
assert.Contains(t, msg, "1.1.0")
|
|
||||||
assert.Contains(t, msg, "https://github.com")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewChecker(t *testing.T) {
|
func TestNewChecker(t *testing.T) {
|
||||||
checker := NewChecker("lukaszraczylo", "lolcathost", "v1.0.0")
|
checker := NewChecker("lukaszraczylo", "lolcathost", "v1.0.0")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user