466 lines
15 KiB
C++
466 lines
15 KiB
C++
#include "stdafx.h"
|
|
#include "NotifyManager.h"
|
|
#include "context.h"
|
|
#include "common/iniFile.h"
|
|
#include "UIBranding.h"
|
|
#include <thread>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <shlobj.h>
|
|
#include <ctime>
|
|
|
|
// Get config directory path (same as GetDbPath directory)
|
|
static std::string GetConfigDir()
|
|
{
|
|
static char path[MAX_PATH];
|
|
static std::string ret;
|
|
if (ret.empty()) {
|
|
if (FAILED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
|
|
ret = ".\\";
|
|
} else {
|
|
ret = std::string(path) + "\\" BRAND_DATA_FOLDER "\\";
|
|
}
|
|
CreateDirectoryA(ret.c_str(), NULL);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
NotifyManager& NotifyManager::Instance()
|
|
{
|
|
static NotifyManager instance;
|
|
return instance;
|
|
}
|
|
|
|
NotifyManager::NotifyManager()
|
|
: m_initialized(false)
|
|
, m_powerShellAvailable(false)
|
|
{
|
|
}
|
|
|
|
void NotifyManager::Initialize()
|
|
{
|
|
if (m_initialized) return;
|
|
|
|
m_powerShellAvailable = DetectPowerShellSupport();
|
|
LoadConfig();
|
|
m_initialized = true;
|
|
}
|
|
|
|
bool NotifyManager::DetectPowerShellSupport()
|
|
{
|
|
// Check if PowerShell Send-MailMessage command is available
|
|
std::string cmd = "powershell -NoProfile -Command \"Get-Command Send-MailMessage -ErrorAction SilentlyContinue\"";
|
|
DWORD exitCode = 1;
|
|
ExecutePowerShell(cmd, &exitCode, true);
|
|
return (exitCode == 0);
|
|
}
|
|
|
|
std::string NotifyManager::GetConfigPath() const
|
|
{
|
|
return GetConfigDir() + "notify.ini";
|
|
}
|
|
|
|
NotifyConfig NotifyManager::GetConfig()
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
return m_config;
|
|
}
|
|
|
|
void NotifyManager::SetConfig(const NotifyConfig& config)
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
// Preserve lastNotifyTime from current config
|
|
auto lastNotifyTime = m_config.lastNotifyTime;
|
|
m_config = config;
|
|
m_config.lastNotifyTime = lastNotifyTime;
|
|
}
|
|
|
|
void NotifyManager::LoadConfig()
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
config cfg(GetConfigPath());
|
|
|
|
// SMTP settings
|
|
m_config.smtp.server = cfg.GetStr("SMTP", "Server", "smtp.gmail.com");
|
|
m_config.smtp.port = cfg.GetInt("SMTP", "Port", 587);
|
|
m_config.smtp.useSSL = cfg.GetInt("SMTP", "UseSSL", 1) != 0;
|
|
m_config.smtp.username = cfg.GetStr("SMTP", "Username", "");
|
|
m_config.smtp.password = DecryptPassword(cfg.GetStr("SMTP", "Password", ""));
|
|
m_config.smtp.recipient = cfg.GetStr("SMTP", "Recipient", "");
|
|
|
|
// Rule settings (currently only one rule)
|
|
NotifyRule& rule = m_config.GetRule();
|
|
rule.enabled = cfg.GetInt("Rule_0", "Enabled", 0) != 0;
|
|
rule.triggerType = (NotifyTriggerType)cfg.GetInt("Rule_0", "TriggerType", NOTIFY_TRIGGER_HOST_ONLINE);
|
|
rule.columnIndex = cfg.GetInt("Rule_0", "ColumnIndex", ONLINELIST_COMPUTER_NAME);
|
|
rule.matchPattern = cfg.GetStr("Rule_0", "MatchPattern", "");
|
|
}
|
|
|
|
void NotifyManager::SaveConfig()
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
config cfg(GetConfigPath());
|
|
|
|
// SMTP settings
|
|
cfg.SetStr("SMTP", "Server", m_config.smtp.server);
|
|
cfg.SetInt("SMTP", "Port", m_config.smtp.port);
|
|
cfg.SetInt("SMTP", "UseSSL", m_config.smtp.useSSL ? 1 : 0);
|
|
cfg.SetStr("SMTP", "Username", m_config.smtp.username);
|
|
cfg.SetStr("SMTP", "Password", EncryptPassword(m_config.smtp.password));
|
|
cfg.SetStr("SMTP", "Recipient", m_config.smtp.recipient);
|
|
|
|
// Rule settings
|
|
const NotifyRule& rule = m_config.GetRule();
|
|
cfg.SetInt("Rule_0", "Enabled", rule.enabled ? 1 : 0);
|
|
cfg.SetInt("Rule_0", "TriggerType", (int)rule.triggerType);
|
|
cfg.SetInt("Rule_0", "ColumnIndex", rule.columnIndex);
|
|
cfg.SetStr("Rule_0", "MatchPattern", rule.matchPattern);
|
|
}
|
|
|
|
bool NotifyManager::ShouldNotify(context* ctx, std::string& outMatchedKeyword, const CString& remark)
|
|
{
|
|
if (!m_powerShellAvailable) return false;
|
|
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
|
|
const NotifyRule& rule = m_config.GetRule();
|
|
if (!rule.enabled) return false;
|
|
if (rule.triggerType != NOTIFY_TRIGGER_HOST_ONLINE) return false;
|
|
if (rule.matchPattern.empty()) return false;
|
|
if (!m_config.smtp.IsValid()) return false;
|
|
|
|
uint64_t clientId = ctx->GetClientID();
|
|
time_t now = time(nullptr);
|
|
|
|
// Cooldown check
|
|
auto it = m_config.lastNotifyTime.find(clientId);
|
|
if (it != m_config.lastNotifyTime.end()) {
|
|
time_t elapsed = now - it->second;
|
|
if (elapsed < NOTIFY_COOLDOWN_MINUTES * 60) {
|
|
return false; // Still in cooldown period
|
|
}
|
|
}
|
|
|
|
// Get column text (for COMPUTER_NAME column, prefer remark if available)
|
|
CString colText;
|
|
if (rule.columnIndex == ONLINELIST_COMPUTER_NAME && !remark.IsEmpty()) {
|
|
colText = remark;
|
|
} else {
|
|
colText = ctx->GetClientData(rule.columnIndex);
|
|
}
|
|
if (colText.IsEmpty()) return false;
|
|
|
|
// Convert to std::string for matching
|
|
std::string colTextStr = CT2A(colText, CP_UTF8);
|
|
|
|
// Split pattern by semicolon and check each keyword
|
|
std::vector<std::string> keywords = SplitString(rule.matchPattern, ';');
|
|
for (const auto& kw : keywords) {
|
|
std::string trimmed = Trim(kw);
|
|
if (trimmed.empty()) continue;
|
|
|
|
// Case-insensitive substring search
|
|
std::string colLower = colTextStr;
|
|
std::string kwLower = trimmed;
|
|
std::transform(colLower.begin(), colLower.end(), colLower.begin(), ::tolower);
|
|
std::transform(kwLower.begin(), kwLower.end(), kwLower.begin(), ::tolower);
|
|
|
|
if (colLower.find(kwLower) != std::string::npos) {
|
|
outMatchedKeyword = trimmed;
|
|
m_config.lastNotifyTime[clientId] = now;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void NotifyManager::BuildHostOnlineEmail(context* ctx, const std::string& matchedKeyword,
|
|
std::string& outSubject, std::string& outBody)
|
|
{
|
|
// Copy rule info under lock
|
|
int columnIndex;
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
columnIndex = m_config.GetRule().columnIndex;
|
|
}
|
|
|
|
// Get host info
|
|
std::string computerName = CT2A(ctx->GetClientData(ONLINELIST_COMPUTER_NAME), CP_UTF8);
|
|
std::string ip = CT2A(ctx->GetClientData(ONLINELIST_IP), CP_UTF8);
|
|
std::string location = CT2A(ctx->GetClientData(ONLINELIST_LOCATION), CP_UTF8);
|
|
std::string os = CT2A(ctx->GetClientData(ONLINELIST_OS), CP_UTF8);
|
|
std::string version = CT2A(ctx->GetClientData(ONLINELIST_VERSION), CP_UTF8);
|
|
|
|
// Get current time
|
|
time_t now = time(nullptr);
|
|
char timeStr[64];
|
|
struct tm tm_info;
|
|
localtime_s(&tm_info, &now);
|
|
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &tm_info);
|
|
|
|
// Build subject
|
|
std::ostringstream ss;
|
|
ss << "[SimpleRemoter] Host Online: " << computerName << " matched \"" << matchedKeyword << "\"";
|
|
outSubject = ss.str();
|
|
|
|
// Build body (HTML format)
|
|
ss.str("");
|
|
ss << "<b>Host Online Notification</b><br><br>";
|
|
ss << "Trigger Time: " << timeStr << "<br>";
|
|
ss << "Match Rule: " << GetColumnName(columnIndex)
|
|
<< " contains \"" << matchedKeyword << "\"<br><br>";
|
|
ss << "<b>Host Information:</b><br>";
|
|
ss << " IP Address: " << ip << "<br>";
|
|
ss << " Location: " << location << "<br>";
|
|
ss << " Computer Name: " << computerName << "<br>";
|
|
ss << " OS: " << os << "<br>";
|
|
ss << " Version: " << version << "<br>";
|
|
ss << "<br>--<br><i>This email was sent automatically by SimpleRemoter</i>";
|
|
|
|
outBody = ss.str();
|
|
}
|
|
|
|
void NotifyManager::SendNotifyEmailAsync(const std::string& subject, const std::string& body)
|
|
{
|
|
if (!m_powerShellAvailable) return;
|
|
|
|
// Copy SMTP config under lock
|
|
SmtpConfig smtp;
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
if (!m_config.smtp.IsValid()) return;
|
|
smtp = m_config.smtp;
|
|
}
|
|
|
|
std::string subjectCopy = subject;
|
|
std::string bodyCopy = body;
|
|
|
|
std::thread([this, smtp, subjectCopy, bodyCopy]() {
|
|
// Build PowerShell command
|
|
std::ostringstream ps;
|
|
ps << "powershell -NoProfile -ExecutionPolicy Bypass -Command \"";
|
|
ps << "$pass = ConvertTo-SecureString '" << EscapePowerShell(smtp.password) << "' -AsPlainText -Force; ";
|
|
ps << "$cred = New-Object PSCredential('" << EscapePowerShell(smtp.username) << "', $pass); ";
|
|
ps << "Send-MailMessage ";
|
|
ps << "-From '" << EscapePowerShell(smtp.username) << "' ";
|
|
ps << "-To '" << EscapePowerShell(smtp.GetRecipient()) << "' ";
|
|
ps << "-Subject '" << EscapePowerShell(subjectCopy) << "' ";
|
|
ps << "-Body '" << EscapePowerShell(bodyCopy) << "' ";
|
|
ps << "-SmtpServer '" << EscapePowerShell(smtp.server) << "' ";
|
|
ps << "-Port " << smtp.port << " ";
|
|
if (smtp.useSSL) {
|
|
ps << "-UseSsl ";
|
|
}
|
|
ps << "-Credential $cred ";
|
|
ps << "-Encoding UTF8 -BodyAsHtml\"";
|
|
|
|
DWORD exitCode;
|
|
ExecutePowerShell(ps.str(), &exitCode, true);
|
|
}).detach();
|
|
}
|
|
|
|
std::string NotifyManager::SendTestEmail()
|
|
{
|
|
if (!m_powerShellAvailable) {
|
|
return "PowerShell is not available. Requires Windows 10 or later.";
|
|
}
|
|
|
|
// Copy SMTP config under lock
|
|
SmtpConfig smtp;
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
if (!m_config.smtp.IsValid()) {
|
|
return "SMTP configuration is incomplete.";
|
|
}
|
|
smtp = m_config.smtp;
|
|
}
|
|
|
|
std::string subject = "[SimpleRemoter] Test Email";
|
|
std::string body = "This is a test email from SimpleRemoter notification system.<br><br>If you received this email, the configuration is correct.";
|
|
|
|
// Build PowerShell command - output error to temp file for capture
|
|
char tempPath[MAX_PATH], tempFile[MAX_PATH];
|
|
GetTempPathA(MAX_PATH, tempPath);
|
|
GetTempFileNameA(tempPath, "notify", 0, tempFile);
|
|
|
|
std::ostringstream ps;
|
|
ps << "powershell -NoProfile -ExecutionPolicy Bypass -Command \"";
|
|
ps << "$ErrorActionPreference = 'Stop'; ";
|
|
ps << "try { ";
|
|
ps << "$pass = ConvertTo-SecureString '" << EscapePowerShell(smtp.password) << "' -AsPlainText -Force; ";
|
|
ps << "$cred = New-Object PSCredential('" << EscapePowerShell(smtp.username) << "', $pass); ";
|
|
ps << "Send-MailMessage ";
|
|
ps << "-From '" << EscapePowerShell(smtp.username) << "' ";
|
|
ps << "-To '" << EscapePowerShell(smtp.GetRecipient()) << "' ";
|
|
ps << "-Subject '" << EscapePowerShell(subject) << "' ";
|
|
ps << "-Body '" << EscapePowerShell(body) << "' ";
|
|
ps << "-SmtpServer '" << EscapePowerShell(smtp.server) << "' ";
|
|
ps << "-Port " << smtp.port << " ";
|
|
if (smtp.useSSL) {
|
|
ps << "-UseSsl ";
|
|
}
|
|
ps << "-Credential $cred ";
|
|
ps << "-Encoding UTF8 -BodyAsHtml; ";
|
|
ps << "'SUCCESS' | Out-File -FilePath '" << tempFile << "' -Encoding UTF8 ";
|
|
ps << "} catch { $_.Exception.Message | Out-File -FilePath '" << tempFile << "' -Encoding UTF8; exit 1 }\"";
|
|
|
|
DWORD exitCode = 1;
|
|
ExecutePowerShell(ps.str(), &exitCode, true);
|
|
|
|
// Read result from temp file (skip UTF-8 BOM if present)
|
|
std::string result;
|
|
std::ifstream ifs(tempFile, std::ios::binary);
|
|
if (ifs.is_open()) {
|
|
std::getline(ifs, result);
|
|
// Skip UTF-8 BOM (EF BB BF) or UTF-16 LE BOM (FF FE)
|
|
if (result.size() >= 3 && (unsigned char)result[0] == 0xEF &&
|
|
(unsigned char)result[1] == 0xBB && (unsigned char)result[2] == 0xBF) {
|
|
result = result.substr(3);
|
|
} else if (result.size() >= 2 && (unsigned char)result[0] == 0xFF &&
|
|
(unsigned char)result[1] == 0xFE) {
|
|
// UTF-16 LE - convert to ASCII (simple case)
|
|
std::string converted;
|
|
for (size_t i = 2; i < result.size(); i += 2) {
|
|
if (result[i] != 0) converted += result[i];
|
|
}
|
|
result = converted;
|
|
}
|
|
ifs.close();
|
|
}
|
|
DeleteFileA(tempFile);
|
|
|
|
if (exitCode == 0 && result.find("SUCCESS") != std::string::npos) {
|
|
return "success";
|
|
} else {
|
|
// Log detailed error for debugging
|
|
if (result.empty()) {
|
|
TRACE("[Notify] SendTestEmail failed, exit code: %d\n", exitCode);
|
|
} else {
|
|
TRACE("[Notify] SendTestEmail failed: %s\n", result.c_str());
|
|
}
|
|
return "failed";
|
|
}
|
|
}
|
|
|
|
bool NotifyManager::ExecutePowerShell(const std::string& command, DWORD* exitCode, bool hidden)
|
|
{
|
|
STARTUPINFOA si = { sizeof(si) };
|
|
PROCESS_INFORMATION pi = { 0 };
|
|
|
|
if (hidden) {
|
|
si.dwFlags = STARTF_USESHOWWINDOW;
|
|
si.wShowWindow = SW_HIDE;
|
|
}
|
|
|
|
// Create command buffer (must be modifiable for CreateProcessA)
|
|
std::vector<char> cmdBuffer(command.begin(), command.end());
|
|
cmdBuffer.push_back('\0');
|
|
|
|
BOOL result = CreateProcessA(
|
|
NULL,
|
|
cmdBuffer.data(),
|
|
NULL, NULL,
|
|
FALSE,
|
|
hidden ? CREATE_NO_WINDOW : 0,
|
|
NULL, NULL,
|
|
&si, &pi
|
|
);
|
|
|
|
if (!result) {
|
|
if (exitCode) *exitCode = GetLastError();
|
|
return false;
|
|
}
|
|
|
|
// Wait for process to complete (with timeout for test email)
|
|
WaitForSingleObject(pi.hProcess, 30000); // 30 second timeout
|
|
|
|
if (exitCode) {
|
|
GetExitCodeProcess(pi.hProcess, exitCode);
|
|
}
|
|
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
|
|
return true;
|
|
}
|
|
|
|
std::string NotifyManager::EscapePowerShell(const std::string& str)
|
|
{
|
|
std::string result;
|
|
result.reserve(str.size() * 2);
|
|
|
|
for (char c : str) {
|
|
if (c == '\'') {
|
|
result += "''"; // Escape single quote by doubling
|
|
} else if (c == '\n') {
|
|
result += "`n"; // PowerShell newline escape
|
|
} else if (c == '\r') {
|
|
result += "`r";
|
|
} else {
|
|
result += c;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::string NotifyManager::EncryptPassword(const std::string& password)
|
|
{
|
|
// Simple XOR obfuscation (not secure, just prevents casual reading)
|
|
const char key[] = "YamaNotify2026";
|
|
std::string result;
|
|
result.reserve(password.size() * 2);
|
|
|
|
for (size_t i = 0; i < password.size(); i++) {
|
|
char c = password[i] ^ key[i % (sizeof(key) - 1)];
|
|
char hex[3];
|
|
sprintf_s(hex, "%02X", (unsigned char)c);
|
|
result += hex;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::string NotifyManager::DecryptPassword(const std::string& encrypted)
|
|
{
|
|
if (encrypted.empty() || encrypted.size() % 2 != 0) {
|
|
return "";
|
|
}
|
|
|
|
const char key[] = "YamaNotify2026";
|
|
std::string result;
|
|
result.reserve(encrypted.size() / 2);
|
|
|
|
for (size_t i = 0; i < encrypted.size(); i += 2) {
|
|
char hex[3] = { encrypted[i], encrypted[i + 1], 0 };
|
|
char c = (char)strtol(hex, nullptr, 16);
|
|
c ^= key[(i / 2) % (sizeof(key) - 1)];
|
|
result += c;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::vector<std::string> NotifyManager::SplitString(const std::string& str, char delimiter)
|
|
{
|
|
std::vector<std::string> tokens;
|
|
std::istringstream stream(str);
|
|
std::string token;
|
|
|
|
while (std::getline(stream, token, delimiter)) {
|
|
tokens.push_back(token);
|
|
}
|
|
|
|
return tokens;
|
|
}
|
|
|
|
std::string NotifyManager::Trim(const std::string& str)
|
|
{
|
|
size_t start = str.find_first_not_of(" \t\r\n");
|
|
if (start == std::string::npos) return "";
|
|
|
|
size_t end = str.find_last_not_of(" \t\r\n");
|
|
return str.substr(start, end - start + 1);
|
|
}
|