From b8f387d2cd881baf4dce36ad690cbf9c5677a84a Mon Sep 17 00:00:00 2001 From: Muspah Date: Tue, 14 Apr 2026 22:51:56 +0200 Subject: [PATCH 1/4] Add HTTP-POST option, POST message as JSON when enabled --- CMakeLists.txt | 1 + Headers/Resource.h | 10 ++ Headers/pdw.h | 10 ++ Misc.cpp | 30 +++- PDW.cpp | 84 +++++++++ Rsrc.rc | 22 +++ utils/CMakeLists.txt | 2 + utils/http_post.cpp | 419 +++++++++++++++++++++++++++++++++++++++++++ utils/http_post.h | 10 ++ 9 files changed, 580 insertions(+), 8 deletions(-) create mode 100644 utils/http_post.cpp create mode 100644 utils/http_post.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ee34a45..99c4a80 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,6 +72,7 @@ target_link_libraries( PDW Comctl32.lib Winmm.lib Ws2_32.lib + Winhttp.lib libssl_static.lib libcrypto_static.lib legacy_stdio_definitions.lib diff --git a/Headers/Resource.h b/Headers/Resource.h index 09e738b..1637732 100644 --- a/Headers/Resource.h +++ b/Headers/Resource.h @@ -27,6 +27,7 @@ #define CLEARSCREENDLGBOX 123 #define MAIL_DLGBOX 124 #define SCREENOPTIONSDLGBOX 125 +#define HTTPPOST_DLGBOX 126 #define IDM_LOGFILE 201 #define IDM_EXIT 202 @@ -43,6 +44,7 @@ #define IDM_OPTIONS 230 #define IDM_GENERAL 231 #define IDM_MAIL 232 +#define IDM_HTTPPOST 233 #define IDM_FILTERS 240 #define IDM_FILTEROPTIONS 241 @@ -439,3 +441,11 @@ #define IDT_FILTERCHECKDUPLICATE 1306 #define IDT_MENU_RESTORE 1310 + +#define IDC_HTTPPOST_ENABLE 1320 +#define IDC_HTTPPOST_URL 1321 +#define IDC_HTTPPOST_AUTH 1322 +#define IDC_HTTPPOST_USER 1323 +#define IDC_HTTPPOST_PASSWORD 1324 +#define IDC_HTTPPOST_QUEUE_MAX 1325 +#define IDC_HTTPPOST_QUEUE_TTL 1326 diff --git a/Headers/pdw.h b/Headers/pdw.h index dd23ed0..afe7b23 100644 --- a/Headers/pdw.h +++ b/Headers/pdw.h @@ -11,6 +11,7 @@ #define FILTER_FILE_LEN 128 // PH: was 256 #define MAX_STR_LEN 5120 +#define HTTP_POST_URL_LEN 512 enum FILTER_TYPE { UNUSED_FILTER = 0, FLEX_FILTER = 1, @@ -188,6 +189,14 @@ typedef struct int iMailPort ; int nMailOptions ; + int http_post_enabled; + int http_post_auth; + char http_post_url[HTTP_POST_URL_LEN]; + char http_post_user[MAIL_TEXT_LEN]; + char http_post_password[MAIL_TEXT_LEN]; + int http_post_queue_max; + int http_post_queue_ttl; + COLORREF color_background; COLORREF color_address; COLORREF color_timestamp; @@ -420,6 +429,7 @@ BOOL FAR PASCAL FilterFindDlgProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lP BOOL FAR PASCAL FilterCheckDuplicateDlgProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); BOOL FAR PASCAL MonStatDlgProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); BOOL FAR PASCAL MailDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam); +BOOL FAR PASCAL HttpPostDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam); BOOL NEAR SetTitle(HWND hWnd, TCHAR *cTitle); BOOL ErrorMessageBox(LPCTSTR lpszText, LPCTSTR lpszTitle, LPCTSTR lpszFile, INT Line); diff --git a/Misc.cpp b/Misc.cpp index 2066e26..7369b3c 100644 --- a/Misc.cpp +++ b/Misc.cpp @@ -17,6 +17,7 @@ #include "headers\helper_funcs.h" #include "utils\binary.h" #include "utils\smtp.h" +#include "utils\http_post.h" #define FILTER_PARAM_LEN 500 #define MAXIMUM_GROUPSIZE 1000 @@ -188,15 +189,15 @@ void display_show_char(PaneStruct *pane, char cin) { if (cin == 0) { - message_buffer[iMessageIndex] = '·'; - mobitex_buffer[iMessageIndex] = '·'; + message_buffer[iMessageIndex] = '�'; + mobitex_buffer[iMessageIndex] = '�'; } else if ((cin > 0) && (cin < 32)) { message_buffer[iMessageIndex] = cin+32; mobitex_buffer[iMessageIndex] = cin; // Keep original characters } - else if ((cin > 126) && (cin != '»')) + else if ((cin > 126) && (cin != '�')) { message_buffer[iMessageIndex] = ' '; mobitex_buffer[iMessageIndex] = cin; // Keep original characters @@ -211,7 +212,7 @@ void display_show_char(PaneStruct *pane, char cin) { if (cin == '\n') { - cin = '»'; // PH: Convert 'new line' to '»' + cin = '�'; // PH: Convert 'new line' to '�' } else if (cin > 127) { @@ -956,7 +957,7 @@ void ShowMessage() } } } - else if (ch == '»') // Check for linefeed character '»' + else if (ch == '�') // Check for linefeed character '�' { if (Profile.Linefeed) { @@ -1328,6 +1329,19 @@ void ShowMessage() szCurrentLabel[0]); } + if (Profile.http_post_enabled) + { + HttpPostQueueMessage(bMATCH, bMONITOR_ONLY, + Current_MSG[MSG_CAPCODE], + Current_MSG[MSG_TIME], + Current_MSG[MSG_DATE], + Current_MSG[MSG_MODE], + Current_MSG[MSG_TYPE], + Current_MSG[MSG_BITRATE], + iMOBITEX ? Current_MSG[MSG_MOBITEX] : Current_MSG[MSG_MESSAGE], + szCurrentLabel[0]); + } + if (Current_MSG[MSG_MOBITEX][0]) Current_MSG[MSG_MOBITEX][0] = '\0'; } // end of ShowMessage() @@ -2119,7 +2133,7 @@ void CollectLogfileLine(char *string, bool bFilter) { for (int pos=0; Current_MSG[MSG_MOBITEX][pos]!=0; pos++) { - if (Current_MSG[MSG_MOBITEX][pos] == '»') + if (Current_MSG[MSG_MOBITEX][pos] == '�') { strcat(szLogFileLine, "\n"); for (int i=0; ihttp_post_enabled = static_cast(GetPrivateProfileInt("HTTPPost", TEXT("Enable"), 0, lpszIniPathName)); + GetPrivateProfileString("HTTPPost", TEXT("Url"), "", pProfile->http_post_url, HTTP_POST_URL_LEN, lpszIniPathName); + pProfile->http_post_auth = static_cast(GetPrivateProfileInt("HTTPPost", TEXT("Auth"), 0, lpszIniPathName)); + GetPrivateProfileString("HTTPPost", TEXT("User"), "", pProfile->http_post_user, MAIL_TEXT_LEN, lpszIniPathName); + GetPrivateProfileString("HTTPPost", TEXT("Password"), "", pProfile->http_post_password, MAIL_TEXT_LEN, lpszIniPathName); + pProfile->http_post_queue_max = static_cast(GetPrivateProfileInt("HTTPPost", TEXT("QueueMax"), 100, lpszIniPathName)); + pProfile->http_post_queue_ttl = static_cast(GetPrivateProfileInt("HTTPPost", TEXT("QueueTTL"), 300, lpszIniPathName)); + + HttpPostInit(pProfile->http_post_enabled, pProfile->http_post_url, pProfile->http_post_auth, pProfile->http_post_user, pProfile->http_post_password, pProfile->http_post_queue_max, pProfile->http_post_queue_ttl); + /***** Get Filter settings *****/ pProfile->filterfile_enabled = (INT) GetPrivateProfileInt("Filter", TEXT("FilterFileEnabled"), pProfile->filterfile_enabled, lpszIniPathName); @@ -10114,6 +10189,15 @@ void WriteSettings() fprintf(pFile, "Options=%i\n", Profile.nMailOptions); fprintf(pFile, "SSL=%i\n", Profile.ssl); + fprintf(pFile, "\n[HTTPPost]\n"); + fprintf(pFile, "Enable=%i\n", Profile.http_post_enabled); + fprintf(pFile, "Url=%s\n", Profile.http_post_url); + fprintf(pFile, "Auth=%i\n", Profile.http_post_auth); + fprintf(pFile, "User=%s\n", Profile.http_post_user); + fprintf(pFile, "Password=%s\n", Profile.http_post_password); + fprintf(pFile, "QueueMax=%i\n", Profile.http_post_queue_max); + fprintf(pFile, "QueueTTL=%i\n", Profile.http_post_queue_ttl); + fprintf(pFile, "\n[Filter]\n"); fprintf(pFile, "FilterFileEnabled=%i\n", Profile.filterfile_enabled); fprintf(pFile, "FilterFile=%s\n", Profile.filterfile); diff --git a/Rsrc.rc b/Rsrc.rc index f9efa22..d035e51 100644 --- a/Rsrc.rc +++ b/Rsrc.rc @@ -84,6 +84,7 @@ BEGIN MENUITEM "&Options...\tCtrl-O", IDM_OPTIONS MENUITEM "&General...\tCtrl-G", IDM_GENERAL MENUITEM "&SMTP/Email...\tCtrl-M", IDM_MAIL + MENUITEM "HTTP &POST...", IDM_HTTPPOST END POPUP "Filters" BEGIN @@ -833,6 +834,27 @@ BEGIN CONTROL "SSL",IDC_SMTP_SSL,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,253,23,25,10 END +HTTPPOST_DLGBOX DIALOGEX 0, 0, 300, 142 +STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "HTTP POST settings" +FONT 8, "Verdana", 0, 0, 0x0 +BEGIN + CONTROL "HTTP POST enabled",IDC_HTTPPOST_ENABLE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,10,8,90,10 + LTEXT "Endpoint URL (http/https)",IDC_STATIC,10,23,109,8 + EDITTEXT IDC_HTTPPOST_URL,10,33,280,13,ES_AUTOHSCROLL + CONTROL "Use Basic Authentication",IDC_HTTPPOST_AUTH,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,10,52,108,10 + LTEXT "Username",IDC_STATIC,10,66,33,8 + EDITTEXT IDC_HTTPPOST_USER,56,63,102,13,ES_AUTOHSCROLL + LTEXT "Password",IDC_STATIC,166,66,32,8 + EDITTEXT IDC_HTTPPOST_PASSWORD,206,63,84,13,ES_PASSWORD | ES_AUTOHSCROLL + LTEXT "Queue max size",IDC_STATIC,10,88,49,8 + EDITTEXT IDC_HTTPPOST_QUEUE_MAX,74,85,40,13,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "Queue TTL (sec)",IDC_STATIC,130,88,52,8 + EDITTEXT IDC_HTTPPOST_QUEUE_TTL,194,85,40,13,ES_AUTOHSCROLL | ES_NUMBER + DEFPUSHBUTTON "OK",IDOK,188,120,47,14 + PUSHBUTTON "Cancel",IDCANCEL,243,120,47,14 +END + #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index a8fda7a..655b571 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -8,6 +8,7 @@ set( PDW_INCLUDES ${CMAKE_CURRENT_SOURCE_DIR}/binary.h #${CMAKE_CURRENT_SOURCE_DIR}/debug.h #${CMAKE_CURRENT_SOURCE_DIR}/globals.h + ${CMAKE_CURRENT_SOURCE_DIR}/http_post.h #${CMAKE_CURRENT_SOURCE_DIR}/ipcnfg.h ${CMAKE_CURRENT_SOURCE_DIR}/rs232.h ${CMAKE_CURRENT_SOURCE_DIR}/smtp.h @@ -19,6 +20,7 @@ set( PDW_INCLUDES set( PDW_SRCS ${PDW_SRCS} ${CMAKE_CURRENT_SOURCE_DIR}/Debug.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/http_post.cpp #${CMAKE_CURRENT_SOURCE_DIR}/OSTYPE.C ${CMAKE_CURRENT_SOURCE_DIR}/Ostype.cpp ${CMAKE_CURRENT_SOURCE_DIR}/binary.cpp diff --git a/utils/http_post.cpp b/utils/http_post.cpp new file mode 100644 index 0000000..c9e173b --- /dev/null +++ b/utils/http_post.cpp @@ -0,0 +1,419 @@ +#include +#include +#include + +#include +#include + +#include "http_post.h" +#include "../utils/debug.h" + +#pragma comment(lib, "winhttp.lib") + +#define HTTPPOST_DEFAULT_QUEUE_MAX 100 +#define HTTPPOST_DEFAULT_TTL_SEC 300 +#define HTTPPOST_MAX_QUEUE_HARD 5000 + +typedef struct _HTTPPOSTCONFIG +{ + int enabled; + int authEnabled; + int queueMax; + int queueTtlSeconds; + std::string url; + std::string user; + std::string password; +} HTTPPOSTCONFIG; + +typedef struct _HTTPPOSTENTRY +{ + ULONGLONG enqueueTick; + std::string payload; +} HTTPPOSTENTRY; + +static HANDLE g_httpThread = 0; +static HANDLE g_httpEvent = 0; +static BOOL g_keepbusy = FALSE; +static BOOL g_httpStateInit = FALSE; +static CRITICAL_SECTION g_httpLock; +static HTTPPOSTCONFIG g_httpConfig; +static std::deque g_httpQueue; + +static int ClampQueueMax(int queueMax) +{ + if (queueMax < 1) return HTTPPOST_DEFAULT_QUEUE_MAX; + if (queueMax > HTTPPOST_MAX_QUEUE_HARD) return HTTPPOST_MAX_QUEUE_HARD; + return queueMax; +} + +static int ClampTtl(int ttl) +{ + if (ttl < 1) return HTTPPOST_DEFAULT_TTL_SEC; + if (ttl > 86400) return 86400; + return ttl; +} + +static std::wstring ToWide(const char *input) +{ + if (!input || !input[0]) return L""; + int needed = MultiByteToWideChar(CP_UTF8, 0, input, -1, NULL, 0); + UINT codePage = CP_UTF8; + if (needed <= 0) + { + codePage = CP_ACP; + needed = MultiByteToWideChar(codePage, 0, input, -1, NULL, 0); + } + if (needed <= 0) return L""; + + std::wstring out; + out.resize(needed); + if (!out.empty()) + { + int ok = MultiByteToWideChar(codePage, 0, input, -1, &out[0], needed); + if (ok <= 0) return L""; + if (!out.empty() && out[out.size() - 1] == L'\0') out.resize(out.size() - 1); + } + return out; +} + +static BOOL BuildBasicAuthHeader(const std::string& user, const std::string& password, std::wstring& outHeader) +{ + std::string combined = user; + combined += ":"; + combined += password; + + DWORD outLen = 0; + if (!CryptBinaryToStringA((const BYTE*)combined.c_str(), (DWORD)combined.size(), CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, NULL, &outLen)) + { + return FALSE; + } + + std::string encoded; + encoded.resize(outLen + 2); + if (!CryptBinaryToStringA((const BYTE*)combined.c_str(), (DWORD)combined.size(), CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, &encoded[0], &outLen)) + { + return FALSE; + } + encoded.resize(outLen); + + std::string header = "Authorization: Basic "; + header += encoded; + header += "\r\n"; + outHeader = ToWide(header.c_str()); + return TRUE; +} + +static std::string ConvertToUtf8(const char *src, UINT codePage) +{ + if (!src || !src[0]) return ""; + + int wideLen = MultiByteToWideChar(codePage, MB_ERR_INVALID_CHARS, src, -1, NULL, 0); + if (wideLen <= 0) return ""; + + std::wstring wide; + wide.resize(wideLen); + + if (MultiByteToWideChar(codePage, MB_ERR_INVALID_CHARS, src, -1, &wide[0], wideLen) <= 0) + { + return ""; + } + + int utf8Len = WideCharToMultiByte(CP_UTF8, 0, wide.c_str(), -1, NULL, 0, NULL, NULL); + if (utf8Len <= 0) return ""; + + std::string utf8; + utf8.resize(utf8Len - 1); + + if (WideCharToMultiByte(CP_UTF8, 0, wide.c_str(), -1, &utf8[0], utf8Len, NULL, NULL) <= 0) + { + return ""; + } + + return utf8; +} + +static std::string ToUtf8Auto(const char *src) +{ + std::string utf8 = ConvertToUtf8(src, CP_UTF8); + if (!utf8.empty() || !src || !src[0]) + { + return utf8; + } + + return ConvertToUtf8(src, CP_ACP); +} + +static std::string JsonEscapeUtf8(const char *src) +{ + std::string utf8 = ToUtf8Auto(src); + std::string out; + + for (size_t i = 0; i < utf8.size(); ++i) + { + unsigned char ch = (unsigned char)utf8[i]; + switch (ch) + { + case '\"': out += "\\\""; break; + case '\\': out += "\\\\"; break; + case '\b': out += "\\b"; break; + case '\f': out += "\\f"; break; + case '\n': out += "\\n"; break; + case '\r': out += "\\r"; break; + case '\t': out += "\\t"; break; + default: + if (ch < 0x20) + { + char tmp[7]; + wsprintfA(tmp, "\\u%04X", ch); + out += tmp; + } + else + { + out += (char)ch; + } + break; + } + } + return out; +} + +static BOOL SendJsonPost(const HTTPPOSTCONFIG& cfg, const std::string& payload) +{ + if (!cfg.enabled || cfg.url.empty()) return FALSE; + + std::wstring wurl = ToWide(cfg.url.c_str()); + if (wurl.empty()) return FALSE; + + URL_COMPONENTS uc; + ZeroMemory(&uc, sizeof(uc)); + uc.dwStructSize = sizeof(uc); + + wchar_t hostName[512] = { 0 }; + wchar_t urlPath[2048] = { 0 }; + wchar_t extraInfo[2048] = { 0 }; + uc.lpszHostName = hostName; + uc.dwHostNameLength = (DWORD)(sizeof(hostName) / sizeof(hostName[0])); + uc.lpszUrlPath = urlPath; + uc.dwUrlPathLength = (DWORD)(sizeof(urlPath) / sizeof(urlPath[0])); + uc.lpszExtraInfo = extraInfo; + uc.dwExtraInfoLength = (DWORD)(sizeof(extraInfo) / sizeof(extraInfo[0])); + + if (!WinHttpCrackUrl(wurl.c_str(), 0, 0, &uc)) + { + return FALSE; + } + + std::wstring whost(hostName, uc.dwHostNameLength); + std::wstring wpath(urlPath, uc.dwUrlPathLength); + wpath += std::wstring(extraInfo, uc.dwExtraInfoLength); + if (wpath.empty()) wpath = L"/"; + + HINTERNET hSession = WinHttpOpen(L"PDW-HTTPPOST/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); + if (!hSession) return FALSE; + + HINTERNET hConnect = WinHttpConnect(hSession, whost.c_str(), uc.nPort, 0); + if (!hConnect) + { + WinHttpCloseHandle(hSession); + return FALSE; + } + + DWORD dwOpenFlags = (uc.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0; + HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"POST", wpath.c_str(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, dwOpenFlags); + if (!hRequest) + { + WinHttpCloseHandle(hConnect); + WinHttpCloseHandle(hSession); + return FALSE; + } + + WinHttpSetTimeouts(hRequest, 5000, 5000, 10000, 15000); + + std::wstring headers = L"Content-Type: application/json\r\n"; + if (cfg.authEnabled) + { + std::wstring authHeader; + if (BuildBasicAuthHeader(cfg.user, cfg.password, authHeader)) + { + headers += authHeader; + } + } + + BOOL sent = WinHttpSendRequest(hRequest, + headers.c_str(), + (DWORD)-1L, + (LPVOID)payload.c_str(), + (DWORD)payload.size(), + (DWORD)payload.size(), + 0); + + if (!sent || !WinHttpReceiveResponse(hRequest, NULL)) + { + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hConnect); + WinHttpCloseHandle(hSession); + return FALSE; + } + + DWORD statusCode = 0; + DWORD statusSize = sizeof(statusCode); + BOOL gotStatus = WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &statusCode, &statusSize, WINHTTP_NO_HEADER_INDEX); + + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hConnect); + WinHttpCloseHandle(hSession); + + if (!gotStatus) return FALSE; + return (statusCode >= 200 && statusCode < 300); +} + +static DWORD WINAPI HttpPostThreadFunc(LPVOID) +{ + while (g_keepbusy) + { + HTTPPOSTENTRY item; + HTTPPOSTCONFIG cfg; + BOOL hasItem = FALSE; + + EnterCriticalSection(&g_httpLock); + if (!g_httpQueue.empty()) + { + item = g_httpQueue.front(); + g_httpQueue.pop_front(); + hasItem = TRUE; + } + cfg = g_httpConfig; + LeaveCriticalSection(&g_httpLock); + + if (!hasItem) + { + WaitForSingleObject(g_httpEvent, 1000); + ResetEvent(g_httpEvent); + continue; + } + + ULONGLONG ageMs = GetTickCount64() - item.enqueueTick; + ULONGLONG ttlMs = (ULONGLONG)cfg.queueTtlSeconds * 1000ULL; + if (ageMs > ttlMs) + { + OUTPUTDEBUGMSG((("HTTP POST queue: dropping expired item (TTL exceeded)"))); + continue; + } + + if (!SendJsonPost(cfg, item.payload)) + { + EnterCriticalSection(&g_httpLock); + g_httpQueue.push_front(item); + LeaveCriticalSection(&g_httpLock); + Sleep(1000); + } + } + return 0; +} + +static void EnsureHttpState() +{ + if (g_httpStateInit) return; + InitializeCriticalSection(&g_httpLock); + g_httpEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + g_httpConfig.enabled = 0; + g_httpConfig.authEnabled = 0; + g_httpConfig.queueMax = HTTPPOST_DEFAULT_QUEUE_MAX; + g_httpConfig.queueTtlSeconds = HTTPPOST_DEFAULT_TTL_SEC; + g_httpStateInit = TRUE; +} + +int HttpPostInit(int enabled, const char *url, int authEnabled, const char *user, const char *password, int queueMax, int queueTtlSeconds) +{ + EnsureHttpState(); + + HTTPPOSTCONFIG cfg; + cfg.enabled = enabled ? 1 : 0; + cfg.authEnabled = authEnabled ? 1 : 0; + cfg.queueMax = ClampQueueMax(queueMax); + cfg.queueTtlSeconds = ClampTtl(queueTtlSeconds); + cfg.url = url ? url : ""; + cfg.user = user ? user : ""; + cfg.password = password ? password : ""; + + BOOL shouldRun = (cfg.enabled && !cfg.url.empty()) ? TRUE : FALSE; + + EnterCriticalSection(&g_httpLock); + g_httpConfig = cfg; + LeaveCriticalSection(&g_httpLock); + + if (shouldRun) + { + if (!g_httpThread) + { + DWORD threadId = 0; + g_keepbusy = TRUE; + g_httpThread = CreateThread(0, 0, HttpPostThreadFunc, 0, 0, &threadId); + } + SetEvent(g_httpEvent); + } + else + { + if (g_httpThread) + { + g_keepbusy = FALSE; + SetEvent(g_httpEvent); + WaitForSingleObject(g_httpThread, 3000); + CloseHandle(g_httpThread); + g_httpThread = 0; + } + + EnterCriticalSection(&g_httpLock); + g_httpQueue.clear(); + LeaveCriticalSection(&g_httpLock); + } + return 0; +} + +void HttpPostShutdown() +{ + HttpPostInit(0, "", 0, "", "", HTTPPOST_DEFAULT_QUEUE_MAX, HTTPPOST_DEFAULT_TTL_SEC); +} + +int HttpPostQueueMessage(int bMatch, int bMonitorOnly, + const char *sz1, const char *sz2, const char *sz3, const char *sz4, + const char *sz5, const char *sz6, const char *sz7, const char *szLabel) +{ + EnsureHttpState(); + + HTTPPOSTCONFIG cfg; + EnterCriticalSection(&g_httpLock); + cfg = g_httpConfig; + LeaveCriticalSection(&g_httpLock); + + if (!cfg.enabled || cfg.url.empty()) return 0; + + std::string json = "{"; + json += "\"address\":\"" + JsonEscapeUtf8(sz1) + "\","; + json += "\"time\":\"" + JsonEscapeUtf8(sz2) + "\","; + json += "\"date\":\"" + JsonEscapeUtf8(sz3) + "\","; + json += "\"mode\":\"" + JsonEscapeUtf8(sz4) + "\","; + json += "\"type\":\"" + JsonEscapeUtf8(sz5) + "\","; + json += "\"bitrate\":\"" + JsonEscapeUtf8(sz6) + "\","; + json += "\"message\":\"" + JsonEscapeUtf8(sz7) + "\","; + json += "\"label\":\"" + JsonEscapeUtf8(szLabel) + "\","; + json += "\"match\":" + std::string(bMatch ? "true" : "false") + ","; + json += "\"monitor_only\":" + std::string(bMonitorOnly ? "true" : "false"); + json += "}"; + + HTTPPOSTENTRY entry; + entry.enqueueTick = GetTickCount64(); + entry.payload = json; + + EnterCriticalSection(&g_httpLock); + if (g_httpQueue.size() >= (size_t)cfg.queueMax) + { + g_httpQueue.pop_front(); + OUTPUTDEBUGMSG((("HTTP POST queue: queue full, dropping oldest item"))); + } + g_httpQueue.push_back(entry); + LeaveCriticalSection(&g_httpLock); + + SetEvent(g_httpEvent); + return 0; +} diff --git a/utils/http_post.h b/utils/http_post.h new file mode 100644 index 0000000..0145766 --- /dev/null +++ b/utils/http_post.h @@ -0,0 +1,10 @@ +#ifndef HTTP_POST_H +#define HTTP_POST_H + +int HttpPostInit(int enabled, const char *url, int authEnabled, const char *user, const char *password, int queueMax, int queueTtlSeconds); +void HttpPostShutdown(); +int HttpPostQueueMessage(int bMatch, int bMonitorOnly, + const char *sz1, const char *sz2, const char *sz3, const char *sz4, + const char *sz5, const char *sz6, const char *sz7, const char *szLabel); + +#endif /* ! HTTP_POST_H */ From b77ec6668fe6e1ef55fed6a8cb5f4926c5d1fef0 Mon Sep 17 00:00:00 2001 From: Muspah Date: Wed, 15 Apr 2026 21:14:57 +0200 Subject: [PATCH 2/4] Use vcpkg and nlohmann:json --- .github/workflows/build.yml | 42 +++++++++++++------- CMakeLists.txt | 10 ++++- utils/Debug.cpp | 2 +- utils/debug.h | 2 +- utils/http_post.cpp | 79 ++++++++++++++++++++++--------------- vcpkg.json | 7 ++++ 6 files changed, 92 insertions(+), 50 deletions(-) create mode 100644 vcpkg.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e3b5ded..200c275 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,26 +10,38 @@ jobs: build: runs-on: windows-latest + strategy: + matrix: + arch: [Win32, x64] + + env: + VCPKG_ROOT: ${{ github.workspace }}/vcpkg + VCPKG_DEFAULT_TRIPLET: ${{ matrix.arch == 'Win32' && 'x86-windows' || 'x64-windows' }} + steps: - - name: Checkout repository + - name: Checkout uses: actions/checkout@v4 - - name: Configure CMake (Win32 / Release) + - name: Install Ninja and Git shell: pwsh run: | - cmake -S . -B build ` - -G "Visual Studio 17 2022" ` - -A Win32 ` - -DCMAKE_BUILD_TYPE=Release + choco install ninja -y --no-progress - - name: Build + - name: Clone vcpkg shell: pwsh run: | - cmake --build build --config Release --target PDW - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: PDW-Win32-Release - path: build\Release\PDW.exe - if-no-files-found: error + git clone https://github.com/microsoft/vcpkg.git $env:VCPKG_ROOT + & "$env:VCPKG_ROOT\bootstrap-vcpkg.bat" + + - name: Configure CMake + shell: pwsh + run: > + cmake -S . -B build + -G "Ninja" + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake + -DVCPKG_TARGET_TRIPLET=${{ env.VCPKG_DEFAULT_TRIPLET }} + + - name: Build + shell: pwsh + run: cmake --build build --config Release diff --git a/CMakeLists.txt b/CMakeLists.txt index 99c4a80..2ee5d15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,10 @@ # Base build script for building with cmake (http://www.cmake.org) cmake_minimum_required( VERSION 3.5 ) -project( PDW ) +project(PDW LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) # Enforce Win32 (x86) if( CMAKE_SIZEOF_VOID_P EQUAL 8 ) @@ -61,6 +64,8 @@ set( PDW_SRCS sound_in.cpp ) +find_package(nlohmann_json CONFIG REQUIRED) + # Create executable add_executable( PDW WIN32 ${PDW_INCLUDES} @@ -68,7 +73,8 @@ add_executable( PDW WIN32 ) # Required libraries -target_link_libraries( PDW +target_link_libraries( PDW PRIVATE + nlohmann_json::nlohmann_json Comctl32.lib Winmm.lib Ws2_32.lib diff --git a/utils/Debug.cpp b/utils/Debug.cpp index b2195a8..6903b16 100644 --- a/utils/Debug.cpp +++ b/utils/Debug.cpp @@ -18,7 +18,7 @@ int nDebugNum ; char szDebug[MAX_DEBUG] ; -int DebugPrintf(char *fmt,...) +int DebugPrintf(const char *fmt,...) { int len ; va_list marker; diff --git a/utils/debug.h b/utils/debug.h index 6df8771..9dd18f0 100644 --- a/utils/debug.h +++ b/utils/debug.h @@ -8,7 +8,7 @@ extern int nDebugNum ; #if defined _DEBUG || defined DBG -extern int DebugPrintf(char *fmt,...) ; +extern int DebugPrintf(const char *fmt,...) ; #define OUTPUTDEBUGMSG(dprintf_exp) ((void) (DebugPrintf dprintf_exp)) diff --git a/utils/http_post.cpp b/utils/http_post.cpp index c9e173b..4dbd184 100644 --- a/utils/http_post.cpp +++ b/utils/http_post.cpp @@ -8,6 +8,8 @@ #include "http_post.h" #include "../utils/debug.h" +#include + #pragma comment(lib, "winhttp.lib") #define HTTPPOST_DEFAULT_QUEUE_MAX 100 @@ -381,39 +383,54 @@ int HttpPostQueueMessage(int bMatch, int bMonitorOnly, { EnsureHttpState(); - HTTPPOSTCONFIG cfg; - EnterCriticalSection(&g_httpLock); - cfg = g_httpConfig; - LeaveCriticalSection(&g_httpLock); + try + { + HTTPPOSTCONFIG cfg; + EnterCriticalSection(&g_httpLock); + cfg = g_httpConfig; + LeaveCriticalSection(&g_httpLock); - if (!cfg.enabled || cfg.url.empty()) return 0; - - std::string json = "{"; - json += "\"address\":\"" + JsonEscapeUtf8(sz1) + "\","; - json += "\"time\":\"" + JsonEscapeUtf8(sz2) + "\","; - json += "\"date\":\"" + JsonEscapeUtf8(sz3) + "\","; - json += "\"mode\":\"" + JsonEscapeUtf8(sz4) + "\","; - json += "\"type\":\"" + JsonEscapeUtf8(sz5) + "\","; - json += "\"bitrate\":\"" + JsonEscapeUtf8(sz6) + "\","; - json += "\"message\":\"" + JsonEscapeUtf8(sz7) + "\","; - json += "\"label\":\"" + JsonEscapeUtf8(szLabel) + "\","; - json += "\"match\":" + std::string(bMatch ? "true" : "false") + ","; - json += "\"monitor_only\":" + std::string(bMonitorOnly ? "true" : "false"); - json += "}"; - - HTTPPOSTENTRY entry; - entry.enqueueTick = GetTickCount64(); - entry.payload = json; + if (!cfg.enabled || cfg.url.empty()) return 0; - EnterCriticalSection(&g_httpLock); - if (g_httpQueue.size() >= (size_t)cfg.queueMax) + nlohmann::json j; + j["address"] = sz1 ? sz1 : ""; + j["time"] = sz2 ? sz2 : ""; + j["date"] = sz3 ? sz3 : ""; + j["mode"] = sz4 ? sz4 : ""; + j["type"] = sz5 ? sz5 : ""; + j["bitrate"] = sz6 ? sz6 : ""; + j["message"] = sz7 ? sz7 : ""; + j["label"] = szLabel ? szLabel : ""; + j["match"] = (bMatch != 0); + j["monitor_only"] = (bMonitorOnly != 0); + + const std::string json = j.dump(); + + HTTPPOSTENTRY entry; + entry.enqueueTick = GetTickCount64(); + entry.payload = json; + + EnterCriticalSection(&g_httpLock); + if (g_httpQueue.size() >= (size_t)cfg.queueMax) + { + g_httpQueue.pop_front(); + OUTPUTDEBUGMSG((("HTTP POST queue: queue full, dropping oldest item"))); + } + g_httpQueue.push_back(entry); + LeaveCriticalSection(&g_httpLock); + + SetEvent(g_httpEvent); + return 0; + } + catch (const std::exception& e) { - g_httpQueue.pop_front(); - OUTPUTDEBUGMSG((("HTTP POST queue: queue full, dropping oldest item"))); + OUTPUTDEBUGMSG(("HTTP POST exception: ")); + OUTPUTDEBUGMSG((e.what())); + return 0; + } + catch (...) + { + OUTPUTDEBUGMSG(("HTTP POST unknown exception")); + return 0; } - g_httpQueue.push_back(entry); - LeaveCriticalSection(&g_httpLock); - - SetEvent(g_httpEvent); - return 0; } diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..c99e1a0 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,7 @@ +{ + "name": "pdw", + "version-string": "3.1.0", + "dependencies": [ + "nlohmann-json" + ] +} \ No newline at end of file From 4f8936b6355199398fc3fc5e4d5f58e00021d50f Mon Sep 17 00:00:00 2001 From: Muspah Date: Wed, 15 Apr 2026 21:23:52 +0200 Subject: [PATCH 3/4] Fix GH building --- .github/workflows/build.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 200c275..41b9748 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,10 +10,6 @@ jobs: build: runs-on: windows-latest - strategy: - matrix: - arch: [Win32, x64] - env: VCPKG_ROOT: ${{ github.workspace }}/vcpkg VCPKG_DEFAULT_TRIPLET: ${{ matrix.arch == 'Win32' && 'x86-windows' || 'x64-windows' }} @@ -36,8 +32,9 @@ jobs: - name: Configure CMake shell: pwsh run: > - cmake -S . -B build - -G "Ninja" + cmake -S . -B build ` + -G "Visual Studio 17 2022" ` + -A Win32 ` -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=${{ env.VCPKG_DEFAULT_TRIPLET }} From db7b21c2fc5b9ed473adcf2abc7892c238ff4dd2 Mon Sep 17 00:00:00 2001 From: Muspah Date: Thu, 23 Apr 2026 17:49:34 +0200 Subject: [PATCH 4/4] Create ISO8601 timestamp and add to JSON --- utils/http_post.cpp | 107 ++++++++++++++++++++++++++++++++++++++------ utils/http_post.h | 4 +- 2 files changed, 96 insertions(+), 15 deletions(-) diff --git a/utils/http_post.cpp b/utils/http_post.cpp index 4dbd184..61dab4c 100644 --- a/utils/http_post.cpp +++ b/utils/http_post.cpp @@ -2,10 +2,12 @@ #include #include +#include #include #include #include "http_post.h" +#include "../headers/pdw.h" #include "../utils/debug.h" #include @@ -41,6 +43,85 @@ static CRITICAL_SECTION g_httpLock; static HTTPPOSTCONFIG g_httpConfig; static std::deque g_httpQueue; +static int Normalize2DigitYear(int y) +{ + if (y >= 0 && y <= 69) return 2000 + y; + if (y >= 70 && y <= 99) return 1900 + y; + return y; +} + +static bool ParseDateByProfileFormat(const char* s, int dateFormat, int& y, int& m, int& d) +{ + int a = 0, b = 0, c = 0; + char sep1 = 0, sep2 = 0; + + if (!s || !*s) return false; + if (std::sscanf(s, "%d%c%d%c%d", &a, &sep1, &b, &sep2, &c) != 5) return false; + if (sep1 != sep2) return false; + + y = m = d = 0; + + switch (dateFormat) + { + case 0: // dd-mm-yy + d = a; m = b; y = Normalize2DigitYear(c); + break; + case 1: // mm-dd-yy + m = a; d = b; y = Normalize2DigitYear(c); + break; + case 2: // yy-mm-dd + y = Normalize2DigitYear(a); m = b; d = c; + break; + default: + return false; + } + + return (y >= 1601 && m >= 1 && m <= 12 && d >= 1 && d <= 31); +} + +static bool ParseTimeHHMMSS(const char* s, int& hh, int& mm, int& ss) +{ + hh = mm = ss = 0; + if (!s || !*s) return false; + return std::sscanf(s, "%d:%d:%d", &hh, &mm, &ss) >= 2; +} + +static std::string BuildIso8601Timestamp(const char* szDate, const char* szTime, const int dateFormat) +{ + int y, m, d, hh, mm, ss; + if (!ParseDateByProfileFormat(szDate, dateFormat, y, m, d)) + return ""; + if (!ParseTimeHHMMSS(szTime, hh, mm, ss)) + return ""; + + char base[32]; + std::snprintf(base, sizeof(base), "%04d-%02d-%02dT%02d:%02d:%02d", y, m, d, hh, mm, ss); + + TIME_ZONE_INFORMATION tzi; + DWORD tzId = GetTimeZoneInformation(&tzi); + + LONG biasMinutes = tzi.Bias; + if (tzId == TIME_ZONE_ID_STANDARD) + biasMinutes += tzi.StandardBias; + else if (tzId == TIME_ZONE_ID_DAYLIGHT) + biasMinutes += tzi.DaylightBias; + + int offsetMinutes = -biasMinutes; + char sign = '+'; + if (offsetMinutes < 0) + { + sign = '-'; + offsetMinutes = -offsetMinutes; + } + + char tzbuf[8]; + std::snprintf(tzbuf, sizeof(tzbuf), "%c%02d:%02d", sign, offsetMinutes / 60, offsetMinutes % 60); + + std::string out = base; + out += tzbuf; + return out; +} + static int ClampQueueMax(int queueMax) { if (queueMax < 1) return HTTPPOST_DEFAULT_QUEUE_MAX; @@ -377,29 +458,29 @@ void HttpPostShutdown() HttpPostInit(0, "", 0, "", "", HTTPPOST_DEFAULT_QUEUE_MAX, HTTPPOST_DEFAULT_TTL_SEC); } -int HttpPostQueueMessage(int bMatch, int bMonitorOnly, - const char *sz1, const char *sz2, const char *sz3, const char *sz4, - const char *sz5, const char *sz6, const char *sz7, const char *szLabel) +int HttpPostQueueMessage(const int bMatch, const int bMonitorOnly, + const char *szCapcode, const char *szTime, const char *szDate, const char *szMode, + const char *szType, const char *szBitrate, const char *szMessage, const char *szLabel) { EnsureHttpState(); try { - HTTPPOSTCONFIG cfg; EnterCriticalSection(&g_httpLock); - cfg = g_httpConfig; + const HTTPPOSTCONFIG cfg = g_httpConfig; LeaveCriticalSection(&g_httpLock); if (!cfg.enabled || cfg.url.empty()) return 0; nlohmann::json j; - j["address"] = sz1 ? sz1 : ""; - j["time"] = sz2 ? sz2 : ""; - j["date"] = sz3 ? sz3 : ""; - j["mode"] = sz4 ? sz4 : ""; - j["type"] = sz5 ? sz5 : ""; - j["bitrate"] = sz6 ? sz6 : ""; - j["message"] = sz7 ? sz7 : ""; + j["address"] = szCapcode ? szCapcode : ""; + j["time"] = szTime ? szTime : ""; + j["date"] = szDate ? szDate : ""; + j["timestamp"] = BuildIso8601Timestamp(szDate, szTime, Profile.DateFormat); + j["mode"] = szMode ? szMode : ""; + j["type"] = szType ? szType : ""; + j["bitrate"] = szBitrate ? szBitrate : ""; + j["message"] = szMessage ? szMessage : ""; j["label"] = szLabel ? szLabel : ""; j["match"] = (bMatch != 0); j["monitor_only"] = (bMonitorOnly != 0); @@ -411,7 +492,7 @@ int HttpPostQueueMessage(int bMatch, int bMonitorOnly, entry.payload = json; EnterCriticalSection(&g_httpLock); - if (g_httpQueue.size() >= (size_t)cfg.queueMax) + if (g_httpQueue.size() >= static_cast(cfg.queueMax)) { g_httpQueue.pop_front(); OUTPUTDEBUGMSG((("HTTP POST queue: queue full, dropping oldest item"))); diff --git a/utils/http_post.h b/utils/http_post.h index 0145766..dafc47e 100644 --- a/utils/http_post.h +++ b/utils/http_post.h @@ -4,7 +4,7 @@ int HttpPostInit(int enabled, const char *url, int authEnabled, const char *user, const char *password, int queueMax, int queueTtlSeconds); void HttpPostShutdown(); int HttpPostQueueMessage(int bMatch, int bMonitorOnly, - const char *sz1, const char *sz2, const char *sz3, const char *sz4, - const char *sz5, const char *sz6, const char *sz7, const char *szLabel); + const char *szCapcode, const char *szTime, const char *szDate, const char *szMode, + const char *szType, const char *szBitrate, const char *szMessage, const char *szLabel); #endif /* ! HTTP_POST_H */