Add opt-in KSwitcher LAN discovery and mutual code pairing preview
This commit is contained in:
1 parent
346af2256f
commit
9b565dd55e
17 files changed
+743
-21
No files matched your search
@@ -559,6 +559,7 @@ App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot, HWND ser
|
||||
}
|
||||
App::~App()
|
||||
{
|
||||
lanService.reset();
|
||||
stopBrowserChatMonitor();
|
||||
if (auto host = historyController(historyProcess)) PostMessageW(host, WM_CLOSE, 0, 0);
|
||||
if (historyProcess) CloseHandle(historyProcess);
|
||||
@@ -721,6 +722,11 @@ bool App::registerHotkeys(const Settings &s, bool rollback)
|
||||
return true;
|
||||
}
|
||||
|
||||
Bytes App::lanCommand(const Bytes &command)
|
||||
{
|
||||
if (!lanService) lanService = std::make_unique<LanService>(data);
|
||||
return lanService->command(command);
|
||||
}
|
||||
void App::applySettings(const Settings &requested)
|
||||
{
|
||||
Settings s = requested;
|
||||
@@ -2332,6 +2338,9 @@ int App::execute()
|
||||
scheduleHistoryOcr(settings);
|
||||
addTray();
|
||||
if (!smoke) startBrowserChatMonitor();
|
||||
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())); }
|
||||
SetTimer(window, 1, 60000, nullptr);
|
||||
if (!smoke) SetTimer(window, 6, 1000, nullptr);
|
||||
SetThreadExecutionState(ES_CONTINUOUS |
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "update.h"
|
||||
#include "sync.h"
|
||||
#include "chat_context.h"
|
||||
#include "lan.h"
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
@@ -92,6 +93,7 @@ class App
|
||||
void checkUpdate(bool interactive, bool install = false, const std::wstring &version = {});
|
||||
std::wstring syncCommand(int action = 0, const std::vector<SyncChoice> &choices = {});
|
||||
Bytes syncConflictData();
|
||||
Bytes lanCommand(const Bytes &);
|
||||
void pollSync();
|
||||
static App *current;
|
||||
|
||||
@@ -99,6 +101,7 @@ class App
|
||||
friend struct InputRegressionAccess;
|
||||
void releaseIdleMemory(ULONGLONG now);
|
||||
HHOOK keyboardHook{}, mouseHook{};
|
||||
std::unique_ptr<LanService> lanService;
|
||||
HANDLE historyProcess{};
|
||||
std::mutex jobsMutex;
|
||||
std::condition_variable jobsReady;
|
||||
|
||||
@@ -112,6 +112,7 @@ void App::reloadLayouts()
|
||||
}
|
||||
void App::clearHistory() { request(*this, ConfigClearHistory); }
|
||||
Bytes App::syncConflictData() { return request(*this, ConfigSyncConflicts); }
|
||||
Bytes App::lanCommand(const Bytes &bytes) { return request(*this, ConfigLan, bytes); }
|
||||
std::wstring App::syncCommand(int action, const std::vector<SyncChoice> &choices)
|
||||
{
|
||||
auto result = action == 4 ? request(*this, ConfigSyncResolve, encodeSyncChoices(choices)) :
|
||||
|
||||
@@ -40,6 +40,13 @@ LRESULT configurationRequest(App &app, HWND sender, const COPYDATASTRUCT &messag
|
||||
{
|
||||
switch (message.dwData)
|
||||
{
|
||||
case ConfigLan:
|
||||
{
|
||||
if (!message.cbData || message.cbData > 4096) throw std::runtime_error("Invalid LAN request.");
|
||||
auto first = static_cast<const unsigned char *>(message.lpData);
|
||||
reply = app.lanCommand(Bytes(first, first + message.cbData));
|
||||
break;
|
||||
}
|
||||
case ConfigSyncConflicts:
|
||||
reply = app.syncConflictData();
|
||||
break;
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ inline constexpr ULONG_PTR ConfigRead = 0x4b530100, ConfigSave = ConfigRead + 1,
|
||||
ConfigCapture = ConfigRead + 4, ConfigLogs = ConfigRead + 5,
|
||||
ConfigReply = ConfigRead + 6, ConfigClearLog = ConfigRead + 7,
|
||||
ConfigSync = ConfigRead + 8, ConfigSyncConflicts = ConfigRead + 9,
|
||||
ConfigSyncResolve = ConfigRead + 10, ConfigEnd = ConfigRead + 11;
|
||||
ConfigSyncResolve = ConfigRead + 10, ConfigLan = ConfigRead + 11, ConfigEnd = ConfigRead + 12;
|
||||
inline constexpr size_t ConfigLimit = 8 * 1024 * 1024;
|
||||
bool configurationPeer(HWND, const wchar_t *filename);
|
||||
void launchConfiguration(App &, bool snippets = false, bool logs = false);
|
||||
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <iphlpapi.h>
|
||||
#include "lan.h"
|
||||
#include "lan_pairing.h"
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <map>
|
||||
|
||||
namespace ks
|
||||
{
|
||||
using lan::Json;
|
||||
namespace
|
||||
{
|
||||
Bytes encoded(const Json &j) { auto s=j.dump(); return Bytes(s.begin(),s.end()); }
|
||||
bool localAddress(ULONG network)
|
||||
{
|
||||
auto ip=ntohl(network);
|
||||
return (ip>>24)==10 || (ip>>20)==0xac1 || (ip>>16)==0xc0a8 || (ip>>16)==0xa9fe;
|
||||
}
|
||||
std::string hostName()
|
||||
{
|
||||
wchar_t name[256]{}; DWORD size=256;
|
||||
return GetComputerNameW(name,&size) ? utf8(std::wstring(name,size)) : "KSwitcher";
|
||||
}
|
||||
std::vector<ULONG> broadcasts()
|
||||
{
|
||||
ULONG size=16384; Bytes data(size);
|
||||
auto flags=GAA_FLAG_SKIP_ANYCAST|GAA_FLAG_SKIP_MULTICAST|GAA_FLAG_SKIP_DNS_SERVER;
|
||||
auto result=GetAdaptersAddresses(AF_INET,flags,nullptr,(IP_ADAPTER_ADDRESSES*)data.data(),&size);
|
||||
if(result==ERROR_BUFFER_OVERFLOW) { data.resize(size); result=GetAdaptersAddresses(AF_INET,flags,nullptr,(IP_ADAPTER_ADDRESSES*)data.data(),&size); }
|
||||
std::vector<ULONG> out;
|
||||
if(result!=NO_ERROR) return out;
|
||||
for(auto a=(IP_ADAPTER_ADDRESSES*)data.data();a;a=a->Next)
|
||||
if(a->OperStatus==IfOperStatusUp)
|
||||
for(auto u=a->FirstUnicastAddress;u;u=u->Next)
|
||||
if(u->Address.lpSockaddr->sa_family==AF_INET && u->OnLinkPrefixLength>0 && u->OnLinkPrefixLength<31)
|
||||
{
|
||||
auto ip=((sockaddr_in*)u->Address.lpSockaddr)->sin_addr.s_addr;
|
||||
if(localAddress(ip)) out.push_back(htonl(ntohl(ip) | (0xffffffffu >> u->OnLinkPrefixLength)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
struct LanService::Impl
|
||||
{
|
||||
struct Peer { std::string name,epoch; sockaddr_in address{}; uint64_t seen{},verified{}; };
|
||||
std::filesystem::path path;
|
||||
std::string id,epoch=lan::randomHex(16),name=hostName(),status="Disabled.";
|
||||
std::map<std::string,Peer> peers;
|
||||
Json trusted=Json::object();
|
||||
std::unique_ptr<lan::Pairing> pending;
|
||||
bool enabled{};
|
||||
std::atomic<bool> stopping{false};
|
||||
std::mutex mutex;
|
||||
std::thread worker;
|
||||
explicit Impl(const std::filesystem::path &root) : path(root/L"lan-pairing.bin")
|
||||
{
|
||||
if(std::filesystem::exists(path))
|
||||
{
|
||||
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");
|
||||
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||
|
||||
!entry.at("name").is_string()||entry.at("name").get<std::string>().size()>128)
|
||||
throw std::runtime_error("Invalid paired computer key.");
|
||||
}
|
||||
else id=lan::randomHex(16);
|
||||
if(enabled) start();
|
||||
}
|
||||
~Impl() { stop(); }
|
||||
void save() { atomicWrite(path,protect(encoded({{"version",1},{"id",id},{"enabled",enabled},{"trusted",trusted}}))); }
|
||||
void start()
|
||||
{
|
||||
stopping=false; epoch=lan::randomHex(16); status="Searching for KSwitcher computers...";
|
||||
worker=std::thread([this]{ run(); });
|
||||
}
|
||||
void stop()
|
||||
{
|
||||
stopping=true;
|
||||
if(worker.joinable()) worker.join();
|
||||
std::lock_guard lock(mutex); pending.reset(); peers.clear();
|
||||
}
|
||||
void send(SOCKET socket,const sockaddr_in &address,const Json &j)
|
||||
{
|
||||
auto bytes=j.dump(); if(bytes.size()>2048) return;
|
||||
sendto(socket,bytes.data(),int(bytes.size()),0,(const sockaddr*)&address,sizeof(address));
|
||||
}
|
||||
std::string heartbeat(const std::string &from,const std::string &to,const std::string &a,const std::string &b)
|
||||
{ return "KSwitcher LAN v1|presence|"+from+"|"+to+"|"+a+"|"+b; }
|
||||
void receive(SOCKET socket,const sockaddr_in &address,const Json &j,uint64_t now)
|
||||
{
|
||||
if(j.value("app","")!="KSwitcher-LAN" || j.value("version",0)!=1) return;
|
||||
auto other=j.value("id",""); if(!lan::identifier(other)||other==id) return;
|
||||
auto type=j.value("type","");
|
||||
if(type=="hello")
|
||||
{
|
||||
auto display=j.value("name",""); auto session=j.value("epoch","");
|
||||
if(display.empty()||display.size()>128||!lan::identifier(session)||
|
||||
std::any_of(display.begin(),display.end(),[](unsigned char c){return c<32||c==127;})) return;
|
||||
// Discovery is untrusted and never grants input access.
|
||||
if(!peers.contains(other) && peers.size()>=64) return;
|
||||
auto &peer=peers[other];
|
||||
if(peer.epoch!=session || peer.address.sin_addr.s_addr!=address.sin_addr.s_addr) peer.verified=0;
|
||||
peer.name=display; peer.epoch=session; peer.address=address; peer.seen=now;
|
||||
if(trusted.contains(other))
|
||||
{
|
||||
auto response=lan::envelope("presence",id); response["to"]=other; response["epoch"]=epoch;
|
||||
response["proof"]=lan::mac(trusted[other].at("key"),heartbeat(id,other,epoch,session));
|
||||
send(socket,address,response);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(!peers.contains(other)||j.value("to","")!=id) return;
|
||||
auto &peer=peers.at(other);
|
||||
if(peer.address.sin_addr.s_addr!=address.sin_addr.s_addr) return;
|
||||
if(type=="presence" && trusted.contains(other) && j.value("epoch","")==peer.epoch &&
|
||||
lan::equal(j.value("proof",""),lan::mac(trusted[other].at("key"),heartbeat(other,id,peer.epoch,epoch))))
|
||||
{ peer.verified=now; return; }
|
||||
if(pending && pending->peer==other)
|
||||
{
|
||||
for(const auto &response:pending->receive(j,now)) send(socket,address,response);
|
||||
if(pending->confirmed && (!trusted.contains(other)||trusted[other].at("key")!=pending->derived))
|
||||
{
|
||||
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.";
|
||||
}
|
||||
}
|
||||
}
|
||||
void run() noexcept
|
||||
{
|
||||
WSADATA startup{}; SOCKET socket=INVALID_SOCKET; bool winsock=false;
|
||||
try
|
||||
{
|
||||
if(WSAStartup(MAKEWORD(2,2),&startup)) throw std::runtime_error("Cannot start Windows networking.");
|
||||
winsock=true; socket=::socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
|
||||
BOOL yes=TRUE; u_long nonblock=1;
|
||||
if(socket==INVALID_SOCKET || setsockopt(socket,SOL_SOCKET,SO_EXCLUSIVEADDRUSE,(char*)&yes,sizeof(yes)) ||
|
||||
setsockopt(socket,SOL_SOCKET,SO_BROADCAST,(char*)&yes,sizeof(yes)) || ioctlsocket(socket,FIONBIO,&nonblock))
|
||||
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;
|
||||
while(!stopping)
|
||||
{
|
||||
auto now=GetTickCount64();
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if(now>=nextHello)
|
||||
{
|
||||
epoch=lan::randomHex(16); // Fresh challenge prevents replayed presence from staying online.
|
||||
auto hello=lan::envelope("hello",id); hello["name"]=name; hello["epoch"]=epoch;
|
||||
for(auto ip:broadcasts()) { auto address=bound; address.sin_addr.s_addr=ip; send(socket,address,hello); }
|
||||
nextHello=now+3000;
|
||||
}
|
||||
for(auto it=peers.begin();it!=peers.end();) {
|
||||
if(now-it->second.seen>lan::PeerLifetime) it=peers.erase(it); else ++it;
|
||||
}
|
||||
if(pending && pending->expired(now)) {
|
||||
status=pending->confirmed ? "Pair saved. Select a computer to manage it." : "Pairing expired. Enter the same code and click Pair on both computers.";
|
||||
pending.reset();
|
||||
}
|
||||
if(pending && now>=nextOffer && peers.contains(pending->peer)) {
|
||||
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};
|
||||
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)
|
||||
{
|
||||
char buffer[2049]; sockaddr_in sender{}; int size=sizeof(sender);
|
||||
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()); }
|
||||
catch(const Json::exception &) {} // Malformed network data cannot interrupt discovery.
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(const std::exception &e) { std::lock_guard lock(mutex); status=std::string("LAN stopped: ")+e.what(); }
|
||||
if(socket!=INVALID_SOCKET) closesocket(socket);
|
||||
if(winsock) WSACleanup();
|
||||
}
|
||||
Bytes command(const Bytes &input)
|
||||
{
|
||||
auto j=Json::parse(input); auto action=j.value("action","status");
|
||||
if(action=="enable")
|
||||
{
|
||||
stop();
|
||||
bool previous=enabled; enabled=j.at("enabled").get<bool>();
|
||||
try {save();} catch(...) {enabled=previous;if(enabled) start();throw;}
|
||||
if(enabled) start(); else status="Disabled. No discovery sockets or worker thread.";
|
||||
}
|
||||
std::lock_guard lock(mutex);
|
||||
if(action=="code") return encoded({{"code",lan::randomHex(16)}});
|
||||
if(action=="cancel") { pending.reset(); status="Pairing cancelled."; }
|
||||
if(action=="pair")
|
||||
{
|
||||
auto peer=j.at("peer").get<std::string>();
|
||||
if(!enabled||!peers.contains(peer)) throw std::runtime_error("Select an available KSwitcher computer first.");
|
||||
if(!trusted.contains(peer)&&trusted.size()>=64) throw std::runtime_error("Remove an old pair before adding another computer.");
|
||||
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()}};
|
||||
auto now=GetTickCount64();
|
||||
for(const auto &[peer,p]:peers)
|
||||
{
|
||||
char ip[INET_ADDRSTRLEN]{}; InetNtopA(AF_INET,(void*)&p.address.sin_addr,ip,sizeof(ip));
|
||||
result["peers"].push_back({{"id",peer},{"name",p.name},{"address",ip},{"paired",trusted.contains(peer)},
|
||||
{"verified",p.verified && now-p.verified<lan::PeerLifetime},{"online",true}});
|
||||
}
|
||||
for(const auto &[peer,p]:trusted.items()) if(!peers.contains(peer))
|
||||
result["peers"].push_back({{"id",peer},{"name",p.at("name")},{"address",""},{"paired",true},{"verified",false},{"online",false}});
|
||||
return encoded(result);
|
||||
}
|
||||
};
|
||||
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); }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
#include "platform.h"
|
||||
#include <memory>
|
||||
|
||||
namespace ks
|
||||
{
|
||||
// Local-only discovery and pairing. No input/clipboard receiver is exposed yet.
|
||||
class LanService
|
||||
{
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl;
|
||||
public:
|
||||
explicit LanService(const std::filesystem::path &);
|
||||
~LanService();
|
||||
Bytes command(const Bytes &);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "lan_pairing.h"
|
||||
#include <bcrypt.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace ks::lan
|
||||
{
|
||||
namespace
|
||||
{
|
||||
void checked(NTSTATUS s) { if (s < 0) throw std::runtime_error("Windows cryptography failed."); }
|
||||
std::string hex(const Bytes &data)
|
||||
{
|
||||
std::string out; out.reserve(data.size()*2);
|
||||
for (auto b : data) { out += "0123456789abcdef"[b>>4]; out += "0123456789abcdef"[b&15]; }
|
||||
return out;
|
||||
}
|
||||
std::string proof(const char *kind, const std::string &from, const std::string &to,
|
||||
const std::string &a, const std::string &b = {})
|
||||
{ return std::string("KSwitcher LAN v1|") + kind + "|" + from + "|" + to + "|" + a + "|" + b; }
|
||||
}
|
||||
std::string randomHex(size_t size)
|
||||
{
|
||||
if (size > 64) throw std::runtime_error("Invalid random size.");
|
||||
Bytes bytes(size); checked(BCryptGenRandom(nullptr, bytes.data(), ULONG(size), BCRYPT_USE_SYSTEM_PREFERRED_RNG));
|
||||
return hex(bytes);
|
||||
}
|
||||
bool identifier(const std::string &s)
|
||||
{ return s.size()==32 && s.find_first_not_of("0123456789abcdef")==s.npos; }
|
||||
std::string normalizeCode(const std::string &input)
|
||||
{
|
||||
std::string result;
|
||||
for (auto c : input)
|
||||
{
|
||||
if (c=='-' || c==' ') continue;
|
||||
if (c>='A' && c<='F') c+= 'a'-'A';
|
||||
result+=c;
|
||||
}
|
||||
if (!identifier(result)) throw std::runtime_error("Use the same generated 32-character pairing code on both computers.");
|
||||
return result;
|
||||
}
|
||||
std::string mac(const std::string &key, const std::string &text)
|
||||
{
|
||||
BCRYPT_ALG_HANDLE alg{}; BCRYPT_HASH_HANDLE hash{};
|
||||
checked(BCryptOpenAlgorithmProvider(&alg, BCRYPT_SHA256_ALGORITHM, nullptr, BCRYPT_ALG_HANDLE_HMAC_FLAG));
|
||||
struct Close { BCRYPT_ALG_HANDLE a; BCRYPT_HASH_HANDLE &h; ~Close(){if(h) BCryptDestroyHash(h); BCryptCloseAlgorithmProvider(a,0);} } close{alg,hash};
|
||||
checked(BCryptCreateHash(alg,&hash,nullptr,0,(PUCHAR)key.data(),ULONG(key.size()),0));
|
||||
checked(BCryptHashData(hash,(PUCHAR)text.data(),ULONG(text.size()),0));
|
||||
Bytes out(32); checked(BCryptFinishHash(hash,out.data(),ULONG(out.size()),0)); return hex(out);
|
||||
}
|
||||
bool equal(const std::string &a, const std::string &b)
|
||||
{
|
||||
if (a.size()!=b.size()) return false;
|
||||
unsigned diff=0; for(size_t i=0;i<a.size();++i) diff |= unsigned(a[i]^b[i]); return diff==0;
|
||||
}
|
||||
Json envelope(const std::string &type, const std::string &id)
|
||||
{ return {{"app","KSwitcher-LAN"},{"version",1},{"type",type},{"id",id}}; }
|
||||
Pairing::Pairing(std::string a,std::string b,const std::string &key,uint64_t now)
|
||||
: local(std::move(a)),peer(std::move(b)),code(normalizeCode(key)),nonce(randomHex(16)),started(now)
|
||||
{
|
||||
if(!identifier(local)||!identifier(peer)||local==peer) throw std::runtime_error("Invalid KSwitcher peer.");
|
||||
}
|
||||
bool Pairing::expired(uint64_t now) const { return now-started >= PairLifetime; }
|
||||
Json Pairing::offer() const
|
||||
{
|
||||
auto j=envelope("offer",local); j["to"]=peer; j["nonce"]=nonce;
|
||||
j["proof"]=mac(code,proof("offer",local,peer,nonce)); return j;
|
||||
}
|
||||
std::vector<Json> Pairing::receive(const Json &j,uint64_t now)
|
||||
{
|
||||
if(expired(now)||j.value("app","")!="KSwitcher-LAN"||j.value("version",0)!=1||
|
||||
j.value("id","")!=peer||j.value("to","")!=local) return {};
|
||||
auto type=j.value("type",""); auto other=j.value("nonce","");
|
||||
if(!identifier(other)) return {};
|
||||
if(type=="offer")
|
||||
{
|
||||
if(!equal(j.value("proof",""),mac(code,proof("offer",peer,local,other)))) return {};
|
||||
// A new attempt cannot replace a nonce already authenticated in this attempt.
|
||||
if(!remoteNonce.empty() && remoteNonce!=other) return {};
|
||||
remoteNonce=other;
|
||||
derived=local<peer ? mac(code,proof("key",local,peer,nonce,other)) : mac(code,proof("key",peer,local,other,nonce));
|
||||
auto ack=envelope("ack",local); ack["to"]=peer; ack["nonce"]=nonce; ack["echo"]=other;
|
||||
ack["proof"]=mac(code,proof("ack",local,peer,other,nonce)); return {ack};
|
||||
}
|
||||
if(type=="ack" && j.value("echo","")==nonce)
|
||||
{
|
||||
if(!equal(j.value("proof",""),mac(code,proof("ack",peer,local,nonce,other)))) return {};
|
||||
if(!remoteNonce.empty()&&remoteNonce!=other) return {};
|
||||
remoteNonce=other;
|
||||
derived=local<peer ? mac(code,proof("key",local,peer,nonce,other)) : mac(code,proof("key",peer,local,other,nonce));
|
||||
acknowledged=true;
|
||||
auto done=envelope("confirm",local); done["to"]=peer; done["nonce"]=nonce;
|
||||
done["proof"]=mac(derived,proof("confirm",local,peer,nonce,other)); return {done};
|
||||
}
|
||||
if(type=="confirm" && acknowledged && other==remoteNonce &&
|
||||
equal(j.value("proof",""),mac(derived,proof("confirm",peer,local,other,nonce)))) confirmed=true;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include "platform.h"
|
||||
#include "../vendor/json.hpp"
|
||||
|
||||
namespace ks::lan
|
||||
{
|
||||
using Json = nlohmann::json;
|
||||
inline constexpr unsigned Port = 45883;
|
||||
inline constexpr uint64_t PairLifetime = 120000, PeerLifetime = 12000;
|
||||
std::string randomHex(size_t bytes);
|
||||
std::string normalizeCode(const std::string &);
|
||||
std::string mac(const std::string &key, const std::string &text);
|
||||
bool equal(const std::string &, const std::string &);
|
||||
bool identifier(const std::string &);
|
||||
Json envelope(const std::string &type, const std::string &id);
|
||||
struct Pairing
|
||||
{
|
||||
std::string local, peer, code, nonce, remoteNonce, derived;
|
||||
uint64_t started{};
|
||||
bool acknowledged{}, confirmed{};
|
||||
Pairing(std::string local, std::string peer, const std::string &code, uint64_t now);
|
||||
bool expired(uint64_t now) const;
|
||||
Json offer() const;
|
||||
std::vector<Json> receive(const Json &, uint64_t now);
|
||||
};
|
||||
}
|
||||
@@ -1528,6 +1528,7 @@ void showOcrOptions(HWND parent, SettingsView *v)
|
||||
showRequestedWindow(w); SetForegroundWindow(w);
|
||||
}
|
||||
#include "ui_sync.inl"
|
||||
#include "ui_lan.inl"
|
||||
bool settingsChanged(HWND w, SettingsView *v)
|
||||
{
|
||||
try
|
||||
@@ -1640,6 +1641,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 == 259) { collectLayout(w, v); showChatLanguages(w, v); return 0; }
|
||||
if (id == 230)
|
||||
{
|
||||
@@ -2103,6 +2105,7 @@ void showSettings(App &app, bool visible)
|
||||
button(w, L"View l&og...", 256, 24, 308, 210);
|
||||
label(w, L"Inspect recent operations and diagnostics.", 254, 312, 430);
|
||||
checkbox(w, L"Write lo&g", 208, 24, 378, 210);
|
||||
button(w, L"KSwitcher &computers...", 262, 254, 374, 260);
|
||||
page();
|
||||
|
||||
control(w, L"STATIC", L"No unsaved changes.", 0, 261, 18, 430, 684, 24);
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
// Uses the resident service; the configurator never opens a discovery socket.
|
||||
struct LanView
|
||||
{
|
||||
HWND parent{};
|
||||
App *app{};
|
||||
nlohmann::json snapshot;
|
||||
std::vector<std::string> ids;
|
||||
std::wstring error;
|
||||
};
|
||||
nlohmann::json lanRequest(App &app,const nlohmann::json &request)
|
||||
{
|
||||
auto text=request.dump(); auto reply=app.lanCommand(Bytes(text.begin(),text.end()));
|
||||
return nlohmann::json::parse(reply);
|
||||
}
|
||||
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)] : "";
|
||||
}
|
||||
void refreshLan(HWND w,LanView &v)
|
||||
{
|
||||
auto selected=selectedLan(w,v);
|
||||
auto next=lanRequest(*v.app,{{"action","status"}});
|
||||
if(v.snapshot.is_null()||v.snapshot["peers"]!=next["peers"])
|
||||
{
|
||||
SendDlgItemMessageW(w,702,WM_SETREDRAW,FALSE,0);
|
||||
SendDlgItemMessageW(w,702,LB_RESETCONTENT,0,0); v.ids.clear();
|
||||
for(const auto &peer:next["peers"])
|
||||
{
|
||||
auto id=peer.at("id").get<std::string>(); v.ids.push_back(id);
|
||||
std::wstring state=!peer.at("online").get<bool>() ? L"Offline" : peer.at("verified").get<bool>() ? L"Paired / verified" :
|
||||
peer.at("paired").get<bool>() ? L"Paired / checking identity" : L"Available / not paired";
|
||||
auto text=wide(peer.at("name").get<std::string>())+L" ["+wide(id.substr(0,8))+L"] — "+state;
|
||||
auto row=SendDlgItemMessageW(w,702,LB_ADDSTRING,0,(LPARAM)text.c_str());
|
||||
if(id==selected) SendDlgItemMessageW(w,702,LB_SETCURSEL,row,0);
|
||||
}
|
||||
SendDlgItemMessageW(w,702,WM_SETREDRAW,TRUE,0); InvalidateRect(GetDlgItem(w,702),nullptr,TRUE);
|
||||
}
|
||||
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);
|
||||
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);
|
||||
EnableWindow(GetDlgItem(w,706),pending);
|
||||
EnableWindow(GetDlgItem(w,707),paired);
|
||||
for(int id:{703,704}) EnableWindow(GetDlgItem(w,id),enabled&&!pending);
|
||||
}
|
||||
LRESULT CALLBACK lanProc(HWND w,UINT msg,WPARAM wp,LPARAM lp)
|
||||
{
|
||||
if(msg==WM_DPICHANGED) {resizeForDpi(w,wp,lp);return 0;}
|
||||
auto v=(LanView*)GetWindowLongPtrW(w,GWLP_USERDATA);
|
||||
if(!v) return DefWindowProcW(w,msg,wp,lp);
|
||||
if(msg==WM_DESTROY) {KillTimer(w,1);EnableWindow(v->parent,TRUE);SetForegroundWindow(v->parent);delete v;SetWindowLongPtrW(w,GWLP_USERDATA,0);return 0;}
|
||||
if(msg==WM_CLOSE||(msg==WM_COMMAND&&LOWORD(wp)==IDCANCEL)) {DestroyWindow(w);return 0;}
|
||||
try
|
||||
{
|
||||
if(msg==WM_TIMER) {refreshLan(w,*v);return 0;}
|
||||
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(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)}});
|
||||
if(id==704)
|
||||
{
|
||||
auto code=lanRequest(*v->app,{{"action","code"}}).at("code").get<std::string>();
|
||||
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==706) lanRequest(*v->app,{{"action","cancel"}});
|
||||
if(id==707) lanRequest(*v->app,{{"action","forget"},{"peer",peer}});
|
||||
refreshLan(w,*v); return 0;
|
||||
}
|
||||
}
|
||||
catch(const std::exception &e) {v->error=wide(e.what());value(w,708,v->error);}
|
||||
return DefWindowProcW(w,msg,wp,lp);
|
||||
}
|
||||
void showLan(HWND parent,App &app)
|
||||
{
|
||||
HWND w=makeWindow(app,L"KSwitcher.LAN",L"KSwitcher computers — pairing preview",lanProc,740,600);
|
||||
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);
|
||||
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);
|
||||
control(w,L"LISTBOX",L"",WS_TABSTOP|WS_VSCROLL|WS_BORDER|LBS_NOTIFY|LBS_NOINTEGRALHEIGHT,702,18,120,700,150);
|
||||
control(w,L"STATIC",L"",0,710,18,278,700,40);
|
||||
label(w,L"Same pairing c&ode on both computers",18,327,700);
|
||||
edit(w,703,18,355,470);SendDlgItemMessageW(w,703,EM_SETLIMITTEXT,80,0);
|
||||
button(w,L"&Generate code",704,504,354,214);
|
||||
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);
|
||||
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