432 lines
30 KiB
C++
432 lines
30 KiB
C++
#include <winsock2.h>
|
|
#include "lan_input.h"
|
|
#include "lan_mesh.h"
|
|
#include <iostream>
|
|
#include <deque>
|
|
|
|
using namespace ks;using namespace ks::lan;
|
|
namespace
|
|
{
|
|
int passed{};
|
|
void check(bool value,const char *text){if(!value)throw std::runtime_error(text);++passed;}
|
|
template<class F>void rejects(F f,const char *text){bool failed=false;try{f();}catch(...){failed=true;}check(failed,text);}
|
|
struct Desktop
|
|
{
|
|
RECT rect{-1920,-200,0,880};POINT point{-1000,300};bool available=true,physicalHeld=false,allow=true;
|
|
std::vector<INPUT> events;int dpi=96;bool delayed=false;InputRouter *router{};
|
|
InputEnvironment environment()
|
|
{
|
|
return {[this]{return rect;},[this]{return point;},[this](const INPUT &i){
|
|
if(!allow)return false;
|
|
events.push_back(i);
|
|
if(i.type==INPUT_MOUSE&&(i.mi.dwFlags&MOUSEEVENTF_MOVE)&&!delayed)
|
|
{
|
|
point={rect.left+LONG(int64_t(i.mi.dx)*(rect.right-rect.left)/65536),rect.top+LONG(int64_t(i.mi.dy)*(rect.bottom-rect.top)/65536)};
|
|
if(router){MSLLHOOKSTRUCT m{};m.pt=point;m.flags=LLMHF_INJECTED;m.dwExtraInfo=i.mi.dwExtraInfo;router->mouse(WM_MOUSEMOVE,m,1000);}
|
|
}
|
|
return true;
|
|
},[this]{return available;},[this]{return physicalHeld;},{},{},[this](POINT){return dpi;}};
|
|
}
|
|
unsigned keyEvents(bool down)const
|
|
{return unsigned(std::count_if(events.begin(),events.end(),[down](auto &i){return i.type==INPUT_KEYBOARD&&bool(i.ki.dwFlags&KEYEVENTF_KEYUP)!=down;}));}
|
|
};
|
|
KBDLLHOOKSTRUCT key(unsigned vk,unsigned scan=0){KBDLLHOOKSTRUCT k{};k.vkCode=vk;k.scanCode=scan;return k;}
|
|
bool motion(InputRouter &router,Desktop &desktop,POINT p,uint64_t now)
|
|
{
|
|
auto before=desktop.point;desktop.point=p;MSLLHOOKSTRUCT m{};m.pt=p;
|
|
bool consumed=router.mouse(WM_MOUSEMOVE,m,now);
|
|
if(consumed&&router.mode()==ControlMode::Sending)desktop.point=before;
|
|
return consumed;
|
|
}
|
|
void exchange(InputRouter &source,InputRouter &target,std::deque<InputFrame> &out,uint64_t now)
|
|
{(void)source;while(!out.empty()){auto f=out.front();out.pop_front();target.receive(f,now);}}
|
|
struct Pair
|
|
{
|
|
Desktop a,b;std::deque<InputFrame> ab,ba;
|
|
InputRouter left,right;
|
|
explicit Pair(int side):left(a.environment(),[this](auto f){ab.push_back(f);return true;},side,true),
|
|
right(b.environment(),[this](auto f){ba.push_back(f);return true;},side^1,false)
|
|
{a.router=&left;b.router=&right;b.rect={0,0,1600,900};b.point={600,400};left.tick(true,1000);right.tick(true,1000);}
|
|
~Pair(){a.router=b.router=nullptr;}
|
|
void deliver(uint64_t now=1100){exchange(left,right,ab,now);right.tick(true,now);exchange(right,left,ba,now);left.tick(true,now);}
|
|
void enter(int edge)
|
|
{
|
|
motion(left,a,edgePoint(a.rect,edge,30000,0),1000);
|
|
check(left.mode()==ControlMode::Requesting,"crossing edge requests ownership, not immediate suppression");
|
|
deliver();check(left.mode()==ControlMode::Sending&&right.mode()==ControlMode::Receiving,"mouse and keyboard ownership acknowledged together");
|
|
}
|
|
};
|
|
void routerChecks()
|
|
{
|
|
{
|
|
std::vector<InputFrame> queue;
|
|
for(int i=0;i<20;++i)enqueueInput(queue,{InputType::Wheel,1,120,0});
|
|
check(queue.size()==1&&queue[0].a==2400,"pending wheel burst coalesces without losing same-direction distance");
|
|
enqueueInput(queue,{InputType::Wheel,1,-120,0});
|
|
check(queue.size()==1&&queue[0].a==-120,"wheel reversal replaces undelivered stale direction");
|
|
enqueueInput(queue,{InputType::Key,1,VK_CONTROL,0,1});enqueueInput(queue,{InputType::Wheel,1,120,0});
|
|
check(queue.size()==3&&queue[1].type==InputType::Key,"wheel coalescing preserves modifier barriers");
|
|
enqueueInput(queue,{InputType::Wheel,1,120,1});enqueueInput(queue,{InputType::Wheel,2,120,1});
|
|
check(queue.size()==5,"wheel coalescing preserves axis and ownership boundaries");
|
|
}
|
|
{
|
|
Pair p(1);p.enter(1);p.a.delayed=true;
|
|
p.left.cancel(1200);
|
|
auto before=p.a.point;
|
|
check(motion(p.left,p.a,{before.x+10,before.y},1201),"physical movement is gated while return-to-edge is queued");
|
|
p.a.point=edgePoint(p.a.rect,1,edgePosition(p.a.point,p.a.rect,1));p.left.tick(true,1500);
|
|
check(!motion(p.left,p.a,{p.a.point.x-10,p.a.point.y},1501),"return movement gate always clears within bounded timeout");
|
|
}
|
|
{
|
|
Pair p(1);p.enter(1);
|
|
for(auto vk:{VK_SHIFT,VK_LSHIFT,VK_RSHIFT,VK_CONTROL,VK_MENU})
|
|
p.right.keyboard(key(vk),false,1200);
|
|
check(p.right.mode()==ControlMode::Receiving&&p.ba.empty(),"layout-generated unpaired modifier releases cannot return remote ownership");
|
|
auto own=key(VK_SHIFT);own.dwExtraInfo=inputMarker();
|
|
p.right.keyboard(own,true,1201);p.right.keyboard(own,false,1202);
|
|
check(p.right.mode()==ControlMode::Receiving&&p.ba.empty(),"own correction marker cannot trigger takeover even with injection flag stripped");
|
|
p.right.keyboard(key('B'),true,1203);
|
|
check(p.right.mode()==ControlMode::Local&&!p.ba.empty(),"real local key press still takes over after conversion release noise");
|
|
}
|
|
{
|
|
Pair p(1);p.enter(1);MSLLHOOKSTRUCT mouse{};mouse.pt=p.b.point;
|
|
p.right.mouse(WM_MOUSEMOVE,mouse,1200);
|
|
check(p.right.mode()==ControlMode::Receiving,"stationary untagged mouse notification does not cancel remote control");
|
|
mouse.pt.x+=20;p.right.mouse(WM_MOUSEMOVE,mouse,1201);
|
|
check(p.right.mode()==ControlMode::Local,"actual local mouse movement still takes control back");
|
|
}
|
|
{
|
|
Pair p(1);p.enter(1);p.b.delayed=true;auto before=p.b.events.back().mi.dx;
|
|
for(int i=0;i<5;++i){auto point=p.a.point;point.x+=10;motion(p.left,p.a,point,1300+i);p.deliver(1300+i);}
|
|
auto pixels=LONG(int64_t(p.b.events.back().mi.dx)*(p.b.rect.right-p.b.rect.left)/65536);
|
|
check(pixels==58&&p.b.point.x==8&&p.b.events.back().mi.dx>before,
|
|
"queued remote moves accumulate from entry edge even before Windows updates cursor position");
|
|
}
|
|
{
|
|
Pair p(1);p.b.delayed=true;
|
|
motion(p.left,p.a,edgePoint(p.a.rect,1,30000,0),1000);p.deliver(1100);
|
|
check(p.left.mode()==ControlMode::Requesting,"source stays local until destination cursor actually reaches entry edge");
|
|
p.right.tick(true,1400);p.deliver(1400);
|
|
check(p.left.mode()==ControlMode::Local&&p.right.mode()==ControlMode::Local,"failed destination positioning returns both peers locally");
|
|
}
|
|
{
|
|
Pair p(1);p.a.delayed=true;p.enter(1);p.left.tick(true,1400);p.deliver(1400);
|
|
check(p.left.mode()==ControlMode::Local&&p.right.mode()==ControlMode::Local,"missing source warp acknowledgement cannot suppress movement indefinitely");
|
|
}
|
|
{
|
|
Pair p(1);p.a.delayed=true;p.enter(1);auto queued=p.ab.size();
|
|
motion(p.left,p.a,edgePoint(p.a.rect,1,30000,0),1200);
|
|
check(p.ab.size()==queued,"pre-parking physical motion cannot become a giant centre-relative delta");
|
|
MSLLHOOKSTRUCT warp{};warp.flags=LLMHF_INJECTED;warp.dwExtraInfo=cursorWarpMarker();
|
|
p.left.mouse(WM_MOUSEMOVE,warp,1201);p.a.point={(p.a.rect.left+p.a.rect.right)/2,(p.a.rect.top+p.a.rect.bottom)/2};
|
|
auto point=p.a.point;++point.x;motion(p.left,p.a,point,1202);
|
|
check(p.ab.size()==queued+1,"source movement resumes after actual warp acknowledgement");
|
|
}
|
|
for(auto modifier:{VK_LCONTROL,VK_LMENU,VK_LSHIFT,VK_LWIN})
|
|
{
|
|
Pair p(1);p.enter(1);p.b.events.clear();
|
|
auto mod=key(modifier,modifier==VK_LCONTROL?0x1d:modifier==VK_LMENU?0x38:modifier==VK_LSHIFT?0x2a:0x5b);
|
|
if(modifier==VK_LWIN)mod.flags=LLKHF_EXTENDED;
|
|
p.left.keyboard(mod,true,1200);p.left.keyboard(key('C',0x2e),true,1201);
|
|
p.left.keyboard(key('C',0x2e),false,1202);p.left.keyboard(mod,false,1203);p.deliver(1204);
|
|
check(p.b.events.size()==4&&p.b.events[0].ki.wVk==modifier&&p.b.events[1].ki.wVk=='C'&&
|
|
!(p.b.events[0].ki.dwFlags&KEYEVENTF_KEYUP)&&(p.b.events[3].ki.dwFlags&KEYEVENTF_KEYUP),
|
|
"modifier chord delivered in order with matching release");
|
|
check(!(p.b.events[0].ki.dwFlags&KEYEVENTF_SCANCODE)&&!(p.b.events[3].ki.dwFlags&KEYEVENTF_SCANCODE),
|
|
"modifier down and up use explicit Windows key identities");
|
|
auto correction=key(VK_CONTROL,0x1d);correction.flags=LLKHF_INJECTED;correction.dwExtraInfo=ks::inputMarker();
|
|
p.right.keyboard(correction,true,1210);p.right.keyboard(correction,false,1211);p.right.tick(true,1220);
|
|
check(p.right.mode()==ControlMode::Receiving&&p.left.mode()==ControlMode::Sending,
|
|
"generated correction keys do not take back remote ownership");
|
|
}
|
|
for(auto dpis:{std::pair{96,192},std::pair{240,96}})
|
|
{
|
|
Pair p(1);p.a.dpi=dpis.first;p.b.dpi=dpis.second;p.enter(1);auto before=p.b.point;
|
|
for(int i=0;i<100;++i){auto point=p.a.point;++point.x;motion(p.left,p.a,point,1300+i);p.deliver(1300+i);}
|
|
check(std::abs(p.b.point.x-before.x-100*dpis.second/dpis.first)<=1,"mouse movement scales between monitor DPIs without losing small deltas");
|
|
}
|
|
for(int side=0;side<4;++side)
|
|
{
|
|
Pair p(side);auto middle=edgePoint(p.a.rect,side,32768,0);
|
|
check(atEdge(middle,p.a.rect,side),"all four virtual-screen edges supported");
|
|
check(std::abs(edgePosition(middle,p.a.rect,side)-32768)<100,"negative monitor coordinates normalize");
|
|
p.enter(side);
|
|
auto old=p.b.point;
|
|
for(int step=0;step<20;++step)
|
|
{
|
|
auto point=p.a.point;if(side==0)--point.x;else if(side==1)++point.x;else if(side==2)--point.y;else ++point.y;
|
|
motion(p.left,p.a,point,1110+step);p.deliver(1110+step);
|
|
}
|
|
check(side<2 ? std::abs(p.b.point.x-old.x)==20 : std::abs(p.b.point.y-old.y)==20,"one-pixel mouse movements have no accumulating warp bias");
|
|
check(p.left.keyboard(key('A',0x1e),true,1200),"source key down suppressed");p.deliver(1200);
|
|
check(p.a.keyEvents(true)==0&&p.b.keyEvents(true)==1,"key only injected on computer with cursor");
|
|
check(p.b.events.back().ki.dwFlags&KEYEVENTF_SCANCODE,"physical scan code preserved across layouts");
|
|
check(p.left.keyboard(key('A',0x1e),false,1210),"source key up suppressed");p.deliver(1210);
|
|
check(p.b.keyEvents(false)==1,"remote key released");
|
|
auto injected=key('A',0x1e);injected.flags=LLKHF_INJECTED;injected.dwExtraInfo=remoteInputMarker();
|
|
check(!p.right.keyboard(injected,true,1220),"received input continues to destination hotkeys/snippets, no routing loop");
|
|
MSLLHOOKSTRUCT click{};check(p.left.mouse(WM_LBUTTONDOWN,click,1230),"source click suppressed");p.deliver(1230);
|
|
check(p.b.events.back().mi.dwFlags==MOUSEEVENTF_LEFTDOWN,"remote left button pressed");
|
|
p.left.mouse(WM_LBUTTONUP,click,1240);p.deliver(1240);
|
|
check(p.b.events.back().mi.dwFlags==MOUSEEVENTF_LEFTUP,"remote left button released");
|
|
click.mouseData=DWORD(120)<<16;p.left.mouse(WM_MOUSEWHEEL,click,1250);p.deliver(1250);
|
|
check(p.b.events.back().mi.dwFlags==MOUSEEVENTF_WHEEL&&p.b.events.back().mi.mouseData==120,"wheel delta forwarded");
|
|
auto centre=p.a.point;POINT moved=centre;
|
|
if(side==0)moved.x-=30;else if(side==1)moved.x+=30;else if(side==2)moved.y-=30;else moved.y+=30;
|
|
check(motion(p.left,p.a,moved,1300),"remote mouse move suppressed locally");p.deliver(1300);
|
|
check(p.b.point.x!=old.x||p.b.point.y!=old.y,"cursor moves on target");
|
|
// Repeated source movement towards the return edge.
|
|
for(int n=0;n<5&&p.left.mode()==ControlMode::Sending;++n)
|
|
{
|
|
moved=p.a.point;if(side==0)moved.x+=400;else if(side==1)moved.x-=400;else if(side==2)moved.y+=400;else moved.y-=400;
|
|
motion(p.left,p.a,moved,1400+n);p.deliver(1400+n);
|
|
}
|
|
check(p.left.mode()==ControlMode::Local&&p.right.mode()==ControlMode::Local,"crossing target return edge restores local ownership");
|
|
check(!atEdge(p.a.point,p.a.rect,side),"return cursor inset avoids immediate bounce");
|
|
check(!p.left.keyboard(key('B'),true,1500),"normal local typing resumes");p.left.keyboard(key('B'),false,1501);
|
|
}
|
|
{
|
|
Pair p(1);p.enter(1);p.left.keyboard(key(VK_LCONTROL),true,1200);p.left.keyboard(key(VK_LMENU),true,1201);p.deliver();
|
|
check(p.left.keyboard(key(VK_ESCAPE),true,1202),"emergency return chord consumed");p.deliver();
|
|
check(p.left.mode()==ControlMode::Local&&p.right.mode()==ControlMode::Local,"emergency chord returns both sides");
|
|
check(p.b.keyEvents(false)==2,"emergency return releases remote modifiers");
|
|
check(p.left.keyboard(key(VK_LCONTROL),false,1203)&&p.left.keyboard(key(VK_LMENU),false,1204),"stray source modifier releases consumed after return");
|
|
}
|
|
{
|
|
Pair p(1);p.enter(1);p.left.keyboard(key('A'),true,1200);p.deliver();
|
|
p.left.tick(false,1300);p.right.tick(false,1300);
|
|
check(p.b.keyEvents(false)==1&&p.left.mode()==ControlMode::Local,"disconnect releases held remote key");
|
|
check(p.left.keyboard(key('A'),false,1400),"source does not leak orphaned key-up after disconnect");
|
|
}
|
|
{
|
|
Pair p(1);p.enter(1);p.left.keyboard(key('A'),true,1200);p.deliver();p.b.available=false;p.right.tick(true,1300);
|
|
check(p.right.mode()==ControlMode::Local&&p.b.keyEvents(false)==0,"secure desktop stops ownership without injecting there");
|
|
p.b.available=true;p.right.tick(true,1400);check(p.b.keyEvents(false)==1,"blocked release retried when normal desktop returns");
|
|
}
|
|
{
|
|
Pair p(1);p.a.physicalHeld=true;motion(p.left,p.a,edgePoint(p.a.rect,1,30000,0),1000);
|
|
check(p.left.mode()==ControlMode::Local,"no cross-screen handoff with physically held key/button");
|
|
p.a.physicalHeld=false;motion(p.left,p.a,edgePoint(p.a.rect,1,30000,0),1100);
|
|
p.left.keyboard(key('X'),true,1101);p.deliver(1200);
|
|
check(p.left.mode()==ControlMode::Local&&p.right.mode()==ControlMode::Local,"typing during handoff cancels stale accept");
|
|
}
|
|
{
|
|
Pair p(1);motion(p.left,p.a,edgePoint(p.a.rect,1,30000,0),1000);motion(p.right,p.b,edgePoint(p.b.rect,0,30000,0),1000);
|
|
p.deliver();p.deliver();check(p.left.mode()==ControlMode::Sending&&p.right.mode()==ControlMode::Receiving,"simultaneous crossing has deterministic ownership");
|
|
}
|
|
{
|
|
Pair p(1);p.enter(1);p.left.keyboard(key('A'),true,1200);p.deliver();
|
|
check(!p.right.keyboard(key('B'),true,1300),"local physical keyboard takes over target");p.deliver();
|
|
check(p.left.mode()==ControlMode::Local&&p.b.keyEvents(false)==1,"local takeover returns source and releases remote keys");
|
|
}
|
|
{
|
|
Pair p(1);p.b.allow=false;motion(p.left,p.a,edgePoint(p.a.rect,1,30000,0),1000);p.deliver();
|
|
check(p.left.mode()==ControlMode::Local&&p.right.mode()==ControlMode::Local,"blocked SendInput fails closed without capturing source");
|
|
}
|
|
{
|
|
Pair p(1);p.enter(1);MSLLHOOKSTRUCT m{};m.mouseData=DWORD(XBUTTON2)<<16;
|
|
p.left.mouse(WM_XBUTTONDOWN,m,1200);p.deliver();p.right.tick(false,1300);
|
|
check(p.b.events.back().mi.dwFlags==MOUSEEVENTF_XUP&&p.b.events.back().mi.mouseData==XBUTTON2,"disconnect releases held extra mouse button");
|
|
}
|
|
{
|
|
Pair p(1);motion(p.left,p.a,edgePoint(p.a.rect,1,30000,0),1000);p.left.tick(true,2101);
|
|
check(p.left.mode()==ControlMode::Local,"unanswered ownership request times out locally");p.deliver(2200);p.deliver(2200);
|
|
check(p.left.mode()==ControlMode::Local&&p.right.mode()==ControlMode::Local,"late handoff acknowledgement cannot steal control");
|
|
}
|
|
{
|
|
Desktop d;bool accept=true;std::deque<InputFrame> frames;
|
|
InputRouter router(d.environment(),[&](auto f){frames.push_back(f);return accept;},1,true);
|
|
router.tick(true,1000);motion(router,d,edgePoint(d.rect,1,30000,0),1000);
|
|
auto token=frames.front().token;router.receive({InputType::Accept,token},1100);
|
|
accept=false;check(router.keyboard(key('Q'),true,1200),"failed queue still consumes the key rather than typing locally");
|
|
check(router.mode()==ControlMode::Local&&router.keyboard(key('Q'),false,1300),"queue failure returns control and consumes orphan release");
|
|
}
|
|
}
|
|
|
|
struct MeshFixture
|
|
{
|
|
struct Message{std::string from,to;InputFrame frame;};
|
|
std::deque<Message> messages;Desktop desktops[3];std::unique_ptr<InputMesh> meshes[3];uint64_t now=1000;
|
|
std::map<std::string,uint64_t> connections[3];
|
|
MeshFixture(bool vertical=false){
|
|
POINT cells[3]={{0,0},{1,0},vertical?POINT{1,-1}:POINT{2,0}};
|
|
for(int i=0;i<3;++i){
|
|
desktops[i].rect={0,0,800,600};desktops[i].point={400,300};std::map<std::string,POINT> positions;
|
|
for(int j=0;j<3;++j)if(i!=j){auto name=std::to_string(j);positions[name]={cells[j].x-cells[i].x,cells[j].y-cells[i].y};connections[i][name]=1;}
|
|
meshes[i]=std::make_unique<InputMesh>(std::to_string(i),desktops[i].environment(),positions,[this,i](auto &peer,auto f){messages.push_back({std::to_string(i),peer,f});return true;});
|
|
}pump();
|
|
}
|
|
~MeshFixture(){for(auto &m:meshes)m.reset();}
|
|
void pump(){for(int rounds=0;rounds<10;++rounds){
|
|
while(!messages.empty()){auto m=messages.front();messages.pop_front();meshes[std::stoi(m.to)]->receive(m.from,m.frame,now);}
|
|
for(int i=0;i<3;++i)meshes[i]->tick(connections[i],now);
|
|
}}
|
|
void move(int x,int y){++now;auto &d=desktops[0];auto old=d.point;MSLLHOOKSTRUCT m{};m.pt={old.x+x,old.y+y};d.point=m.pt;
|
|
bool consumed=meshes[0]->mouse(WM_MOUSEMOVE,m,now);if(consumed&&meshes[0]->mode()==ControlMode::Sending)d.point=old;pump();}
|
|
void enter(){desktops[0].point={798,300};move(1,0);check(meshes[0]->mode()==ControlMode::Sending&&meshes[1]->mode()==ControlMode::Receiving,"mesh enters first neighbor");}
|
|
};
|
|
void meshChecks()
|
|
{
|
|
{
|
|
MeshFixture f;f.enter();f.move(900,0);
|
|
check(f.meshes[0]->peer()=="2"&&f.meshes[2]->mode()==ControlMode::Receiving&&f.meshes[1]->mode()==ControlMode::Local,"A routes directly to C through B right edge");
|
|
f.desktops[1].events.clear();f.desktops[2].events.clear();
|
|
for(auto k:{VK_LSHIFT,int('A')})f.meshes[0]->keyboard(key(k),true,++f.now);
|
|
for(auto k:{int('A'),VK_LSHIFT})f.meshes[0]->keyboard(key(k),false,++f.now);
|
|
f.pump();
|
|
check(f.desktops[1].keyEvents(true)==0&&f.desktops[2].keyEvents(true)==2&&f.desktops[2].keyEvents(false)==2,"modifier chord goes only to current destination C");
|
|
f.move(-900,0);check(f.meshes[0]->peer()=="1"&&f.meshes[1]->mode()==ControlMode::Receiving,"C left edge returns through B");
|
|
f.move(-900,0);check(f.meshes[0]->mode()==ControlMode::Local&&f.desktops[0].point.x>780,"B left edge restores source at its right edge");
|
|
}
|
|
{
|
|
MeshFixture f(true);f.enter();f.move(0,-900);
|
|
check(f.meshes[0]->peer()=="2"&&f.desktops[2].point.y>580,"vertical turn enters C at bottom edge");
|
|
f.move(0,900);check(f.meshes[0]->peer()=="1","vertical return routes back to B");
|
|
}
|
|
{
|
|
MeshFixture f;f.connections[0].erase("2");f.pump();f.enter();f.move(900,0);
|
|
check(f.meshes[0]->peer()=="1"&&f.meshes[1]->mode()==ControlMode::Receiving,"offline neighbor blocks passage without losing current control");
|
|
f.meshes[0]->keyboard(key(VK_LSHIFT),true,++f.now);f.pump();f.connections[0].erase("1");f.connections[1].erase("0");f.pump();
|
|
check(f.meshes[0]->mode()==ControlMode::Local&&f.meshes[1]->mode()==ControlMode::Local&&f.desktops[1].keyEvents(false)==1,"disconnect restores local control and releases held modifier");
|
|
}
|
|
{
|
|
MeshFixture f;f.enter();f.meshes[1]->receive("2",{InputType::Enter,99,1,20000},++f.now);
|
|
check(f.meshes[1]->peer()=="0"&&f.meshes[1]->mode()==ControlMode::Receiving,"another controller cannot steal receiving desktop");f.pump();
|
|
f.move(900,0);f.meshes[0]->receive("1",{InputType::Return,1,20000},++f.now);
|
|
check(f.meshes[0]->mode()==ControlMode::Sending&&f.meshes[0]->peer()=="2","late previous peer return cannot cancel current destination");
|
|
f.connections[0]["2"]=2;f.pump();check(f.meshes[0]->mode()==ControlMode::Local,"new connection generation does not continue stale ownership");
|
|
}
|
|
}
|
|
uint16_t freePort()
|
|
{
|
|
SOCKET s=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);if(s==INVALID_SOCKET)throw std::runtime_error("test socket");
|
|
sockaddr_in a{};a.sin_family=AF_INET;a.sin_addr.s_addr=htonl(INADDR_LOOPBACK);
|
|
if(bind(s,(sockaddr*)&a,sizeof(a))){closesocket(s);throw std::runtime_error("test bind");}
|
|
int size=sizeof(a);getsockname(s,(sockaddr*)&a,&size);auto port=ntohs(a.sin_port);closesocket(s);return port;
|
|
}
|
|
|
|
void hubChecks()
|
|
{
|
|
WSADATA data{};if(WSAStartup(MAKEWORD(2,2),&data))throw std::runtime_error("WSAStartup");
|
|
struct Close{~Close(){WSACleanup();}} close;
|
|
std::string ids[3]={std::string(32,'a'),std::string(32,'b'),std::string(32,'c')},secret(64,'f');
|
|
uint16_t ports[3]={freePort(),freePort(),freePort()};
|
|
std::unique_ptr<InputHub> hubs[3];std::vector<InputEndpoint> endpoints[3];std::vector<RoutedInput> incoming[3];
|
|
for(int i=0;i<3;++i){hubs[i]=std::make_unique<InputHub>(ids[i],ports[i]);for(int j=0;j<3;++j)if(i!=j)endpoints[i].push_back({ids[j],secret,htonl(INADDR_LOOPBACK),ports[j]});}
|
|
auto pump=[&](uint64_t offset=0){for(int i=2;i>=0;--i)hubs[i]->poll(endpoints[i],{},incoming[i],GetTickCount64()+offset);Sleep(1);};
|
|
auto deadline=GetTickCount64()+5000;
|
|
while((hubs[0]->ready().size()!=2||hubs[1]->ready().size()!=2||hubs[2]->ready().size()!=2)&&GetTickCount64()<deadline)pump();
|
|
check(hubs[0]->ready().size()==2&&hubs[1]->ready().size()==2&&hubs[2]->ready().size()==2,"three TCP hubs authenticate all independent pair channels on one listener per PC");
|
|
auto generation=hubs[0]->ready();
|
|
for(auto &list:endpoints)for(auto &e:list)e.address=0;
|
|
for(int n=0;n<20;++n)pump();
|
|
check(hubs[0]->ready()==generation&&hubs[1]->ready().size()==2&&hubs[2]->ready().size()==2,"expired discovery does not destroy authenticated channels");
|
|
for(auto &list:endpoints)for(auto &e:list)e.address=htonl(INADDR_LOOPBACK);
|
|
InputFrame keyFrame{InputType::Key,7,'A',0,1},cross{InputType::Cross,8,2,30000};
|
|
hubs[0]->poll(endpoints[0],{{ids[1],keyFrame},{ids[2],cross}},incoming[0],GetTickCount64());
|
|
for(int n=0;n<100&&(incoming[1].empty()||incoming[2].empty());++n)pump();
|
|
check(incoming[1].size()==1&&incoming[1][0].peer==ids[0]&&incoming[1][0].frame==keyFrame&&incoming[2].size()==1&&incoming[2][0].frame==cross,"hub routes encrypted frames only to their named destinations");
|
|
for(auto &in:incoming)in.clear();
|
|
auto survivor=hubs[0]->ready().at(ids[2]),old=hubs[0]->ready().at(ids[1]);
|
|
hubs[1]->disconnect();for(int n=0;n<30;++n)pump();
|
|
check(hubs[0]->ready().contains(ids[2])&&hubs[0]->ready().at(ids[2])==survivor,"one computer disconnect leaves other authenticated channel intact");
|
|
for(int n=0;n<300&&(hubs[0]->ready().size()!=2||hubs[1]->ready().size()!=2);++n)pump(1800);
|
|
check(hubs[0]->ready().size()==2&&hubs[0]->ready().at(ids[1])!=old,"mesh reconnect restores channel with fresh ownership generation");
|
|
{
|
|
auto serverPort=freePort();InputHub server(ids[2],serverPort),client(ids[0],freePort());
|
|
std::vector<InputEndpoint> unknown{{ids[0],secret,0}},known{{ids[2],secret,htonl(INADDR_LOOPBACK),serverPort}};
|
|
std::vector<RoutedInput> a,b;
|
|
for(int n=0;n<300&&(server.ready().empty()||client.ready().empty());++n){
|
|
server.poll(unknown,{},a,GetTickCount64());client.poll(known,{},b,GetTickCount64());Sleep(1);
|
|
}
|
|
check(server.ready().size()==1&&client.ready().size()==1,"saved pair listens and authenticates without receiving a discovery announcement");
|
|
}
|
|
endpoints[0].erase(endpoints[0].begin());hubs[0]->poll(endpoints[0],{},incoming[0],GetTickCount64()+1800);
|
|
check(!hubs[0]->ready().contains(ids[1])&&hubs[0]->ready().contains(ids[2]),"forgetting one pair revokes only its channel");
|
|
}
|
|
void channelChecks()
|
|
{
|
|
std::string a(32,'a'),b(32,'b'),secret(64,'f');auto na=randomHex(16),nb=randomHex(16);
|
|
auto k=inputSessionKey(secret,a,b,na,nb);check(k!=inputSessionKey(secret,b,a,nb,na),"directional session keys differ");
|
|
check(inputSessionKey(secret,a,b,na,nb,0)!=inputSessionKey(secret,a,b,na,nb,1),"screen geometry bound into authenticated session");
|
|
InputCipher tx(k),rx(k);InputFrame f{InputType::Key,37,'A',0x1e,1};auto packet=tx.encrypt(f);
|
|
check(packet.size()==36&&rx.decrypt(packet)==f,"AES-GCM authenticated input roundtrip");
|
|
rejects([&]{rx.decrypt(packet);},"encrypted frame replay rejected");
|
|
auto next=tx.encrypt({InputType::Move,37,-12,400});auto tampered=next;tampered[4]^=1;
|
|
rejects([&]{rx.decrypt(tampered);},"ciphertext modification rejected");
|
|
check(rx.decrypt(next)==InputFrame{InputType::Move,37,-12,400},"failed authentication does not expose or advance data");
|
|
InputCipher wrong(inputSessionKey(secret,b,a,nb,na));rejects([&]{wrong.decrypt(packet);},"reflection across directions rejected");
|
|
InputCipher otherSession(inputSessionKey(secret,a,b,randomHex(16),nb));rejects([&]{otherSession.decrypt(packet);},"old-session input rejected");
|
|
WSADATA data{};if(WSAStartup(MAKEWORD(2,2),&data))throw std::runtime_error("WSAStartup");
|
|
struct Close{~Close(){WSACleanup();}} close;
|
|
{
|
|
auto port=freePort();InputChannel client(a,b,secret,1,port),server(b,a,secret,0,port);
|
|
std::vector<InputFrame> received;auto start=GetTickCount64();
|
|
server.poll(htonl(INADDR_LOOPBACK),{},received,start);
|
|
client.poll(htonl(INADDR_LOOPBACK),{},received,start);
|
|
auto generation=client.generation();
|
|
client.poll(htonl(INADDR_LOOPBACK),{},received,start+3000);
|
|
check(client.generation()==generation,"initial handshake allows more than the active heartbeat timeout");
|
|
for(int n=0;n<200&&(!client.ready()||!server.ready());++n)
|
|
{
|
|
auto now=GetTickCount64()+3000;
|
|
server.poll(htonl(INADDR_LOOPBACK),{},received,now);client.poll(htonl(INADDR_LOOPBACK),{},received,now);Sleep(1);
|
|
}
|
|
check(client.ready()&&server.ready(),"delayed initial handshake still authenticates");
|
|
}
|
|
{
|
|
auto port=freePort();InputChannel client(a,b,secret,1,port),server(b,a,secret,0,port);
|
|
std::vector<InputFrame> received;auto start=GetTickCount64();
|
|
server.poll(htonl(INADDR_LOOPBACK),{},received,start);
|
|
client.poll(htonl(INADDR_LOOPBACK),{},received,start);
|
|
client.poll(htonl(INADDR_LOOPBACK),{},received,start+100);
|
|
client.poll(htonl(INADDR_LOOPBACK),{},received,start+11000);
|
|
check(!client.ready()&&client.status().find("authentication timed out")!=std::string::npos,
|
|
"initial authentication timeout distinguished from lost active connection");
|
|
}
|
|
{
|
|
auto port=freePort();InputChannel client(a,b,secret,1,port),server(b,a,secret,0,port);
|
|
auto deadline=GetTickCount64()+5000;std::vector<InputFrame> received;
|
|
while((!client.ready()||!server.ready())&&GetTickCount64()<deadline)
|
|
{
|
|
server.poll(htonl(INADDR_LOOPBACK),{},received,GetTickCount64());client.poll(htonl(INADDR_LOOPBACK),{},received,GetTickCount64());Sleep(1);
|
|
}
|
|
check(client.ready()&&server.ready(),"real TCP peers mutually authenticate with paired key");
|
|
check(received.empty(),"handshake produces no input events");
|
|
client.poll(htonl(INADDR_LOOPBACK),{f},received,GetTickCount64());
|
|
for(int n=0;n<100&&received.empty();++n){server.poll(htonl(INADDR_LOOPBACK),{},received,GetTickCount64());client.poll(htonl(INADDR_LOOPBACK),{},received,GetTickCount64());Sleep(1);}
|
|
check(received.size()==1&&received[0]==f,"real TCP channel delivers one ordered authenticated key");received.clear();
|
|
InputFrame reverse{InputType::Wheel,37,-120,1};server.poll(htonl(INADDR_LOOPBACK),{reverse},received,GetTickCount64());
|
|
for(int n=0;n<100&&received.empty();++n){client.poll(htonl(INADDR_LOOPBACK),{},received,GetTickCount64());Sleep(1);}
|
|
check(received.size()==1&&received[0]==reverse,"real input channel is bidirectional");
|
|
received.clear();server.disconnect();
|
|
client.poll(htonl(INADDR_LOOPBACK),{},received,GetTickCount64());
|
|
for(int n=0;n<300&&(!client.ready()||!server.ready());++n)
|
|
{
|
|
auto now=GetTickCount64()+1800;
|
|
server.poll(htonl(INADDR_LOOPBACK),{},received,now);client.poll(htonl(INADDR_LOOPBACK),{},received,now);Sleep(1);
|
|
}
|
|
check(client.ready()&&server.ready(),"paired channel reconnects automatically after peer disconnect without new pairing");
|
|
auto generation=client.generation();client.poll(htonl(INADDR_LOOPBACK),{},received,GetTickCount64()+5000);
|
|
check(!client.ready()&&client.generation()!=generation,"heartbeat timeout closes channel and changes generation");
|
|
}
|
|
{
|
|
auto port=freePort();InputChannel client(a,b,secret,1,port),server(b,a,std::string(64,'e'),0,port);
|
|
std::vector<InputFrame> received;auto start=GetTickCount64();
|
|
while(GetTickCount64()-start<400){server.poll(htonl(INADDR_LOOPBACK),{},received,GetTickCount64());client.poll(htonl(INADDR_LOOPBACK),{},received,GetTickCount64());Sleep(1);}
|
|
check(!client.ready()&&!server.ready()&&received.empty(),"wrong saved pair key never authorizes input");
|
|
}
|
|
{
|
|
auto port=freePort();InputChannel client(a,b,secret,1,port),server(b,a,secret,1,port);
|
|
std::vector<InputFrame> received;auto start=GetTickCount64();
|
|
while(GetTickCount64()-start<400){server.poll(htonl(INADDR_LOOPBACK),{},received,GetTickCount64());client.poll(htonl(INADDR_LOOPBACK),{},received,GetTickCount64());Sleep(1);}
|
|
check(!client.ready()&&!server.ready()&&received.empty(),"conflicting screen arrangement rejected before input");
|
|
}
|
|
}
|
|
}
|
|
int main()
|
|
{
|
|
try{channelChecks();hubChecks();routerChecks();meshChecks();std::cout<<"Passed "<<passed<<" LAN input checks\n";}
|
|
catch(const std::exception &e){std::cerr<<"FAIL: "<<e.what()<<'\n';return 1;}
|
|
}
|