Init: Migrate SimpleRemoter (Since v1.3.1) to Gitea
This commit is contained in:
465
server/2015Remote/NotifyManager.cpp
Normal file
465
server/2015Remote/NotifyManager.cpp
Normal file
@@ -0,0 +1,465 @@
|
||||
#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);
|
||||
}
|
||||
Reference in New Issue
Block a user