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
+36
-1
@@ -725,7 +725,11 @@ bool App::registerHotkeys(const Settings &s, bool rollback)
|
||||
Bytes App::lanCommand(const Bytes &command)
|
||||
{
|
||||
if (!lanService) lanService = std::make_unique<LanService>(data);
|
||||
return lanService->command(command);
|
||||
auto reply = lanService->command(command);
|
||||
if (lanService->inputEnabled()) SetTimer(window, 7, 10, nullptr);
|
||||
else KillTimer(window, 7);
|
||||
if (lanService->takeInputChange()) resetInput();
|
||||
return reply;
|
||||
}
|
||||
void App::applySettings(const Settings &requested)
|
||||
{
|
||||
@@ -1124,6 +1128,13 @@ void App::replayTyping()
|
||||
}
|
||||
LRESULT CALLBACK App::keyProc(int code, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
if (code == HC_ACTION && current && current->lanService &&
|
||||
(wp == WM_KEYDOWN || wp == WM_SYSKEYDOWN || wp == WM_KEYUP || wp == WM_SYSKEYUP))
|
||||
{
|
||||
bool consumed = current->lanService->keyboard(*(KBDLLHOOKSTRUCT *)lp, wp == WM_KEYDOWN || wp == WM_SYSKEYDOWN);
|
||||
if (current->lanService->takeInputChange()) current->resetInput();
|
||||
if (consumed) return 1; // Forwarded keys must not reach local hotkeys/snippets or other routers.
|
||||
}
|
||||
// MWB suppresses source-machine events that it forwards elsewhere. Let
|
||||
// downstream hooks route the event before tracking it or starting a snippet.
|
||||
// Call the chain exactly once; a local event can still be consumed below.
|
||||
@@ -1153,6 +1164,13 @@ LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
if (code == HC_ACTION && current)
|
||||
{
|
||||
if (current->lanService)
|
||||
{
|
||||
bool consumed = current->lanService->mouse(UINT(wp), *(MSLLHOOKSTRUCT *)lp,
|
||||
!current->busy && !current->suspended && !current->hotkeyCapture);
|
||||
if (current->lanService->takeInputChange()) current->resetInput();
|
||||
if (consumed) return 1;
|
||||
}
|
||||
// KSwitcher generates no mouse input. Remote clicks invalidate a typing
|
||||
// range just like local clicks, including while a transaction is pending.
|
||||
if (wp == WM_LBUTTONDOWN || wp == WM_RBUTTONDOWN || wp == WM_MBUTTONDOWN || wp == WM_MOUSEWHEEL)
|
||||
@@ -2127,6 +2145,13 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
case WM_HOTKEY:
|
||||
app->dispatch((int)wp);
|
||||
return 0;
|
||||
case WM_POWERBROADCAST:
|
||||
if (wp == PBT_APMSUSPEND && app->lanService)
|
||||
{
|
||||
app->lanService->cancelInput();
|
||||
app->resetInput();
|
||||
}
|
||||
return TRUE;
|
||||
case ReplayTypingMessage:
|
||||
app->replayTyping();
|
||||
return 0;
|
||||
@@ -2198,6 +2223,15 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
return TRUE;
|
||||
}
|
||||
case WM_TIMER:
|
||||
if (wp == 7)
|
||||
{
|
||||
if (app->lanService)
|
||||
{
|
||||
if (app->suspended) app->lanService->cancelInput();
|
||||
if (app->lanService->tickInput(!app->busy && !app->hotkeyCapture)) app->resetInput();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (wp == 6)
|
||||
{
|
||||
if (app->notificationRegistration && WaitForSingleObject(app->notificationRegistration, 0) == WAIT_OBJECT_0)
|
||||
@@ -2341,6 +2375,7 @@ int App::execute()
|
||||
if (!smoke && std::filesystem::exists(data / L"lan-pairing.bin"))
|
||||
try { lanService = std::make_unique<LanService>(data); }
|
||||
catch (const std::exception &e) { report(L"LAN module: " + wide(e.what())); }
|
||||
if (lanService && lanService->inputEnabled()) SetTimer(window, 7, 10, nullptr);
|
||||
SetTimer(window, 1, 60000, nullptr);
|
||||
if (!smoke) SetTimer(window, 6, 1000, nullptr);
|
||||
SetThreadExecutionState(ES_CONTINUOUS |
|
||||
|
||||
+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;}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace ks
|
||||
{
|
||||
// Local-only discovery and pairing. No input/clipboard receiver is exposed yet.
|
||||
// Discovery, pairing and optional encrypted input within the resident process.
|
||||
class LanService
|
||||
{
|
||||
struct Impl;
|
||||
@@ -13,5 +13,11 @@ class LanService
|
||||
explicit LanService(const std::filesystem::path &);
|
||||
~LanService();
|
||||
Bytes command(const Bytes &);
|
||||
bool inputEnabled() const;
|
||||
bool tickInput(bool allowEnter = true);
|
||||
bool keyboard(const KBDLLHOOKSTRUCT &, bool down);
|
||||
bool mouse(UINT, const MSLLHOOKSTRUCT &, bool allowEnter);
|
||||
bool takeInputChange();
|
||||
void cancelInput();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include "lan_channel.h"
|
||||
#include <bcrypt.h>
|
||||
#include <limits>
|
||||
|
||||
namespace ks::lan
|
||||
{
|
||||
namespace
|
||||
{
|
||||
void checked(NTSTATUS s){if(s<0) throw std::runtime_error("Input channel authentication failed.");}
|
||||
void put32(Bytes &b,uint32_t v){for(int n=24;n>=0;n-=8)b.push_back(static_cast<unsigned char>(v>>n));}
|
||||
uint32_t get32(const unsigned char *p){return (uint32_t(p[0])<<24)|(uint32_t(p[1])<<16)|(uint32_t(p[2])<<8)|p[3];}
|
||||
Bytes keyBytes(const std::string &key)
|
||||
{
|
||||
if(key.size()!=64||key.find_first_not_of("0123456789abcdef")!=key.npos) throw std::runtime_error("Invalid input key.");
|
||||
Bytes bytes;for(size_t i=0;i<key.size();i+=2)bytes.push_back(static_cast<unsigned char>(std::stoul(key.substr(i,2),nullptr,16)));return bytes;
|
||||
}
|
||||
bool nonblocking(SOCKET s){u_long value=1;BOOL yes=TRUE;return !ioctlsocket(s,FIONBIO,&value)&&!setsockopt(s,IPPROTO_TCP,TCP_NODELAY,(char*)&yes,sizeof(yes));}
|
||||
void closeSocket(SOCKET &s){if(s!=INVALID_SOCKET){closesocket(s);s=INVALID_SOCKET;}}
|
||||
}
|
||||
struct InputCipher::Impl
|
||||
{
|
||||
BCRYPT_ALG_HANDLE alg{};BCRYPT_KEY_HANDLE key{};uint64_t sequence{};
|
||||
explicit Impl(const std::string &secret)
|
||||
{
|
||||
auto bytes=keyBytes(secret);
|
||||
try {
|
||||
checked(BCryptOpenAlgorithmProvider(&alg,BCRYPT_AES_ALGORITHM,nullptr,0));
|
||||
checked(BCryptSetProperty(alg,BCRYPT_CHAINING_MODE,(PUCHAR)BCRYPT_CHAIN_MODE_GCM,sizeof(BCRYPT_CHAIN_MODE_GCM),0));
|
||||
checked(BCryptGenerateSymmetricKey(alg,&key,nullptr,0,bytes.data(),ULONG(bytes.size()),0));
|
||||
} catch(...) {if(key)BCryptDestroyKey(key);if(alg)BCryptCloseAlgorithmProvider(alg,0);throw;}
|
||||
SecureZeroMemory(bytes.data(),bytes.size());
|
||||
}
|
||||
~Impl(){if(key)BCryptDestroyKey(key);if(alg)BCryptCloseAlgorithmProvider(alg,0);}
|
||||
Bytes crypt(Bytes payload,Bytes &tag,bool encrypt)
|
||||
{
|
||||
if(sequence==std::numeric_limits<uint64_t>::max())throw std::runtime_error("Input session exhausted.");
|
||||
unsigned char nonce[12]{};for(int i=0;i<8;++i)nonce[11-i]=static_cast<unsigned char>(sequence>>(i*8));
|
||||
BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO info;BCRYPT_INIT_AUTH_MODE_INFO(info);
|
||||
info.pbNonce=nonce;info.cbNonce=sizeof(nonce);info.pbTag=tag.data();info.cbTag=ULONG(tag.size());
|
||||
// Protocol/direction are bound by key derivation; sequence and payload length by GCM.
|
||||
Bytes result(payload.size());ULONG written{};
|
||||
auto fn=encrypt?BCryptEncrypt:BCryptDecrypt;
|
||||
checked(fn(key,payload.data(),ULONG(payload.size()),&info,nullptr,0,result.data(),ULONG(result.size()),&written,0));
|
||||
if(written!=payload.size())throw std::runtime_error("Invalid input frame size.");
|
||||
++sequence;return result;
|
||||
}
|
||||
};
|
||||
InputCipher::InputCipher(const std::string &key):impl(std::make_unique<Impl>(key)){}
|
||||
InputCipher::~InputCipher()=default;
|
||||
Bytes InputCipher::encrypt(const InputFrame &f)
|
||||
{
|
||||
Bytes plain;put32(plain,uint32_t(f.type));put32(plain,f.token);put32(plain,uint32_t(f.a));put32(plain,uint32_t(f.b));put32(plain,uint32_t(f.c));
|
||||
Bytes tag(16);auto result=impl->crypt(std::move(plain),tag,true);result.insert(result.end(),tag.begin(),tag.end());return result;
|
||||
}
|
||||
InputFrame InputCipher::decrypt(const Bytes &packet)
|
||||
{
|
||||
if(packet.size()!=36)throw std::runtime_error("Invalid encrypted input frame.");
|
||||
Bytes tag(packet.begin()+20,packet.end());auto plain=impl->crypt(Bytes(packet.begin(),packet.begin()+20),tag,false);
|
||||
auto type=get32(plain.data());if(type>uint32_t(InputType::Wheel))throw std::runtime_error("Unsupported input frame.");
|
||||
return {InputType(type),get32(plain.data()+4),int32_t(get32(plain.data()+8)),int32_t(get32(plain.data()+12)),int32_t(get32(plain.data()+16))};
|
||||
}
|
||||
std::string inputSessionKey(const std::string &secret,const std::string &from,const std::string &to,const std::string &a,const std::string &b,int edge)
|
||||
{
|
||||
if(!identifier(from)||!identifier(to)||from==to||!identifier(a)||!identifier(b))throw std::runtime_error("Invalid session identity.");
|
||||
return mac(secret,"KSwitcher input v1|"+from+"|"+to+"|"+a+"|"+b+"|"+std::to_string(edge));
|
||||
}
|
||||
struct InputChannel::Impl
|
||||
{
|
||||
std::string local,peer,secret,nonce,state="Waiting for the paired computer...";
|
||||
int edge;uint16_t port;SOCKET listener=INVALID_SOCKET,socket=INVALID_SOCKET;
|
||||
bool connecting{},authenticated{},helloRead{};
|
||||
uint64_t changed{},lastRead{},lastPing{},retry{};
|
||||
Bytes incoming,outgoing;
|
||||
std::unique_ptr<InputCipher> tx,rx;
|
||||
Impl(std::string a,std::string b,std::string key,int side,uint16_t p):local(std::move(a)),peer(std::move(b)),secret(std::move(key)),edge(side),port(p){}
|
||||
~Impl(){closeSocket(socket);closeSocket(listener);}
|
||||
void reset(uint64_t now,const std::string &reason)
|
||||
{
|
||||
closeSocket(socket);connecting=authenticated=helloRead=false;incoming.clear();outgoing.clear();tx.reset();rx.reset();++changed;retry=now+1500;state=reason;
|
||||
}
|
||||
void begin(uint64_t now)
|
||||
{
|
||||
nonce=randomHex(16);lastRead=lastPing=now;
|
||||
auto hello=Json{{"app","KSwitcher-input"},{"version",1},{"id",local},{"to",peer},{"nonce",nonce},{"edge",edge}}.dump();
|
||||
put32(outgoing,uint32_t(hello.size()));outgoing.insert(outgoing.end(),hello.begin(),hello.end());
|
||||
state="Authenticating paired KSwitcher...";
|
||||
}
|
||||
void poll(uint32_t address,const std::vector<InputFrame> &frames,std::vector<InputFrame> &received,uint64_t now)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(socket==INVALID_SOCKET&&now<retry)return;
|
||||
if(listener==INVALID_SOCKET && local>peer)
|
||||
{
|
||||
listener=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);BOOL yes=TRUE;
|
||||
if(listener==INVALID_SOCKET||setsockopt(listener,SOL_SOCKET,SO_EXCLUSIVEADDRUSE,(char*)&yes,sizeof(yes))||!nonblocking(listener)){closeSocket(listener);throw std::runtime_error("Cannot open input listener.");}
|
||||
sockaddr_in a{};a.sin_family=AF_INET;a.sin_port=htons(port);
|
||||
if(bind(listener,(sockaddr*)&a,sizeof(a))||listen(listener,1)) {closeSocket(listener);throw std::runtime_error("TCP input port is unavailable.");}
|
||||
}
|
||||
if(socket==INVALID_SOCKET && address && now>=retry)
|
||||
{
|
||||
if(local<peer)
|
||||
{
|
||||
socket=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);if(socket==INVALID_SOCKET||!nonblocking(socket))throw std::runtime_error("Cannot open input connection.");
|
||||
sockaddr_in a{};a.sin_family=AF_INET;a.sin_port=htons(port);a.sin_addr.s_addr=address;
|
||||
int result=connect(socket,(sockaddr*)&a,sizeof(a));
|
||||
if(result && WSAGetLastError()!=WSAEWOULDBLOCK)throw std::runtime_error("Paired computer is not accepting input connections.");
|
||||
connecting=result!=0;begin(now);
|
||||
}
|
||||
else
|
||||
{
|
||||
sockaddr_in a{};int length=sizeof(a);socket=accept(listener,(sockaddr*)&a,&length);
|
||||
if(socket!=INVALID_SOCKET)
|
||||
{
|
||||
if(a.sin_addr.s_addr!=address){closeSocket(socket);return;}
|
||||
if(!nonblocking(socket))throw std::runtime_error("Cannot configure input connection.");
|
||||
begin(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(socket==INVALID_SOCKET)return;
|
||||
if(now-lastRead>2000)throw std::runtime_error("Input connection timed out; control returned locally.");
|
||||
if(connecting)
|
||||
{
|
||||
fd_set writes,errors;FD_ZERO(&writes);FD_ZERO(&errors);FD_SET(socket,&writes);FD_SET(socket,&errors);timeval wait{};
|
||||
if(select(0,nullptr,&writes,&errors,&wait)<=0)return;
|
||||
int error=0;int size=sizeof(error);if(getsockopt(socket,SOL_SOCKET,SO_ERROR,(char*)&error,&size)||error)throw std::runtime_error("Cannot connect to paired KSwitcher.");
|
||||
connecting=false;
|
||||
}
|
||||
if(authenticated)
|
||||
{
|
||||
for(auto &f:frames){auto bytes=tx->encrypt(f);outgoing.insert(outgoing.end(),bytes.begin(),bytes.end());}
|
||||
if(now-lastPing>=400){auto bytes=tx->encrypt({InputType::Ping});outgoing.insert(outgoing.end(),bytes.begin(),bytes.end());lastPing=now;}
|
||||
}
|
||||
if(outgoing.size()>32768)throw std::runtime_error("Input queue overflow; control returned locally.");
|
||||
if(!outgoing.empty())
|
||||
{
|
||||
int count=::send(socket,(char*)outgoing.data(),int(outgoing.size()),0);
|
||||
if(count>0)outgoing.erase(outgoing.begin(),outgoing.begin()+count);
|
||||
else if(count==0||WSAGetLastError()!=WSAEWOULDBLOCK)throw std::runtime_error("Input send failed.");
|
||||
}
|
||||
unsigned char data[4096];
|
||||
for(int batch=0;batch<8;++batch)
|
||||
{
|
||||
int count=recv(socket,(char*)data,sizeof(data),0);
|
||||
if(count==0)throw std::runtime_error("Other KSwitcher disconnected.");
|
||||
if(count<0){if(WSAGetLastError()!=WSAEWOULDBLOCK)throw std::runtime_error("Input receive failed.");break;}
|
||||
incoming.insert(incoming.end(),data,data+count);
|
||||
}
|
||||
if(incoming.size()>32768)throw std::runtime_error("Input receive queue overflow.");
|
||||
if(!helloRead && incoming.size()>=4)
|
||||
{
|
||||
size_t length=get32(incoming.data());if(length>512||length<32)throw std::runtime_error("Invalid input handshake.");
|
||||
if(incoming.size()<length+4)return;
|
||||
auto hello=Json::parse(incoming.begin()+4,incoming.begin()+4+length);
|
||||
if(hello.value("app","")!="KSwitcher-input"||hello.value("version",0)!=1||hello.value("id","")!=peer||hello.value("to","")!=local)
|
||||
throw std::runtime_error("Unexpected input peer.");
|
||||
if(hello.value("edge",-1)!=(edge^1))throw std::runtime_error("Screen positions must be opposite on the two computers.");
|
||||
auto remoteNonce=hello.value("nonce","");
|
||||
tx=std::make_unique<InputCipher>(inputSessionKey(secret,local,peer,nonce,remoteNonce,edge));
|
||||
rx=std::make_unique<InputCipher>(inputSessionKey(secret,peer,local,remoteNonce,nonce,edge^1));
|
||||
incoming.erase(incoming.begin(),incoming.begin()+4+length);helloRead=true;
|
||||
auto bytes=tx->encrypt({InputType::Ready});outgoing.insert(outgoing.end(),bytes.begin(),bytes.end());
|
||||
}
|
||||
while(helloRead && incoming.size()>=36)
|
||||
{
|
||||
auto f=rx->decrypt(Bytes(incoming.begin(),incoming.begin()+36));incoming.erase(incoming.begin(),incoming.begin()+36);lastRead=now;
|
||||
if(!authenticated){if(f.type!=InputType::Ready)throw std::runtime_error("Input received before authentication.");authenticated=true;state="Input connected. Move through the configured screen edge.";}
|
||||
else if(f.type==InputType::Ready)throw std::runtime_error("Repeated input handshake.");
|
||||
else if(f.type!=InputType::Ping)received.push_back(f);
|
||||
}
|
||||
}
|
||||
catch(const std::exception &e){received.clear();reset(now,e.what());}
|
||||
}
|
||||
};
|
||||
InputChannel::InputChannel(std::string a,std::string b,std::string key,int edge,uint16_t port):impl(std::make_unique<Impl>(std::move(a),std::move(b),std::move(key),edge,port)){}
|
||||
InputChannel::~InputChannel()=default;
|
||||
void InputChannel::poll(uint32_t ip,const std::vector<InputFrame>&out,std::vector<InputFrame>&in,uint64_t now){impl->poll(ip,out,in,now);}
|
||||
bool InputChannel::ready()const{return impl->authenticated;}
|
||||
uint64_t InputChannel::generation()const{return impl->changed;}
|
||||
std::string InputChannel::status()const{return impl->state;}
|
||||
void InputChannel::disconnect(){impl->reset(GetTickCount64(),"Input disconnected; local control restored.");}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
#include "lan_pairing.h"
|
||||
#include <memory>
|
||||
namespace ks::lan
|
||||
{
|
||||
enum class InputType : uint32_t { Ready, Ping, Enter, Accept, Return, Move, Key, Button, Wheel };
|
||||
struct InputFrame
|
||||
{
|
||||
InputType type{};
|
||||
uint32_t token{};
|
||||
int32_t a{},b{},c{};
|
||||
bool operator==(const InputFrame &) const = default;
|
||||
};
|
||||
// One directional AES-256-GCM key per session. Strict sequence numbers prevent replay.
|
||||
class InputCipher
|
||||
{
|
||||
struct Impl; std::unique_ptr<Impl> impl;
|
||||
public:
|
||||
explicit InputCipher(const std::string &hexKey);
|
||||
~InputCipher();
|
||||
Bytes encrypt(const InputFrame &);
|
||||
InputFrame decrypt(const Bytes &);
|
||||
};
|
||||
std::string inputSessionKey(const std::string &secret,const std::string &from,const std::string &to,
|
||||
const std::string &fromNonce,const std::string &toNonce,int fromEdge=-1);
|
||||
// Network-thread-only nonblocking TCP channel. No Windows input is injected here.
|
||||
class InputChannel
|
||||
{
|
||||
struct Impl; std::unique_ptr<Impl> impl;
|
||||
public:
|
||||
InputChannel(std::string local,std::string peer,std::string secret,int edge,
|
||||
uint16_t port=45884);
|
||||
~InputChannel();
|
||||
void poll(uint32_t peerAddress,const std::vector<InputFrame> &out,std::vector<InputFrame> &in,uint64_t now);
|
||||
bool ready() const;
|
||||
uint64_t generation() const;
|
||||
std::string status() const;
|
||||
void disconnect();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
#include "lan_input.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace ks::lan
|
||||
{
|
||||
ULONG_PTR remoteInputMarker(){return inputMarker() ^ ULONG_PTR(0x4c414e49);}
|
||||
ULONG_PTR cursorWarpMarker(){return inputMarker() ^ ULONG_PTR(0x4c414e57);}
|
||||
namespace
|
||||
{
|
||||
LRESULT CALLBACK controlOverlay(HWND w,UINT message,WPARAM wp,LPARAM lp)
|
||||
{
|
||||
if(message==WM_SETCURSOR){SetCursor(nullptr);return TRUE;}
|
||||
if(message==WM_MOUSEACTIVATE)return MA_NOACTIVATE;
|
||||
if(message==WM_PAINT)
|
||||
{
|
||||
PAINTSTRUCT paint{};HDC dc=BeginPaint(w,&paint);RECT r{};GetClientRect(w,&r);
|
||||
FillRect(dc,&r,(HBRUSH)(COLOR_INFOBK+1));SetBkMode(dc,TRANSPARENT);SetTextColor(dc,GetSysColor(COLOR_INFOTEXT));
|
||||
auto previous=SelectObject(dc,GetStockObject(DEFAULT_GUI_FONT));
|
||||
DrawTextW(dc,L"KSwitcher: mouse + keyboard on the other PC\nCtrl+Alt+Esc returns control here",-1,&r,DT_CENTER|DT_WORDBREAK);
|
||||
SelectObject(dc,previous);EndPaint(w,&paint);return 0;
|
||||
}
|
||||
return DefWindowProcW(w,message,wp,lp);
|
||||
}
|
||||
struct ControlOverlay
|
||||
{
|
||||
HWND cover{},notice{};
|
||||
~ControlOverlay(){hide();}
|
||||
void hide(){if(notice)DestroyWindow(notice);if(cover)DestroyWindow(cover);notice=cover=nullptr;}
|
||||
void show()
|
||||
{
|
||||
if(cover)return;
|
||||
WNDCLASSW cls{};cls.hInstance=GetModuleHandleW(nullptr);cls.lpfnWndProc=controlOverlay;cls.lpszClassName=L"KSwitcher.RemoteControlOverlay";
|
||||
RegisterClassW(&cls);
|
||||
auto style=WS_EX_TOPMOST|WS_EX_TOOLWINDOW|WS_EX_NOACTIVATE;
|
||||
POINT cursor{};GetCursorPos(&cursor);MONITORINFO monitor{sizeof(monitor)};GetMonitorInfoW(MonitorFromPoint(cursor,MONITOR_DEFAULTTONEAREST),&monitor);
|
||||
// Only cover the parked cursor, not a full-screen layered bitmap.
|
||||
int x=(monitor.rcMonitor.left+monitor.rcMonitor.right)/2-16,y=(monitor.rcMonitor.top+monitor.rcMonitor.bottom)/2-16;
|
||||
cover=CreateWindowExW(style|WS_EX_LAYERED,cls.lpszClassName,L"",WS_POPUP,x,y,32,32,nullptr,nullptr,cls.hInstance,nullptr);
|
||||
if(cover){SetLayeredWindowAttributes(cover,0,1,LWA_ALPHA);ShowWindow(cover,SW_SHOWNOACTIVATE);}
|
||||
notice=CreateWindowExW(style,cls.lpszClassName,L"KSwitcher remote control",WS_POPUP,monitor.rcMonitor.left+20,monitor.rcMonitor.top+20,400,48,nullptr,nullptr,cls.hInstance,nullptr);
|
||||
if(notice)ShowWindow(notice,SW_SHOWNOACTIVATE);
|
||||
SetCursor(nullptr);
|
||||
}
|
||||
};
|
||||
bool ordinaryDesktop()
|
||||
{
|
||||
HDESK d=OpenInputDesktop(0,FALSE,DESKTOP_READOBJECTS);if(!d)return false;
|
||||
wchar_t name[256]{};DWORD count{};bool ok=GetUserObjectInformationW(d,UOI_NAME,name,sizeof(name),&count)!=0;
|
||||
CloseDesktop(d);return ok&&_wcsicmp(name,L"Default")==0;
|
||||
}
|
||||
INPUT button(unsigned index,bool down)
|
||||
{
|
||||
INPUT input{};input.type=INPUT_MOUSE;
|
||||
static constexpr DWORD flags[5][2]={{MOUSEEVENTF_LEFTUP,MOUSEEVENTF_LEFTDOWN},{MOUSEEVENTF_RIGHTUP,MOUSEEVENTF_RIGHTDOWN},
|
||||
{MOUSEEVENTF_MIDDLEUP,MOUSEEVENTF_MIDDLEDOWN},{MOUSEEVENTF_XUP,MOUSEEVENTF_XDOWN},{MOUSEEVENTF_XUP,MOUSEEVENTF_XDOWN}};
|
||||
input.mi.dwFlags=flags[index][down];if(index>=3)input.mi.mouseData=index==3?XBUTTON1:XBUTTON2;return input;
|
||||
}
|
||||
}
|
||||
InputEnvironment windowsInputEnvironment()
|
||||
{
|
||||
auto overlay=std::make_shared<ControlOverlay>();
|
||||
return {
|
||||
[]{LONG x=GetSystemMetrics(SM_XVIRTUALSCREEN),y=GetSystemMetrics(SM_YVIRTUALSCREEN);return RECT{x,y,x+GetSystemMetrics(SM_CXVIRTUALSCREEN),y+GetSystemMetrics(SM_CYVIRTUALSCREEN)};},
|
||||
[]{POINT p{};GetCursorPos(&p);return p;},
|
||||
[](const INPUT &i){INPUT input=i;return SendInput(1,&input,sizeof(input))==1;},
|
||||
ordinaryDesktop,
|
||||
[]{for(int key=1;key<256;++key)if(GetAsyncKeyState(key)&0x8000)return true;return false;},
|
||||
[]{POINT p{};GetCursorPos(&p);MONITORINFO info{sizeof(info)};
|
||||
if(GetMonitorInfoW(MonitorFromPoint(p,MONITOR_DEFAULTTONEAREST),&info))p={(info.rcMonitor.left+info.rcMonitor.right)/2,(info.rcMonitor.top+info.rcMonitor.bottom)/2};
|
||||
return p;},
|
||||
[overlay](ControlMode mode){if(mode==ControlMode::Sending)overlay->show();else overlay->hide();}
|
||||
};
|
||||
}
|
||||
bool atEdge(POINT p,RECT r,int side)
|
||||
{return side==0?p.x<=r.left:side==1?p.x>=r.right-1:side==2?p.y<=r.top:p.y>=r.bottom-1;}
|
||||
int edgePosition(POINT p,RECT r,int side)
|
||||
{
|
||||
auto length=side<2?r.bottom-r.top:r.right-r.left;
|
||||
auto offset=side<2?p.y-r.top:p.x-r.left;
|
||||
return int(std::clamp<int64_t>(int64_t(offset)*65535/std::max<LONG>(1,length-1),0,65535));
|
||||
}
|
||||
POINT edgePoint(RECT r,int side,int position,int inset)
|
||||
{
|
||||
position=std::clamp(position,0,65535);
|
||||
LONG x=r.left+LONG(int64_t(position)*std::max<LONG>(0,r.right-r.left-1)/65535);
|
||||
LONG y=r.top+LONG(int64_t(position)*std::max<LONG>(0,r.bottom-r.top-1)/65535);
|
||||
POINT p=side==0?POINT{r.left+inset,y}:side==1?POINT{r.right-1-inset,y}:side==2?POINT{x,r.top+inset}:POINT{x,r.bottom-1-inset};
|
||||
p.x=std::clamp(p.x,r.left,r.right-1);p.y=std::clamp(p.y,r.top,r.bottom-1);return p;
|
||||
}
|
||||
InputRouter::InputRouter(InputEnvironment e,std::function<bool(InputFrame)> s,int side,bool prefer)
|
||||
:env(std::move(e)),send(std::move(s)),edge(side),preferLocal(prefer),nextToken(prefer?1:2)
|
||||
{if(side<0||side>3)throw std::runtime_error("Invalid screen position.");}
|
||||
InputRouter::~InputRouter(){cancel(GetTickCount64());release();}
|
||||
void InputRouter::mode(ControlMode value){if(mode_!=value){mode_=value;++revision_;if(env.modeChanged)env.modeChanged(value);}}
|
||||
bool InputRouter::emit(INPUT i)
|
||||
{
|
||||
if(i.type==INPUT_KEYBOARD)i.ki.dwExtraInfo=remoteInputMarker();
|
||||
else i.mi.dwExtraInfo=remoteInputMarker();
|
||||
return env.desktop()&&env.inject(i);
|
||||
}
|
||||
bool InputRouter::move(POINT p,bool warp)
|
||||
{
|
||||
auto r=env.bounds();if(r.right<=r.left||r.bottom<=r.top)return false;
|
||||
INPUT input{};input.type=INPUT_MOUSE;input.mi.dwFlags=MOUSEEVENTF_MOVE|MOUSEEVENTF_ABSOLUTE|MOUSEEVENTF_VIRTUALDESK;
|
||||
// Target pixel centres, avoiding a one-pixel bias on every relative mouse event.
|
||||
input.mi.dx=LONG((2*int64_t(std::clamp(p.x,r.left,r.right-1)-r.left)+1)*32768/(r.right-r.left));
|
||||
input.mi.dy=LONG((2*int64_t(std::clamp(p.y,r.top,r.bottom-1)-r.top)+1)*32768/(r.bottom-r.top));
|
||||
input.mi.dwExtraInfo=warp?cursorWarpMarker():remoteInputMarker();return env.desktop()&&env.inject(input);
|
||||
}
|
||||
bool InputRouter::post(InputFrame f){return connected&&send(f);}
|
||||
bool InputRouter::held()const
|
||||
{
|
||||
return std::any_of(physical.begin(),physical.end(),[](bool b){return b;})||env.held();
|
||||
}
|
||||
void InputRouter::release()
|
||||
{
|
||||
for(size_t i=0;i<remoteKeys.size();++i)if(remoteKeys[i]&&emit(releases[i]))remoteKeys[i]=false;
|
||||
for(unsigned i=0;i<5;++i)if(remoteButtons[i]&&emit(button(i,false)))remoteButtons[i]=false;
|
||||
}
|
||||
void InputRouter::local(uint64_t now,int position)
|
||||
{
|
||||
bool sending=mode_==ControlMode::Sending;
|
||||
if(position<0)position=edgePosition(returnPoint,env.bounds(),edge);
|
||||
release();mode(ControlMode::Local);cooldown=now+600;token=0;
|
||||
if(sending)move(edgePoint(env.bounds(),edge,position),true);
|
||||
}
|
||||
void InputRouter::cancel(uint64_t now)
|
||||
{
|
||||
if(mode_!=ControlMode::Local)post({InputType::Return,token,edgePosition(env.cursor(),env.bounds(),edge)});
|
||||
local(now);
|
||||
}
|
||||
void InputRouter::tick(bool ready,uint64_t now)
|
||||
{
|
||||
connected=ready;
|
||||
if(!ready||!env.desktop()||(mode_==ControlMode::Requesting&&now-requested>1000))cancel(now);
|
||||
if(mode_==ControlMode::Local)release(); // Retry releases blocked by a temporarily secure desktop.
|
||||
}
|
||||
void InputRouter::receive(const InputFrame &f,uint64_t now)
|
||||
{
|
||||
if(!connected||!env.desktop()){cancel(now);return;}
|
||||
if(f.type==InputType::Enter)
|
||||
{
|
||||
if(!f.token||f.a!=(edge^1)||f.b<0||f.b>65535||held()||
|
||||
mode_==ControlMode::Sending||mode_==ControlMode::Receiving||(mode_==ControlMode::Requesting&&preferLocal))
|
||||
{post({InputType::Return,f.token,32768});return;}
|
||||
if(mode_==ControlMode::Requesting)cancel(now);
|
||||
token=f.token;mode(ControlMode::Receiving);
|
||||
if(!move(edgePoint(env.bounds(),edge,f.b))||!post({InputType::Accept,token}))cancel(now);
|
||||
return;
|
||||
}
|
||||
if(f.type==InputType::Accept)
|
||||
{
|
||||
if(mode_!=ControlMode::Requesting||f.token!=token||held()||!atEdge(env.cursor(),env.bounds(),edge)){post({InputType::Return,f.token,32768});if(f.token==token)cancel(now);return;}
|
||||
auto r=env.bounds();anchor=env.anchor?env.anchor():POINT{(r.left+r.right)/2,(r.top+r.bottom)/2};mode(ControlMode::Sending);
|
||||
if(!move(anchor,true))cancel(now);
|
||||
return;
|
||||
}
|
||||
if(f.token!=token||!token)return;
|
||||
if(f.type==InputType::Return){local(now,std::clamp(f.a,0,65535));return;}
|
||||
if(mode_!=ControlMode::Receiving)return;
|
||||
bool ok=true;
|
||||
if(f.type==InputType::Move)
|
||||
{
|
||||
if(std::abs(int64_t(f.a))>32768||std::abs(int64_t(f.b))>32768){cancel(now);return;}
|
||||
auto p=env.cursor();auto r=env.bounds();p.x=LONG(std::clamp<int64_t>(int64_t(p.x)+f.a,r.left,r.right-1));p.y=LONG(std::clamp<int64_t>(int64_t(p.y)+f.b,r.top,r.bottom-1));
|
||||
if(atEdge(p,r,edge)&&!std::any_of(remoteKeys.begin(),remoteKeys.end(),[](bool b){return b;})&&
|
||||
!std::any_of(remoteButtons.begin(),remoteButtons.end(),[](bool b){return b;}))
|
||||
{post({InputType::Return,token,edgePosition(p,r,edge)});local(now);return;}
|
||||
ok=move(p);
|
||||
}
|
||||
else if(f.type==InputType::Key)
|
||||
{
|
||||
if(f.a<1||f.a>255||f.b<0||f.b>255||f.c<0||f.c>3){cancel(now);return;}
|
||||
INPUT input{};input.type=INPUT_KEYBOARD;input.ki.wVk=WORD(f.a);input.ki.wScan=WORD(f.b);
|
||||
input.ki.dwFlags=(f.c&1?0:KEYEVENTF_KEYUP)|(f.c&2?KEYEVENTF_EXTENDEDKEY:0);
|
||||
if(f.b&&f.a!=VK_PAUSE)input.ki.dwFlags|=KEYEVENTF_SCANCODE;
|
||||
ok=emit(input);if(ok){remoteKeys[size_t(f.a)]=(f.c&1)!=0;input.ki.dwFlags|=KEYEVENTF_KEYUP;releases[size_t(f.a)]=input;}
|
||||
}
|
||||
else if(f.type==InputType::Button)
|
||||
{
|
||||
if(f.a<0||f.a>4||f.b<0||f.b>1){cancel(now);return;}
|
||||
ok=emit(button(unsigned(f.a),f.b!=0));if(ok)remoteButtons[size_t(f.a)]=f.b!=0;
|
||||
}
|
||||
else if(f.type==InputType::Wheel)
|
||||
{
|
||||
if(f.a<-32768||f.a>32767||f.b<0||f.b>1){cancel(now);return;}
|
||||
INPUT input{};input.type=INPUT_MOUSE;input.mi.dwFlags=f.b?MOUSEEVENTF_HWHEEL:MOUSEEVENTF_WHEEL;input.mi.mouseData=DWORD(f.a);ok=emit(input);
|
||||
}
|
||||
if(!ok)cancel(now);
|
||||
}
|
||||
bool InputRouter::keyboard(const KBDLLHOOKSTRUCT &k,bool down,uint64_t now)
|
||||
{
|
||||
if(k.flags&LLKHF_INJECTED||k.vkCode>255||!k.vkCode)return false;
|
||||
auto key=size_t(k.vkCode);physical[key]=down;
|
||||
if(mode_!=ControlMode::Local&&down&&key==VK_ESCAPE&&(physical[VK_LCONTROL]||physical[VK_RCONTROL]||physical[VK_CONTROL])&&
|
||||
(physical[VK_LMENU]||physical[VK_RMENU]||physical[VK_MENU]))
|
||||
{outKeys[key]=true;cancel(now);return true;}
|
||||
if(mode_==ControlMode::Receiving||mode_==ControlMode::Requesting)cancel(now);
|
||||
if(mode_==ControlMode::Sending)
|
||||
{
|
||||
outKeys[key]=down;
|
||||
if(!post({InputType::Key,token,int32_t(k.vkCode),int32_t(k.scanCode),int32_t((down?1:0)|((k.flags&LLKHF_EXTENDED)?2:0))}))cancel(now);
|
||||
return true;
|
||||
}
|
||||
if(outKeys[key]){if(!down)outKeys[key]=false;return true;}
|
||||
return false;
|
||||
}
|
||||
bool InputRouter::mouse(UINT message,const MSLLHOOKSTRUCT &m,uint64_t now,bool allowEnter)
|
||||
{
|
||||
if(m.flags&LLMHF_INJECTED)return false;
|
||||
int index=-1;bool down=false;
|
||||
switch(message){case WM_LBUTTONDOWN:down=true;[[fallthrough]];case WM_LBUTTONUP:index=0;break;
|
||||
case WM_RBUTTONDOWN:down=true;[[fallthrough]];case WM_RBUTTONUP:index=1;break;
|
||||
case WM_MBUTTONDOWN:down=true;[[fallthrough]];case WM_MBUTTONUP:index=2;break;
|
||||
case WM_XBUTTONDOWN:down=true;[[fallthrough]];case WM_XBUTTONUP:index=HIWORD(m.mouseData)==XBUTTON1?3:4;break;}
|
||||
if(mode_==ControlMode::Receiving||(mode_==ControlMode::Requesting&&message!=WM_MOUSEMOVE))cancel(now);
|
||||
if(mode_==ControlMode::Requesting&&message==WM_MOUSEMOVE&&!atEdge(m.pt,env.bounds(),edge))cancel(now);
|
||||
if(mode_==ControlMode::Sending)
|
||||
{
|
||||
bool ok=true;
|
||||
if(message==WM_MOUSEMOVE)
|
||||
{
|
||||
auto dx=m.pt.x-anchor.x,dy=m.pt.y-anchor.y;
|
||||
if(dx||dy)ok=post({InputType::Move,token,dx,dy});
|
||||
ok=move(anchor,true)&&ok;
|
||||
}
|
||||
else if(index>=0){outButtons[size_t(index)]=down;ok=post({InputType::Button,token,index,down?1:0});}
|
||||
else if(message==WM_MOUSEWHEEL||message==WM_MOUSEHWHEEL)ok=post({InputType::Wheel,token,int16_t(HIWORD(m.mouseData)),message==WM_MOUSEHWHEEL?1:0});
|
||||
if(!ok)cancel(now);
|
||||
return true;
|
||||
}
|
||||
if(index>=0&&outButtons[size_t(index)]){if(!down)outButtons[size_t(index)]=false;return true;}
|
||||
if(mode_==ControlMode::Local&&connected&&allowEnter&&message==WM_MOUSEMOVE&&now>=cooldown&&
|
||||
env.desktop()&&!held()&&atEdge(m.pt,env.bounds(),edge))
|
||||
{
|
||||
token=nextToken;nextToken+=2;if(!nextToken)nextToken=preferLocal?1:2;
|
||||
returnPoint=m.pt;requested=now;mode(ControlMode::Requesting);
|
||||
if(!post({InputType::Enter,token,edge,edgePosition(m.pt,env.bounds(),edge)}))cancel(now);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
#include "lan_channel.h"
|
||||
#include <array>
|
||||
namespace ks::lan
|
||||
{
|
||||
enum class ControlMode { Local, Requesting, Sending, Receiving };
|
||||
struct InputEnvironment
|
||||
{
|
||||
std::function<RECT()> bounds;
|
||||
std::function<POINT()> cursor;
|
||||
std::function<bool(const INPUT &)> inject;
|
||||
std::function<bool()> desktop;
|
||||
std::function<bool()> held;
|
||||
std::function<POINT()> anchor;
|
||||
std::function<void(ControlMode)> modeChanged;
|
||||
};
|
||||
InputEnvironment windowsInputEnvironment();
|
||||
ULONG_PTR remoteInputMarker();
|
||||
ULONG_PTR cursorWarpMarker();
|
||||
bool atEdge(POINT,RECT,int edge);
|
||||
int edgePosition(POINT,RECT,int edge);
|
||||
POINT edgePoint(RECT,int edge,int position,int inset=8);
|
||||
class InputRouter
|
||||
{
|
||||
InputEnvironment env;
|
||||
std::function<bool(InputFrame)> send;
|
||||
ControlMode mode_=ControlMode::Local;
|
||||
int edge{};bool preferLocal{},connected{};
|
||||
uint32_t nextToken{},token{};
|
||||
uint64_t cooldown{},requested{},revision_{};
|
||||
POINT anchor{},returnPoint{};
|
||||
std::array<bool,256> physical{},outKeys{},remoteKeys{};
|
||||
std::array<INPUT,256> releases{};
|
||||
std::array<bool,5> outButtons{},remoteButtons{};
|
||||
void mode(ControlMode);
|
||||
bool emit(INPUT);
|
||||
bool move(POINT,bool warp=false);
|
||||
bool post(InputFrame);
|
||||
bool held() const;
|
||||
void release();
|
||||
void local(uint64_t now,int position=-1);
|
||||
public:
|
||||
InputRouter(InputEnvironment,std::function<bool(InputFrame)>,int edge,bool preferLocal);
|
||||
~InputRouter();
|
||||
ControlMode mode()const{return mode_;}
|
||||
uint64_t revision()const{return revision_;}
|
||||
void tick(bool ready,uint64_t now);
|
||||
void receive(const InputFrame &,uint64_t now);
|
||||
bool keyboard(const KBDLLHOOKSTRUCT &,bool down,uint64_t now);
|
||||
bool mouse(UINT,const MSLLHOOKSTRUCT &,uint64_t now,bool allowEnter=true);
|
||||
void cancel(uint64_t now);
|
||||
};
|
||||
}
|
||||
+32
-4
@@ -6,6 +6,7 @@ struct LanView
|
||||
nlohmann::json snapshot;
|
||||
std::vector<std::string> ids;
|
||||
std::wstring error;
|
||||
bool positionEdited{};
|
||||
};
|
||||
nlohmann::json lanRequest(App &app,const nlohmann::json &request)
|
||||
{
|
||||
@@ -47,6 +48,18 @@ void refreshLan(HWND w,LanView &v)
|
||||
EnableWindow(GetDlgItem(w,706),pending);
|
||||
EnableWindow(GetDlgItem(w,707),paired);
|
||||
for(int id:{703,704}) EnableWindow(GetDlgItem(w,id),enabled&&!pending);
|
||||
EnableWindow(GetDlgItem(w,715),enabled&&paired);
|
||||
EnableWindow(GetDlgItem(w,716),v.snapshot.value("shareInput",false));
|
||||
for(int id:{711,712,713,714})EnableWindow(GetDlgItem(w,id),paired);
|
||||
if(!v.positionEdited)CheckRadioButton(w,711,714,711+v.snapshot.value("inputEdge",1));
|
||||
int position=1;for(int i=0;i<4;++i)if(checked(w,711+i))position=i;
|
||||
const wchar_t *diagrams[]={L"[ Other PC ] ← [ This PC ]",L"[ This PC ] → [ Other PC ]",L"Other PC is ABOVE this PC",L"Other PC is BELOW this PC"};
|
||||
value(w,719,diagrams[position]);
|
||||
auto inputStatus=wide(v.snapshot.value("inputStatus",std::string("Input sharing disabled.")));
|
||||
auto mode=v.snapshot.value("controlMode",0);
|
||||
if(mode==2)inputStatus=L"Mouse and keyboard control the OTHER PC. Ctrl+Alt+Esc returns here.";
|
||||
else if(mode==3)inputStatus=L"This PC is controlled remotely. Local physical input takes control back.";
|
||||
value(w,718,inputStatus);
|
||||
}
|
||||
LRESULT CALLBACK lanProc(HWND w,UINT msg,WPARAM wp,LPARAM lp)
|
||||
{
|
||||
@@ -61,7 +74,7 @@ LRESULT CALLBACK lanProc(HWND w,UINT msg,WPARAM wp,LPARAM lp)
|
||||
if(msg==WM_COMMAND)
|
||||
{
|
||||
int id=LOWORD(wp); auto peer=selectedLan(w,*v);
|
||||
if(id==702&&HIWORD(wp)==LBN_SELCHANGE) {refreshLan(w,*v);return 0;}
|
||||
if(id==702&&HIWORD(wp)==LBN_SELCHANGE) {v->positionEdited=false;refreshLan(w,*v);return 0;}
|
||||
if(HIWORD(wp)!=BN_CLICKED) return DefWindowProcW(w,msg,wp,lp);
|
||||
v->error.clear();
|
||||
if(id==701) lanRequest(*v->app,{{"action","enable"},{"enabled",!v->snapshot.value("enabled",false)}});
|
||||
@@ -74,6 +87,14 @@ LRESULT CALLBACK lanProc(HWND w,UINT msg,WPARAM wp,LPARAM lp)
|
||||
if(id==705) {lanRequest(*v->app,{{"action","pair"},{"peer",peer},{"code",utf8(value(w,703))}});value(w,703,L"");}
|
||||
if(id==706) lanRequest(*v->app,{{"action","cancel"}});
|
||||
if(id==707) lanRequest(*v->app,{{"action","forget"},{"peer",peer}});
|
||||
if(id>=711&&id<=714) v->positionEdited=true;
|
||||
if(id==715)
|
||||
{
|
||||
int position=1;for(int i=0;i<4;++i)if(checked(w,711+i))position=i;
|
||||
lanRequest(*v->app,{{"action","input"},{"enabled",true},{"peer",peer},{"edge",position}});
|
||||
v->positionEdited=false;
|
||||
}
|
||||
if(id==716)lanRequest(*v->app,{{"action","input"},{"enabled",false}});
|
||||
refreshLan(w,*v); return 0;
|
||||
}
|
||||
}
|
||||
@@ -82,7 +103,7 @@ LRESULT CALLBACK lanProc(HWND w,UINT msg,WPARAM wp,LPARAM lp)
|
||||
}
|
||||
void showLan(HWND parent,App &app)
|
||||
{
|
||||
HWND w=makeWindow(app,L"KSwitcher.LAN",L"KSwitcher computers — pairing preview",lanProc,740,600);
|
||||
HWND w=makeWindow(app,L"KSwitcher.LAN",L"KSwitcher computers — mouse and keyboard",lanProc,740,790);
|
||||
SetWindowLongPtrW(w,GWLP_HWNDPARENT,(LONG_PTR)parent);
|
||||
auto v=new LanView{parent,&app};SetWindowLongPtrW(w,GWLP_USERDATA,(LONG_PTR)v);
|
||||
label(w,L"Find running KSwitcher instances on your local network and pair them.",18,16,700);
|
||||
@@ -97,8 +118,15 @@ void showLan(HWND parent,App &app)
|
||||
label(w,L"Generate once, enter that code on both PCs, select each other, then click Pair on both.",18,392,700);
|
||||
button(w,L"&Pair",705,18,424,140);button(w,L"Cancel &pairing",706,174,424,160);button(w,L"&Forget pair",707,350,424,160);
|
||||
control(w,L"STATIC",L"",0,708,18,466,700,40);
|
||||
label(w,L"Changes apply immediately. This preview pairs PCs; mouse, keyboard and clipboard sharing comes next.",18,512,700);
|
||||
button(w,L"&Close",IDCANCEL,618,550,100);
|
||||
label(w,L"Selected paired computer stands relative to this PC:",18,512,700);
|
||||
const wchar_t *positions[]={L"← &Left",L"&Right →",L"↑ &Above",L"↓ &Below"};
|
||||
for(int i=0;i<4;++i)control(w,L"BUTTON",positions[i],WS_TABSTOP|BS_AUTORADIOBUTTON|(i==0?WS_GROUP:0),711+i,18+i*175,540,166,28);
|
||||
control(w,L"STATIC",L"",SS_CENTER,719,18,578,700,28);
|
||||
button(w,L"Apply position / enable &input",715,18,620,330);
|
||||
button(w,L"&Stop input sharing",716,370,620,240);
|
||||
control(w,L"STATIC",L"",0,718,18,660,700,40);
|
||||
control(w,L"STATIC",L"Choose opposite sides on the two PCs (right ↔ left, above ↔ below). Changes apply immediately. Keyboard follows the mouse. Clipboard sharing is not included yet.",0,720,18,706,700,36);
|
||||
button(w,L"&Close",IDCANCEL,618,748,100);
|
||||
EnableWindow(parent,FALSE);showRequestedWindow(w);SetForegroundWindow(w);
|
||||
SetTimer(w,1,1000,nullptr);
|
||||
try {refreshLan(w,*v);} catch(const std::exception &e){v->error=wide(e.what());value(w,708,v->error);}
|
||||
|
||||
Reference in new issue
Block a user