v0.5.0 removed tcp tls, basic auth hash changed to argon2, refactor
This commit is contained in:
@ -9,6 +9,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"logwisp/src/internal/config"
|
||||
"logwisp/src/internal/core"
|
||||
"logwisp/src/internal/format"
|
||||
|
||||
@ -121,7 +122,9 @@ func (s *StdoutSink) processLoop(ctx context.Context) {
|
||||
// Format and write
|
||||
formatted, err := s.formatter.Format(entry)
|
||||
if err != nil {
|
||||
s.logger.Error("msg", "Failed to format log entry for stdout", "error", err)
|
||||
s.logger.Error("msg", "Failed to format log entry for stdout",
|
||||
"component", "stdout_sink",
|
||||
"error", err)
|
||||
continue
|
||||
}
|
||||
s.output.Write(formatted)
|
||||
@ -234,7 +237,9 @@ func (s *StderrSink) processLoop(ctx context.Context) {
|
||||
// Format and write
|
||||
formatted, err := s.formatter.Format(entry)
|
||||
if err != nil {
|
||||
s.logger.Error("msg", "Failed to format log entry for stderr", "error", err)
|
||||
s.logger.Error("msg", "Failed to format log entry for stderr",
|
||||
"component", "stderr_sink",
|
||||
"error", err)
|
||||
continue
|
||||
}
|
||||
s.output.Write(formatted)
|
||||
@ -245,4 +250,12 @@ func (s *StderrSink) processLoop(ctx context.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StdoutSink) SetAuth(auth *config.AuthConfig) {
|
||||
// Authentication does not apply to stdout sink
|
||||
}
|
||||
|
||||
func (s *StderrSink) SetAuth(auth *config.AuthConfig) {
|
||||
// Authentication does not apply to stderr sink
|
||||
}
|
||||
@ -4,6 +4,7 @@ package sink
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"logwisp/src/internal/config"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@ -43,7 +44,7 @@ func NewFileSink(options map[string]any, logger *log.Logger, formatter format.Fo
|
||||
writerConfig := log.DefaultConfig()
|
||||
writerConfig.Directory = directory
|
||||
writerConfig.Name = name
|
||||
writerConfig.EnableStdout = false // File only
|
||||
writerConfig.EnableConsole = false // File only
|
||||
writerConfig.ShowTimestamp = false // We already have timestamps in entries
|
||||
writerConfig.ShowLevel = false // We already have levels in entries
|
||||
|
||||
@ -164,4 +165,8 @@ func (fs *FileSink) processLoop(ctx context.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *FileSink) SetAuth(auth *config.AuthConfig) {
|
||||
// Authentication does not apply to file sink
|
||||
}
|
||||
@ -142,31 +142,28 @@ func NewHTTPSink(options map[string]any, logger *log.Logger, formatter format.Fo
|
||||
}
|
||||
|
||||
// Extract net limit config
|
||||
if rl, ok := options["net_limit"].(map[string]any); ok {
|
||||
if nl, ok := options["net_limit"].(map[string]any); ok {
|
||||
cfg.NetLimit = &config.NetLimitConfig{}
|
||||
cfg.NetLimit.Enabled, _ = rl["enabled"].(bool)
|
||||
if rps, ok := rl["requests_per_second"].(float64); ok {
|
||||
cfg.NetLimit.Enabled, _ = nl["enabled"].(bool)
|
||||
if rps, ok := nl["requests_per_second"].(float64); ok {
|
||||
cfg.NetLimit.RequestsPerSecond = rps
|
||||
}
|
||||
if burst, ok := rl["burst_size"].(int64); ok {
|
||||
if burst, ok := nl["burst_size"].(int64); ok {
|
||||
cfg.NetLimit.BurstSize = burst
|
||||
}
|
||||
if limitBy, ok := rl["limit_by"].(string); ok {
|
||||
cfg.NetLimit.LimitBy = limitBy
|
||||
}
|
||||
if respCode, ok := rl["response_code"].(int64); ok {
|
||||
if respCode, ok := nl["response_code"].(int64); ok {
|
||||
cfg.NetLimit.ResponseCode = respCode
|
||||
}
|
||||
if msg, ok := rl["response_message"].(string); ok {
|
||||
if msg, ok := nl["response_message"].(string); ok {
|
||||
cfg.NetLimit.ResponseMessage = msg
|
||||
}
|
||||
if maxPerIP, ok := rl["max_connections_per_ip"].(int64); ok {
|
||||
if maxPerIP, ok := nl["max_connections_per_ip"].(int64); ok {
|
||||
cfg.NetLimit.MaxConnectionsPerIP = maxPerIP
|
||||
}
|
||||
if maxTotal, ok := rl["max_total_connections"].(int64); ok {
|
||||
cfg.NetLimit.MaxTotalConnections = maxTotal
|
||||
if maxTotal, ok := nl["max_connections_total"].(int64); ok {
|
||||
cfg.NetLimit.MaxConnectionsTotal = maxTotal
|
||||
}
|
||||
if ipWhitelist, ok := rl["ip_whitelist"].([]any); ok {
|
||||
if ipWhitelist, ok := nl["ip_whitelist"].([]any); ok {
|
||||
cfg.NetLimit.IPWhitelist = make([]string, 0, len(ipWhitelist))
|
||||
for _, entry := range ipWhitelist {
|
||||
if str, ok := entry.(string); ok {
|
||||
@ -174,7 +171,7 @@ func NewHTTPSink(options map[string]any, logger *log.Logger, formatter format.Fo
|
||||
}
|
||||
}
|
||||
}
|
||||
if ipBlacklist, ok := rl["ip_blacklist"].([]any); ok {
|
||||
if ipBlacklist, ok := nl["ip_blacklist"].([]any); ok {
|
||||
cfg.NetLimit.IPBlacklist = make([]string, 0, len(ipBlacklist))
|
||||
for _, entry := range ipBlacklist {
|
||||
if str, ok := entry.(string); ok {
|
||||
@ -806,8 +803,8 @@ func (h *HTTPSink) GetHost() string {
|
||||
return h.config.Host
|
||||
}
|
||||
|
||||
// Configures http sink authentication
|
||||
func (h *HTTPSink) SetAuthConfig(authCfg *config.AuthConfig) {
|
||||
// Configures http sink auth
|
||||
func (h *HTTPSink) SetAuth(authCfg *config.AuthConfig) {
|
||||
if authCfg == nil || authCfg.Type == "none" {
|
||||
return
|
||||
}
|
||||
|
||||
@ -6,7 +6,10 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"logwisp/src/internal/auth"
|
||||
"logwisp/src/internal/config"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
@ -23,16 +26,17 @@ import (
|
||||
|
||||
// Forwards log entries to a remote HTTP endpoint
|
||||
type HTTPClientSink struct {
|
||||
input chan core.LogEntry
|
||||
config HTTPClientConfig
|
||||
client *fasthttp.Client
|
||||
batch []core.LogEntry
|
||||
batchMu sync.Mutex
|
||||
done chan struct{}
|
||||
wg sync.WaitGroup
|
||||
startTime time.Time
|
||||
logger *log.Logger
|
||||
formatter format.Formatter
|
||||
input chan core.LogEntry
|
||||
config HTTPClientConfig
|
||||
client *fasthttp.Client
|
||||
batch []core.LogEntry
|
||||
batchMu sync.Mutex
|
||||
done chan struct{}
|
||||
wg sync.WaitGroup
|
||||
startTime time.Time
|
||||
logger *log.Logger
|
||||
formatter format.Formatter
|
||||
authenticator *auth.Authenticator
|
||||
|
||||
// Statistics
|
||||
totalProcessed atomic.Uint64
|
||||
@ -44,7 +48,9 @@ type HTTPClientSink struct {
|
||||
}
|
||||
|
||||
// Holds HTTP client sink configuration
|
||||
// TODO: missing toml tags
|
||||
type HTTPClientConfig struct {
|
||||
// Config
|
||||
URL string
|
||||
BufferSize int64
|
||||
BatchSize int64
|
||||
@ -57,6 +63,10 @@ type HTTPClientConfig struct {
|
||||
RetryDelay time.Duration
|
||||
RetryBackoff float64 // Multiplier for exponential backoff
|
||||
|
||||
// Security
|
||||
Username string
|
||||
Password string
|
||||
|
||||
// TLS configuration
|
||||
InsecureSkipVerify bool
|
||||
CAFile string
|
||||
@ -118,6 +128,12 @@ func NewHTTPClientSink(options map[string]any, logger *log.Logger, formatter for
|
||||
if insecure, ok := options["insecure_skip_verify"].(bool); ok {
|
||||
cfg.InsecureSkipVerify = insecure
|
||||
}
|
||||
if username, ok := options["username"].(string); ok {
|
||||
cfg.Username = username
|
||||
}
|
||||
if password, ok := options["password"].(string); ok {
|
||||
cfg.Password = password // TODO: change to Argon2 hashed password
|
||||
}
|
||||
|
||||
// Extract headers
|
||||
if headers, ok := options["headers"].(map[string]any); ok {
|
||||
@ -422,8 +438,16 @@ func (h *HTTPClientSink) sendBatch(batch []core.LogEntry) {
|
||||
|
||||
req.SetRequestURI(h.config.URL)
|
||||
req.Header.SetMethod("POST")
|
||||
req.Header.SetContentType("application/json")
|
||||
req.SetBody(body)
|
||||
|
||||
// Add Basic Auth header if credentials configured
|
||||
if h.config.Username != "" && h.config.Password != "" {
|
||||
creds := h.config.Username + ":" + h.config.Password
|
||||
encodedCreds := base64.StdEncoding.EncodeToString([]byte(creds))
|
||||
req.Header.Set("Authorization", "Basic "+encodedCreds)
|
||||
}
|
||||
|
||||
// Set headers
|
||||
for k, v := range h.config.Headers {
|
||||
req.Header.Set(k, v)
|
||||
@ -494,4 +518,10 @@ func (h *HTTPClientSink) sendBatch(batch []core.LogEntry) {
|
||||
"retries", h.config.MaxRetries,
|
||||
"last_error", lastErr)
|
||||
h.failedBatches.Add(1)
|
||||
}
|
||||
|
||||
// Not applicable, Clients authenticate to remote servers using Username/Password in config
|
||||
func (h *HTTPClientSink) SetAuth(authCfg *config.AuthConfig) {
|
||||
// No-op: client sinks don't validate incoming connections
|
||||
// They authenticate to remote servers using Username/Password fields
|
||||
}
|
||||
@ -9,19 +9,22 @@ import (
|
||||
"logwisp/src/internal/core"
|
||||
)
|
||||
|
||||
// Represents an output destination for log entries
|
||||
// Represents an output data stream
|
||||
type Sink interface {
|
||||
// Input returns the channel for sending log entries to this sink
|
||||
// Returns the channel for sending log entries to this sink
|
||||
Input() chan<- core.LogEntry
|
||||
|
||||
// Start begins processing log entries
|
||||
// Begins processing log entries
|
||||
Start(ctx context.Context) error
|
||||
|
||||
// Stop gracefully shuts down the sink
|
||||
// Gracefully shuts down the sink
|
||||
Stop()
|
||||
|
||||
// GetStats returns sink statistics
|
||||
// Returns sink statistics
|
||||
GetStats() SinkStats
|
||||
|
||||
// Configure authentication
|
||||
SetAuth(auth *config.AuthConfig)
|
||||
}
|
||||
|
||||
// Contains statistics about a sink
|
||||
@ -32,9 +35,4 @@ type SinkStats struct {
|
||||
StartTime time.Time
|
||||
LastProcessed time.Time
|
||||
Details map[string]any
|
||||
}
|
||||
|
||||
// Interface for sinks that can accept an AuthConfig
|
||||
type AuthSetter interface {
|
||||
SetAuthConfig(auth *config.AuthConfig)
|
||||
}
|
||||
@ -17,7 +17,6 @@ import (
|
||||
"logwisp/src/internal/core"
|
||||
"logwisp/src/internal/format"
|
||||
"logwisp/src/internal/limit"
|
||||
"logwisp/src/internal/tls"
|
||||
|
||||
"github.com/lixenwraith/log"
|
||||
"github.com/lixenwraith/log/compat"
|
||||
@ -26,23 +25,20 @@ import (
|
||||
|
||||
// Streams log entries via TCP
|
||||
type TCPSink struct {
|
||||
input chan core.LogEntry
|
||||
config TCPConfig
|
||||
server *tcpServer
|
||||
done chan struct{}
|
||||
activeConns atomic.Int64
|
||||
startTime time.Time
|
||||
engine *gnet.Engine
|
||||
engineMu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
netLimiter *limit.NetLimiter
|
||||
logger *log.Logger
|
||||
formatter format.Formatter
|
||||
|
||||
// Security components
|
||||
// C
|
||||
input chan core.LogEntry
|
||||
config TCPConfig
|
||||
server *tcpServer
|
||||
done chan struct{}
|
||||
activeConns atomic.Int64
|
||||
startTime time.Time
|
||||
engine *gnet.Engine
|
||||
engineMu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
netLimiter *limit.NetLimiter
|
||||
logger *log.Logger
|
||||
formatter format.Formatter
|
||||
authenticator *auth.Authenticator
|
||||
tlsManager *tls.Manager
|
||||
authConfig *config.AuthConfig
|
||||
|
||||
// Statistics
|
||||
totalProcessed atomic.Uint64
|
||||
@ -62,7 +58,6 @@ type TCPConfig struct {
|
||||
Port int64
|
||||
BufferSize int64
|
||||
Heartbeat *config.HeartbeatConfig
|
||||
TLS *config.TLSConfig
|
||||
NetLimit *config.NetLimitConfig
|
||||
}
|
||||
|
||||
@ -99,58 +94,35 @@ func NewTCPSink(options map[string]any, logger *log.Logger, formatter format.For
|
||||
}
|
||||
}
|
||||
|
||||
// Extract TLS config
|
||||
if tc, ok := options["tls"].(map[string]any); ok {
|
||||
cfg.TLS = &config.TLSConfig{}
|
||||
cfg.TLS.Enabled, _ = tc["enabled"].(bool)
|
||||
if certFile, ok := tc["cert_file"].(string); ok {
|
||||
cfg.TLS.CertFile = certFile
|
||||
}
|
||||
if keyFile, ok := tc["key_file"].(string); ok {
|
||||
cfg.TLS.KeyFile = keyFile
|
||||
}
|
||||
cfg.TLS.ClientAuth, _ = tc["client_auth"].(bool)
|
||||
if caFile, ok := tc["client_ca_file"].(string); ok {
|
||||
cfg.TLS.ClientCAFile = caFile
|
||||
}
|
||||
cfg.TLS.VerifyClientCert, _ = tc["verify_client_cert"].(bool)
|
||||
if minVer, ok := tc["min_version"].(string); ok {
|
||||
cfg.TLS.MinVersion = minVer
|
||||
}
|
||||
if maxVer, ok := tc["max_version"].(string); ok {
|
||||
cfg.TLS.MaxVersion = maxVer
|
||||
}
|
||||
if ciphers, ok := tc["cipher_suites"].(string); ok {
|
||||
cfg.TLS.CipherSuites = ciphers
|
||||
}
|
||||
}
|
||||
|
||||
// Extract net limit config
|
||||
if rl, ok := options["net_limit"].(map[string]any); ok {
|
||||
if nl, ok := options["net_limit"].(map[string]any); ok {
|
||||
cfg.NetLimit = &config.NetLimitConfig{}
|
||||
cfg.NetLimit.Enabled, _ = rl["enabled"].(bool)
|
||||
if rps, ok := rl["requests_per_second"].(float64); ok {
|
||||
cfg.NetLimit.Enabled, _ = nl["enabled"].(bool)
|
||||
if rps, ok := nl["requests_per_second"].(float64); ok {
|
||||
cfg.NetLimit.RequestsPerSecond = rps
|
||||
}
|
||||
if burst, ok := rl["burst_size"].(int64); ok {
|
||||
if burst, ok := nl["burst_size"].(int64); ok {
|
||||
cfg.NetLimit.BurstSize = burst
|
||||
}
|
||||
if limitBy, ok := rl["limit_by"].(string); ok {
|
||||
cfg.NetLimit.LimitBy = limitBy
|
||||
}
|
||||
if respCode, ok := rl["response_code"].(int64); ok {
|
||||
if respCode, ok := nl["response_code"].(int64); ok {
|
||||
cfg.NetLimit.ResponseCode = respCode
|
||||
}
|
||||
if msg, ok := rl["response_message"].(string); ok {
|
||||
if msg, ok := nl["response_message"].(string); ok {
|
||||
cfg.NetLimit.ResponseMessage = msg
|
||||
}
|
||||
if maxPerIP, ok := rl["max_connections_per_ip"].(int64); ok {
|
||||
if maxPerIP, ok := nl["max_connections_per_ip"].(int64); ok {
|
||||
cfg.NetLimit.MaxConnectionsPerIP = maxPerIP
|
||||
}
|
||||
if maxTotal, ok := rl["max_total_connections"].(int64); ok {
|
||||
cfg.NetLimit.MaxTotalConnections = maxTotal
|
||||
if maxPerUser, ok := nl["max_connections_per_user"].(int64); ok {
|
||||
cfg.NetLimit.MaxConnectionsPerUser = maxPerUser
|
||||
}
|
||||
if ipWhitelist, ok := rl["ip_whitelist"].([]any); ok {
|
||||
if maxPerToken, ok := nl["max_connections_per_token"].(int64); ok {
|
||||
cfg.NetLimit.MaxConnectionsPerToken = maxPerToken
|
||||
}
|
||||
if maxTotal, ok := nl["max_connections_total"].(int64); ok {
|
||||
cfg.NetLimit.MaxConnectionsTotal = maxTotal
|
||||
}
|
||||
if ipWhitelist, ok := nl["ip_whitelist"].([]any); ok {
|
||||
cfg.NetLimit.IPWhitelist = make([]string, 0, len(ipWhitelist))
|
||||
for _, entry := range ipWhitelist {
|
||||
if str, ok := entry.(string); ok {
|
||||
@ -158,7 +130,7 @@ func NewTCPSink(options map[string]any, logger *log.Logger, formatter format.For
|
||||
}
|
||||
}
|
||||
}
|
||||
if ipBlacklist, ok := rl["ip_blacklist"].([]any); ok {
|
||||
if ipBlacklist, ok := nl["ip_blacklist"].([]any); ok {
|
||||
cfg.NetLimit.IPBlacklist = make([]string, 0, len(ipBlacklist))
|
||||
for _, entry := range ipBlacklist {
|
||||
if str, ok := entry.(string); ok {
|
||||
@ -290,18 +262,6 @@ func (t *TCPSink) GetStats() SinkStats {
|
||||
netLimitStats = t.netLimiter.GetStats()
|
||||
}
|
||||
|
||||
var authStats map[string]any
|
||||
if t.authenticator != nil {
|
||||
authStats = t.authenticator.GetStats()
|
||||
authStats["failures"] = t.authFailures.Load()
|
||||
authStats["successes"] = t.authSuccesses.Load()
|
||||
}
|
||||
|
||||
var tlsStats map[string]any
|
||||
if t.tlsManager != nil {
|
||||
tlsStats = t.tlsManager.GetStats()
|
||||
}
|
||||
|
||||
return SinkStats{
|
||||
Type: "tcp",
|
||||
TotalProcessed: t.totalProcessed.Load(),
|
||||
@ -312,8 +272,7 @@ func (t *TCPSink) GetStats() SinkStats {
|
||||
"port": t.config.Port,
|
||||
"buffer_size": t.config.BufferSize,
|
||||
"net_limit": netLimitStats,
|
||||
"auth": authStats,
|
||||
"tls": tlsStats,
|
||||
"auth": map[string]any{"enabled": false},
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -347,37 +306,7 @@ func (t *TCPSink) broadcastLoop(ctx context.Context) {
|
||||
"entry_source", entry.Source)
|
||||
continue
|
||||
}
|
||||
|
||||
// Broadcast only to authenticated clients
|
||||
t.server.mu.RLock()
|
||||
for conn, client := range t.server.clients {
|
||||
if client.authenticated {
|
||||
// Send through TLS bridge if present
|
||||
if client.tlsBridge != nil {
|
||||
if _, err := client.tlsBridge.Write(data); err != nil {
|
||||
// TLS write failed, connection likely dead
|
||||
t.logger.Debug("msg", "TLS write failed",
|
||||
"component", "tcp_sink",
|
||||
"error", err)
|
||||
conn.Close()
|
||||
}
|
||||
} else {
|
||||
conn.AsyncWrite(data, func(c gnet.Conn, err error) error {
|
||||
if err != nil {
|
||||
t.writeErrors.Add(1)
|
||||
t.handleWriteError(c, err)
|
||||
} else {
|
||||
// Reset consecutive error count on success
|
||||
t.errorMu.Lock()
|
||||
delete(t.consecutiveWriteErrors, c)
|
||||
t.errorMu.Unlock()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
t.server.mu.RUnlock()
|
||||
t.broadcastData(data)
|
||||
|
||||
case <-tickerChan:
|
||||
heartbeatEntry := t.createHeartbeatEntry()
|
||||
@ -388,37 +317,7 @@ func (t *TCPSink) broadcastLoop(ctx context.Context) {
|
||||
"error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
t.server.mu.RLock()
|
||||
for conn, client := range t.server.clients {
|
||||
if client.authenticated {
|
||||
// Validate session is still active
|
||||
if t.authenticator != nil && client.session != nil {
|
||||
if !t.authenticator.ValidateSession(client.session.ID) {
|
||||
// Session expired, close connection
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if client.tlsBridge != nil {
|
||||
if _, err := client.tlsBridge.Write(data); err != nil {
|
||||
t.logger.Debug("msg", "TLS heartbeat write failed",
|
||||
"component", "tcp_sink",
|
||||
"error", err)
|
||||
conn.Close()
|
||||
}
|
||||
} else {
|
||||
conn.AsyncWrite(data, func(c gnet.Conn, err error) error {
|
||||
if err != nil {
|
||||
t.writeErrors.Add(1)
|
||||
t.handleWriteError(c, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
t.server.mu.RUnlock()
|
||||
t.broadcastData(data)
|
||||
|
||||
case <-t.done:
|
||||
return
|
||||
@ -426,6 +325,28 @@ func (t *TCPSink) broadcastLoop(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TCPSink) broadcastData(data []byte) {
|
||||
t.server.mu.RLock()
|
||||
defer t.server.mu.RUnlock()
|
||||
|
||||
for conn, client := range t.server.clients {
|
||||
if client.authenticated {
|
||||
conn.AsyncWrite(data, func(c gnet.Conn, err error) error {
|
||||
if err != nil {
|
||||
t.writeErrors.Add(1)
|
||||
t.handleWriteError(c, err)
|
||||
} else {
|
||||
// Reset consecutive error count on success
|
||||
t.errorMu.Lock()
|
||||
delete(t.consecutiveWriteErrors, c)
|
||||
t.errorMu.Unlock()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle write errors with threshold-based connection termination
|
||||
func (t *TCPSink) handleWriteError(c gnet.Conn, err error) {
|
||||
t.errorMu.Lock()
|
||||
@ -487,13 +408,11 @@ func (t *TCPSink) GetActiveConnections() int64 {
|
||||
|
||||
// Represents a connected TCP client with auth state
|
||||
type tcpClient struct {
|
||||
conn gnet.Conn
|
||||
buffer bytes.Buffer
|
||||
authenticated bool
|
||||
session *auth.Session
|
||||
authTimeout time.Time
|
||||
tlsBridge *tls.GNetTLSConn
|
||||
authTimeoutSet bool
|
||||
conn gnet.Conn
|
||||
buffer bytes.Buffer
|
||||
authenticated bool
|
||||
authTimeout time.Time
|
||||
session *auth.Session
|
||||
}
|
||||
|
||||
// Handles gnet events with authentication
|
||||
@ -550,24 +469,12 @@ func (s *tcpServer) OnOpen(c gnet.Conn) (out []byte, action gnet.Action) {
|
||||
|
||||
// Create client state without auth timeout initially
|
||||
client := &tcpClient{
|
||||
conn: c,
|
||||
authenticated: s.sink.authenticator == nil, // No auth = auto authenticated
|
||||
authTimeoutSet: false, // Auth timeout not started yet
|
||||
conn: c,
|
||||
authenticated: s.sink.authenticator == nil,
|
||||
}
|
||||
|
||||
// Initialize TLS bridge if enabled
|
||||
if s.sink.tlsManager != nil {
|
||||
tlsConfig := s.sink.tlsManager.GetTCPConfig()
|
||||
client.tlsBridge = tls.NewServerConn(c, tlsConfig)
|
||||
client.tlsBridge.Handshake() // Start async handshake
|
||||
|
||||
s.sink.logger.Debug("msg", "TLS handshake initiated",
|
||||
"component", "tcp_sink",
|
||||
"remote_addr", remoteAddr)
|
||||
} else if s.sink.authenticator != nil {
|
||||
// Only set auth timeout if no TLS (plain connection)
|
||||
client.authTimeout = time.Now().Add(30 * time.Second) // TODO: configurable or non-hardcoded timer
|
||||
client.authTimeoutSet = true
|
||||
if s.sink.authenticator != nil {
|
||||
client.authTimeout = time.Now().Add(30 * time.Second)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
@ -578,12 +485,11 @@ func (s *tcpServer) OnOpen(c gnet.Conn) (out []byte, action gnet.Action) {
|
||||
s.sink.logger.Debug("msg", "TCP connection opened",
|
||||
"remote_addr", remoteAddr,
|
||||
"active_connections", newCount,
|
||||
"requires_auth", s.sink.authenticator != nil)
|
||||
"auth_enabled", s.sink.authenticator != nil)
|
||||
|
||||
// Send auth prompt if authentication is required
|
||||
if s.sink.authenticator != nil && s.sink.tlsManager == nil {
|
||||
authPrompt := []byte("AUTH REQUIRED\nFormat: AUTH <method> <credentials>\nMethods: basic, token\n")
|
||||
return authPrompt, gnet.None
|
||||
if s.sink.authenticator != nil {
|
||||
return []byte("AUTH_REQUIRED\n"), gnet.None
|
||||
}
|
||||
|
||||
return nil, gnet.None
|
||||
@ -594,17 +500,9 @@ func (s *tcpServer) OnClose(c gnet.Conn, err error) gnet.Action {
|
||||
|
||||
// Remove client state
|
||||
s.mu.Lock()
|
||||
client := s.clients[c]
|
||||
delete(s.clients, c)
|
||||
s.mu.Unlock()
|
||||
|
||||
// Clean up TLS bridge if present
|
||||
if client != nil && client.tlsBridge != nil {
|
||||
client.tlsBridge.Close()
|
||||
s.sink.logger.Debug("msg", "TLS connection closed",
|
||||
"remote_addr", remoteAddr)
|
||||
}
|
||||
|
||||
// Clean up write error tracking
|
||||
s.sink.errorMu.Lock()
|
||||
delete(s.sink.consecutiveWriteErrors, c)
|
||||
@ -632,98 +530,34 @@ func (s *tcpServer) OnTraffic(c gnet.Conn) gnet.Action {
|
||||
return gnet.Close
|
||||
}
|
||||
|
||||
// Read all available data
|
||||
data, err := c.Next(-1)
|
||||
if err != nil {
|
||||
s.sink.logger.Error("msg", "Error reading from connection",
|
||||
"component", "tcp_sink",
|
||||
"error", err)
|
||||
return gnet.Close
|
||||
}
|
||||
|
||||
// Process through TLS bridge if present
|
||||
if client.tlsBridge != nil {
|
||||
// Feed encrypted data into TLS engine
|
||||
if err := client.tlsBridge.ProcessIncoming(data); err != nil {
|
||||
s.sink.logger.Error("msg", "TLS processing error",
|
||||
"component", "tcp_sink",
|
||||
"remote_addr", c.RemoteAddr().String(),
|
||||
"error", err)
|
||||
return gnet.Close
|
||||
}
|
||||
|
||||
// Check if handshake is complete
|
||||
if !client.tlsBridge.IsHandshakeDone() {
|
||||
// Still handshaking, wait for more data
|
||||
return gnet.None
|
||||
}
|
||||
|
||||
// Check handshake result
|
||||
_, hsErr := client.tlsBridge.HandshakeComplete()
|
||||
if hsErr != nil {
|
||||
s.sink.logger.Error("msg", "TLS handshake failed",
|
||||
"component", "tcp_sink",
|
||||
"remote_addr", c.RemoteAddr().String(),
|
||||
"error", hsErr)
|
||||
return gnet.Close
|
||||
}
|
||||
|
||||
// Set auth timeout only after TLS handshake completes
|
||||
if !client.authTimeoutSet && s.sink.authenticator != nil && !client.authenticated {
|
||||
client.authTimeout = time.Now().Add(30 * time.Second)
|
||||
client.authTimeoutSet = true
|
||||
s.sink.logger.Debug("msg", "Auth timeout started after TLS handshake",
|
||||
// Authentication phase
|
||||
if !client.authenticated {
|
||||
// Check auth timeout
|
||||
if time.Now().After(client.authTimeout) {
|
||||
s.sink.logger.Warn("msg", "Authentication timeout",
|
||||
"component", "tcp_sink",
|
||||
"remote_addr", c.RemoteAddr().String())
|
||||
return gnet.Close
|
||||
}
|
||||
|
||||
// Read decrypted plaintext
|
||||
data = client.tlsBridge.Read()
|
||||
if data == nil || len(data) == 0 {
|
||||
// No plaintext available yet
|
||||
// Read auth data
|
||||
data, _ := c.Next(-1)
|
||||
if len(data) == 0 {
|
||||
return gnet.None
|
||||
}
|
||||
|
||||
// First data after TLS handshake - send auth prompt if needed
|
||||
if s.sink.authenticator != nil && !client.authenticated &&
|
||||
len(client.buffer.Bytes()) == 0 {
|
||||
authPrompt := []byte("AUTH REQUIRED\n")
|
||||
client.tlsBridge.Write(authPrompt)
|
||||
}
|
||||
}
|
||||
|
||||
// Only check auth timeout if it has been set
|
||||
if !client.authenticated && client.authTimeoutSet && time.Now().After(client.authTimeout) {
|
||||
s.sink.logger.Warn("msg", "Authentication timeout",
|
||||
"component", "tcp_sink",
|
||||
"remote_addr", c.RemoteAddr().String())
|
||||
if client.tlsBridge != nil && client.tlsBridge.IsHandshakeDone() {
|
||||
client.tlsBridge.Write([]byte("AUTH TIMEOUT\n"))
|
||||
} else if client.tlsBridge == nil {
|
||||
c.AsyncWrite([]byte("AUTH TIMEOUT\n"), nil)
|
||||
}
|
||||
return gnet.Close
|
||||
}
|
||||
|
||||
// If not authenticated, expect auth command
|
||||
if !client.authenticated {
|
||||
client.buffer.Write(data)
|
||||
|
||||
// Look for complete auth line
|
||||
if line, err := client.buffer.ReadBytes('\n'); err == nil {
|
||||
line = bytes.TrimSpace(line)
|
||||
if idx := bytes.IndexByte(client.buffer.Bytes(), '\n'); idx >= 0 {
|
||||
line := client.buffer.Bytes()[:idx]
|
||||
client.buffer.Next(idx + 1)
|
||||
|
||||
// Parse AUTH command: AUTH <method> <credentials>
|
||||
parts := strings.SplitN(string(line), " ", 3)
|
||||
if len(parts) != 3 || parts[0] != "AUTH" {
|
||||
// Send error through TLS if enabled
|
||||
errMsg := []byte("AUTH FAILED\n")
|
||||
if client.tlsBridge != nil {
|
||||
client.tlsBridge.Write(errMsg)
|
||||
} else {
|
||||
c.AsyncWrite(errMsg, nil)
|
||||
}
|
||||
return gnet.None
|
||||
c.AsyncWrite([]byte("AUTH_FAIL\n"), nil)
|
||||
return gnet.Close
|
||||
}
|
||||
|
||||
// Authenticate
|
||||
@ -734,13 +568,7 @@ func (s *tcpServer) OnTraffic(c gnet.Conn) gnet.Action {
|
||||
"remote_addr", c.RemoteAddr().String(),
|
||||
"method", parts[1],
|
||||
"error", err)
|
||||
// Send error through TLS if enabled
|
||||
errMsg := []byte("AUTH FAILED\n")
|
||||
if client.tlsBridge != nil {
|
||||
client.tlsBridge.Write(errMsg)
|
||||
} else {
|
||||
c.AsyncWrite(errMsg, nil)
|
||||
}
|
||||
c.AsyncWrite([]byte("AUTH_FAIL\n"), nil)
|
||||
return gnet.Close
|
||||
}
|
||||
|
||||
@ -755,35 +583,25 @@ func (s *tcpServer) OnTraffic(c gnet.Conn) gnet.Action {
|
||||
"component", "tcp_sink",
|
||||
"remote_addr", c.RemoteAddr().String(),
|
||||
"username", session.Username,
|
||||
"method", session.Method,
|
||||
"tls", client.tlsBridge != nil)
|
||||
"method", session.Method)
|
||||
|
||||
// Send success through TLS if enabled
|
||||
successMsg := []byte("AUTH OK\n")
|
||||
if client.tlsBridge != nil {
|
||||
client.tlsBridge.Write(successMsg)
|
||||
} else {
|
||||
c.AsyncWrite(successMsg, nil)
|
||||
}
|
||||
|
||||
// Clear buffer after auth
|
||||
c.AsyncWrite([]byte("AUTH_OK\n"), nil)
|
||||
client.buffer.Reset()
|
||||
}
|
||||
return gnet.None
|
||||
}
|
||||
|
||||
// Authenticated clients shouldn't send data, just discard
|
||||
// Clients shouldn't send data, just discard
|
||||
c.Discard(-1)
|
||||
return gnet.None
|
||||
}
|
||||
|
||||
// Configures tcp sink authentication
|
||||
func (t *TCPSink) SetAuthConfig(authCfg *config.AuthConfig) {
|
||||
// Configures tcp sink auth
|
||||
func (t *TCPSink) SetAuth(authCfg *config.AuthConfig) {
|
||||
if authCfg == nil || authCfg.Type == "none" {
|
||||
return
|
||||
}
|
||||
|
||||
t.authConfig = authCfg
|
||||
authenticator, err := auth.New(authCfg, t.logger)
|
||||
if err != nil {
|
||||
t.logger.Error("msg", "Failed to initialize authenticator for TCP sink",
|
||||
@ -793,22 +611,7 @@ func (t *TCPSink) SetAuthConfig(authCfg *config.AuthConfig) {
|
||||
}
|
||||
t.authenticator = authenticator
|
||||
|
||||
// Initialize TLS manager if TLS is configured
|
||||
if t.config.TLS != nil && t.config.TLS.Enabled {
|
||||
tlsManager, err := tls.NewManager(t.config.TLS, t.logger)
|
||||
if err != nil {
|
||||
t.logger.Error("msg", "Failed to create TLS manager",
|
||||
"component", "tcp_sink",
|
||||
"error", err)
|
||||
// Continue without TLS
|
||||
return
|
||||
}
|
||||
t.tlsManager = tlsManager
|
||||
}
|
||||
|
||||
t.logger.Info("msg", "Authentication configured for TCP sink",
|
||||
"component", "tcp_sink",
|
||||
"auth_type", authCfg.Type,
|
||||
"tls_enabled", t.tlsManager != nil,
|
||||
"tls_bridge", t.tlsManager != nil)
|
||||
"auth_type", authCfg.Type)
|
||||
}
|
||||
@ -2,41 +2,37 @@
|
||||
package sink
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"logwisp/src/internal/auth"
|
||||
"logwisp/src/internal/config"
|
||||
"logwisp/src/internal/core"
|
||||
"logwisp/src/internal/format"
|
||||
tlspkg "logwisp/src/internal/tls"
|
||||
|
||||
"github.com/lixenwraith/log"
|
||||
)
|
||||
|
||||
// Forwards log entries to a remote TCP endpoint
|
||||
type TCPClientSink struct {
|
||||
input chan core.LogEntry
|
||||
config TCPClientConfig
|
||||
conn net.Conn
|
||||
connMu sync.RWMutex
|
||||
done chan struct{}
|
||||
wg sync.WaitGroup
|
||||
startTime time.Time
|
||||
logger *log.Logger
|
||||
formatter format.Formatter
|
||||
|
||||
// TLS support
|
||||
tlsManager *tlspkg.Manager
|
||||
tlsConfig *tls.Config
|
||||
input chan core.LogEntry
|
||||
config TCPClientConfig
|
||||
conn net.Conn
|
||||
connMu sync.RWMutex
|
||||
done chan struct{}
|
||||
wg sync.WaitGroup
|
||||
startTime time.Time
|
||||
logger *log.Logger
|
||||
formatter format.Formatter
|
||||
authenticator *auth.Authenticator
|
||||
|
||||
// Reconnection state
|
||||
reconnecting atomic.Bool
|
||||
@ -60,6 +56,10 @@ type TCPClientConfig struct {
|
||||
ReadTimeout time.Duration
|
||||
KeepAlive time.Duration
|
||||
|
||||
// Security
|
||||
Username string
|
||||
Password string
|
||||
|
||||
// Reconnection settings
|
||||
ReconnectDelay time.Duration
|
||||
MaxReconnectDelay time.Duration
|
||||
@ -120,27 +120,11 @@ func NewTCPClientSink(options map[string]any, logger *log.Logger, formatter form
|
||||
if backoff, ok := options["reconnect_backoff"].(float64); ok && backoff >= 1.0 {
|
||||
cfg.ReconnectBackoff = backoff
|
||||
}
|
||||
|
||||
// Extract TLS config
|
||||
if tc, ok := options["tls"].(map[string]any); ok {
|
||||
cfg.TLS = &config.TLSConfig{}
|
||||
cfg.TLS.Enabled, _ = tc["enabled"].(bool)
|
||||
if certFile, ok := tc["cert_file"].(string); ok {
|
||||
cfg.TLS.CertFile = certFile
|
||||
}
|
||||
if keyFile, ok := tc["key_file"].(string); ok {
|
||||
cfg.TLS.KeyFile = keyFile
|
||||
}
|
||||
cfg.TLS.ClientAuth, _ = tc["client_auth"].(bool)
|
||||
if caFile, ok := tc["client_ca_file"].(string); ok {
|
||||
cfg.TLS.ClientCAFile = caFile
|
||||
}
|
||||
if insecure, ok := tc["insecure_skip_verify"].(bool); ok {
|
||||
cfg.TLS.InsecureSkipVerify = insecure
|
||||
}
|
||||
if caFile, ok := tc["ca_file"].(string); ok {
|
||||
cfg.TLS.CAFile = caFile
|
||||
}
|
||||
if username, ok := options["username"].(string); ok {
|
||||
cfg.Username = username
|
||||
}
|
||||
if password, ok := options["password"].(string); ok {
|
||||
cfg.Password = password
|
||||
}
|
||||
|
||||
t := &TCPClientSink{
|
||||
@ -154,62 +138,6 @@ func NewTCPClientSink(options map[string]any, logger *log.Logger, formatter form
|
||||
t.lastProcessed.Store(time.Time{})
|
||||
t.connectionUptime.Store(time.Duration(0))
|
||||
|
||||
// Initialize TLS manager if TLS is configured
|
||||
if cfg.TLS != nil && cfg.TLS.Enabled {
|
||||
// Build custom TLS config for client
|
||||
t.tlsConfig = &tls.Config{
|
||||
InsecureSkipVerify: cfg.TLS.InsecureSkipVerify,
|
||||
}
|
||||
|
||||
// Extract server name from address for SNI
|
||||
host, _, err := net.SplitHostPort(cfg.Address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse address for SNI: %w", err)
|
||||
}
|
||||
t.tlsConfig.ServerName = host
|
||||
|
||||
// Load custom CA for server verification
|
||||
if cfg.TLS.CAFile != "" {
|
||||
caCert, err := os.ReadFile(cfg.TLS.CAFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read CA file '%s': %w", cfg.TLS.CAFile, err)
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
if !caCertPool.AppendCertsFromPEM(caCert) {
|
||||
return nil, fmt.Errorf("failed to parse CA certificate from '%s'", cfg.TLS.CAFile)
|
||||
}
|
||||
t.tlsConfig.RootCAs = caCertPool
|
||||
logger.Debug("msg", "Custom CA loaded for server verification",
|
||||
"component", "tcp_client_sink",
|
||||
"ca_file", cfg.TLS.CAFile)
|
||||
}
|
||||
|
||||
// Load client certificate for mTLS
|
||||
if cfg.TLS.CertFile != "" && cfg.TLS.KeyFile != "" {
|
||||
cert, err := tls.LoadX509KeyPair(cfg.TLS.CertFile, cfg.TLS.KeyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load client certificate: %w", err)
|
||||
}
|
||||
t.tlsConfig.Certificates = []tls.Certificate{cert}
|
||||
logger.Info("msg", "Client certificate loaded for mTLS",
|
||||
"component", "tcp_client_sink",
|
||||
"cert_file", cfg.TLS.CertFile)
|
||||
}
|
||||
|
||||
// Set minimum TLS version if configured
|
||||
if cfg.TLS.MinVersion != "" {
|
||||
t.tlsConfig.MinVersion = parseTLSVersion(cfg.TLS.MinVersion, tls.VersionTLS12)
|
||||
} else {
|
||||
t.tlsConfig.MinVersion = tls.VersionTLS12 // Default minimum
|
||||
}
|
||||
|
||||
logger.Info("msg", "TLS enabled for TCP client",
|
||||
"component", "tcp_client_sink",
|
||||
"address", cfg.Address,
|
||||
"server_name", host,
|
||||
"insecure", cfg.TLS.InsecureSkipVerify,
|
||||
"mtls", cfg.TLS.CertFile != "")
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
@ -376,33 +304,44 @@ func (t *TCPClientSink) connect() (net.Conn, error) {
|
||||
tcpConn.SetKeepAlivePeriod(t.config.KeepAlive)
|
||||
}
|
||||
|
||||
// Wrap with TLS if configured
|
||||
if t.tlsConfig != nil {
|
||||
t.logger.Debug("msg", "Initiating TLS handshake",
|
||||
"component", "tcp_client_sink",
|
||||
"address", t.config.Address)
|
||||
|
||||
tlsConn := tls.Client(conn, t.tlsConfig)
|
||||
|
||||
// Perform handshake with timeout
|
||||
handshakeCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := tlsConn.HandshakeContext(handshakeCtx); err != nil {
|
||||
// Handle authentication if credentials configured
|
||||
if t.config.Username != "" && t.config.Password != "" {
|
||||
// Read auth challenge
|
||||
reader := bufio.NewReader(conn)
|
||||
challenge, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("TLS handshake failed: %w", err)
|
||||
return nil, fmt.Errorf("failed to read auth challenge: %w", err)
|
||||
}
|
||||
|
||||
// Log connection details
|
||||
state := tlsConn.ConnectionState()
|
||||
t.logger.Info("msg", "TLS connection established",
|
||||
"component", "tcp_client_sink",
|
||||
"address", t.config.Address,
|
||||
"tls_version", tlsVersionString(state.Version),
|
||||
"cipher_suite", tls.CipherSuiteName(state.CipherSuite),
|
||||
"server_name", state.ServerName)
|
||||
if strings.TrimSpace(challenge) == "AUTH_REQUIRED" {
|
||||
// Send credentials
|
||||
creds := t.config.Username + ":" + t.config.Password
|
||||
encodedCreds := base64.StdEncoding.EncodeToString([]byte(creds))
|
||||
authCmd := fmt.Sprintf("AUTH basic %s\n", encodedCreds)
|
||||
|
||||
return tlsConn, nil
|
||||
if _, err := conn.Write([]byte(authCmd)); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to send auth: %w", err)
|
||||
}
|
||||
|
||||
// Read response
|
||||
response, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to read auth response: %w", err)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(response) != "AUTH_OK" {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("authentication failed: %s", response)
|
||||
}
|
||||
|
||||
t.logger.Debug("msg", "TCP authentication successful",
|
||||
"component", "tcp_client_sink",
|
||||
"address", t.config.Address,
|
||||
"username", t.config.Username)
|
||||
}
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
@ -504,34 +443,8 @@ func (t *TCPClientSink) sendEntry(entry core.LogEntry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns human-readable TLS version
|
||||
func tlsVersionString(version uint16) string {
|
||||
switch version {
|
||||
case tls.VersionTLS10:
|
||||
return "TLS1.0"
|
||||
case tls.VersionTLS11:
|
||||
return "TLS1.1"
|
||||
case tls.VersionTLS12:
|
||||
return "TLS1.2"
|
||||
case tls.VersionTLS13:
|
||||
return "TLS1.3"
|
||||
default:
|
||||
return fmt.Sprintf("0x%04x", version)
|
||||
}
|
||||
}
|
||||
|
||||
// Converts string to TLS version constant
|
||||
func parseTLSVersion(version string, defaultVersion uint16) uint16 {
|
||||
switch strings.ToUpper(version) {
|
||||
case "TLS1.0", "TLS10":
|
||||
return tls.VersionTLS10
|
||||
case "TLS1.1", "TLS11":
|
||||
return tls.VersionTLS11
|
||||
case "TLS1.2", "TLS12":
|
||||
return tls.VersionTLS12
|
||||
case "TLS1.3", "TLS13":
|
||||
return tls.VersionTLS13
|
||||
default:
|
||||
return defaultVersion
|
||||
}
|
||||
// Not applicable, Clients authenticate to remote servers using Username/Password in config
|
||||
func (h *TCPClientSink) SetAuth(authCfg *config.AuthConfig) {
|
||||
// No-op: client sinks don't validate incoming connections
|
||||
// They authenticate to remote servers using Username/Password fields
|
||||
}
|
||||
Reference in New Issue
Block a user