update readme and build
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package internals
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// authFileLog writes structured JSON-lines to a file; nil = file logging off.
|
||||
// Console logging always fires regardless of this setting.
|
||||
var authFileLog *log.Logger
|
||||
|
||||
// authLogEntry is the structured format for each auth event.
|
||||
// One JSON object per line — compatible with CrowdSec, fail2ban, jq.
|
||||
type authLogEntry struct {
|
||||
Time string `json:"time"` // RFC3339 UTC
|
||||
RemoteIP string `json:"remote_ip"` // real client IP (proxy-aware)
|
||||
Username string `json:"username"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"` // login_success | invalid_credentials | csrf_invalid | invalid_input
|
||||
}
|
||||
|
||||
// initAuthLogger opens (or creates) the log file.
|
||||
// path "off" disables file logging; console output is always on.
|
||||
func initAuthLogger(path string) {
|
||||
if strings.EqualFold(path, "off") {
|
||||
fmt.Println("auth log: disabled (console only)")
|
||||
return
|
||||
}
|
||||
dir := filepath.Dir(path)
|
||||
if dir != "" && dir != "." {
|
||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "auth log: cannot create dir %q: %v\n", dir, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0640)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "auth log: cannot open %q: %v\n", path, err)
|
||||
return
|
||||
}
|
||||
// log.New with empty flags → raw lines, no timestamp prefix (timestamp is in JSON)
|
||||
authFileLog = log.New(f, "", 0)
|
||||
fmt.Printf("auth log: %s\n", path)
|
||||
}
|
||||
|
||||
// logAuthAttempt records one auth event.
|
||||
// Always prints to stdout; also writes to file if enabled.
|
||||
func logAuthAttempt(r *http.Request, username string, success bool, message string) {
|
||||
entry := authLogEntry{
|
||||
Time: time.Now().UTC().Format(time.RFC3339),
|
||||
RemoteIP: realIP(r),
|
||||
Username: username,
|
||||
Success: success,
|
||||
Message: message,
|
||||
}
|
||||
b, _ := json.Marshal(entry)
|
||||
line := string(b)
|
||||
|
||||
// Console — always visible
|
||||
fmt.Println(line)
|
||||
|
||||
// File — if enabled
|
||||
if authFileLog != nil {
|
||||
authFileLog.Println(line) // log.Logger serialises concurrent writes
|
||||
}
|
||||
}
|
||||
|
||||
// realIP returns the originating client IP, respecting common reverse-proxy
|
||||
// headers in priority order: Cloudflare → X-Forwarded-For → X-Real-IP → RemoteAddr.
|
||||
func realIP(r *http.Request) string {
|
||||
// Cloudflare sets CF-Connecting-IP to the unmodified client IP.
|
||||
if ip := r.Header.Get("CF-Connecting-IP"); ip != "" && net.ParseIP(ip) != nil {
|
||||
return ip
|
||||
}
|
||||
|
||||
// X-Forwarded-For may be a comma-separated list; the leftmost entry is the
|
||||
// originating client (rightmost entries are added by each successive proxy).
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
if idx := strings.IndexByte(xff, ','); idx != -1 {
|
||||
xff = xff[:idx]
|
||||
}
|
||||
xff = strings.TrimSpace(xff)
|
||||
if net.ParseIP(xff) != nil {
|
||||
return xff
|
||||
}
|
||||
}
|
||||
|
||||
// Nginx / Traefik single-value header.
|
||||
if ip := r.Header.Get("X-Real-IP"); ip != "" && net.ParseIP(ip) != nil {
|
||||
return ip
|
||||
}
|
||||
|
||||
// Direct connection.
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return host
|
||||
}
|
||||
Reference in New Issue
Block a user