Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ func (m *Manager) SetSessionCookie(w http.ResponseWriter) string {
Path: "/",
MaxAge: int(SessionDuration.Seconds()),
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
return token
Expand All @@ -174,6 +175,7 @@ func (m *Manager) ClearSessionCookie(w http.ResponseWriter) {
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
}
Expand Down
15 changes: 11 additions & 4 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,30 @@ type Environment struct {
DefaultPort string
DefaultMode bool // true = test mode (simulated weights), false = real weights
}
// Common configuration constants
const (
EnvRemoteName = "REMOTO"
EnvLocalName = "LOCAL"
DefaultComPort = "COM3"
)


// TODO: Make Port inyectable via ldflags. Same port and addres could cause conflicts

// Environments defines available deployment configurations
var Environments = map[string]Environment{
"remote": {
Name: "REMOTO",
Name: EnvRemoteName,
ServiceName: ServiceName,
ListenAddr: "0.0.0.0:" + ServerPort,
DefaultPort: "COM3",
DefaultPort: DefaultComPort,
DefaultMode: false,
},
"local": {
Name: "LOCAL",
Name: EnvLocalName,
ServiceName: ServiceName,
ListenAddr: "localhost:" + ServerPort,
DefaultPort: "COM3",
DefaultPort: DefaultComPort,
DefaultMode: false,
},
}
Expand Down
100 changes: 100 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package config

import (
"bytes"
"log"
"strings"
"testing"
)

func TestGetEnvironment(t *testing.T) {
tests := []struct {
name string
env string
wantName string
wantLog bool
}{
{
name: "Valid remote environment",
env: "remote",
wantName: EnvRemoteName,
wantLog: false,
},
{
name: "Valid local environment",
env: "local",
wantName: EnvLocalName,
wantLog: false,
},
{
name: "Unknown environment falls back to local",
env: "unknown",
wantName: EnvLocalName,
wantLog: true,
},
{
name: "Empty environment falls back to local",
env: "",
wantName: EnvLocalName,
wantLog: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
originalOutput := log.Writer()
log.SetOutput(&buf)
t.Cleanup(func() {
log.SetOutput(originalOutput)
})

got := GetEnvironment(tt.env)

if got.Name != tt.wantName {
t.Errorf("GetEnvironment(%q) got environment name = %v, want %v", tt.env, got.Name, tt.wantName)
}

if tt.wantLog {
if buf.Len() == 0 {
t.Errorf("GetEnvironment(%q) expected log output, but got none", tt.env)
}
} else {
if buf.Len() > 0 {
t.Errorf("GetEnvironment(%q) unexpected log output: %v", tt.env, buf.String())
}
}
})
}
}

func TestEnvironmentsConsistency(t *testing.T) {
// The Environments map is initialized with global variables that can be overridden via ldflags.
// This test ensures that the map entries accurately reflect the current state of these globals.

// ServerPort is empty by default
expectedListenSuffix := ServerPort

// We test what is currently set in the global state
remoteEnv := Environments["remote"]
if remoteEnv.Name != EnvRemoteName {
t.Errorf("Expected 'remote' environment Name to be 'REMOTO', got %s", remoteEnv.Name)
}
if !strings.HasSuffix(remoteEnv.ListenAddr, expectedListenSuffix) {
t.Errorf("Expected 'remote' ListenAddr to end with %q, got %s", expectedListenSuffix, remoteEnv.ListenAddr)
}
if remoteEnv.DefaultPort != DefaultComPort {
t.Errorf("Expected 'remote' DefaultPort to be 'COM3', got %s", remoteEnv.DefaultPort)
}

localEnv := Environments["local"]
if localEnv.Name != EnvLocalName {
t.Errorf("Expected 'local' environment Name to be 'LOCAL', got %s", localEnv.Name)
}
if !strings.HasSuffix(localEnv.ListenAddr, expectedListenSuffix) {
t.Errorf("Expected 'local' ListenAddr to end with %q, got %s", expectedListenSuffix, localEnv.ListenAddr)
}
if localEnv.DefaultPort != DefaultComPort {
t.Errorf("Expected 'local' DefaultPort to be 'COM3', got %s", localEnv.DefaultPort)
}
}
16 changes: 9 additions & 7 deletions internal/logging/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,13 @@
mgr.FilePath = filepath.Join(logDir, serviceName+".log")

// Try to create log directory
//nolint:gosec

if err := os.MkdirAll(logDir, 0750); err != nil {

Check failure on line 81 in internal/logging/logging.go

View workflow job for this annotation

GitHub Actions / πŸ” Linting

G703: Path traversal via taint analysis (gosec)
// Permission denied - fallback to stdout (console mode)
log.SetOutput(os.Stdout)
mgr.FilePath = ""
//nolint:gosec

log.Printf("[i] Logging to stdout (no write access to %q)", logDir)

Check failure on line 86 in internal/logging/logging.go

View workflow job for this annotation

GitHub Actions / πŸ” Linting

G706: Log injection via taint analysis (gosec)
return mgr, nil
}

Expand All @@ -93,17 +93,18 @@
}

