Release v0.6.57: multi-PC input, shared clipboard and resilient Settings Save
This commit is contained in:
1 parent
82292c8a7e
commit
5de010ee44
39 files changed
+1707
-312
No files matched your search
+100
-14
@@ -49,6 +49,16 @@ struct InputCipher::Impl
|
||||
};
|
||||
InputCipher::InputCipher(const std::string &key):impl(std::make_unique<Impl>(key)){}
|
||||
InputCipher::~InputCipher()=default;
|
||||
Bytes InputCipher::encryptPayload(const Bytes &plain)
|
||||
{
|
||||
if(plain.empty()||plain.size()>1024)throw std::runtime_error("Invalid file packet size.");
|
||||
Bytes tag(16);auto result=impl->crypt(plain,tag,true);result.insert(result.end(),tag.begin(),tag.end());return result;
|
||||
}
|
||||
Bytes InputCipher::decryptPayload(const Bytes &packet)
|
||||
{
|
||||
if(packet.size()<17||packet.size()>1040)throw std::runtime_error("Invalid file packet size.");
|
||||
Bytes tag(packet.end()-16,packet.end());return impl->crypt(Bytes(packet.begin(),packet.end()-16),tag,false);
|
||||
}
|
||||
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));
|
||||
@@ -58,7 +68,7 @@ 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.");
|
||||
auto type=get32(plain.data());if(type>uint32_t(InputType::Cross))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)
|
||||
@@ -70,11 +80,11 @@ 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{};
|
||||
bool connecting{},authenticated{},helloRead{},managed{};
|
||||
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(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)
|
||||
{
|
||||
@@ -83,7 +93,7 @@ struct InputChannel::Impl
|
||||
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();
|
||||
auto hello=Json{{"app","KSwitcher-input"},{"version",managed?2: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...";
|
||||
}
|
||||
@@ -92,7 +102,7 @@ struct InputChannel::Impl
|
||||
try
|
||||
{
|
||||
if(socket==INVALID_SOCKET&&now<retry)return;
|
||||
if(listener==INVALID_SOCKET && local>peer)
|
||||
if(!managed && 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.");}
|
||||
@@ -108,8 +118,9 @@ struct InputChannel::Impl
|
||||
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...";
|
||||
}
|
||||
else
|
||||
else if(!managed)
|
||||
{
|
||||
sockaddr_in a{};int length=sizeof(a);socket=accept(listener,(sockaddr*)&a,&length);
|
||||
if(socket!=INVALID_SOCKET)
|
||||
@@ -121,13 +132,20 @@ struct InputChannel::Impl
|
||||
}
|
||||
}
|
||||
if(socket==INVALID_SOCKET)return;
|
||||
if(now-lastRead>2000)throw std::runtime_error("Input connection timed out; control returned locally.");
|
||||
// Give TCP establishment time for SYN retransmission / firewall approval.
|
||||
// Keep the short fail-safe only for an already authenticated input session.
|
||||
if(now-lastRead>(authenticated?2000:10000))
|
||||
throw std::runtime_error(authenticated ? "Input connection lost (no heartbeat for 2 seconds); control returned locally." :
|
||||
connecting ? "Cannot reach TCP 45884. Enable input on both PCs and check the receiving PC firewall." :
|
||||
"TCP connected, but input authentication timed out. Check pairing and input setup on both PCs.");
|
||||
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;
|
||||
int error=0;int size=sizeof(error);
|
||||
if(getsockopt(socket,SOL_SOCKET,SO_ERROR,(char*)&error,&size))error=WSAGetLastError();
|
||||
if(error)throw std::runtime_error("Cannot connect to TCP 45884 (Windows error "+std::to_string(error)+"). Enable input on both PCs and check the receiving PC firewall.");
|
||||
connecting=false;state="TCP connected; authenticating paired KSwitcher...";
|
||||
}
|
||||
if(authenticated)
|
||||
{
|
||||
@@ -155,12 +173,12 @@ struct InputChannel::Impl
|
||||
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)
|
||||
if(hello.value("app","")!="KSwitcher-input"||hello.value("version",0)!=(managed?2: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.");
|
||||
if(!managed&&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));
|
||||
tx=std::make_unique<InputCipher>(inputSessionKey(secret,local,peer,nonce,remoteNonce,managed?-1:edge));
|
||||
rx=std::make_unique<InputCipher>(inputSessionKey(secret,peer,local,remoteNonce,nonce,managed?-1:(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());
|
||||
}
|
||||
@@ -175,11 +193,79 @@ struct InputChannel::Impl
|
||||
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(std::string a,std::string b,std::string key,int edge,uint16_t port,bool managed):impl(std::make_unique<Impl>(std::move(a),std::move(b),std::move(key),edge,port,managed)){}
|
||||
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;
|
||||
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;}
|
||||
void InputChannel::disconnect(){impl->reset(GetTickCount64(),"Input disconnected; local control restored.");}
|
||||
struct InputHub::Impl
|
||||
{
|
||||
struct Link { InputEndpoint endpoint;std::unique_ptr<InputChannel> channel;uint64_t generation{},epoch{}; };
|
||||
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;
|
||||
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)
|
||||
{
|
||||
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&&e.address==it->second.endpoint.address;});
|
||||
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));
|
||||
}
|
||||
bool listenNeeded=std::any_of(endpoints.begin(),endpoints.end(),[&](auto &e){return e.id<local;});
|
||||
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);
|
||||
if(listener==INVALID_SOCKET||setsockopt(listener,SOL_SOCKET,SO_EXCLUSIVEADDRUSE,(char*)&yes,sizeof(yes))||!nonblocking(listener)||bind(listener,(sockaddr*)&a,sizeof(a))||listen(listener,SOMAXCONN)){
|
||||
closeSocket(listener);retry=now+1500;error="TCP input port is unavailable.";
|
||||
}else error.clear();
|
||||
}
|
||||
if(listener!=INVALID_SOCKET)for(int n=0;n<16&&pending.size()<64;++n){
|
||||
sockaddr_in a{};int size=sizeof(a);SOCKET sock=accept(listener,(sockaddr*)&a,&size);if(sock==INVALID_SOCKET)break;
|
||||
if(!nonblocking(sock)){closeSocket(sock);continue;}pending.push_back({sock,a.sin_addr.s_addr,now});
|
||||
}
|
||||
for(auto it=pending.begin();it!=pending.end();){
|
||||
bool done=now-it->since>2000;unsigned char bytes[516];int count=recv(it->socket,(char*)bytes,sizeof(bytes),MSG_PEEK);
|
||||
if(count==0||(count<0&&WSAGetLastError()!=WSAEWOULDBLOCK))done=true;
|
||||
if(!done&&count>=4){
|
||||
auto length=get32(bytes);if(length<32||length>512)done=true;
|
||||
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.endpoint.address==it->address&&link->second.channel->adopt(uintptr_t(it->socket),now))it->socket=INVALID_SOCKET;
|
||||
}catch(const std::exception &){}
|
||||
}
|
||||
}
|
||||
if(done){closeSocket(it->socket);it=pending.erase(it);}else ++it;
|
||||
}
|
||||
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);
|
||||
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});
|
||||
}
|
||||
}
|
||||
};
|
||||
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::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.";
|
||||
if(!impl->error.empty())return impl->error;
|
||||
for(auto &[id,l]:impl->links)if(!l.channel->status().empty())return l.channel->status();
|
||||
return "Waiting for paired computers...";
|
||||
}
|
||||
void InputHub::disconnect(){for(auto &[id,l]:impl->links)l.channel->disconnect();}
|
||||
}
|
||||
Reference in new issue
Block a user