added Public IP monitoring and fixing issue where the app would stop recording session event until user logout.

This commit is contained in:
ghostersk
2025-05-28 06:16:50 +01:00
parent 795ab2839a
commit 52d20c8f0b
7 changed files with 288 additions and 48 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
# User Session Monitor Agent # User Session Monitor Agent
- For Windows
- created with AI
## Overview ## Overview
This application monitors Windows session events (logon, logoff, lock, unlock) and sends them to a specified API endpoint. It's designed to run as a Windows service on any version of Windows, collecting user session activity and securely transmitting it with an API key. This application monitors Windows session events (logon, logoff, lock, unlock) and sends them to a specified API endpoint. It's designed to run as a Windows service on any version of Windows, collecting user session activity and securely transmitting it with an API key.
+5 -3
View File
@@ -14,7 +14,8 @@ type EventData struct {
EventType string `json:"EventType"` EventType string `json:"EventType"`
UserName string `json:"UserName"` UserName string `json:"UserName"`
ComputerName string `json:"ComputerName"` ComputerName string `json:"ComputerName"`
IPAddress string `json:"IPAddress"` LocalIP string `json:"LocalIP"`
PublicIP string `json:"PublicIP"`
Timestamp string `json:"Timestamp"` Timestamp string `json:"Timestamp"`
Retry int `json:"retry,omitempty"` Retry int `json:"retry,omitempty"`
} }
@@ -48,13 +49,14 @@ func NewClient(apiKey, serverURL string) *Client {
} }
// SendEvent sends an event to the API // SendEvent sends an event to the API
func (c *Client) SendEvent(eventType, username, computerName, ipAddress, timestamp string, retry int) (bool, error) { func (c *Client) SendEvent(eventType, username, computerName, localIP, publicIP, timestamp string, retry int) (bool, error) {
// Create event data // Create event data
eventData := EventData{ eventData := EventData{
EventType: eventType, EventType: eventType,
UserName: username, UserName: username,
ComputerName: computerName, ComputerName: computerName,
IPAddress: ipAddress, LocalIP: localIP,
PublicIP: publicIP,
Timestamp: timestamp, Timestamp: timestamp,
} }
+4 -3
View File
@@ -138,7 +138,6 @@ func (hc *HealthChecker) retryFailedEvents() {
// Create API client for retry // Create API client for retry
cfg := config.GetConfig() cfg := config.GetConfig()
apiClient := NewClient(cfg.APIKey, cfg.ServerURL) apiClient := NewClient(cfg.APIKey, cfg.ServerURL)
// Try to send each failed event // Try to send each failed event
successCount := 0 successCount := 0
var successfulEvents []map[string]string var successfulEvents []map[string]string
@@ -148,7 +147,8 @@ func (hc *HealthChecker) retryFailedEvents() {
event["eventtype"], event["eventtype"],
event["username"], event["username"],
event["hostname"], event["hostname"],
event["ipaddress"], event["localip"],
event["publicip"],
event["timestamp"], event["timestamp"],
1, // Retry flag 1, // Retry flag
) )
@@ -166,7 +166,8 @@ func (hc *HealthChecker) retryFailedEvents() {
event["eventtype"], event["eventtype"],
event["username"], event["username"],
event["hostname"], event["hostname"],
event["ipaddress"], event["localip"],
event["publicip"],
event["timestamp"], event["timestamp"],
logging.SendStatusRetrySuccess, logging.SendStatusRetrySuccess,
); err != nil { ); err != nil {
Binary file not shown.
+42 -5
View File
@@ -39,6 +39,12 @@ const (
DefaultLogRotationSizeMB = 5 DefaultLogRotationSizeMB = 5
) )
// DefaultPublicIPHTTPURLs are the default URLs for HTTP-based public IP detection
var DefaultPublicIPHTTPURLs = []string{
"https://ifconfig.me/ip",
"https://ipv4.icanhazip.com",
}
// Config represents the application configuration // Config represents the application configuration
type Config struct { type Config struct {
APIKey string APIKey string
@@ -46,11 +52,12 @@ type Config struct {
DebugLogs bool DebugLogs bool
Timezone string Timezone string
InstallDir string InstallDir string
HealthCheckInterval int // Health check interval in minutes (0 = disabled) HealthCheckInterval int // Health check interval in minutes (0 = disabled)
HealthCheckPath string // Health check endpoint path HealthCheckPath string // Health check endpoint path
SessionLogRotationSizeMB int // Session monitor log rotation size in MB (0 = no rotation, max 20MB) PublicIPHTTPURLs []string // Custom HTTP URLs for public IP detection
ErrorLogRotationSizeMB int // Error log rotation size in MB (0 = no rotation, max 20MB) SessionLogRotationSizeMB int // Session monitor log rotation size in MB (0 = no rotation, max 20MB)
EventLogRotationSizeMB int // Event CSV log rotation size in MB (0 = no rotation, max 20MB) ErrorLogRotationSizeMB int // Error log rotation size in MB (0 = no rotation, max 20MB)
EventLogRotationSizeMB int // Event CSV log rotation size in MB (0 = no rotation, max 20MB)
mu sync.RWMutex mu sync.RWMutex
} }
@@ -71,10 +78,12 @@ func GetConfig() *Config {
InstallDir: DefaultInstallDir, InstallDir: DefaultInstallDir,
HealthCheckInterval: DefaultHealthCheckInterval, HealthCheckInterval: DefaultHealthCheckInterval,
HealthCheckPath: DefaultHealthCheckPath, HealthCheckPath: DefaultHealthCheckPath,
PublicIPHTTPURLs: make([]string, len(DefaultPublicIPHTTPURLs)),
SessionLogRotationSizeMB: DefaultLogRotationSizeMB, SessionLogRotationSizeMB: DefaultLogRotationSizeMB,
ErrorLogRotationSizeMB: DefaultLogRotationSizeMB, ErrorLogRotationSizeMB: DefaultLogRotationSizeMB,
EventLogRotationSizeMB: DefaultLogRotationSizeMB, EventLogRotationSizeMB: DefaultLogRotationSizeMB,
} }
copy(instance.PublicIPHTTPURLs, DefaultPublicIPHTTPURLs)
}) })
return instance return instance
} }
@@ -137,6 +146,19 @@ func (c *Config) Load(configFile string) error {
c.HealthCheckInterval = apiSection.Key("health_check_interval").MustInt(DefaultHealthCheckInterval) c.HealthCheckInterval = apiSection.Key("health_check_interval").MustInt(DefaultHealthCheckInterval)
c.HealthCheckPath = apiSection.Key("health_check_path").MustString(DefaultHealthCheckPath) c.HealthCheckPath = apiSection.Key("health_check_path").MustString(DefaultHealthCheckPath)
// Load custom public IP HTTP URLs
publicIPURLsStr := apiSection.Key("public_ip_http_urls").MustString("")
if publicIPURLsStr != "" {
c.PublicIPHTTPURLs = strings.Split(publicIPURLsStr, ",")
// Trim whitespace from URLs
for i := range c.PublicIPHTTPURLs {
c.PublicIPHTTPURLs[i] = strings.TrimSpace(c.PublicIPHTTPURLs[i])
}
} else {
c.PublicIPHTTPURLs = make([]string, len(DefaultPublicIPHTTPURLs))
copy(c.PublicIPHTTPURLs, DefaultPublicIPHTTPURLs)
}
// Read Logging section // Read Logging section
loggingSection := cfg.Section("Logging") loggingSection := cfg.Section("Logging")
c.SessionLogRotationSizeMB = loggingSection.Key("session_log_rotation_size_mb").MustInt(DefaultLogRotationSizeMB) c.SessionLogRotationSizeMB = loggingSection.Key("session_log_rotation_size_mb").MustInt(DefaultLogRotationSizeMB)
@@ -195,6 +217,19 @@ func (c *Config) LoadFromSource(configFile string) error {
c.HealthCheckInterval = apiSection.Key("health_check_interval").MustInt(DefaultHealthCheckInterval) c.HealthCheckInterval = apiSection.Key("health_check_interval").MustInt(DefaultHealthCheckInterval)
c.HealthCheckPath = apiSection.Key("health_check_path").MustString(DefaultHealthCheckPath) c.HealthCheckPath = apiSection.Key("health_check_path").MustString(DefaultHealthCheckPath)
// Load custom public IP HTTP URLs
publicIPURLsStr := apiSection.Key("public_ip_http_urls").MustString("")
if publicIPURLsStr != "" {
c.PublicIPHTTPURLs = strings.Split(publicIPURLsStr, ",")
// Trim whitespace from URLs
for i := range c.PublicIPHTTPURLs {
c.PublicIPHTTPURLs[i] = strings.TrimSpace(c.PublicIPHTTPURLs[i])
}
} else {
c.PublicIPHTTPURLs = make([]string, len(DefaultPublicIPHTTPURLs))
copy(c.PublicIPHTTPURLs, DefaultPublicIPHTTPURLs)
}
// Read Logging section // Read Logging section
loggingSection := cfg.Section("Logging") loggingSection := cfg.Section("Logging")
c.SessionLogRotationSizeMB = loggingSection.Key("session_log_rotation_size_mb").MustInt(DefaultLogRotationSizeMB) c.SessionLogRotationSizeMB = loggingSection.Key("session_log_rotation_size_mb").MustInt(DefaultLogRotationSizeMB)
@@ -241,6 +276,7 @@ func (c *Config) Save(configFile string) error {
apiSection.Key("install_dir").SetValue(c.InstallDir) apiSection.Key("install_dir").SetValue(c.InstallDir)
apiSection.Key("health_check_interval").SetValue(fmt.Sprintf("%d", c.HealthCheckInterval)) apiSection.Key("health_check_interval").SetValue(fmt.Sprintf("%d", c.HealthCheckInterval))
apiSection.Key("health_check_path").SetValue(c.HealthCheckPath) apiSection.Key("health_check_path").SetValue(c.HealthCheckPath)
apiSection.Key("public_ip_http_urls").SetValue(strings.Join(c.PublicIPHTTPURLs, ","))
// Create Logging section // Create Logging section
loggingSection, err := cfg.NewSection("Logging") loggingSection, err := cfg.NewSection("Logging")
@@ -348,6 +384,7 @@ func (c *Config) createDefaultConfig(configFile string) error {
apiSection.Key("install_dir").SetValue(c.InstallDir) apiSection.Key("install_dir").SetValue(c.InstallDir)
apiSection.Key("health_check_interval").SetValue(fmt.Sprintf("%d", c.HealthCheckInterval)) apiSection.Key("health_check_interval").SetValue(fmt.Sprintf("%d", c.HealthCheckInterval))
apiSection.Key("health_check_path").SetValue(c.HealthCheckPath) apiSection.Key("health_check_path").SetValue(c.HealthCheckPath)
apiSection.Key("public_ip_http_urls").SetValue(strings.Join(c.PublicIPHTTPURLs, ","))
// Create Logging section // Create Logging section
loggingSection, err := cfg.NewSection("Logging") loggingSection, err := cfg.NewSection("Logging")
+17 -15
View File
@@ -26,9 +26,10 @@ const (
EventTypeCol = 0 EventTypeCol = 0
UsernameCol = 1 UsernameCol = 1
HostnameCol = 2 HostnameCol = 2
IPAddressCol = 3 LocalIPCol = 3
TimestampCol = 4 PublicIPCol = 4
SendStatusCol = 5 TimestampCol = 5
SendStatusCol = 6
// Send status values // Send status values
SendStatusFailed = 0 SendStatusFailed = 0
@@ -41,8 +42,8 @@ const (
// Headers for CSV files // Headers for CSV files
var ( var (
MainCSVHeaders = []string{"eventtype", "username", "hostname", "ipaddress", "timestamp", "send_status"} MainCSVHeaders = []string{"eventtype", "username", "hostname", "localip", "publicip", "timestamp", "send_status"}
RetryCSVHeaders = []string{"eventtype", "username", "hostname", "ipaddress", "timestamp"} RetryCSVHeaders = []string{"eventtype", "username", "hostname", "localip", "publicip", "timestamp"}
) )
// NewCSVLogger creates a new CSV logger // NewCSVLogger creates a new CSV logger
@@ -109,7 +110,7 @@ func (l *CSVLogger) initCSVFile(filePath string, headers []string) error {
} }
// LogEvent logs an event to the CSV file // LogEvent logs an event to the CSV file
func (l *CSVLogger) LogEvent(eventType, username, hostname, ipAddress, timestamp string, sendStatus int) error { func (l *CSVLogger) LogEvent(eventType, username, hostname, localIP, publicIP, timestamp string, sendStatus int) error {
l.mu.Lock() l.mu.Lock()
defer l.mu.Unlock() defer l.mu.Unlock()
@@ -125,11 +126,11 @@ func (l *CSVLogger) LogEvent(eventType, username, hostname, ipAddress, timestamp
if sendStatus == SendStatusFailed { if sendStatus == SendStatusFailed {
// For failed events, use the retry file // For failed events, use the retry file
filePath = l.retryFile filePath = l.retryFile
record = []string{eventType, username, hostname, ipAddress, timestamp} record = []string{eventType, username, hostname, localIP, publicIP, timestamp}
} else { } else {
// For successful events, use the main file // For successful events, use the main file
filePath = l.csvFile filePath = l.csvFile
record = []string{eventType, username, hostname, ipAddress, timestamp, fmt.Sprintf("%d", sendStatus)} record = []string{eventType, username, hostname, localIP, publicIP, timestamp, fmt.Sprintf("%d", sendStatus)}
} }
// Open the file in append mode // Open the file in append mode
@@ -236,15 +237,15 @@ func (l *CSVLogger) RemoveSuccessfulRetries(successfulEvents []map[string]string
if err != nil { if err != nil {
return fmt.Errorf("failed to get failed events: %v", err) return fmt.Errorf("failed to get failed events: %v", err)
} }
// Create a map of successful events for quick lookup // Create a map of successful events for quick lookup
successfulMap := make(map[string]bool) successfulMap := make(map[string]bool)
for _, event := range successfulEvents { for _, event := range successfulEvents {
key := fmt.Sprintf("%s|%s|%s|%s|%s", key := fmt.Sprintf("%s|%s|%s|%s|%s|%s",
event["eventtype"], event["eventtype"],
event["username"], event["username"],
event["hostname"], event["hostname"],
event["ipaddress"], event["localip"],
event["publicip"],
event["timestamp"]) event["timestamp"])
successfulMap[key] = true successfulMap[key] = true
} }
@@ -252,11 +253,12 @@ func (l *CSVLogger) RemoveSuccessfulRetries(successfulEvents []map[string]string
// Filter out successful events from the failed events list // Filter out successful events from the failed events list
var remainingEvents []map[string]string var remainingEvents []map[string]string
for _, event := range allFailedEvents { for _, event := range allFailedEvents {
key := fmt.Sprintf("%s|%s|%s|%s|%s", key := fmt.Sprintf("%s|%s|%s|%s|%s|%s",
event["eventtype"], event["eventtype"],
event["username"], event["username"],
event["hostname"], event["hostname"],
event["ipaddress"], event["localip"],
event["publicip"],
event["timestamp"]) event["timestamp"])
if !successfulMap[key] { if !successfulMap[key] {
remainingEvents = append(remainingEvents, event) remainingEvents = append(remainingEvents, event)
@@ -280,14 +282,14 @@ func (l *CSVLogger) RemoveSuccessfulRetries(successfulEvents []map[string]string
return fmt.Errorf("failed to open retry file for writing: %v", err) return fmt.Errorf("failed to open retry file for writing: %v", err)
} }
defer file.Close() defer file.Close()
writer := csv.NewWriter(file) writer := csv.NewWriter(file)
for _, event := range remainingEvents { for _, event := range remainingEvents {
record := []string{ record := []string{
event["eventtype"], event["eventtype"],
event["username"], event["username"],
event["hostname"], event["hostname"],
event["ipaddress"], event["localip"],
event["publicip"],
event["timestamp"], event["timestamp"],
} }
if err := writer.Write(record); err != nil { if err := writer.Write(record); err != nil {
+218 -21
View File
@@ -1,9 +1,13 @@
package session package session
import ( import (
"context"
"fmt" "fmt"
"io"
"net" "net"
"net/http"
"os" "os"
"strings"
"sync" "sync"
"syscall" "syscall"
"time" "time"
@@ -11,6 +15,7 @@ import (
"unsafe" "unsafe"
"monitoring-agent-win/api" "monitoring-agent-win/api"
"monitoring-agent-win/config"
"monitoring-agent-win/logging" "monitoring-agent-win/logging"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
@@ -95,6 +100,10 @@ type Monitor struct {
mu sync.RWMutex mu sync.RWMutex
stopChan chan struct{} stopChan chan struct{}
hwnd HWND hwnd HWND
// Add session notification health monitoring
lastNotificationTime time.Time
healthCheckTicker *time.Ticker
notificationHealthy bool
} }
// NewMonitor creates a new session monitor // NewMonitor creates a new session monitor
@@ -187,16 +196,23 @@ func (m *Monitor) Start() error {
} }
m.hwnd = HWND(hwnd) m.hwnd = HWND(hwnd)
// Register for session notifications // Register for session notifications
if err := wtsRegisterSessionNotification(m.hwnd, NOTIFY_FOR_ALL_SESSIONS); err != nil { if err := wtsRegisterSessionNotification(m.hwnd, NOTIFY_FOR_ALL_SESSIONS); err != nil {
destroyWindow(m.hwnd) destroyWindow(m.hwnd)
return fmt.Errorf("failed to register for session notifications: %v", err) return fmt.Errorf("failed to register for session notifications: %v", err)
} }
// Initialize health monitoring
m.lastNotificationTime = time.Now()
m.notificationHealthy = true
m.healthCheckTicker = time.NewTicker(5 * time.Minute) // Check every 5 minutes
// Start message loop in a goroutine // Start message loop in a goroutine
go m.messageLoop() go m.messageLoop()
// Start health monitoring goroutine
go m.healthMonitor()
return nil return nil
} }
@@ -204,6 +220,11 @@ func (m *Monitor) Start() error {
func (m *Monitor) Stop() { func (m *Monitor) Stop() {
close(m.stopChan) close(m.stopChan)
// Stop health monitoring
if m.healthCheckTicker != nil {
m.healthCheckTicker.Stop()
}
// Unregister session notifications and destroy window // Unregister session notifications and destroy window
if m.hwnd != 0 { if m.hwnd != 0 {
wtsUnRegisterSessionNotification(m.hwnd) wtsUnRegisterSessionNotification(m.hwnd)
@@ -269,24 +290,29 @@ func (m *Monitor) messageLoop() {
func (m *Monitor) wndProc(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr { func (m *Monitor) wndProc(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr {
switch msg { switch msg {
case WM_WTSSESSION_CHANGE: case WM_WTSSESSION_CHANGE:
// Capture timestamp immediately when event occurs for accuracy
eventTime := time.Now()
// Update last notification time for health monitoring
m.lastNotificationTime = eventTime
sessionID := uint32(lParam) sessionID := uint32(lParam)
eventType := uint32(wParam) eventType := uint32(wParam)
// Get session info // Get session info
username := m.getSessionUsername(sessionID) username := m.getSessionUsername(sessionID)
computerName := getComputerName() computerName := getComputerName()
ipAddress := getDefaultIPv4() localIP := getDefaultIPv4()
// Log the event // Log the event with the captured timestamp
switch eventType { switch eventType {
case WTS_SESSION_LOGON: case WTS_SESSION_LOGON:
m.logEvent(EventLogon, username, computerName, ipAddress) m.logEventWithTime(EventLogon, username, computerName, localIP, eventTime)
case WTS_SESSION_LOGOFF: case WTS_SESSION_LOGOFF:
m.logEvent(EventLogoff, username, computerName, ipAddress) m.logEventWithTime(EventLogoff, username, computerName, localIP, eventTime)
case WTS_SESSION_LOCK: case WTS_SESSION_LOCK:
m.logEvent(EventLock, username, computerName, ipAddress) m.logEventWithTime(EventLock, username, computerName, localIP, eventTime)
case WTS_SESSION_UNLOCK: case WTS_SESSION_UNLOCK:
m.logEvent(EventUnlock, username, computerName, ipAddress) m.logEventWithTime(EventUnlock, username, computerName, localIP, eventTime)
} }
} }
@@ -332,31 +358,105 @@ func destroyWindow(hwnd HWND) {
proc.Call(uintptr(hwnd)) proc.Call(uintptr(hwnd))
} }
// logEvent logs an event to the CSV file and sends it to the API // getPublicIP retrieves public IP with HTTP first, then DNS fallback
func (m *Monitor) logEvent(eventType, username, computerName, ipAddress string) { func getPublicIP() (string, error) {
// Get current timestamp in the configured timezone // Get config to check for custom HTTP URLs
cfg := config.GetConfig()
// Use custom URLs if configured, otherwise use defaults
urls := []string{
"https://ipv4.icanhazip.com",
"https://ifconfig.me/ip",
}
if len(cfg.PublicIPHTTPURLs) > 0 {
urls = cfg.PublicIPHTTPURLs
}
// Try HTTP services
client := &http.Client{Timeout: 2 * time.Second}
for _, url := range urls {
if resp, err := client.Get(url); err == nil && resp.StatusCode == 200 {
defer resp.Body.Close()
if body, err := io.ReadAll(resp.Body); err == nil {
if ip := strings.TrimSpace(string(body)); net.ParseIP(ip) != nil && net.ParseIP(ip).To4() != nil {
return ip, nil
}
}
}
}
// DNS fallback using Google's nameserver (equivalent to: dig TXT +short o-o.myaddr.l.google.com @ns1.google.com)
// First resolve ns1.google.com to get its IP dynamically
nsIPs, err := net.LookupHost("ns1.google.com")
if err != nil || len(nsIPs) == 0 {
return "", fmt.Errorf("failed to resolve ns1.google.com")
}
// Use the first IPv4 address from ns1.google.com
var nsIP string
for _, ip := range nsIPs {
if net.ParseIP(ip).To4() != nil {
nsIP = ip
break
}
}
if nsIP == "" {
return "", fmt.Errorf("no IPv4 address found for ns1.google.com")
}
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{Timeout: 2 * time.Second}
return d.DialContext(ctx, network, nsIP+":53")
},
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if txts, err := resolver.LookupTXT(ctx, "o-o.myaddr.l.google.com"); err == nil {
for _, txt := range txts {
if parsedIP := net.ParseIP(txt); parsedIP != nil && parsedIP.To4() != nil {
return txt, nil
}
}
}
return "", fmt.Errorf("failed to retrieve public IP")
}
// logEventWithTime logs an event with a pre-captured timestamp
func (m *Monitor) logEventWithTime(eventType, username, computerName, localIP string, eventTime time.Time) {
// Get public IP (this can be done in background as it's less time-sensitive)
publicIP, err := getPublicIP()
if err != nil {
m.logger.Warning("Failed to get public IP: %v - using 'Unknown'", err)
publicIP = "Unknown"
}
// Format the timestamp using the pre-captured event time
var timestamp string var timestamp string
if m.timezone != "" { if m.timezone != "" {
// Load the configured timezone // Try to load the configured timezone with Windows-specific handling
loc, err := time.LoadLocation(m.timezone) loc, err := loadTimezoneWithFallback(m.timezone)
if err != nil { if err != nil {
// If timezone loading fails, fall back to local time but log the error // If timezone loading fails, fall back to local time but log the error
m.logger.Warning("Failed to load timezone '%s': %v - using local time", m.timezone, err) m.logger.Warning("Failed to load timezone '%s': %v - using local time", m.timezone, err)
timestamp = time.Now().Format("2006-01-02T15:04:05-07:00") timestamp = eventTime.Format("2006-01-02T15:04:05-07:00")
} else { } else {
// Use the configured timezone // Use the configured timezone
timestamp = time.Now().In(loc).Format("2006-01-02T15:04:05-07:00") timestamp = eventTime.In(loc).Format("2006-01-02T15:04:05-07:00")
} }
} else { } else {
// If no timezone configured, use local time // If no timezone configured, use local time
timestamp = time.Now().Format("2006-01-02T15:04:05-07:00") timestamp = eventTime.Format("2006-01-02T15:04:05-07:00")
} }
// Log the event // Log the event
m.logger.Info("User: %s - Event: %s - Computer: %s - IP: %s", username, eventType, computerName, ipAddress) m.logger.Info("User: %s - Event: %s - Computer: %s - LocalIP: %s - PublicIP: %s", username, eventType, computerName, localIP, publicIP)
// Send to API // Send to API
success, err := m.apiClient.SendEvent(eventType, username, computerName, ipAddress, timestamp, 0) success, err := m.apiClient.SendEvent(eventType, username, computerName, localIP, publicIP, timestamp, 0)
if err != nil { if err != nil {
m.logger.Error("Failed to send event: %v", err) m.logger.Error("Failed to send event: %v", err)
} }
@@ -367,7 +467,7 @@ func (m *Monitor) logEvent(eventType, username, computerName, ipAddress string)
sendStatus = logging.SendStatusFailed sendStatus = logging.SendStatusFailed
} }
if err := m.csvLogger.LogEvent(eventType, username, computerName, ipAddress, timestamp, sendStatus); err != nil { if err := m.csvLogger.LogEvent(eventType, username, computerName, localIP, publicIP, timestamp, sendStatus); err != nil {
m.logger.Error("Failed to log event to CSV: %v", err) m.logger.Error("Failed to log event to CSV: %v", err)
} }
@@ -399,7 +499,8 @@ func (m *Monitor) retryFailedEvents() {
event["eventtype"], event["eventtype"],
event["username"], event["username"],
event["hostname"], event["hostname"],
event["ipaddress"], event["localip"],
event["publicip"],
event["timestamp"], event["timestamp"],
1, // Retry flag 1, // Retry flag
) )
@@ -415,7 +516,8 @@ func (m *Monitor) retryFailedEvents() {
event["eventtype"], event["eventtype"],
event["username"], event["username"],
event["hostname"], event["hostname"],
event["ipaddress"], event["localip"],
event["publicip"],
event["timestamp"], event["timestamp"],
logging.SendStatusRetrySuccess, logging.SendStatusRetrySuccess,
); err != nil { ); err != nil {
@@ -617,3 +719,98 @@ func utf16PtrToString(p *uint16) string {
return string(utf16.Decode(s)) return string(utf16.Decode(s))
} }
// loadTimezoneWithFallback loads timezone with Windows-specific handling
func loadTimezoneWithFallback(timezone string) (*time.Location, error) {
// First try the standard IANA timezone
loc, err := time.LoadLocation(timezone)
if err == nil {
return loc, nil
}
// Map common IANA timezones to Windows timezones
windowsTimezoneMap := map[string]string{
"Europe/London": "GMT Standard Time",
"Europe/Paris": "Romance Standard Time",
"Europe/Berlin": "W. Europe Standard Time",
"Europe/Madrid": "Romance Standard Time",
"Europe/Rome": "W. Europe Standard Time",
"Europe/Amsterdam": "W. Europe Standard Time",
"America/New_York": "Eastern Standard Time",
"America/Chicago": "Central Standard Time",
"America/Denver": "Mountain Standard Time",
"America/Los_Angeles": "Pacific Standard Time",
"Asia/Tokyo": "Tokyo Standard Time",
"Asia/Shanghai": "China Standard Time",
"Asia/Kolkata": "India Standard Time",
"Australia/Sydney": "AUS Eastern Standard Time",
"UTC": "UTC",
}
// Try Windows timezone name
if windowsName, exists := windowsTimezoneMap[timezone]; exists {
loc, err = time.LoadLocation(windowsName)
if err == nil {
return loc, nil
}
}
// If both fail, return the original error
return nil, fmt.Errorf("timezone '%s' not found (tried IANA and Windows formats)", timezone)
}
// reregisterSessionNotifications re-registers for session notifications
// This helps fix the issue where session notifications stop working
func (m *Monitor) reregisterSessionNotifications() error {
if m.hwnd == 0 {
return fmt.Errorf("no window handle available")
}
// Unregister first
wtsUnRegisterSessionNotification(m.hwnd)
// Small delay before re-registering
time.Sleep(100 * time.Millisecond)
// Re-register
err := wtsRegisterSessionNotification(m.hwnd, NOTIFY_FOR_ALL_SESSIONS)
if err != nil {
m.logger.Error("Failed to re-register session notifications: %v", err)
return err
}
m.logger.Info("Successfully re-registered session notifications")
return nil
}
// healthMonitor monitors session notification health and re-registers if needed
func (m *Monitor) healthMonitor() {
for {
select {
case <-m.stopChan:
if m.healthCheckTicker != nil {
m.healthCheckTicker.Stop()
}
return
case <-m.healthCheckTicker.C:
// Check if we've received any notifications in the last 10 minutes
// Note: This assumes some activity on the system, adjust threshold as needed
timeSinceLastNotification := time.Since(m.lastNotificationTime)
// If no notifications for more than 15 minutes and system isn't locked,
// consider re-registering (aggressive approach for testing)
if timeSinceLastNotification > 15*time.Minute {
m.logger.Warning("No session notifications received for %v, attempting re-registration", timeSinceLastNotification)
if err := m.reregisterSessionNotifications(); err != nil {
m.logger.Error("Failed to re-register session notifications: %v", err)
m.notificationHealthy = false
} else {
m.notificationHealthy = true
// Reset the timer to give the re-registration a chance
m.lastNotificationTime = time.Now()
}
}
}
}
}