Release v0.6.63: automatic LAN transport fallback and connection help
This commit is contained in:
1 parent
3761b790ae
commit
5a2106899d
16 files changed
+376
-42
No files matched your search
+27
-4
@@ -72,6 +72,7 @@ struct LanService::Impl
|
||||
std::map<std::string,uint64_t> inputReady;
|
||||
std::unique_ptr<lan::InputMesh> router;
|
||||
std::string inputStatus="Input sharing disabled.";
|
||||
bool inputNeedsHelp{};
|
||||
HWND inputWindow{};std::atomic<bool> inputNotification{false};
|
||||
explicit Impl(const std::filesystem::path &root,HWND window) : path(root/L"lan-pairing.bin"),inputWindow(window)
|
||||
{
|
||||
@@ -210,7 +211,7 @@ struct LanService::Impl
|
||||
auto previous=trusted;auto previousPositions=positions;
|
||||
trusted[other]={{"name",peer.name},{"key",pending->derived}};
|
||||
try {lan::fillPositions(positions,trusted,inputPeer,inputEdge);save();} catch(...) {trusted=std::move(previous);positions=std::move(previousPositions);throw;}
|
||||
layoutChanged=true;peer.verified=now; status="Paired. Select a screen position and enable mouse and keyboard on both PCs.";
|
||||
layoutChanged=true;peer.verified=now; status="Pair saved automatically. Arrange the monitors, then enable input sharing on both PCs.";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,6 +233,10 @@ struct LanService::Impl
|
||||
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;
|
||||
lan::InputHub hub(id);
|
||||
hub.datagrams([&](const auto &target,const auto &packet){
|
||||
std::lock_guard lock(mutex);
|
||||
if(shareInput&&trusted.contains(target)&&peers.contains(target))send(socket,peers.at(target).address,packet);
|
||||
});
|
||||
while(!stopping)
|
||||
{
|
||||
auto now=GetTickCount64();bool clipboardActive=false;
|
||||
@@ -279,7 +284,7 @@ struct LanService::Impl
|
||||
}
|
||||
if(!in.empty()&&inputWindow&&!inputNotification.exchange(true))
|
||||
if(!PostMessageW(inputWindow,LanInputReadyMessage,0,0))inputNotification=false;
|
||||
{std::lock_guard lock(mutex);inputStatus=hub.status();}
|
||||
{std::lock_guard lock(mutex);inputStatus=hub.status();inputNeedsHelp=hub.needsHelp();}
|
||||
fd_set read; FD_ZERO(&read); FD_SET(socket,&read); timeval delay{0,!endpoints.empty()||clipboardActive?5000:100000};
|
||||
int ready=select(0,&read,nullptr,nullptr,&delay);
|
||||
if(ready==SOCKET_ERROR) throw std::runtime_error("LAN discovery socket failed.");
|
||||
@@ -289,7 +294,14 @@ struct LanService::Impl
|
||||
int received=recvfrom(socket,buffer,sizeof(buffer),0,(sockaddr*)&sender,&size);
|
||||
if(received<=0) break;
|
||||
if(received>2048||sender.sin_port!=htons(lan::Port)||!localAddress(sender.sin_addr.s_addr)) continue;
|
||||
try { auto j=Json::parse(buffer,buffer+received); std::lock_guard lock(mutex); receive(socket,sender,j,GetTickCount64()); }
|
||||
try {
|
||||
auto j=Json::parse(buffer,buffer+received);
|
||||
if(j.value("type","")=="input-udp"){
|
||||
bool allowed=false;auto peer=j.value("id","");
|
||||
{std::lock_guard lock(mutex);allowed=shareInput&&trusted.contains(peer)&&peers.contains(peer)&&peers.at(peer).address.sin_addr.s_addr==sender.sin_addr.s_addr;}
|
||||
if(allowed)hub.receiveDatagram(j,GetTickCount64());
|
||||
}else {std::lock_guard lock(mutex);receive(socket,sender,j,GetTickCount64());}
|
||||
}
|
||||
catch(const Json::exception &) {} // Malformed network data cannot interrupt discovery.
|
||||
}
|
||||
}
|
||||
@@ -334,6 +346,14 @@ struct LanService::Impl
|
||||
configureRouter();inputStatus=active?"Connecting input channel...":"Input sharing disabled.";
|
||||
}
|
||||
if(action=="code") return encoded({{"code",lan::randomHex(16)}});
|
||||
if(action=="connectionHelp"){
|
||||
auto exe=utf8(executable());std::string quoted;
|
||||
for(char c:exe){quoted+=c;if(c=='\'')quoted+='\'';}
|
||||
auto script="$ksPath = '"+quoted+"'\r\n"
|
||||
"New-NetFirewallRule -DisplayName 'KSwitcher LAN TCP' -Direction Inbound -Action Allow -Program $ksPath -Protocol TCP -LocalPort 45884 -RemoteAddress LocalSubnet -Profile Any\r\n"
|
||||
"New-NetFirewallRule -DisplayName 'KSwitcher LAN UDP' -Direction Inbound -Action Allow -Program $ksPath -Protocol UDP -LocalPort 45883 -RemoteAddress LocalSubnet -Profile Any";
|
||||
return encoded({{"script",script}});
|
||||
}
|
||||
if(action=="cancel") { pending.reset(); status="Pairing cancelled."; }
|
||||
if(action=="pair")
|
||||
{
|
||||
@@ -354,9 +374,12 @@ struct LanService::Impl
|
||||
}
|
||||
Json result={{"enabled",enabled},{"id",id},{"name",name},{"status",status},{"pending",pending&&!pending->confirmed},{"peers",Json::array()},
|
||||
{"positions",positions},{"shareInput",shareInput},{"inputPeer",inputPeer},{"controlPeer",router?router->peer():std::string{}},{"inputEdge",inputEdge},{"inputConnected",inputConnected.load()},
|
||||
{"inputStatus",enabled&&shareInput?inputStatus:"Input sharing disabled."},{"controlMode",router?int(router->mode()):0}};
|
||||
{"inputStatus",enabled&&shareInput?inputStatus:"Input sharing disabled."},{"inputNeedsHelp",enabled&&shareInput&&inputNeedsHelp},{"controlMode",router?int(router->mode()):0}};
|
||||
{std::lock_guard inputLock(inputMutex);result["inputReady"]=inputReady;}
|
||||
auto now=GetTickCount64();
|
||||
result["pendingPeer"]=pending&&!pending->confirmed ? pending->peer : "";
|
||||
result["pairSecondsRemaining"]=pending&&!pending->confirmed&&!pending->expired(now)
|
||||
? (lan::PairLifetime-(now-pending->started)+999)/1000 : 0;
|
||||
for(const auto &[peer,p]:peers)
|
||||
{
|
||||
char ip[INET_ADDRSTRLEN]{}; InetNtopA(AF_INET,(void*)&p.address.sin_addr,ip,sizeof(ip));
|
||||
|
||||
+60
-18
@@ -1,6 +1,7 @@
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include "lan_channel.h"
|
||||
#include "lan_datagram.h"
|
||||
#include <bcrypt.h>
|
||||
#include <limits>
|
||||
|
||||
@@ -78,17 +79,18 @@ std::string inputSessionKey(const std::string &secret,const std::string &from,co
|
||||
}
|
||||
struct InputChannel::Impl
|
||||
{
|
||||
std::string local,peer,secret,nonce,state="Waiting for the paired computer...";
|
||||
std::string local,peer,secret,nonce,state="Waiting for the paired computer...",lastError;
|
||||
int edge;uint16_t port;SOCKET listener=INVALID_SOCKET,socket=INVALID_SOCKET;
|
||||
bool connecting{},authenticated{},helloRead{},managed{};
|
||||
uint64_t changed{},lastRead{},lastPing{},retry{};
|
||||
uint64_t changed{},lastRead{},lastPing{},retry{},waitingSince{};
|
||||
bool reverse{};
|
||||
Bytes incoming,outgoing;
|
||||
std::unique_ptr<InputCipher> tx,rx;
|
||||
Impl(std::string a,std::string b,std::string key,int side,uint16_t p,bool multiple):local(std::move(a)),peer(std::move(b)),secret(std::move(key)),edge(side),port(p),managed(multiple){}
|
||||
~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;
|
||||
closeSocket(socket);connecting=authenticated=helloRead=false;incoming.clear();outgoing.clear();tx.reset();rx.reset();++changed;retry=now+1500;waitingSince=0;state=reason;lastError=reason;
|
||||
}
|
||||
void begin(uint64_t now)
|
||||
{
|
||||
@@ -101,6 +103,7 @@ struct InputChannel::Impl
|
||||
{
|
||||
try
|
||||
{
|
||||
if(!waitingSince)waitingSince=now;
|
||||
if(socket==INVALID_SOCKET&&now<retry)return;
|
||||
if(!managed && listener==INVALID_SOCKET && local>peer)
|
||||
{
|
||||
@@ -111,14 +114,15 @@ struct InputChannel::Impl
|
||||
}
|
||||
if(socket==INVALID_SOCKET && address && now>=retry)
|
||||
{
|
||||
if(local<peer)
|
||||
if(local<peer||(managed&&now-waitingSince>=1500))
|
||||
{
|
||||
reverse=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);
|
||||
if(connecting)state="Connecting to paired PC on TCP 45884...";
|
||||
if(connecting)state=reverse?"Trying reverse TCP connection...":"Trying direct TCP connection...";
|
||||
}
|
||||
else if(!managed)
|
||||
{
|
||||
@@ -185,7 +189,7 @@ struct InputChannel::Impl
|
||||
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.";}
|
||||
if(!authenticated){if(f.type!=InputType::Ready)throw std::runtime_error("Input received before authentication.");authenticated=true;lastError.clear();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);
|
||||
}
|
||||
@@ -198,30 +202,44 @@ 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::adopt(uintptr_t accepted,uint64_t now)
|
||||
{
|
||||
if(!impl->managed||impl->socket!=INVALID_SOCKET||!nonblocking(SOCKET(accepted)))return false;
|
||||
if(!impl->managed||impl->authenticated||!nonblocking(SOCKET(accepted)))return false;
|
||||
// Reverse connection wins a simultaneous connect race. Never replace an
|
||||
// authenticated channel or accept two sockets for the same input stream.
|
||||
if(impl->socket!=INVALID_SOCKET){
|
||||
if(impl->local>impl->peer||!impl->connecting)return false;
|
||||
impl->reset(now,"Accepting reverse TCP connection.");
|
||||
}
|
||||
impl->reverse=impl->local<impl->peer;
|
||||
impl->connecting=false;impl->begin(now);impl->socket=SOCKET(accepted);return true;
|
||||
}
|
||||
bool InputChannel::ready()const{return impl->authenticated;}
|
||||
uint64_t InputChannel::generation()const{return impl->changed;}
|
||||
std::string InputChannel::status()const{return impl->state;}
|
||||
std::string InputChannel::status()const{
|
||||
return impl->lastError.empty() ? impl->state : impl->lastError+" Retrying automatically.";
|
||||
}
|
||||
void InputChannel::disconnect(){impl->reset(GetTickCount64(),"Input disconnected; local control restored.");}
|
||||
std::string InputChannel::transport()const{return impl->reverse?"reverse TCP":"direct TCP";}
|
||||
struct InputHub::Impl
|
||||
{
|
||||
struct Link { InputEndpoint endpoint;std::unique_ptr<InputChannel> channel;uint64_t generation{},epoch{};std::string lastState; };
|
||||
struct Link { InputEndpoint endpoint;std::unique_ptr<InputChannel> channel;std::unique_ptr<InputDatagram> udp;uint64_t generation{},epoch{},since{},failedSince{};bool udpReady{};std::string lastState; };
|
||||
struct Pending {SOCKET socket;uint32_t address;uint64_t since;};
|
||||
std::string local;uint16_t port;SOCKET listener=INVALID_SOCKET;
|
||||
std::map<std::string,Link> links;std::vector<Pending> pending;uint64_t epoch{},retry{};
|
||||
std::string error;
|
||||
InputHub::SendDatagram sendDatagram;
|
||||
uint64_t currentTime{};
|
||||
Impl(std::string id,uint16_t p):local(std::move(id)),port(p){}
|
||||
~Impl(){closeSocket(listener);for(auto &p:pending)closeSocket(p.socket);}
|
||||
void poll(const std::vector<InputEndpoint> &endpoints,const std::vector<RoutedInput> &out,std::vector<RoutedInput> &in,uint64_t now)
|
||||
{
|
||||
currentTime=now;
|
||||
for(auto it=links.begin();it!=links.end();){
|
||||
auto match=std::find_if(endpoints.begin(),endpoints.end(),[&](auto &e){return e.id==it->first&&e.key==it->second.endpoint.key&&e.port==it->second.endpoint.port;});
|
||||
if(match==endpoints.end())it=links.erase(it);else ++it;
|
||||
}
|
||||
for(auto &e:endpoints)if(e.id!=local&&!links.contains(e.id)){
|
||||
Link link;link.endpoint=e;link.channel=std::make_unique<InputChannel>(local,e.id,e.key,0,e.port,true);link.epoch=++epoch;links.emplace(e.id,std::move(link));
|
||||
Link link;link.endpoint=e;link.channel=std::make_unique<InputChannel>(local,e.id,e.key,0,e.port,true);
|
||||
link.udp=std::make_unique<InputDatagram>(local,e.id,e.key);link.since=link.failedSince=now;link.epoch=++epoch;links.emplace(e.id,std::move(link));
|
||||
}
|
||||
// Discovery is a hint, not the lifetime of authenticated input sessions.
|
||||
// Keep the last usable address across missed announcements. A new address
|
||||
@@ -231,7 +249,7 @@ struct InputHub::Impl
|
||||
link.channel->disconnect();link.endpoint.address=e.address;
|
||||
}
|
||||
}
|
||||
bool listenNeeded=std::any_of(endpoints.begin(),endpoints.end(),[&](auto &e){return e.id<local;});
|
||||
bool listenNeeded=!links.empty();
|
||||
if(!listenNeeded){closeSocket(listener);for(auto &p:pending)closeSocket(p.socket);pending.clear();}
|
||||
if(listenNeeded&&listener==INVALID_SOCKET&&now>=retry){
|
||||
listener=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);BOOL yes=TRUE;sockaddr_in a{};a.sin_family=AF_INET;a.sin_port=htons(port);
|
||||
@@ -253,7 +271,7 @@ struct InputHub::Impl
|
||||
else if(count>=int(length+4)){
|
||||
done=true;
|
||||
try{auto hello=Json::parse(bytes+4,bytes+4+length);auto id=hello.value("id","");auto link=links.find(id);
|
||||
if(id<local&&hello.value("to","")==local&&hello.value("version",0)==2&&link!=links.end()&&link->second.channel->adopt(uintptr_t(it->socket),now))it->socket=INVALID_SOCKET;
|
||||
if(hello.value("app","")=="KSwitcher-input"&&hello.value("to","")==local&&hello.value("version",0)==2&&link!=links.end()&&!link->second.udp->active()&&link->second.channel->adopt(uintptr_t(it->socket),now))it->socket=INVALID_SOCKET;
|
||||
}catch(const std::exception &){}
|
||||
}
|
||||
}
|
||||
@@ -261,8 +279,16 @@ struct InputHub::Impl
|
||||
}
|
||||
for(auto &[id,link]:links){
|
||||
std::vector<InputFrame> frames,received;for(auto &f:out)if(f.peer==id&&(!f.epoch||f.epoch==link.epoch))enqueueInput(frames,f.frame);
|
||||
link.channel->poll(link.endpoint.address,frames,received,now);
|
||||
auto state=link.channel->status();if(state!=link.lastState){link.lastState=state;log(L"LAN input peer "+wide(id.substr(0,8))+L": "+wide(state));}
|
||||
if(sendDatagram&&!link.channel->ready()&&!link.udp->active()&&local<id&&now-link.since>=5000){link.udp->start(now);link.channel->disconnect();frames.clear();}
|
||||
if(link.udp->active()){
|
||||
link.udp->poll(frames,received,now,sendDatagram);
|
||||
if(!link.udp->active()){link.since=now;link.channel->disconnect();received.clear();}
|
||||
}else link.channel->poll(link.endpoint.address,frames,received,now);
|
||||
if(link.udpReady!=link.udp->ready()){link.udpReady=link.udp->ready();link.epoch=++epoch;}
|
||||
bool ready=link.channel->ready()||link.udp->ready();
|
||||
if(ready)link.failedSince=link.since=now;
|
||||
auto state=link.udp->ready()?"Input connected via encrypted UDP.":link.udp->active()?"Trying encrypted UDP over discovery...":link.channel->ready()?"Input connected via "+link.channel->transport()+".":link.channel->status();
|
||||
if(state!=link.lastState){link.lastState=state;log(L"LAN input peer "+wide(id.substr(0,8))+L": "+wide(state));}
|
||||
if(link.generation!=link.channel->generation()){link.generation=link.channel->generation();link.epoch=++epoch;}
|
||||
for(auto &f:received)in.push_back({id,f,link.epoch});
|
||||
}
|
||||
@@ -271,12 +297,28 @@ struct InputHub::Impl
|
||||
InputHub::InputHub(std::string id,uint16_t port):impl(std::make_unique<Impl>(std::move(id),port)){}
|
||||
InputHub::~InputHub()=default;
|
||||
void InputHub::poll(const std::vector<InputEndpoint> &peers,const std::vector<RoutedInput> &out,std::vector<RoutedInput> &in,uint64_t now){impl->poll(peers,out,in,now);}
|
||||
std::map<std::string,uint64_t> InputHub::ready()const{std::map<std::string,uint64_t> result;for(auto &[id,l]:impl->links)if(l.channel->ready())result[id]=l.epoch;return result;}
|
||||
std::map<std::string,uint64_t> InputHub::ready()const{std::map<std::string,uint64_t> result;for(auto &[id,l]:impl->links)if(l.channel->ready()||l.udp->ready())result[id]=l.epoch;return result;}
|
||||
std::string InputHub::status()const{
|
||||
auto count=ready().size();if(count)return "Input connected to "+std::to_string(count)+" computer(s). Move through a neighboring screen edge.";
|
||||
auto count=ready().size();if(count==impl->links.size()&&count){
|
||||
std::string routes;for(auto &[id,l]:impl->links){if(!routes.empty())routes+=", ";routes+=id.substr(0,8)+": "+(l.udp->ready()?"encrypted UDP":l.channel->transport());}
|
||||
return "Input connected ("+routes+"). Move through a neighboring screen edge.";
|
||||
}
|
||||
if(needsHelp())return "Automatic connection attempts have not succeeded for every PC. Enable input on both PCs. Open Connection help for firewall commands.";
|
||||
if(!impl->error.empty())return impl->error;
|
||||
for(auto &[id,l]:impl->links)if(!l.channel->status().empty())return l.channel->status();
|
||||
for(auto &[id,l]:impl->links)if(!l.channel->ready()&&!l.udp->ready())return l.lastState;
|
||||
return "Waiting for paired computers...";
|
||||
}
|
||||
void InputHub::disconnect(){for(auto &[id,l]:impl->links)l.channel->disconnect();}
|
||||
void InputHub::disconnect(){for(auto &[id,l]:impl->links){l.channel->disconnect();l.udp->reset();l.since=impl->currentTime;}}
|
||||
void InputHub::datagrams(SendDatagram send){impl->sendDatagram=std::move(send);}
|
||||
void InputHub::receiveDatagram(const Json &j,uint64_t now){
|
||||
if(!impl->sendDatagram)return;
|
||||
auto it=impl->links.find(j.value("id",""));if(it==impl->links.end()||it->second.channel->ready())return;
|
||||
auto &link=it->second;bool active=link.udp->active();
|
||||
try{link.udp->receive(j,now,impl->sendDatagram);}catch(const std::exception &){return;}
|
||||
if(!active&&link.udp->active())link.channel->disconnect();
|
||||
}
|
||||
bool InputHub::needsHelp()const{
|
||||
for(auto &[id,l]:impl->links)if(!l.channel->ready()&&!l.udp->ready()&&impl->currentTime-l.failedSince>=15000)return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "lan_pairing.h"
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
namespace ks::lan
|
||||
{
|
||||
enum class InputType : uint32_t { Ready, Ping, Enter, Accept, Return, Move, Key, Button, Wheel, Cross };
|
||||
@@ -57,6 +58,7 @@ class InputChannel
|
||||
uint64_t generation() const;
|
||||
std::string status() const;
|
||||
void disconnect();
|
||||
std::string transport()const;
|
||||
};
|
||||
struct InputEndpoint {std::string id,key;uint32_t address{};uint16_t port=45884;};
|
||||
struct RoutedInput {std::string peer;InputFrame frame;uint64_t epoch{};};
|
||||
@@ -70,5 +72,9 @@ class InputHub
|
||||
std::map<std::string,uint64_t> ready()const;
|
||||
std::string status()const;
|
||||
void disconnect();
|
||||
using SendDatagram=std::function<void(const std::string &,const Json &)>;
|
||||
void datagrams(SendDatagram);
|
||||
void receiveDatagram(const Json &,uint64_t now);
|
||||
bool needsHelp()const;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
#include "lan_channel.h"
|
||||
#include <deque>
|
||||
|
||||
namespace ks::lan
|
||||
{
|
||||
// Ordered, bounded input on the discovery socket. Retransmission uses fresh
|
||||
// authenticated packets; event sequence numbers prevent duplicate injection.
|
||||
// Fresh challenge/response nonces bind every reconnect to a new session.
|
||||
class InputDatagram
|
||||
{
|
||||
std::string local,peer,secret,nonce,remote,txKey,rxKey;
|
||||
uint64_t started{},lastRead{},lastSend{},sentNumber{},seenNumber{},nextEvent{1},receivedEvent{},acked{},progress{};
|
||||
bool live{},ackDue{};
|
||||
std::deque<std::pair<uint64_t,InputFrame>> queue;
|
||||
std::vector<InputFrame> incoming;
|
||||
static std::string hex(const Bytes &b){std::string s;for(auto v:b){s+="0123456789abcdef"[v>>4];s+="0123456789abcdef"[v&15];}return s;}
|
||||
static Bytes unhex(const std::string &s){
|
||||
if(s.empty()||s.size()>2080||s.size()%2||s.find_first_not_of("0123456789abcdef")!=s.npos)throw std::runtime_error("Invalid UDP input packet.");
|
||||
Bytes b;for(size_t i=0;i<s.size();i+=2)b.push_back(static_cast<unsigned char>(std::stoul(s.substr(i,2),nullptr,16)));return b;
|
||||
}
|
||||
std::string proof(const std::string &from,const std::string &to,const std::string &a,const std::string &b)const{
|
||||
return mac(secret,"KSwitcher UDP input v1|"+from+"|"+to+"|"+a+"|"+b);
|
||||
}
|
||||
void keys(){
|
||||
// Handshake proofs are public. Session keys use a separate domain and
|
||||
// must never be the MAC transmitted in a welcome packet.
|
||||
txKey=mac(secret,"KSwitcher UDP input key v1|"+local+"|"+peer+"|"+nonce+"|"+remote);
|
||||
rxKey=mac(secret,"KSwitcher UDP input key v1|"+peer+"|"+local+"|"+remote+"|"+nonce);
|
||||
}
|
||||
public:
|
||||
using Send=InputHub::SendDatagram;
|
||||
explicit InputDatagram(std::string a,std::string b,std::string key):local(std::move(a)),peer(std::move(b)),secret(std::move(key)){}
|
||||
bool active()const{return !nonce.empty();}
|
||||
bool ready()const{return live;}
|
||||
void reset(){nonce.clear();remote.clear();txKey.clear();rxKey.clear();queue.clear();incoming.clear();live=ackDue=false;sentNumber=seenNumber=receivedEvent=acked=lastSend=0;nextEvent=1;}
|
||||
void start(uint64_t now){reset();nonce=randomHex(16);started=lastRead=progress=now;}
|
||||
void receive(const Json &j,uint64_t now,const Send &send)
|
||||
{
|
||||
if(j.value("app","")!="KSwitcher-LAN"||j.value("version",0)!=1||j.value("type","")!="input-udp"||j.value("id","")!=peer||j.value("to","")!=local)return;
|
||||
auto op=j.value("op","");
|
||||
if(op=="hello"){
|
||||
auto other=j.value("nonce","");if(local<peer||!identifier(other)||!equal(j.value("proof",""),proof(peer,local,other,"")))return;
|
||||
if(active()&&remote!=other)return;
|
||||
if(!active()){start(now);remote=other;keys();}
|
||||
auto response=envelope("input-udp",local);response["to"]=peer;response["op"]="welcome";response["nonce"]=nonce;response["echo"]=remote;response["proof"]=proof(local,peer,nonce,remote);send(peer,response);return;
|
||||
}
|
||||
if(op=="welcome"){
|
||||
auto other=j.value("nonce","");
|
||||
if(local>peer||!active()||j.value("echo","")!=nonce||!identifier(other)||!equal(j.value("proof",""),proof(peer,local,other,nonce)))return;
|
||||
if(!remote.empty()&&remote!=other)return;
|
||||
remote=other;keys();ackDue=true;return;
|
||||
}
|
||||
if(op!="data"||rxKey.empty()||j.value("session","")!=nonce||j.value("fromSession","")!=remote)return;
|
||||
auto number=j.value("number",uint64_t{});if(!number||number<=seenNumber)return;
|
||||
InputCipher cipher(mac(rxKey,"packet|"+std::to_string(number)));
|
||||
auto body=Json::from_cbor(cipher.decryptPayload(unhex(j.at("data").get<std::string>())));
|
||||
auto ack=body.at("ack").get<uint64_t>(),first=body.at("first").get<uint64_t>();auto frames=body.at("frames");
|
||||
if(ack>=nextEvent||!frames.is_array()||frames.size()>12||!first)return;
|
||||
std::vector<InputFrame> decoded;
|
||||
for(auto &f:frames){
|
||||
if(!f.is_array()||f.size()!=5)return;
|
||||
auto type=f[0].get<uint32_t>();if(type<uint32_t(InputType::Enter)||type>uint32_t(InputType::Cross))return;
|
||||
decoded.push_back({InputType(type),f[1].get<uint32_t>(),f[2].get<int32_t>(),f[3].get<int32_t>(),f[4].get<int32_t>()});
|
||||
}
|
||||
seenNumber=number;lastRead=now;live=true;
|
||||
if(ack>acked){acked=ack;progress=now;while(!queue.empty()&&queue.front().first<=ack)queue.pop_front();}
|
||||
if(!decoded.empty()){
|
||||
if(first==receivedEvent+1){
|
||||
if(incoming.size()+decoded.size()>512){reset();return;}
|
||||
incoming.insert(incoming.end(),decoded.begin(),decoded.end());receivedEvent+=decoded.size();
|
||||
}
|
||||
ackDue=true;
|
||||
}
|
||||
}
|
||||
void poll(const std::vector<InputFrame> &out,std::vector<InputFrame> &in,uint64_t now,const Send &send)
|
||||
{
|
||||
if(!active())return;
|
||||
if((live&&(now-lastRead>2000||(!queue.empty()&&now-progress>2000)))||(!live&&now-started>3000)){reset();return;}
|
||||
if(live){
|
||||
if(queue.empty()&&!out.empty())progress=now;
|
||||
if(queue.size()+out.size()>512){reset();return;}
|
||||
for(auto &f:out)queue.emplace_back(nextEvent++,f);
|
||||
in.insert(in.end(),incoming.begin(),incoming.end());incoming.clear();
|
||||
}
|
||||
if(remote.empty()){
|
||||
if(!lastSend||now-lastSend>=250){auto j=envelope("input-udp",local);j["to"]=peer;j["op"]="hello";j["nonce"]=nonce;j["proof"]=proof(local,peer,nonce,"");send(peer,j);lastSend=now;}return;
|
||||
}
|
||||
if(!ackDue&&lastSend&&now-lastSend<(queue.empty()?300:50))return;
|
||||
Json frames=Json::array();for(auto &[sequence,f]:queue){frames.push_back(Json::array({uint32_t(f.type),f.token,f.a,f.b,f.c}));if(frames.size()==12)break;}
|
||||
Json body={{"ack",receivedEvent},{"first",queue.empty()?nextEvent:queue.front().first},{"frames",frames}};
|
||||
auto number=++sentNumber;InputCipher cipher(mac(txKey,"packet|"+std::to_string(number)));
|
||||
auto j=envelope("input-udp",local);j["to"]=peer;j["op"]="data";j["number"]=number;j["session"]=remote;j["fromSession"]=nonce;j["data"]=hex(cipher.encryptPayload(Json::to_cbor(body)));
|
||||
send(peer,j);lastSend=now;ackDue=false;
|
||||
}
|
||||
};
|
||||
}
|
||||
+4
-3
@@ -1045,7 +1045,7 @@ struct SettingsView
|
||||
int selection = -1;
|
||||
bool filling{};
|
||||
std::vector<std::vector<HWND>> pages;
|
||||
bool ready{}, refreshing{};
|
||||
bool ready{}, refreshing{}, visitedLan{};
|
||||
};
|
||||
void updateSettingsState(HWND w, SettingsView *v);
|
||||
void selectSettingsPage(HWND w, SettingsView *v)
|
||||
@@ -1646,7 +1646,8 @@ void updateSettingsState(HWND w, SettingsView *v)
|
||||
v->refreshing = true;
|
||||
bool changed = settingsChanged(w, v);
|
||||
EnableWindow(GetDlgItem(w, IDOK), changed);
|
||||
const wchar_t *text = changed ? L"Unsaved changes — click Save to apply." : L"No unsaved changes.";
|
||||
const wchar_t *text = changed ? L"Unsaved changes — click Save to apply." :
|
||||
v->visitedLan ? L"No unsaved settings. Network changes save automatically in the Computers window." : L"No unsaved changes.";
|
||||
if (value(w, 261) != text) value(w, 261, text);
|
||||
v->refreshing = false;
|
||||
}
|
||||
@@ -1743,7 +1744,7 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
}
|
||||
if (id == 257) { showOcrOptions(w, v); return 0; }
|
||||
if (id == 258) { showSyncOptions(w, *v->app); return 0; }
|
||||
if (id == 262) { showLan(w, *v->app); return 0; }
|
||||
if (id == 262) { v->visitedLan=true; showLan(w, *v->app); return 0; }
|
||||
if (id == 263) { collectLayout(w, v); showAppLanguages(w, v); return 0; }
|
||||
if (id == 259) { collectLayout(w, v); showChatLanguages(w, v); return 0; }
|
||||
if (id == 230)
|
||||
|
||||
+44
-7
@@ -20,6 +20,26 @@ std::string selectedLan(HWND w,LanView &v)
|
||||
auto index=SendDlgItemMessageW(w,702,LB_GETCURSEL,0,0);
|
||||
return index>=0 && size_t(index)<v.ids.size() ? v.ids[size_t(index)] : "";
|
||||
}
|
||||
LRESULT CALLBACK lanHelpProc(HWND w,UINT msg,WPARAM wp,LPARAM lp)
|
||||
{
|
||||
if(msg==WM_DPICHANGED){resizeForDpi(w,wp,lp);return 0;}
|
||||
if(msg==WM_COMMAND&&LOWORD(wp)==724){SendDlgItemMessageW(w,723,EM_SETSEL,0,-1);SendDlgItemMessageW(w,723,WM_COPY,0,0);value(w,724,L"Copied");return 0;}
|
||||
if(msg==WM_CLOSE||(msg==WM_COMMAND&&LOWORD(wp)==IDCANCEL)){DestroyWindow(w);return 0;}
|
||||
if(msg==WM_DESTROY){auto parent=GetWindow(w,GW_OWNER);EnableWindow(parent,TRUE);SetForegroundWindow(parent);return 0;}
|
||||
return DefWindowProcW(w,msg,wp,lp);
|
||||
}
|
||||
void showLanHelp(HWND parent,App &app)
|
||||
{
|
||||
auto script=wide(lanRequest(app,{{"action","connectionHelp"}}).at("script").get<std::string>());
|
||||
auto w=makeWindow(app,L"KSwitcher.LAN.Help",L"KSwitcher — connection help",lanHelpProc,740,430,0,false);
|
||||
SetWindowLongPtrW(w,GWLP_HWNDPARENT,(LONG_PTR)parent);
|
||||
control(w,L"STATIC",L"First enable input sharing on both PCs. KSwitcher tries direct TCP, reverse TCP, then encrypted UDP automatically. Both PCs need an updated version for all three methods.",0,725,18,16,700,60);
|
||||
control(w,L"STATIC",L"Still disconnected? A firewall may be blocking the app. On EACH PC, open this help and run its own command in PowerShell as administrator. This allows only local-network connections to that PC's current KSwitcher executable. No restart or new pairing is needed.",0,726,18,88,700,80);
|
||||
auto field=edit(w,723,18,180,700,180,ES_MULTILINE|ES_READONLY|ES_AUTOVSCROLL|WS_VSCROLL);
|
||||
SetWindowTextW(field,script.c_str());
|
||||
button(w,L"&Copy command",724,18,380,180);button(w,L"&Close",IDCANCEL,618,380,100);
|
||||
EnableWindow(parent,FALSE);showRequestedWindow(w);SetForegroundWindow(w);
|
||||
}
|
||||
void refreshLan(HWND w,LanView &v)
|
||||
{
|
||||
auto selected=selectedLan(w,v);
|
||||
@@ -60,12 +80,26 @@ void refreshLan(HWND w,LanView &v)
|
||||
for(size_t i=0;i<v.ids.size();++i)if(v.ids[i]==selected)
|
||||
SendDlgItemMessageW(w,702,LB_SETCURSEL,i,0);
|
||||
selected=selectedLan(w,v);
|
||||
// Keep the code available while the user enters it on the other PC, and
|
||||
// after cancellation/timeout for retry. Clear it only after trust is saved.
|
||||
if(v.snapshot.value("pending",false))
|
||||
for(const auto &peer:next["peers"])
|
||||
if(peer.at("id")==v.snapshot.value("pendingPeer",std::string{})&&peer.value("paired",false))value(w,703,L"");
|
||||
v.snapshot=std::move(next);
|
||||
bool enabled=v.snapshot.at("enabled"), pending=v.snapshot.at("pending"), online=false, paired=false;
|
||||
for(const auto &peer:v.snapshot["peers"]) if(peer.at("id")==selected) {online=peer.at("online");paired=peer.at("paired");}
|
||||
value(w,701,enabled ? L"&Disable discovery" : L"&Enable discovery");
|
||||
value(w,709,L"This KSwitcher: "+wide(v.snapshot.at("name").get<std::string>())+L" ["+wide(v.snapshot.at("id").get<std::string>().substr(0,8))+L"]");
|
||||
value(w,708,v.error.empty() ? wide(v.snapshot.at("status").get<std::string>()) : v.error);
|
||||
auto pairingStatus=wide(v.snapshot.at("status").get<std::string>());
|
||||
if(pending)
|
||||
{
|
||||
std::wstring peerName;
|
||||
for(const auto &peer:v.snapshot["peers"])
|
||||
if(peer.at("id")==v.snapshot.value("pendingPeer",std::string{}))peerName=wide(peer.at("name").get<std::string>());
|
||||
pairingStatus=L"Waiting for "+peerName+L" ("+std::to_wstring(v.snapshot.value("pairSecondsRemaining",0))+L" s).\nEnter the same code and click Pair there too. Pair is not saved yet.";
|
||||
}
|
||||
value(w,708,v.error.empty() ? pairingStatus : v.error);
|
||||
value(w,705,pending ? L"Waiting..." : L"&Pair");
|
||||
value(w,710,v.ids.empty() ? (enabled ? L"No KSwitcher found. Enable discovery on the other PC too. UDP 45883 must be allowed on your private network." : L"Discovery is disabled on this computer.") : L"Select the matching KSwitcher name and ID shown on the other computer.");
|
||||
EnableWindow(GetDlgItem(w,705),enabled&&online&&!pending&&!paired);
|
||||
EnableWindow(GetDlgItem(w,706),pending);
|
||||
@@ -89,6 +123,7 @@ void refreshLan(HWND w,LanView &v)
|
||||
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);
|
||||
value(w,722,v.snapshot.value("inputNeedsHelp",false)?L"Connection failed — &help":L"Connection &help");
|
||||
}
|
||||
#include "ui_lan_layout.inl"
|
||||
LRESULT CALLBACK lanProc(HWND w,UINT msg,WPARAM wp,LPARAM lp)
|
||||
@@ -108,6 +143,7 @@ LRESULT CALLBACK lanProc(HWND w,UINT msg,WPARAM wp,LPARAM lp)
|
||||
if(HIWORD(wp)!=BN_CLICKED) return DefWindowProcW(w,msg,wp,lp);
|
||||
if(!IsWindowEnabled(GetDlgItem(w,id)))return 0;
|
||||
v->error.clear();
|
||||
if(id==722){showLanHelp(w,*v->app);return 0;}
|
||||
if(id==701) lanRequest(*v->app,{{"action","enable"},{"enabled",!v->snapshot.value("enabled",false)}});
|
||||
if(id==704)
|
||||
{
|
||||
@@ -115,7 +151,7 @@ LRESULT CALLBACK lanProc(HWND w,UINT msg,WPARAM wp,LPARAM lp)
|
||||
std::string grouped; for(size_t i=0;i<code.size();++i) {if(i&&i%4==0) grouped+='-';grouped+=code[i];}
|
||||
value(w,703,wide(grouped)); SetFocus(GetDlgItem(w,703)); SendDlgItemMessageW(w,703,EM_SETSEL,0,-1);
|
||||
}
|
||||
if(id==705) {lanRequest(*v->app,{{"action","pair"},{"peer",peer},{"code",utf8(value(w,703))}});value(w,703,L"");}
|
||||
if(id==705) lanRequest(*v->app,{{"action","pair"},{"peer",peer},{"code",utf8(value(w,703))}});
|
||||
if(id==706) lanRequest(*v->app,{{"action","cancel"}});
|
||||
if(id==707) lanRequest(*v->app,{{"action","forget"},{"peer",peer}});
|
||||
if(id==715)
|
||||
@@ -131,10 +167,10 @@ 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 — mouse and keyboard",lanProc,740,836,0,false);
|
||||
HWND w=makeWindow(app,L"KSwitcher.LAN",L"KSwitcher computers — mouse and keyboard",lanProc,740,908,0,false);
|
||||
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);
|
||||
label(w,L"Changes here save automatically. No need to click Save in Settings.",18,16,700);
|
||||
control(w,L"STATIC",L"",0,709,18,45,700,24);
|
||||
button(w,L"&Enable discovery",701,18,78,210);
|
||||
label(w,L"Only enabled KSwitcher instances announce themselves.",244,82,470);
|
||||
@@ -154,9 +190,10 @@ void showLan(HWND parent,App &app)
|
||||
control(w,L"STATIC",L"",0,719,18,674,700,24);
|
||||
button(w,L"Enable &input sharing",715,18,706,330);
|
||||
button(w,L"&Stop input sharing",716,370,706,240);
|
||||
control(w,L"STATIC",L"",0,718,18,744,700,28);
|
||||
control(w,L"STATIC",L"Arrange the same physical layout on every PC. Pair each input source with every destination and enable input on all PCs. Clipboard goes to all paired PCs.",0,720,18,776,580,40);
|
||||
button(w,L"&Close",IDCANCEL,618,792,100);
|
||||
control(w,L"STATIC",L"",0,718,18,744,700,64);
|
||||
control(w,L"STATIC",L"Arrange the same physical layout on every PC. Pair each input source with every destination and enable input on all PCs. Clipboard goes to all paired PCs.",0,720,18,816,580,40);
|
||||
button(w,L"Connection &help",722,18,864,300);
|
||||
button(w,L"&Close",IDCANCEL,618,864,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);}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.62";
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.63";
|
||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||
|
||||
Reference in new issue
Block a user