Add encrypted LAN mouse and keyboard sharing with four-edge handoff
This commit is contained in:
1 parent
9b565dd55e
commit
e0bfdd9a90
15 files changed
+1093
-104
No files matched your search
+116
-6
@@ -3,6 +3,7 @@
|
||||
#include <iphlpapi.h>
|
||||
#include "lan.h"
|
||||
#include "lan_pairing.h"
|
||||
#include "lan_input.h"
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
@@ -55,6 +56,17 @@ struct LanService::Impl
|
||||
std::atomic<bool> stopping{false};
|
||||
std::mutex mutex;
|
||||
std::thread worker;
|
||||
bool shareInput{};
|
||||
std::string inputPeer;
|
||||
int inputEdge=1;
|
||||
std::atomic<bool> inputConnected{false},abortInput{false};
|
||||
std::atomic<uint64_t> inputEpoch{0};
|
||||
std::atomic<uint64_t> inputConfiguration{0};
|
||||
uint64_t observedEpoch{},observedRevision{};
|
||||
std::mutex inputMutex;
|
||||
std::vector<lan::InputFrame> inputOut,inputIn;
|
||||
std::unique_ptr<lan::InputRouter> router;
|
||||
std::string inputStatus="Input sharing disabled.";
|
||||
explicit Impl(const std::filesystem::path &root) : path(root/L"lan-pairing.bin")
|
||||
{
|
||||
if(std::filesystem::exists(path))
|
||||
@@ -62,6 +74,8 @@ struct LanService::Impl
|
||||
auto j=Json::parse(unprotect(readFile(path,256*1024)));
|
||||
if(j.value("version",0)!=1 || !lan::identifier(j.value("id",""))) throw std::runtime_error("Invalid local LAN configuration.");
|
||||
id=j.at("id"); enabled=j.value("enabled",false); trusted=j.at("trusted");
|
||||
shareInput=j.value("shareInput",false);inputPeer=j.value("inputPeer","");inputEdge=j.value("inputEdge",1);
|
||||
if(inputEdge<0||inputEdge>3||(shareInput&&!trusted.contains(inputPeer)))throw std::runtime_error("Invalid input sharing configuration.");
|
||||
if(!trusted.is_object()||trusted.size()>64) throw std::runtime_error("Invalid paired computer list.");
|
||||
for(auto &[key,entry]:trusted.items())
|
||||
if(!lan::identifier(key)||key==id||entry.at("key").get<std::string>().size()!=64||
|
||||
@@ -69,10 +83,43 @@ struct LanService::Impl
|
||||
throw std::runtime_error("Invalid paired computer key.");
|
||||
}
|
||||
else id=lan::randomHex(16);
|
||||
configureRouter();
|
||||
if(enabled) start();
|
||||
}
|
||||
~Impl() { stop(); }
|
||||
void save() { atomicWrite(path,protect(encoded({{"version",1},{"id",id},{"enabled",enabled},{"trusted",trusted}}))); }
|
||||
void save() { atomicWrite(path,protect(encoded({{"version",1},{"id",id},{"enabled",enabled},{"trusted",trusted},
|
||||
{"shareInput",shareInput},{"inputPeer",inputPeer},{"inputEdge",inputEdge}}))); }
|
||||
bool queue(lan::InputFrame frame)
|
||||
{
|
||||
if(!inputConnected)return false;
|
||||
std::lock_guard lock(inputMutex);
|
||||
if(inputOut.size()>=512){abortInput=true;return false;}
|
||||
inputOut.push_back(frame);return true;
|
||||
}
|
||||
void configureRouter()
|
||||
{
|
||||
router.reset();inputConnected=false;abortInput=true;++inputEpoch;++inputConfiguration;
|
||||
{std::lock_guard lock(inputMutex);inputOut.clear();inputIn.clear();}
|
||||
if(enabled&&shareInput)router=std::make_unique<lan::InputRouter>(lan::windowsInputEnvironment(),[this](auto f){return queue(f);},inputEdge,id<inputPeer);
|
||||
inputStatus=enabled&&shareInput?"Connecting input channel...":"Input sharing disabled.";
|
||||
observedRevision=0;
|
||||
}
|
||||
bool takeChange()
|
||||
{
|
||||
auto revision=router?router->revision():0;bool changed=revision!=observedRevision;observedRevision=revision;return changed;
|
||||
}
|
||||
bool tickInput(bool allowEnter)
|
||||
{
|
||||
if(!router)return false;
|
||||
auto epoch=inputEpoch.load();if(epoch!=observedEpoch){router->cancel(GetTickCount64());observedEpoch=epoch;}
|
||||
router->tick(inputConnected,GetTickCount64());
|
||||
std::vector<lan::InputFrame> incoming;
|
||||
{std::lock_guard lock(inputMutex);incoming.swap(inputIn);}
|
||||
for(auto &frame:incoming)
|
||||
if(!allowEnter&&frame.type==lan::InputType::Enter)queue({lan::InputType::Return,frame.token,32768});
|
||||
else router->receive(frame,GetTickCount64());
|
||||
return takeChange();
|
||||
}
|
||||
void start()
|
||||
{
|
||||
stopping=false; epoch=lan::randomHex(16); status="Searching for KSwitcher computers...";
|
||||
@@ -80,8 +127,11 @@ struct LanService::Impl
|
||||
}
|
||||
void stop()
|
||||
{
|
||||
if(router)router->cancel(GetTickCount64());
|
||||
stopping=true;
|
||||
if(worker.joinable()) worker.join();
|
||||
inputConnected=false;++inputEpoch;
|
||||
{std::lock_guard lock(inputMutex);inputOut.clear();inputIn.clear();}
|
||||
std::lock_guard lock(mutex); pending.reset(); peers.clear();
|
||||
}
|
||||
void send(SOCKET socket,const sockaddr_in &address,const Json &j)
|
||||
@@ -128,7 +178,7 @@ struct LanService::Impl
|
||||
auto previous=trusted;
|
||||
trusted[other]={{"name",peer.name},{"key",pending->derived}};
|
||||
try {save();} catch(...) {trusted=std::move(previous);throw;}
|
||||
peer.verified=now; status="Paired. Input and clipboard sharing are not enabled in this preview.";
|
||||
peer.verified=now; status="Paired. Select a screen position and enable mouse and keyboard on both PCs.";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,12 +195,21 @@ struct LanService::Impl
|
||||
throw std::runtime_error("Cannot open LAN discovery socket.");
|
||||
sockaddr_in bound{}; bound.sin_family=AF_INET; bound.sin_port=htons(lan::Port);
|
||||
if(bind(socket,(sockaddr*)&bound,sizeof(bound))) throw std::runtime_error("UDP port 45883 is in use. Only one LAN-enabled KSwitcher may run per computer.");
|
||||
uint64_t nextHello=0,nextOffer=0;
|
||||
uint64_t nextHello=0,nextOffer=0,lastChannelGeneration=0;
|
||||
std::unique_ptr<lan::InputChannel> channel;
|
||||
std::string channelPeer,channelSecret;int channelEdge=-1;
|
||||
while(!stopping)
|
||||
{
|
||||
auto now=GetTickCount64();
|
||||
std::string selectedPeer,selectedSecret;int selectedEdge=-1;uint32_t selectedAddress=0;uint64_t configuration=0;
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
configuration=inputConfiguration.load();
|
||||
if(shareInput&&trusted.contains(inputPeer))
|
||||
{
|
||||
selectedPeer=inputPeer;selectedSecret=trusted.at(inputPeer).at("key");selectedEdge=inputEdge;
|
||||
if(peers.contains(inputPeer))selectedAddress=peers.at(inputPeer).address.sin_addr.s_addr;
|
||||
}
|
||||
if(now>=nextHello)
|
||||
{
|
||||
epoch=lan::randomHex(16); // Fresh challenge prevents replayed presence from staying online.
|
||||
@@ -169,7 +228,30 @@ struct LanService::Impl
|
||||
send(socket,peers.at(pending->peer).address,pending->offer()); nextOffer=now+1000;
|
||||
}
|
||||
}
|
||||
fd_set read; FD_ZERO(&read); FD_SET(socket,&read); timeval delay{0,100000};
|
||||
if(channelPeer!=selectedPeer||channelEdge!=selectedEdge||channelSecret!=selectedSecret)
|
||||
{
|
||||
inputConnected=false;++inputEpoch;channel.reset();
|
||||
{std::lock_guard lock(inputMutex);inputOut.clear();inputIn.clear();}
|
||||
channelPeer=selectedPeer;channelEdge=selectedEdge;channelSecret=selectedSecret;lastChannelGeneration=0;
|
||||
if(!selectedPeer.empty())channel=std::make_unique<lan::InputChannel>(id,selectedPeer,selectedSecret,selectedEdge);
|
||||
}
|
||||
if(channel)
|
||||
{
|
||||
std::vector<lan::InputFrame> out,in;
|
||||
{std::lock_guard lock(inputMutex);out.swap(inputOut);}
|
||||
if(abortInput.exchange(false)){channel->disconnect();out.clear();}
|
||||
channel->poll(selectedAddress,out,in,now);
|
||||
if(configuration!=inputConfiguration.load()){in.clear();channel->disconnect();}
|
||||
if(lastChannelGeneration!=channel->generation())
|
||||
{
|
||||
lastChannelGeneration=channel->generation();inputConnected=false;++inputEpoch;
|
||||
std::lock_guard lock(inputMutex);inputOut.clear();inputIn.clear();
|
||||
}
|
||||
inputConnected=channel->ready()&&!abortInput.load()&&configuration==inputConfiguration.load();
|
||||
{std::lock_guard lock(inputMutex);if(inputIn.size()+in.size()>512)abortInput=true;else inputIn.insert(inputIn.end(),in.begin(),in.end());}
|
||||
{std::lock_guard lock(mutex);inputStatus=channel->status();}
|
||||
}
|
||||
fd_set read; FD_ZERO(&read); FD_SET(socket,&read); timeval delay{0,channel?5000:100000};
|
||||
int ready=select(0,&read,nullptr,nullptr,&delay);
|
||||
if(ready==SOCKET_ERROR) throw std::runtime_error("LAN discovery socket failed.");
|
||||
if(ready>0) for(int count=0;count<64;++count)
|
||||
@@ -184,6 +266,8 @@ struct LanService::Impl
|
||||
}
|
||||
}
|
||||
catch(const std::exception &e) { std::lock_guard lock(mutex); status=std::string("LAN stopped: ")+e.what(); }
|
||||
inputConnected=false;++inputEpoch;
|
||||
{std::lock_guard lock(mutex);if(shareInput)inputStatus="Input connection closed.";}
|
||||
if(socket!=INVALID_SOCKET) closesocket(socket);
|
||||
if(winsock) WSACleanup();
|
||||
}
|
||||
@@ -195,9 +279,19 @@ struct LanService::Impl
|
||||
stop();
|
||||
bool previous=enabled; enabled=j.at("enabled").get<bool>();
|
||||
try {save();} catch(...) {enabled=previous;if(enabled) start();throw;}
|
||||
configureRouter();
|
||||
if(enabled) start(); else status="Disabled. No discovery sockets or worker thread.";
|
||||
}
|
||||
std::lock_guard lock(mutex);
|
||||
if(action=="input")
|
||||
{
|
||||
bool active=j.at("enabled");auto peer=j.value("peer",inputPeer);int edge=j.value("edge",inputEdge);
|
||||
if(edge<0||edge>3||(active&&(!enabled||!trusted.contains(peer))))throw std::runtime_error("Enable discovery and select a paired KSwitcher first.");
|
||||
auto beforePeer=inputPeer;int beforeEdge=inputEdge;bool beforeActive=shareInput;
|
||||
shareInput=active;inputPeer=peer;inputEdge=edge;
|
||||
try{save();}catch(...){shareInput=beforeActive;inputPeer=beforePeer;inputEdge=beforeEdge;throw;}
|
||||
configureRouter();inputStatus=active?"Connecting input channel...":"Input sharing disabled.";
|
||||
}
|
||||
if(action=="code") return encoded({{"code",lan::randomHex(16)}});
|
||||
if(action=="cancel") { pending.reset(); status="Pairing cancelled."; }
|
||||
if(action=="pair")
|
||||
@@ -208,8 +302,18 @@ struct LanService::Impl
|
||||
pending=std::make_unique<lan::Pairing>(id,peer,j.at("code").get<std::string>(),GetTickCount64());
|
||||
status="Waiting for the same code and Pair on the other KSwitcher (2 minutes)...";
|
||||
}
|
||||
if(action=="forget") { auto peer=j.at("peer").get<std::string>(); trusted.erase(peer); if(pending&&pending->peer==peer) pending.reset(); save(); status="Pair removed on this computer."; }
|
||||
Json result={{"enabled",enabled},{"id",id},{"name",name},{"status",status},{"pending",pending&&!pending->confirmed},{"peers",Json::array()}};
|
||||
if(action=="forget")
|
||||
{
|
||||
auto peer=j.at("peer").get<std::string>();auto oldTrusted=trusted;bool oldSharing=shareInput;
|
||||
trusted.erase(peer);if(peer==inputPeer)shareInput=false;
|
||||
try{save();}catch(...){trusted=std::move(oldTrusted);shareInput=oldSharing;throw;}
|
||||
if(pending&&pending->peer==peer)pending.reset();
|
||||
if(peer==inputPeer)configureRouter();
|
||||
status="Pair removed on this computer.";
|
||||
}
|
||||
Json result={{"enabled",enabled},{"id",id},{"name",name},{"status",status},{"pending",pending&&!pending->confirmed},{"peers",Json::array()},
|
||||
{"shareInput",shareInput},{"inputPeer",inputPeer},{"inputEdge",inputEdge},{"inputConnected",inputConnected.load()},
|
||||
{"inputStatus",enabled&&shareInput?inputStatus:"Input sharing disabled."},{"controlMode",router?int(router->mode()):0}};
|
||||
auto now=GetTickCount64();
|
||||
for(const auto &[peer,p]:peers)
|
||||
{
|
||||
@@ -225,4 +329,10 @@ struct LanService::Impl
|
||||
LanService::LanService(const std::filesystem::path &path) : impl(std::make_unique<Impl>(path)) {}
|
||||
LanService::~LanService()=default;
|
||||
Bytes LanService::command(const Bytes &command) { return impl->command(command); }
|
||||
bool LanService::inputEnabled()const{return impl->enabled&&impl->shareInput;}
|
||||
bool LanService::tickInput(bool allowEnter){return impl->tickInput(allowEnter);}
|
||||
bool LanService::keyboard(const KBDLLHOOKSTRUCT &k,bool down){return impl->router&&impl->router->keyboard(k,down,GetTickCount64());}
|
||||
bool LanService::mouse(UINT message,const MSLLHOOKSTRUCT &m,bool allowEnter){return impl->router&&impl->router->mouse(message,m,GetTickCount64(),allowEnter);}
|
||||
bool LanService::takeInputChange(){return impl->takeChange();}
|
||||
void LanService::cancelInput(){if(impl->router)impl->router->cancel(GetTickCount64());impl->abortInput=true;}
|
||||
}
|
||||
Reference in new issue
Block a user