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 @@ -145,6 +145,7 @@ func (m *Manager) SetSessionCookie(w http.ResponseWriter) string {
Value: token,
Path: "/",
MaxAge: int(SessionDuration.Seconds()),
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
})
Expand All @@ -158,6 +159,7 @@ func (m *Manager) ClearSessionCookie(w http.ResponseWriter) {
Value: "",
Path: "/",
MaxAge: -1,
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
})
Expand Down
5 changes: 3 additions & 2 deletions internal/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
embed "github.com/adcondev/ticket-daemon"
"github.com/adcondev/ticket-daemon/internal/auth"
"github.com/adcondev/ticket-daemon/internal/config"
"github.com/adcondev/ticket-daemon/internal/posprinter"
"github.com/adcondev/ticket-daemon/internal/server"
"github.com/adcondev/ticket-daemon/internal/worker"
)
Expand Down Expand Up @@ -133,7 +134,7 @@ func (p *Program) Start() error {
Uptime: int(time.Since(p.startTime).Seconds()),
}

if response.Printers.Status == "error" {
if response.Printers.Status == posprinter.StatusError {
response.Status = "degraded"
}

Expand Down Expand Up @@ -318,7 +319,7 @@ func serveDashboard(tmpl *template.Template) http.HandlerFunc {
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
//nolint:gosec // False positive: passing token to internal template

data := struct{ AuthToken string }{AuthToken: config.AuthToken}
if err := tmpl.Execute(w, data); err != nil {
log.Printf("[X] Error rendering dashboard: %v", err)
Expand Down
4 changes: 2 additions & 2 deletions internal/daemon/printer_discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (pd *PrinterDiscovery) GetPrinters(forceRefresh bool) ([]connection.Printer
func (pd *PrinterDiscovery) GetSummary() posprinter.Summary {
printers, err := pd.GetPrinters(false)
if err != nil {
return posprinter.Summary{Status: "error", DetectedCount: 0}
return posprinter.Summary{Status: posprinter.StatusError, DetectedCount: 0}
}

thermal := connection.FilterThermalPrinters(printers)
Expand All @@ -74,7 +74,7 @@ func (pd *PrinterDiscovery) GetSummary() posprinter.Summary {
if len(thermal) == 0 && len(physical) > 0 {
status = "warning"
} else if len(physical) == 0 {
status = "error"
status = posprinter.StatusError
}

return posprinter.Summary{
Expand Down
3 changes: 3 additions & 0 deletions internal/posprinter/types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Package posprinter contains shared types to avoid import cycles.
package posprinter

const StatusError = "error"


// Summary PrinterSummary provides lightweight overview for health checks
type Summary struct {
Status string `json:"status"` // "ok", "warning", "error"
Expand Down
9 changes: 8 additions & 1 deletion internal/server/rate_limit.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ type JobRateLimiter struct {
mu sync.Mutex
attempts map[string][]time.Time
maxPerMin int
now func() time.Time
}

// NewJobRateLimiter creates a limiter allowing maxPerMinute jobs per client.
func NewJobRateLimiter(maxPerMinute int) *JobRateLimiter {
return &JobRateLimiter{
attempts: make(map[string][]time.Time),
maxPerMin: maxPerMinute,
now: time.Now,
}
}

Expand All @@ -26,7 +28,12 @@ func (rl *JobRateLimiter) Allow(clientAddr string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()

now := time.Now()
var now time.Time
if rl.now != nil {
now = rl.now()
} else {
now = time.Now()
}
cutoff := now.Add(-time.Minute)

recent := make([]time.Time, 0, rl.maxPerMin)
Expand Down
70 changes: 70 additions & 0 deletions internal/server/rate_limit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package server

import (
"testing"
"time"
)

func TestJobRateLimiter(t *testing.T) {
// We set max to 3 requests per minute
maxPerMin := 3
rl := NewJobRateLimiter(maxPerMin)

// Mock time
currentTime := time.Now()
rl.now = func() time.Time {
return currentTime
}

clientAddr := "192.168.1.1:1234"

// 1st request - should be allowed
if !rl.Allow(clientAddr) {
t.Errorf("Expected 1st request to be allowed")
}

// 2nd request - should be allowed
if !rl.Allow(clientAddr) {
t.Errorf("Expected 2nd request to be allowed")
}

// 3rd request - should be allowed
if !rl.Allow(clientAddr) {
t.Errorf("Expected 3rd request to be allowed")
}

// 4th request - should be denied (limit is 3)
if rl.Allow(clientAddr) {
t.Errorf("Expected 4th request to be denied")
}

// Fast forward time by 30 seconds
currentTime = currentTime.Add(30 * time.Second)

// Still denied
if rl.Allow(clientAddr) {
t.Errorf("Expected request after 30s to be denied")
}

// Fast forward time to clear the first request (1 minute and 1 second total)
currentTime = currentTime.Add(31 * time.Second)

// Now it should be allowed again
if !rl.Allow(clientAddr) {
t.Errorf("Expected request after 1m1s to be allowed")
}

// Another client should not be affected
if !rl.Allow("192.168.1.2:5678") {
t.Errorf("Expected request from different client to be allowed")
}
}

func TestJobRateLimiter_FallbackTime(t *testing.T) {
rl := NewJobRateLimiter(3)
rl.now = nil // Force the nil branch in Allow

if !rl.Allow("127.0.0.1") {
t.Errorf("Expected request to be allowed with fallback time")
}
}
6 changes: 3 additions & 3 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"github.com/adcondev/poster/pkg/connection"
"github.com/adcondev/ticket-daemon/internal/config"
"github.com/adcondev/ticket-daemon/internal/posprinter"
"github.com/adcondev/ticket-daemon/internal/utils"

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

View workflow job for this annotation

GitHub Actions / πŸ§ͺ Test and Coverage

no required module provides package github.com/adcondev/ticket-daemon/internal/utils; to add it:

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

View workflow job for this annotation

GitHub Actions / πŸ§ͺ Test and Coverage

no required module provides package github.com/adcondev/ticket-daemon/internal/utils; to add it:

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

View workflow job for this annotation

GitHub Actions / πŸ§ͺ Test and Coverage

no required module provides package github.com/adcondev/ticket-daemon/internal/utils; to add it:

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

View workflow job for this annotation

GitHub Actions / πŸ§ͺ Test and Coverage

no required module provides package github.com/adcondev/ticket-daemon/internal/utils; to add it:
)

const maxJobsPerMinute = 30
Expand Down Expand Up @@ -46,7 +47,6 @@
Tipo string `json:"tipo"`
ID string `json:"id,omitempty"`
Datos json.RawMessage `json:"datos,omitempty"`
//nolint:gosec // Required JSON key for client payload
AuthToken string `json:"auth_token,omitempty"`
}

Expand Down Expand Up @@ -188,7 +188,7 @@
// ── RATE LIMIT ────────────────────────────────────────
clientAddr := fmt.Sprintf("%p", conn) // Using pointer address as client identifier
if !s.jobLimiter.Allow(clientAddr) {
log.Printf("[AUDIT] JOB_RATE_LIMITED | client=%s", clientAddr)
log.Printf("[AUDIT] JOB_RATE_LIMITED | client=%s", utils.SanitizeLog(clientAddr))
s.sendError(ctx, conn, jobID, "Rate limit exceeded: please wait before submitting more jobs")
return
}
Expand All @@ -197,7 +197,7 @@
if config.AuthToken != "" {
token := msg.AuthToken
if token != config.AuthToken {
log.Printf("[AUDIT] JOB_REJECTED | reason=invalid_token | client=%s", clientAddr)
log.Printf("[AUDIT] JOB_REJECTED | reason=invalid_token | client=%s", utils.SanitizeLog(clientAddr))
s.sendError(ctx, conn, jobID, "Invalid or missing auth token")
return
}
Expand Down
Loading