- Created a new Go module named 'teleport' for secure port forwarding. - Added essential files including .gitignore, LICENSE, and README.md with project details. - Implemented configuration management with YAML support in config package. - Developed core client and server functionalities for handling port forwarding. - Introduced DNS server capabilities and integrated logging with sanitization. - Established rate limiting and metrics tracking for performance monitoring. - Included comprehensive tests for core components and functionalities. - Set up CI workflows for automated testing and release management using Gitea actions.
270 lines
6.9 KiB
Go
270 lines
6.9 KiB
Go
package logger
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sync"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
var (
|
|
Log *logrus.Logger
|
|
once sync.Once
|
|
mu sync.RWMutex
|
|
)
|
|
|
|
// Config holds logging configuration
|
|
type Config struct {
|
|
Level string `yaml:"level"` // debug, info, warn, error
|
|
Format string `yaml:"format"` // json, text
|
|
File string `yaml:"file"` // log file path (empty for stdout)
|
|
MaxSize int `yaml:"max_size"` // max log file size in MB
|
|
MaxBackups int `yaml:"max_backups"` // max number of backup files
|
|
MaxAge int `yaml:"max_age"` // max age of backup files in days
|
|
Compress bool `yaml:"compress"` // compress backup files
|
|
}
|
|
|
|
// Init initializes the global logger with the given configuration
|
|
func Init(config Config) error {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
Log = logrus.New()
|
|
|
|
// Set log level
|
|
level, err := logrus.ParseLevel(config.Level)
|
|
if err != nil {
|
|
level = logrus.InfoLevel
|
|
}
|
|
Log.SetLevel(level)
|
|
|
|
// Set log format with sanitization
|
|
switch config.Format {
|
|
case "json":
|
|
Log.SetFormatter(&SanitizedJSONFormatter{
|
|
TimestampFormat: "2006-01-02 15:04:05",
|
|
})
|
|
default:
|
|
Log.SetFormatter(&SanitizedTextFormatter{
|
|
FullTimestamp: true,
|
|
TimestampFormat: "2006-01-02 15:04:05",
|
|
})
|
|
}
|
|
|
|
// Set output
|
|
if config.File != "" {
|
|
// Ensure directory exists
|
|
dir := filepath.Dir(config.File)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Open log file with secure permissions (owner read/write only)
|
|
file, err := os.OpenFile(config.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set output to both file and stdout
|
|
Log.SetOutput(io.MultiWriter(file, os.Stdout))
|
|
} else {
|
|
Log.SetOutput(os.Stdout)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetLogger returns the global logger instance
|
|
func GetLogger() *logrus.Logger {
|
|
mu.RLock()
|
|
if Log != nil {
|
|
mu.RUnlock()
|
|
return Log
|
|
}
|
|
mu.RUnlock()
|
|
|
|
// Initialize with default config if not already initialized
|
|
once.Do(func() {
|
|
Init(Config{
|
|
Level: "info",
|
|
Format: "text",
|
|
File: "",
|
|
})
|
|
})
|
|
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
return Log
|
|
}
|
|
|
|
// Helper functions for common logging operations
|
|
func Debug(args ...interface{}) {
|
|
GetLogger().Debug(args...)
|
|
}
|
|
|
|
func Debugf(format string, args ...interface{}) {
|
|
GetLogger().Debugf(format, args...)
|
|
}
|
|
|
|
func Info(args ...interface{}) {
|
|
GetLogger().Info(args...)
|
|
}
|
|
|
|
func Infof(format string, args ...interface{}) {
|
|
GetLogger().Infof(format, args...)
|
|
}
|
|
|
|
func Warn(args ...interface{}) {
|
|
GetLogger().Warn(args...)
|
|
}
|
|
|
|
func Warnf(format string, args ...interface{}) {
|
|
GetLogger().Warnf(format, args...)
|
|
}
|
|
|
|
func Error(args ...interface{}) {
|
|
GetLogger().Error(args...)
|
|
}
|
|
|
|
func Errorf(format string, args ...interface{}) {
|
|
GetLogger().Errorf(format, args...)
|
|
}
|
|
|
|
func Fatal(args ...interface{}) {
|
|
GetLogger().Fatal(args...)
|
|
}
|
|
|
|
func Fatalf(format string, args ...interface{}) {
|
|
GetLogger().Fatalf(format, args...)
|
|
}
|
|
|
|
// WithField creates a new logger entry with a field
|
|
func WithField(key string, value interface{}) *logrus.Entry {
|
|
return GetLogger().WithField(key, value)
|
|
}
|
|
|
|
// WithFields creates a new logger entry with multiple fields
|
|
func WithFields(fields logrus.Fields) *logrus.Entry {
|
|
return GetLogger().WithFields(fields)
|
|
}
|
|
|
|
// SanitizedTextFormatter sanitizes log output
|
|
type SanitizedTextFormatter struct {
|
|
FullTimestamp bool
|
|
TimestampFormat string
|
|
}
|
|
|
|
// Format formats the log entry with sanitization
|
|
func (f *SanitizedTextFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
|
// Sanitize sensitive data
|
|
sanitizedEntry := *entry
|
|
sanitizedEntry.Message = sanitizeString(entry.Message)
|
|
|
|
// Sanitize fields
|
|
sanitizedFields := make(logrus.Fields)
|
|
for k, v := range entry.Data {
|
|
sanitizedFields[k] = sanitizeValue(v)
|
|
}
|
|
sanitizedEntry.Data = sanitizedFields
|
|
|
|
// Use default text formatter
|
|
formatter := &logrus.TextFormatter{
|
|
FullTimestamp: f.FullTimestamp,
|
|
TimestampFormat: f.TimestampFormat,
|
|
}
|
|
return formatter.Format(&sanitizedEntry)
|
|
}
|
|
|
|
// SanitizedJSONFormatter sanitizes JSON log output
|
|
type SanitizedJSONFormatter struct {
|
|
TimestampFormat string
|
|
}
|
|
|
|
// Format formats the log entry with sanitization
|
|
func (f *SanitizedJSONFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
|
// Sanitize sensitive data
|
|
sanitizedEntry := *entry
|
|
sanitizedEntry.Message = sanitizeString(entry.Message)
|
|
|
|
// Sanitize fields
|
|
sanitizedFields := make(logrus.Fields)
|
|
for k, v := range entry.Data {
|
|
sanitizedFields[k] = sanitizeValue(v)
|
|
}
|
|
sanitizedEntry.Data = sanitizedFields
|
|
|
|
// Use default JSON formatter
|
|
formatter := &logrus.JSONFormatter{
|
|
TimestampFormat: f.TimestampFormat,
|
|
}
|
|
return formatter.Format(&sanitizedEntry)
|
|
}
|
|
|
|
// sanitizeString removes or masks sensitive information
|
|
func sanitizeString(s string) string {
|
|
// Remove potential encryption keys (hex strings longer than 32 chars)
|
|
keyPattern := regexp.MustCompile(`[a-fA-F0-9]{32,}`)
|
|
s = keyPattern.ReplaceAllString(s, "[REDACTED_KEY]")
|
|
|
|
// Remove potential passwords and tokens
|
|
passwordPattern := regexp.MustCompile(`(?i)(password|pass|pwd|secret|key|token|auth|credential)\s*[:=]\s*[^\s]+`)
|
|
s = passwordPattern.ReplaceAllString(s, "$1=[REDACTED]")
|
|
|
|
// Remove potential API keys and tokens
|
|
apiKeyPattern := regexp.MustCompile(`(?i)(api[_-]?key|access[_-]?token|bearer[_-]?token)\s*[:=]\s*[^\s]+`)
|
|
s = apiKeyPattern.ReplaceAllString(s, "$1=[REDACTED]")
|
|
|
|
// Remove potential database connection strings
|
|
dbPattern := regexp.MustCompile(`(?i)(mysql|postgres|mongodb|redis)://[^@]+@[^\s]+`)
|
|
s = dbPattern.ReplaceAllString(s, "[REDACTED_DB_CONNECTION]")
|
|
|
|
// Remove potential JWT tokens
|
|
jwtPattern := regexp.MustCompile(`eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*`)
|
|
s = jwtPattern.ReplaceAllString(s, "[REDACTED_JWT]")
|
|
|
|
// Remove potential credit card numbers
|
|
ccPattern := regexp.MustCompile(`\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b`)
|
|
s = ccPattern.ReplaceAllString(s, "[REDACTED_CC]")
|
|
|
|
// Remove potential SSNs
|
|
ssnPattern := regexp.MustCompile(`\b\d{3}-\d{2}-\d{4}\b`)
|
|
s = ssnPattern.ReplaceAllString(s, "[REDACTED_SSN]")
|
|
|
|
// Remove potential IP addresses in sensitive contexts
|
|
ipPattern := regexp.MustCompile(`\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b`)
|
|
s = ipPattern.ReplaceAllString(s, "[REDACTED_IP]")
|
|
|
|
// Remove potential email addresses
|
|
emailPattern := regexp.MustCompile(`\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b`)
|
|
s = emailPattern.ReplaceAllString(s, "[REDACTED_EMAIL]")
|
|
|
|
return s
|
|
}
|
|
|
|
// sanitizeValue sanitizes various value types
|
|
func sanitizeValue(v interface{}) interface{} {
|
|
switch val := v.(type) {
|
|
case string:
|
|
return sanitizeString(val)
|
|
case []byte:
|
|
return "[BINARY_DATA]"
|
|
case map[string]interface{}:
|
|
sanitized := make(map[string]interface{})
|
|
for k, v := range val {
|
|
sanitized[k] = sanitizeValue(v)
|
|
}
|
|
return sanitized
|
|
case []interface{}:
|
|
sanitized := make([]interface{}, len(val))
|
|
for i, v := range val {
|
|
sanitized[i] = sanitizeValue(v)
|
|
}
|
|
return sanitized
|
|
default:
|
|
return v
|
|
}
|
|
}
|