Files
ghostwire/native/webhook_probe.cpp

155 lines
12 KiB
C++

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winhttp.h>
#include <inspectable.h>
#include <winstring.h>
#include <stdint.h>
#include <stdio.h>
#include <wchar.h>
#include <string>
#include "core_discovery.h"
struct Packet { volatile LONG state; LONG hr; DWORD thread,protocol; wchar_t script[8192],result[32768]; };
static const UINT Wake=WM_APP+0x499;
static const GUID CoreIID={0x3a3f559a,0xe5e9,0x5338,{0xbb,0x67,0x4e,0xb0,0x50,0x4a,0x4f,0x14}};
static const GUID InfoIID={0x00000036,0,0,{0xc0,0,0,0,0,0,0,0x46}};
static IUnknown *core=nullptr,*operation=nullptr,*info=nullptr;
static UINT_PTR timer=0;
static HMODULE resident=nullptr;
static DWORD uiThread=0;
static unsigned short port=0;
static bool secure=false;
static std::wstring endpoint,hostname=L"127.0.0.1",path=L"/whatsapp-webhook",token;
static volatile LONG wipeAfterWorker=0;
static HANDLE worker=nullptr;
static volatile LONG httpOK=0;
static ULONG delivered=0,attempts=0;
static std::wstring body,ack;
static bool acknowledging=false;
static ULONGLONG operationStarted=0;
static void** table(void* p){return *(void***)p;}
static void wipe(std::wstring& value){if(!value.empty())SecureZeroMemory(value.data(),value.size()*sizeof(wchar_t));value.clear();}
struct Destination {unsigned short port=0;bool secure=false;std::wstring endpoint,host,path,token;~Destination(){wipe(token);}};
static bool parseDestination(const wchar_t* text,Destination& out){
const wchar_t* split=wcschr(text,L'\n');
if(!split){wchar_t* end=nullptr;auto value=wcstoul(text,&end,10);if(!value||value>65535||!end||*end)return false;out.port=(unsigned short)value;out.host=L"127.0.0.1";out.path=L"/whatsapp-webhook";return true;}
out.endpoint.assign(text,split-text);out.token.assign(split+1);
if(out.token.size()<32||out.token.size()>128)return false;
for(auto ch:out.token)if(ch<33||ch>126)return false;
if(out.endpoint.find_first_of(L"\r\n?#")!=std::wstring::npos)return false;
URL_COMPONENTS url{};url.dwStructSize=sizeof(url);url.dwHostNameLength=url.dwUrlPathLength=url.dwUserNameLength=url.dwPasswordLength=url.dwExtraInfoLength=(DWORD)-1;
if(!WinHttpCrackUrl(out.endpoint.c_str(),(DWORD)out.endpoint.size(),0,&url)||url.nScheme!=INTERNET_SCHEME_HTTPS||!url.dwHostNameLength||url.dwUserNameLength||url.dwPasswordLength||url.dwExtraInfoLength)return false;
out.secure=true;out.port=url.nPort;out.host.assign(url.lpszHostName,url.dwHostNameLength);out.path.assign(url.lpszUrlPath,url.dwUrlPathLength);
return !out.path.empty()&&out.path[0]==L'/';
}
static void clearAsync(){
if(info){using Action=HRESULT(STDMETHODCALLTYPE*)(void*);((Action)table(info)[9])(info);((Action)table(info)[10])(info);info->Release();info=nullptr;}
if(operation){operation->Release();operation=nullptr;}
}
static HRESULT findCore(){
auto module=(uintptr_t)GetModuleHandleW(L"Microsoft.Web.WebView2.Core.dll");
CoreLayout layout{};
if(!module||!discoverCoreLayout(module,layout))return HRESULT_FROM_WIN32(ERROR_REVISION_MISMATCH);
MEMORY_BASIC_INFORMATION mbi;uintptr_t address=0,scanned=0;
while(VirtualQuery((void*)address,&mbi,sizeof(mbi))){
uintptr_t end=(uintptr_t)mbi.BaseAddress+mbi.RegionSize;if(end<=address)break;address=end;
if(mbi.State!=MEM_COMMIT||mbi.Type!=MEM_PRIVATE||mbi.Protect!=PAGE_READWRITE||mbi.RegionSize>64*1024*1024)continue;
if(scanned+mbi.RegionSize>256*1024*1024)break;scanned+=mbi.RegionSize;
for(uintptr_t p=(uintptr_t)mbi.BaseAddress;p+0x118<=end;p+=8){
if(*(uintptr_t*)p!=layout.primary||*(uintptr_t*)(p+0x108)!=layout.secondary||table((void*)p)[0]!=(void*)layout.query)continue;
if(FAILED(((IUnknown*)p)->QueryInterface(CoreIID,(void**)&core)))continue;
if(!supportedExecute(table(core)[49])){core->Release();core=nullptr;continue;}return S_OK;
}
}return HRESULT_FROM_WIN32(ERROR_NOT_FOUND);
}
static HRESULT execute(const std::wstring& script){
HSTRING text=nullptr;auto hr=WindowsCreateString(script.c_str(),(UINT32)script.size(),&text);
if(FAILED(hr))return hr;
using Execute=HRESULT(STDMETHODCALLTYPE*)(void*,HSTRING,IUnknown**);
hr=((Execute)table(core)[49])(core,text,&operation);WindowsDeleteString(text);
if(SUCCEEDED(hr))hr=operation->QueryInterface(InfoIID,(void**)&info);
if(FAILED(hr))clearAsync();else operationStarted=GetTickCount64();return hr;
}
static DWORD WINAPI post(void*){
int count=WideCharToMultiByte(CP_UTF8,WC_ERR_INVALID_CHARS,body.c_str(),(int)body.size(),nullptr,0,nullptr,nullptr);
if(count<=0||count>131072)return 0;
std::string bytes(count,'\0');WideCharToMultiByte(CP_UTF8,WC_ERR_INVALID_CHARS,body.c_str(),(int)body.size(),bytes.data(),count,nullptr,nullptr);
HINTERNET session=WinHttpOpen(L"WhatsAppLocalWebhook/1",WINHTTP_ACCESS_TYPE_NO_PROXY,nullptr,nullptr,0);
HINTERNET connection=nullptr,request=nullptr;
if(session){WinHttpSetTimeouts(session,2000,2000,2000,2000);connection=WinHttpConnect(session,hostname.c_str(),port,0);}
if(connection)request=WinHttpOpenRequest(connection,L"POST",path.c_str(),nullptr,nullptr,nullptr,secure?WINHTTP_FLAG_SECURE:0);
DWORD policy=WINHTTP_OPTION_REDIRECT_POLICY_NEVER;
if(request){WinHttpSetOption(request,WINHTTP_OPTION_REDIRECT_POLICY,&policy,sizeof(policy));
std::wstring headers=L"Content-Type: application/json\r\n";if(secure)headers+=L"X-Webhook-Token: "+token+L"\r\n";
if(WinHttpSendRequest(request,headers.c_str(),(DWORD)headers.size(),bytes.data(),count,count,0)&&WinHttpReceiveResponse(request,nullptr)){
DWORD code=0,size=sizeof(code);if(WinHttpQueryHeaders(request,WINHTTP_QUERY_STATUS_CODE|WINHTTP_QUERY_FLAG_NUMBER,nullptr,&code,&size,nullptr)&&code>=200&&code<300)InterlockedExchange(&httpOK,1);
}wipe(headers);WinHttpCloseHandle(request);
}if(connection)WinHttpCloseHandle(connection);if(session)WinHttpCloseHandle(session);
if(InterlockedExchange(&wipeAfterWorker,0))wipe(token);return 0;
}
static void tick(){
if(!timer)return; // KillTimer may leave an already queued callback.
if(worker){if(WaitForSingleObject(worker,0)!=WAIT_OBJECT_0)return;CloseHandle(worker);worker=nullptr;
if(InterlockedExchange(&httpOK,0)){++delivered;acknowledging=true;if(FAILED(execute(ack)))acknowledging=false;return;}body.clear();ack.clear();}
if(operation){using Status=HRESULT(STDMETHODCALLTYPE*)(void*,INT32*);INT32 status=0;
if(FAILED(((Status)table(info)[7])(info,&status))||status>1||GetTickCount64()-operationStarted>10000){clearAsync();acknowledging=false;return;}
if(status==0)return;
using Results=HRESULT(STDMETHODCALLTYPE*)(void*,HSTRING*);HSTRING value=nullptr;
auto hr=((Results)table(operation)[8])(operation,&value);std::wstring result;
if(SUCCEEDED(hr)){UINT32 length=0;auto raw=WindowsGetStringRawBuffer(value,&length);if(length<=32768)result.assign(raw,length);WindowsDeleteString(value);}clearAsync();
if(acknowledging){acknowledging=false;body.clear();ack.clear();return;}
// Script returns an object, so WebView returns JSON directly, not an escaped JSON string.
auto start=result.find(L"\"seqs\":[");if(start==std::wstring::npos)return;start+=8;auto end=result.find(L']',start);
if(end==std::wstring::npos||end==start)return;
auto seqs=result.substr(start,end-start);if(seqs.find_first_not_of(L"0123456789,")!=std::wstring::npos)return;
body=result;ack=L"(()=>{const w=window.__whatsappWebhookV1;if(!w)return false;const ids=new Set(["+seqs+L"]);const before=w.queue.length;w.queue=w.queue.filter(e=>!ids.has(e.seq));w.delivered=(w.delivered||0)+before-w.queue.length;w.last_error=null;return true;})()";
++attempts;worker=CreateThread(nullptr,0,post,nullptr,0,nullptr);return;
}
execute(L"(()=>{const w=window.__whatsappWebhookV1;if(!w||w.stopped)return {seqs:[],events:[]};const events=[];for(const e of w.queue.slice(0,16)){if(JSON.stringify([...events,e]).length>30000)break;events.push(e);}return {listener_id:w.listener_id,seqs:events.map(e=>e.seq),events};})()");
}
static void CALLBACK timerProc(HWND,UINT,UINT_PTR,DWORD){__try{tick();}__except(EXCEPTION_EXECUTE_HANDLER){clearAsync();}}
static void command(){
wchar_t name[96];swprintf_s(name,L"Local\\WhatsAppWebhook-v1-%lu",GetCurrentProcessId());
HANDLE mapping=OpenFileMappingW(FILE_MAP_ALL_ACCESS,FALSE,name);if(!mapping)return;
Packet* packet=(Packet*)MapViewOfFile(mapping,FILE_MAP_ALL_ACCESS,0,0,sizeof(Packet));if(!packet){CloseHandle(mapping);return;}
HRESULT hr=S_OK;LONG result=-1;
if(packet->protocol!=0x57484231||packet->thread!=GetCurrentThreadId())hr=E_INVALIDARG;
else if(packet->state==0){
if(worker&&WaitForSingleObject(worker,0)==WAIT_OBJECT_0){CloseHandle(worker);worker=nullptr;body.clear();ack.clear();InterlockedExchange(&httpOK,0);}
packet->script[8191]=0;Destination destination;
bool parsed=parseDestination(packet->script,destination);SecureZeroMemory(packet->script,sizeof(packet->script));
if(!parsed)hr=E_INVALIDARG;
else if(timer){if(port!=destination.port||secure!=destination.secure||endpoint!=destination.endpoint||token!=destination.token)hr=HRESULT_FROM_WIN32(ERROR_BUSY);}
else if(worker)hr=HRESULT_FROM_WIN32(ERROR_BUSY);
else{if(!core)hr=findCore();if(SUCCEEDED(hr)){
if(!resident)GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,(LPCWSTR)&command,&resident);
if(!resident)hr=HRESULT_FROM_WIN32(GetLastError());else{wipe(token);port=destination.port;secure=destination.secure;endpoint=destination.endpoint;hostname=destination.host;path=destination.path;token=destination.token;uiThread=GetCurrentThreadId();timer=SetTimer(nullptr,0,500,timerProc);if(!timer){hr=HRESULT_FROM_WIN32(GetLastError());wipe(token);}}
}}result=2;
}else if(packet->state==6){if(timer){KillTimer(nullptr,timer);timer=0;}clearAsync();if(core){core->Release();core=nullptr;}
if(worker&&WaitForSingleObject(worker,0)!=WAIT_OBJECT_0){InterlockedExchange(&wipeAfterWorker,1);if(WaitForSingleObject(worker,0)==WAIT_OBJECT_0){InterlockedExchange(&wipeAfterWorker,0);wipe(token);}}
else wipe(token);result=7;
// Keep one module reference until process exit: a pending worker/callback may still execute.
}else if(packet->state==3)result=4;
else if(packet->state==2||packet->state==4||packet->state==7||packet->state==-1){
// The controller may queue several wake messages while discovery is running.
// Completed requests must retain their result when those messages arrive.
UnmapViewOfFile(packet);CloseHandle(mapping);return;
}else hr=E_INVALIDARG;
swprintf_s(packet->result,L"{\"running\":%s,\"port\":%u,\"delivered_batches\":%lu,\"attempts\":%lu}",timer?L"true":L"false",port,delivered,attempts);
packet->hr=hr;InterlockedExchange(&packet->state,FAILED(hr)?-1:result);UnmapViewOfFile(packet);CloseHandle(mapping);
}
extern "C" __declspec(dllexport) LRESULT CALLBACK ProbeHook(int code,WPARAM wParam,LPARAM lParam){
if(code>=0&&wParam==PM_REMOVE&&((MSG*)lParam)->message==Wake){((MSG*)lParam)->message=WM_NULL;__try{command();}__except(EXCEPTION_EXECUTE_HANDLER){}}
return CallNextHookEx(nullptr,code,wParam,lParam);
}
BOOL WINAPI DllMain(HINSTANCE,DWORD,LPVOID){return TRUE;}
#ifdef WEBHOOK_CONFIG_TEST
int main(){
const wchar_t* valid[]={L"8765",L"https://example.test/events\n0123456789abcdef0123456789abcdef",L"https://example.test:8443/events\n0123456789abcdef0123456789abcdef"};
const wchar_t* invalid[]={L"0",L"65536",L"8765x",L"http://example.test/events\n0123456789abcdef0123456789abcdef",L"https://user:[email protected]/events\n0123456789abcdef0123456789abcdef",L"https://example.test/events?secret=x\n0123456789abcdef0123456789abcdef",L"https://example.test/events#fragment\n0123456789abcdef0123456789abcdef",L"https://example.test/events\nshort",L"https://example.test/events\n0123456789abcdef0123456789abc\rdef",L"https://example.test/events\n0123456789abcdef0123456789abc\ndef"};
for(auto text:valid){Destination destination;if(!parseDestination(text,destination))return 1;}
for(auto text:invalid){Destination destination;if(parseDestination(text,destination))return 2;}
Destination remote;if(!parseDestination(valid[2],remote)||remote.port!=8443||!remote.secure||remote.host!=L"example.test"||remote.path!=L"/events")return 3;
puts("14 synthetic configuration checks passed");return 0;
}
#endif