Files
ja4sentinel/internal/output/writers.go
Jacquin Antoine c7e8fe874f
Some checks failed
Build RPM Package / Build RPM Packages (CentOS 7, Rocky 8/9/10) (push) Has been cancelled
fix: renforcer limites TLS, timeouts socket et validation config
Co-authored-by: aider (openrouter/openai/gpt-5.3-codex) <aider@aider.chat>
2026-02-28 20:01:39 +01:00

277 lines
5.9 KiB
Go

// Package output provides writers for ja4sentinel log records
package output
import (
"encoding/json"
"fmt"
"io"
"net"
"os"
"sync"
"time"
"ja4sentinel/api"
)
// StdoutWriter writes log records to stdout
type StdoutWriter struct {
encoder *json.Encoder
mutex sync.Mutex
}
// NewStdoutWriter creates a new stdout writer
func NewStdoutWriter() *StdoutWriter {
return &StdoutWriter{
encoder: json.NewEncoder(os.Stdout),
}
}
// Write writes a log record to stdout
func (w *StdoutWriter) Write(rec api.LogRecord) error {
w.mutex.Lock()
defer w.mutex.Unlock()
return w.encoder.Encode(rec)
}
// Close closes the writer (no-op for stdout)
func (w *StdoutWriter) Close() error {
return nil
}
// FileWriter writes log records to a file
type FileWriter struct {
file *os.File
encoder *json.Encoder
mutex sync.Mutex
}
// NewFileWriter creates a new file writer
func NewFileWriter(path string) (*FileWriter, error) {
file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil, fmt.Errorf("failed to open file %s: %w", path, err)
}
return &FileWriter{
file: file,
encoder: json.NewEncoder(file),
}, nil
}
// Write writes a log record to the file
func (w *FileWriter) Write(rec api.LogRecord) error {
w.mutex.Lock()
defer w.mutex.Unlock()
return w.encoder.Encode(rec)
}
// Close closes the file
func (w *FileWriter) Close() error {
w.mutex.Lock()
defer w.mutex.Unlock()
if w.file != nil {
return w.file.Close()
}
return nil
}
// UnixSocketWriter writes log records to a UNIX socket
type UnixSocketWriter struct {
socketPath string
conn net.Conn
mutex sync.Mutex
dialTimeout time.Duration
writeTimeout time.Duration
}
// NewUnixSocketWriter creates a new UNIX socket writer
func NewUnixSocketWriter(socketPath string) (*UnixSocketWriter, error) {
w := &UnixSocketWriter{
socketPath: socketPath,
dialTimeout: 2 * time.Second,
writeTimeout: 2 * time.Second,
}
// Try to connect (socket may not exist yet)
conn, err := net.DialTimeout("unix", socketPath, w.dialTimeout)
if err == nil {
w.conn = conn
}
return w, nil
}
// Write writes a log record to the UNIX socket
func (w *UnixSocketWriter) Write(rec api.LogRecord) error {
w.mutex.Lock()
defer w.mutex.Unlock()
ensureConn := func() error {
if w.conn != nil {
return nil
}
conn, err := net.DialTimeout("unix", w.socketPath, w.dialTimeout)
if err != nil {
return fmt.Errorf("failed to connect to socket %s: %w", w.socketPath, err)
}
w.conn = conn
return nil
}
if err := ensureConn(); err != nil {
return err
}
data, err := json.Marshal(rec)
if err != nil {
return fmt.Errorf("failed to marshal record: %w", err)
}
data = append(data, '\n')
if err := w.conn.SetWriteDeadline(time.Now().Add(w.writeTimeout)); err != nil {
return fmt.Errorf("failed to set write deadline: %w", err)
}
if _, err = w.conn.Write(data); err == nil {
return nil
}
_ = w.conn.Close()
w.conn = nil
if errConn := ensureConn(); errConn != nil {
return fmt.Errorf("failed to write to socket and reconnect failed: %w", errConn)
}
if errDeadline := w.conn.SetWriteDeadline(time.Now().Add(w.writeTimeout)); errDeadline != nil {
_ = w.conn.Close()
w.conn = nil
return fmt.Errorf("failed to set write deadline after reconnect: %w", errDeadline)
}
if _, errRetry := w.conn.Write(data); errRetry != nil {
_ = w.conn.Close()
w.conn = nil
return fmt.Errorf("failed to write to socket after reconnect: %w", errRetry)
}
return nil
}
// Close closes the UNIX socket connection
func (w *UnixSocketWriter) Close() error {
w.mutex.Lock()
defer w.mutex.Unlock()
if w.conn != nil {
return w.conn.Close()
}
return nil
}
// MultiWriter combines multiple writers
type MultiWriter struct {
writers []api.Writer
mutex sync.Mutex
}
// NewMultiWriter creates a new multi-writer
func NewMultiWriter() *MultiWriter {
return &MultiWriter{
writers: make([]api.Writer, 0),
}
}
// Write writes a log record to all writers
func (mw *MultiWriter) Write(rec api.LogRecord) error {
mw.mutex.Lock()
defer mw.mutex.Unlock()
var lastErr error
for _, w := range mw.writers {
if err := w.Write(rec); err != nil {
lastErr = err
}
}
return lastErr
}
// Add adds a writer to the multi-writer
func (mw *MultiWriter) Add(writer api.Writer) {
mw.mutex.Lock()
defer mw.mutex.Unlock()
mw.writers = append(mw.writers, writer)
}
// CloseAll closes all writers
func (mw *MultiWriter) CloseAll() error {
mw.mutex.Lock()
defer mw.mutex.Unlock()
var lastErr error
for _, w := range mw.writers {
if closer, ok := w.(io.Closer); ok {
if err := closer.Close(); err != nil {
lastErr = err
}
}
}
return lastErr
}
// BuilderImpl implements the api.Builder interface
type BuilderImpl struct{}
// NewBuilder creates a new output builder
func NewBuilder() *BuilderImpl {
return &BuilderImpl{}
}
// NewFromConfig constructs writers from AppConfig
func (b *BuilderImpl) NewFromConfig(cfg api.AppConfig) (api.Writer, error) {
multiWriter := NewMultiWriter()
for _, outputCfg := range cfg.Outputs {
if !outputCfg.Enabled {
continue
}
var writer api.Writer
var err error
switch outputCfg.Type {
case "stdout":
writer = NewStdoutWriter()
case "file":
path := outputCfg.Params["path"]
if path == "" {
return nil, fmt.Errorf("file output requires 'path' parameter")
}
writer, err = NewFileWriter(path)
if err != nil {
return nil, err
}
case "unix_socket":
socketPath := outputCfg.Params["socket_path"]
if socketPath == "" {
return nil, fmt.Errorf("unix_socket output requires 'socket_path' parameter")
}
writer, err = NewUnixSocketWriter(socketPath)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unknown output type: %s", outputCfg.Type)
}
multiWriter.Add(writer)
}
// If no outputs configured, default to stdout
if len(multiWriter.writers) == 0 {
multiWriter.Add(NewStdoutWriter())
}
return multiWriter, nil
}