// Open log file
f, err := os.OpenFile(mgr.FilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600)
securePath := filepath.Clean(mgr.FilePath)
f, err := os.OpenFile(securePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600)
if err != nil {
// Fallback to stdout
log.SetOutput(os.Stdout)
log.Printf("[i] Logging to stdout (cannot open %s: %v)", mgr.FilePath, err)
log.Printf("[i] Logging to stdout (cannot open %q: %v)", filepath.Clean(mgr.FilePath), err)

Check failure on line 101 in internal/logging/logging.go

View workflow job for this annotation

GitHub Actions / πŸ” Linting

G706: Log injection via taint analysis (gosec)
return mgr, nil
}

mgr.file = f
log.SetOutput(NewFilteredLogger(f, &mgr.Verbose, &mgr.mu))
log.Printf("[i] Logging to: %s", mgr.FilePath)
log.Printf("[i] Logging to: %q", filepath.Clean(mgr.FilePath))

Check failure on line 107 in internal/logging/logging.go

View workflow job for this annotation

GitHub Actions / πŸ” Linting

G706: Log injection via taint analysis (gosec)

return mgr, nil
}
Expand Down Expand Up @@ -151,10 +152,11 @@
}

// Reopen file
f, err := os.OpenFile(m.FilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600)
securePath := filepath.Clean(m.FilePath)
f, err := os.OpenFile(securePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600)
if err != nil {
log.SetOutput(os.Stdout)
log.Printf("[i] Logging to stdout (cannot open %s: %v)", m.FilePath, err)
log.Printf("[i] Logging to stdout (cannot open %q: %v)", filepath.Clean(m.FilePath), err)
return err
}

Expand Down
15 changes: 9 additions & 6 deletions internal/logging/rotation.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
// RotateIfNeeded truncates the log file if it exceeds MaxLogSize
// Keeps the last 1000 lines for continuity
func RotateIfNeeded(path string) error {
info, err := os.Stat(path)
securePath := filepath.Clean(path)
info, err := os.Stat(securePath)
if err != nil {
if os.IsNotExist(err) {
return nil
Expand All @@ -32,7 +33,7 @@
}

content := strings.Join(lines, "\n") + "\n"
return os.WriteFile(path, []byte(content), 0600)
return os.WriteFile(securePath, []byte(content), 0600)
}

// ReadLastNLines reads the last n lines from a file efficiently
Expand All @@ -43,7 +44,7 @@
if err != nil {
return []string{}
}
file, err := os.Open(securePath) //nolint:gosec
file, err := os.Open(securePath)

Check failure on line 47 in internal/logging/rotation.go

View workflow job for this annotation

GitHub Actions / πŸ” Linting

G304: Potential file inclusion via variable (gosec)
if err != nil {
return []string{}
}
Expand Down Expand Up @@ -101,17 +102,19 @@

// Flush reduces the log file to the last 50 lines
func Flush(path string) error {
lines := ReadLastNLines(path, 50)
securePath := filepath.Clean(path)
lines := ReadLastNLines(securePath, 50)
content := ""
if len(lines) > 0 {
content = strings.Join(lines, "\n") + "\n"
}
return os.WriteFile(path, []byte(content), 0600)
return os.WriteFile(securePath, []byte(content), 0600)
}

// GetFileSize returns the size of the log file in bytes
func GetFileSize(path string) int64 {
info, err := os.Stat(path)
securePath := filepath.Clean(path)
info, err := os.Stat(securePath)
if err != nil {
return 0
}
Expand Down
2 changes: 1 addition & 1 deletion internal/server/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type ConfigMessage struct {
Marca string `json:"marca"`
ModoPrueba bool `json:"modoPrueba"`
Dir string `json:"dir,omitempty"`
//nolint:gosec

AuthToken string `json:"auth_token"` // Required for config changes
}

Expand Down
8 changes: 4 additions & 4 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@

w.Header().Set("Content-Type", "text/html; charset=utf-8")
data := struct {
//nolint:gosec

AuthToken string
}{
AuthToken: config.AuthToken,
Expand All @@ -192,8 +192,8 @@

// Check lockout FIRST
if s.auth.IsLockedOut(ip) {
//nolint:gosec

log.Printf("[AUDIT] LOGIN_BLOCKED | IP=%q | reason=lockout", ip)

Check failure on line 196 in internal/server/server.go

View workflow job for this annotation

GitHub Actions / πŸ” Linting

G706: Log injection via taint analysis (gosec)
http.Redirect(w, r, "/login?locked=1", http.StatusSeeOther)
return
}
Expand All @@ -201,8 +201,8 @@
password := r.FormValue("password")
if !s.auth.ValidatePassword(password) {
s.auth.RecordFailedLogin(ip)
//nolint:gosec

log.Printf("[AUDIT] LOGIN_FAILED | IP=%q", ip)

Check failure on line 205 in internal/server/server.go

View workflow job for this annotation

GitHub Actions / πŸ” Linting

G706: Log injection via taint analysis (gosec)
http.Redirect(w, r, "/login?error=1", http.StatusSeeOther)
return
}
Expand All @@ -210,8 +210,8 @@
// Success
s.auth.ClearFailedLogins(ip)
s.auth.SetSessionCookie(w)
//nolint:gosec

log.Printf("[AUDIT] LOGIN_SUCCESS | IP=%q", ip)

Check failure on line 214 in internal/server/server.go

View workflow job for this annotation

GitHub Actions / πŸ” Linting

G706: Log injection via taint analysis (gosec)
http.Redirect(w, r, "/", http.StatusSeeOther)
}

Expand Down
Loading