// -- Uzaktan komut SOKET katmani (/socket.io/) ----------------------------------- // Genel Socket.IO kanalina baglanir; Cihaz_kimlik header'i ile sunucudaki // soketCihazMap ve goCihazlar kaydini doldurur. "FikoYT.sistem.io.cihaz.komut" // olayini dinler, uzakKomutMesajiIsle()'e verir, cevabi geri gonderir. // // NOT: Mevcut lisans_canli.cpp KANAL (/isans) KULLANIR ve dokunulmamistir. // Bu modul AYRI bir baglanti acar (/socket.io/) — iki kanal birden acik kalir: // /isans -> kill-switch kararlari (engelle/izin) [mevcut, degismedi] // /socket.io/ -> uzaktan komut + cihaz kaydi [yeni] #define WIN32_LEAN_AND_MEAN #ifndef NOMINMAX #define NOMINMAX #endif #include #include #include #include #include #include #include #include #include #include #include #include "lisans_uzaktan.hpp" #include "lisans_uzak.hpp" // uzakKomutMesajiIsle (bu klasorde tanimli) #include "fiko/cekirdek/cekirdek.hpp" #include "fiko/cekirdek/kripto.hpp" #include "fiko/cekirdek/makine_kimlik.hpp" #include "fiko/cekirdek/surum.hpp" #include "fiko/core/gunluk.hpp" #include "ayar_kalici.hpp" #include "lisans_yukle.hpp" namespace fiko { namespace { constexpr char NL = 10; constexpr char TAB = 9; constexpr char TERS = 92; // backslash constexpr char TIRNAK = 34; constexpr DWORD YENIDEN_MIN_MS = 5000; constexpr DWORD YENIDEN_MAX_MS = 30000; // -- TLS sertifika sabitleme (pinned sunucu) ----------------------------------- // ⚠️ BU MODÜL, ESKİDEN TÜM AĞ MODÜLLERİNDE KULLANILAN // SECURITY_FLAG_IGNORE_UNKNOWN_CA | IGNORE_CERT_CN_INVALID | // IGNORE_CERT_DATE_INVALID AYARLARININ YERİNE GEÇER. // // Neden gerekli: "sunucu adresi gizli" gerekçesiyle CA/CN doğrulaması // tamamen kapatılmıştı. Bu, sunucu adresini bilen herhangi birinin MITM // proxy kurup TÜM trafiği okumasına/karıştırmasına izin verir. // // Nasıl çalışır: normal Windows sertifika doğrulaması AÇIK kalır // (kötü CA → bağlantı reddedilir). Ek olarak sunucunun sertifika // parmak izi (SHA-256) üretici tarafından gömülür; eşleşmezse bağlantı // reddedilir. Böylece hem geçerli hem de O DOĞRU sunucu şartı sağlanır. // // Sertifika yenilendikçe gömülü iz güncellenmelidir. İz boşsa // (geliştirme aşaması) yalnız CA doğrulaması uygulanır — bu durumda // bağlantı yine de MITM'e açıktır ve günlüğe UYARI düşülür. // 1) SPKI pinleri (BIRINCIL): SHA-256(SubjectPublicKeyInfo DER) - sertifikanin degil // ACIK ANAHTARIN izi. Sertifika ayni anahtarla yenilenirse (certbot --reuse-key) // iz DEGISMEZ. Liste: [0] = su an sunulan anahtar, [1..] = yedek/gelecek anahtarlar // (yeni anahtarli sertifikaya gecmeden ONCE buraya eklenip surum yayinlanmali). // Olcum: 2026-10-08, fikoyt.com:9000, RSA-2048, CN=fikoyt.com (Let's Encrypt YR2). // Yeniden uretmek icin (bash/cmd; PowerShell 5.1'de ikili veriyi boruya SOKMAYIN - // DER bozulur ve yanlis iz cikar, bu dosyada bir kez yasandi; dosyaya yazin): // openssl s_client -connect fikoyt.com:9000 -servername fikoyt.com pCertInfo okuyordu → tampon // dışı okuma → CRYPT32 içinde access violation → OCCT SEH çevirici → // terminate → abort. Doğrusu: işaretçiyi al, kullan, CertFreeCertificateContext. PCCERT_CONTEXT sert = nullptr; DWORD boyut = sizeof(sert); if (!WinHttpQueryOption(istek, WINHTTP_OPTION_SERVER_CERT_CONTEXT, &sert, &boyut) || !sert) return false; // WinCrypt, CERT_SHA256_HASH_PROP_ID ile sertifikanın SHA-256 özetini verir. std::vector h(32); DWORD len = (DWORD)h.size(); const BOOL ok = CertGetCertificateContextProperty(sert, CERT_SHA256_HASH_PROP_ID, h.data(), &len); // SPKI izi: SHA-256(DER(SubjectPublicKeyInfo)). CryptHashPublicKeyInfo, genel anahtar // bilgisini DER'e kodlayip ozetler - openssl'deki // x509 -pubkey | pkey -pubin -outform DER | dgst -sha256 // ile ayni deger. Sertifika serbest birakilmadan ONCE hesaplanmali (pCertInfo bunun icinde). std::vector sp(32); DWORD spLen = (DWORD)sp.size(); const BOOL spOk = CryptHashPublicKeyInfo(0, CALG_SHA_256, 0, X509_ASN_ENCODING, &sert->pCertInfo->SubjectPublicKeyInfo, sp.data(), &spLen); CertFreeCertificateContext(sert); static constexpr char hex[] = "0123456789abcdef"; auto hexle = [&](const std::vector& v, std::string& cikti) { cikti.clear(); for (unsigned char c : v) { cikti += hex[c >> 4]; cikti += hex[c & 0x0F]; } }; if (ok && len == h.size()) hexle(h, iz); if (spOk && spLen == sp.size()) hexle(sp, spki); // En az biri hesaplanabilmis olmali; ikisi de bossa dogrulama yapilamaz. return !iz.empty() || !spki.empty(); } // Sertifika doğrulamasını uygula. true = bağlantı güvenli. bool tlsSunucuDogrula(HINTERNET istek) { std::string iz, spki; if (!sunucuParmakIzi(istek, iz, spki)) { gunluk::uyari("TLS: sunucu parmak izi alinamadi (GLE {})", GetLastError()); return false; } // Birincil: acik anahtar (SPKI) pini. for (const char* pin : SUNUCU_SPKI_PINLERI) if (!spki.empty() && spki == pin) return true; // Ikincil (gecis donemi): tam sertifika izi. for (const char* pin : SUNUCU_SERTIFIKA_IZLERI) { if (!iz.empty() && iz == pin) { gunluk::uyari("TLS: SPKI pini eslesmedi, eski tip sertifika izi ile kabul edildi " "(spki={}) - yeni anahtar SUNUCU_SPKI_PINLERI'ne eklenmeli", spki); return true; } } gunluk::uyari("TLS: sunucu ESLESMEDI. gelen spki={} sertifika={}", spki, iz); return false; } struct Kanal { UzakKanalDurum durum = UzakKanalDurum::Kapali; std::string hata; std::string sunucu; int komutSayisi = 0; std::atomic calisiyor{false}; std::atomic yenidenBaglanIste{false}; std::atomic durduruldu{false}; int komutBeyazLimitSn = 30; }; Kanal g_kanal; std::mutex g_mtx; // ⚠️ ATOMİK OLMALI: thread detatched, kapanışta bu değer değişir/okunur. // Düz (atomik olmayan) pointer, kapanış sırasında yarış durumu (data race) // yaratıyor ve dangling pointer okumasına yol açıyordu. // Program kapanırken Cekirdek yok edilirken thread hâlâ okuyordu → // 0xC0000409 fast-fail. std::atomic g_cekirdek{nullptr}; // Kanalın gerçekten durduğunu beklemek için (join yerine, detach zorunluysa). std::mutex g_durmus_mtx; std::condition_variable g_durmus_cv; bool g_durdu = true; // -- URL cozumleme -------------------------------------------------------------- struct UrlParca { bool https = true; int port = 443; std::string host, yol; }; bool urlCoz(const std::string& url, UrlParca& u) { std::string kalan = url; for (char& c : kalan) if (c == TERS) c = '/'; if (kalan.rfind("https://", 0) == 0) { u.https = true; kalan.erase(0, 8); } else if (kalan.rfind("http://", 0) == 0) { u.https = false; kalan.erase(0, 7); } if (kalan.empty()) return false; std::string yetki = kalan, yol = "/socket.io/"; const size_t e = kalan.find('/'); if (e != std::string::npos) { yetki = kalan.substr(0, e); yol = kalan.substr(e); } std::string host = yetki; const size_t p = yetki.rfind(':'); if (p != std::string::npos) { host = yetki.substr(0, p); u.port = std::atoi(yetki.c_str() + p + 1); } if (host.empty()) return false; u.host = host; u.yol = yol; if (u.port <= 0) u.port = u.https ? 443 : 80; return true; } std::string urlOku() { std::string url = ayarOku("lisans_uzak_soket_url", ""); std::string kirp; for (char c : url) if (!std::isspace((unsigned char)c)) kirp += c; if (kirp.empty()) return "https://fikoyt.com:9000/socket.io/"; return kirp; } // -- Socket.IO v3 paket yardimcilari (namespace YOK: "2event,{args}") ----------- std::size_t utf16Say(const std::string& s) { std::size_t n = 0; for (std::size_t i = 0; i < s.size();) { const unsigned char c = (unsigned char)s[i]; if (c < 0x80) { i += 1; n += 1; } else if ((c & 0xE0) == 0xC0) { i += 2; n += 1; } else if ((c & 0xF0) == 0xE0) { i += 3; n += 1; } else if ((c & 0xF8) == 0xF0) { i += 4; n += 2; } // emoji = astral else { i += 1; n += 1; } } return n; } std::string jsonDizgeEkle(std::string& j, const char* a, const std::string& d) { j += TIRNAK; j += a; j += TIRNAK; j += ':'; j += TIRNAK; for (char c : d) { if (c == TIRNAK || c == TERS) { j += TERS; j += c; } else if (c == NL) { j += TERS; j += 'n'; } else if ((unsigned char)c < 0x20) j += ' '; else j += c; } j += TIRNAK; } std::string eventUret(const std::string& event, const std::string& args); // ileri bildirim bool baglantiKaydiGonder(HINTERNET ws, const UrlParca& u) { (void)u; const std::string ck = cihazKimlikUret(); if (ck.empty()) return false; // kimliksiz harita kaydi zaten tutmaz // Sunucu kaydi: FikoYT.sistem.io.cihaz.baglandi {cihaz_kimlik, cihaz} // (fiko_socket_io.go: baglandiHandler -> soketCihazMap doldurur). Bu olmadan // motorun hedefli Emit'i yanlis odaya gider, komut cihaza ULASMAZ. std::string pc; if (const char* cn = std::getenv("COMPUTERNAME"); cn && *cn) pc = cn; std::string args = "{\"cihaz_kimlik\":\"" + ck + "\""; args += ",\"cihaz\":{\"kimlik\":\"" + ck + "\""; args += ",\"tipi\":\"fikocad\",\"uygulama\":\"fikocad\""; args += ",\"bilgiler\":{\"bilgisayar\":\"" + pc + "\""; args += ",\"surum\":\"" + std::string(SURUM_METIN ? SURUM_METIN : "") + "\"}}}"; // eventUret yukarida ileri bildirimli; ayni isimsiz alanda oldugundan // niteleyici GEREKMEZ (once ::fiko:: denenmisti: isimsiz alan disi // aramaya zorlayip C2039/C3861 veriyordu). const std::string paket = eventUret("FikoYT.sistem.io.cihaz.baglandi", args); return WinHttpWebSocketSend(ws, WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE, (PVOID)paket.data(), (DWORD)paket.size()) == NO_ERROR; } // (yardimcilar dosya sirasi: eventUret ileri bildirim -> baglantiKaydiGonder // -> eventUret TANIMI) // EVENT packet: 2, (namespace yok) std::string eventUret(const std::string& event, const std::string& args) { const std::string packet = "2" + event + "," + args; return std::to_string(utf16Say(packet)) + ":" + packet; } // engine.io v3 payload -> socket.io EVENT ayriştır bool payloadCoz(const std::string& payload, std::string& ev, std::string& json) { const size_t kolon = payload.find(':'); if (kolon == std::string::npos) return false; const std::string packet = payload.substr(kolon + 1); if (packet.size() < 2 || packet[0] != '2') return false; const size_t virgul = packet.find(',', 1); if (virgul == std::string::npos) return false; ev = packet.substr(1, virgul - 1); const size_t jb = packet.find('{', virgul); if (jb == std::string::npos) { json.clear(); return true; } const size_t je = packet.find_last_of('}'); if (je == std::string::npos) return false; json = packet.substr(jb, je - jb + 1); return true; } // -- Handshake (polling) -------------------------------------------------------- // Cihaz_kimlik header'i sunucudaki soketCihazMap'i doldurur; bu olmadan // hedefli komut yonlendirmesi calismaz. bool handshake(HINTERNET oturum, const UrlParca& u, std::string& sid, std::string& hata) { const std::string yolQuery = u.yol + "?EIO=3&transport=polling"; const std::wstring wpath(yolQuery.begin(), yolQuery.end()); // ⚠️ WinHttpOpenRequest bir BAĞLANTI handle'ı ister, oturum handle'ı DEĞİL. // Bu adım atlanırsa WinHTTP ERROR_WINHTTP_SECURE_FAILURE (12018) döner ve // "handshake istek acilamadi" hatası çıkar — önceki sürümde bu eksikti. // ⚠️ std::wstring(...) geçici nesnesi yerine sabit tampon kullanılır; geçici // nesnenin yaşam süresi belirsiz olduğu için 0xC0000409 fast-fail // üretiyordu. wchar_t wbuf[256] = {0}; { const size_t n = u.host.size() < 255 ? u.host.size() : 255; for (size_t i = 0; i < n; ++i) wbuf[i] = (wchar_t)(unsigned char)u.host[i]; wbuf[n] = 0; } HINTERNET baglanti = WinHttpConnect(oturum, wbuf, (INTERNET_PORT)u.port, 0); if (!baglanti) { hata = "sunucuya baglanilamadi (connect)"; return false; } HINTERNET istek = WinHttpOpenRequest(baglanti, L"GET", wpath.c_str(), nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, u.https ? WINHTTP_FLAG_SECURE : 0); if (!istek) { WinHttpCloseHandle(baglanti); hata = "handshake istek acilamadi"; return false; } // -- TLS doğrulama (sertifika sabitleme) ------------------------------------ // Bu kontrol WinHttpReceiveResponse'TAN SONRA yapılmalıdır: sunucu sertifikası // yalnızca el sıkışma tamamlandıktan sonra elde edilir. Önceden (SendRequest'ten // önce) sorulduğunda WinHttp 12019 (SECURE_FAILURE) dönüyordu. // // ⚠️ ÖNCEKİ HATA: burada SECURITY_FLAG_IGNORE_* kullanılıyordu. Gevşetme // MITM'e kapı açıyordu. Artık CA/CN doğrulaması AÇIK + parmak izi sabit. // Kanıtın gücü korunur: karar/bilet ECDSA imzası ayrıca doğrulanır. // Cihaz kimligi header'i -> sunucudaki soketCihazMap ve goCihazlar dolar. // Bu olmadan hedefli komut yonlendirmesi calismaz. const std::string ck = cihazKimlikUret(); const std::wstring wbaslik = L"Cihaz_kimlik: " + std::wstring(ck.begin(), ck.end()); if (!WinHttpSendRequest(istek, wbaslik.c_str(), (DWORD)-1, WINHTTP_NO_REQUEST_DATA, 0, 0, 0) || !WinHttpReceiveResponse(istek, nullptr)) { WinHttpCloseHandle(istek); WinHttpCloseHandle(baglanti); hata = "handshake gonderilemedi"; return false; } // El sıkışma tamam → şimdi sertifika doğrulanabilir. if (u.https && !tlsSunucuDogrula(istek)) { hata = "sunucu sertifikasi dogrulanamadi (parmak izi eslesmedi)"; WinHttpCloseHandle(istek); WinHttpCloseHandle(baglanti); return false; } DWORD kod = 0, boyut = sizeof(kod); WinHttpQueryHeaders(istek, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &kod, &boyut, WINHTTP_NO_HEADER_INDEX); if (kod != 200) { WinHttpCloseHandle(istek); WinHttpCloseHandle(baglanti); hata = "handshake HTTP " + std::to_string(kod); return false; } std::string yanit; char tampon[2048]; DWORD okunan = 0, toplam = 0; do { okunan = 0; if (!WinHttpReadData(istek, tampon, sizeof(tampon), &okunan) || okunan == 0) break; yanit.append(tampon, okunan); toplam += okunan; } while (toplam < sizeof(tampon) * 8); WinHttpCloseHandle(istek); WinHttpCloseHandle(baglanti); const size_t sp = yanit.find("\"sid\""); if (sp == std::string::npos) { hata = "sid bulunamadi"; return false; } size_t n = yanit.find(':', sp); if (n == std::string::npos) { hata = "sid hatasi"; return false; } ++n; while (n < yanit.size() && (yanit[n] == ' ' || yanit[n] == TIRNAK)) ++n; const size_t b = n; while (n < yanit.size() && yanit[n] != TIRNAK) ++n; sid = yanit.substr(b, n - b); if (sid.empty()) { hata = "sid bos"; return false; } return true; } // Bir websocket oturumu: handshake -> upgrade -> dinle -> cevap ver DWORD oturumAc(const UrlParca& u) { HINTERNET oturum = WinHttpOpen(L"FikoYT-UzakKomut/1.0", WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); if (!oturum) { std::lock_guard lk(g_mtx); g_kanal.hata = "WinHttpOpen basarisiz"; return YENIDEN_MIN_MS; } WinHttpSetTimeouts(oturum, 8000, 8000, 12000, INFINITE); std::string sid, hata; if (!handshake(oturum, u, sid, hata)) { std::lock_guard lk(g_mtx); g_kanal.hata = hata; gunluk::uyari("uzak: handshake FAIL - {}", hata); WinHttpCloseHandle(oturum); return YENIDEN_MIN_MS; } gunluk::bilgi("uzak: handshake OK (sid={}), cihaz={}", sid, cihazKimlikUret()); // WebSocket'e yukselt HINTERNET ws = nullptr; HINTERNET baglantiWs = nullptr; // ws yasadigi surece ACIK kalmali (ws bunun alt handle'i) { const std::string wsPath = u.yol + "?EIO=3&transport=websocket&sid=" + sid; const std::wstring wpath(wsPath.begin(), wsPath.end()); // AYNI KURAL: WinHttpOpenRequest BAGLANTI handle'i ister (oturum degil). // ⚠️ std::wstring(...) gecici nesnesi yerine sabit tampon: gecici // nesnenin yasam suresi belirsiz oldugu icin 0xC0000409 fast-fail // uretiyordu. wchar_t wbuf[256] = {0}; { const size_t n = u.host.size() < 255 ? u.host.size() : 255; for (size_t i = 0; i < n; ++i) wbuf[i] = (wchar_t)(unsigned char)u.host[i]; wbuf[n] = 0; } HINTERNET baglanti = WinHttpConnect(oturum, wbuf, (INTERNET_PORT)u.port, 0); if (!baglanti) { std::lock_guard lk(g_mtx); g_kanal.hata = "websocket: connect basarisiz"; WinHttpCloseHandle(oturum); return YENIDEN_MIN_MS; } HINTERNET istek = WinHttpOpenRequest(baglanti, L"GET", wpath.c_str(), nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, u.https ? WINHTTP_FLAG_SECURE : 0); if (istek) { // Önceden verilmiş herhangi bir sertifika gevşetme bayrağını temizle. // Sunucu sertifikası el sıkışma SONRASI parmak iziyle doğrulanır. if (u.https) { DWORD sifir = 0; WinHttpSetOption(istek, WINHTTP_OPTION_SECURITY_FLAGS, &sifir, sizeof(sifir)); } // ⚠️ WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET bir BAYRAK seçeneğidir, tampon // ALMAZ. (nullptr, 0) verilmesi zorunludur; DWORD değer adresi // verilirse ERROR_INVALID_PARAMETER (87) döner ve WebSocket hiç // kurulamaz (lisans_canli.cpp:495 çalışan haliyle aynı). if (WinHttpSetOption(istek, WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, nullptr, 0)) { if (WinHttpSendRequest(istek, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0) && WinHttpReceiveResponse(istek, nullptr)) { // El sıkışma tamam → sertifika şimdi doğrulanabilir. if (u.https && !tlsSunucuDogrula(istek)) { gunluk::uyari("uzak: ws upgrade - sunucu sertifikasi dogrulanamadi"); WinHttpCloseHandle(istek); WinHttpCloseHandle(baglanti); WinHttpCloseHandle(oturum); return YENIDEN_MIN_MS; } DWORD kod = 0, bs = sizeof(kod); WinHttpQueryHeaders(istek, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &kod, &bs, WINHTTP_NO_HEADER_INDEX); if (kod == 101) { // ⚠️ ÖNCEKİ ÇÖKME (0xDEAD0002): Başarılı yükseltmeden sonra // WinHttpCloseHandle(istek) çağrılıyordu. WebSocket handle'ı // aynı yükleme (load) yapısını paylaştığı için kapatma onu da // geçersizleştirir → sonraki Receive'de stack cookie ihlali. // DÜZELTME: istek handle'ı yalnızca BAŞARISIZ yükseltmede kapatılır. // (Eski "istek kapatilirsa ws olur" yorumu YANLIS TESHISTI: gercek cokme // sunucuParmakIzi'ndeki PCCERT_CONTEXT hatasiydi. MSDN ornegi de // CompleteUpgrade'den sonra istek handle'ini kapatir.) ws = WinHttpWebSocketCompleteUpgrade(istek, 0); if (!ws) gunluk::uyari("uzak: CompleteUpgrade basarisiz, GLE {}", GetLastError()); } else { // ÖNCEKİ HATA: HTTP kodu ne olursa olsun sessizce geçiliyordu. gunluk::uyari("uzak: websocket upgrade HTTP {} (101 bekleniyordu)", kod); } } else { gunluk::uyari("uzak: upgrade send/recv FAIL, GLE {}", GetLastError()); } } else { gunluk::uyari("uzak: UPGRADE_TO_WEB_SOCKET setoption FAIL, GLE {}", GetLastError()); } if (istek) WinHttpCloseHandle(istek); // CompleteUpgrade sonrasi da kapatilir (MSDN) // ws basariliysa baglanti handle'i ACIK kalir (ws onun alt handle'i); // oturum sonunda ws'den SONRA kapatilir. if (ws) baglantiWs = baglanti; else WinHttpCloseHandle(baglanti); } else { gunluk::uyari("uzak: OpenRequest (ws) basarisiz, GLE {}", GetLastError()); WinHttpCloseHandle(baglanti); } } if (!ws) { std::lock_guard lk(g_mtx); g_kanal.durum = UzakKanalDurum::Kopuk; g_kanal.hata = "websocket upgrade basarisiz"; gunluk::uyari("uzak: KANAL KURULAMADI - 5 sn sonra tekrar denenecek"); WinHttpCloseHandle(oturum); return YENIDEN_MIN_MS; } // ── Merhaba paketi (cihaz kimliğini sunucuya bildir) ────────────────────── // ⚠️ ÖNCEKİ HATA: polling handshake sonrası "0" + u.yol paketi gönderiliyordu // ("0/socket.io/"). u.yol bir ENDPOINT yoludur, namespace DEĞİL — sunucu // bunu geçersiz namespace sayıp bağlantıyı ANINDA kapatıyordu // (WinHttp 12030 ERROR_WINHTTP_CONNECTION_CLOSED, 1 ms sonra). // Doğrusu: engine.io tip 0 (CONNECT) boş namespace ile, yani "0". // Bu, çalışan lisans kanalının (lisans_canli.cpp:483) birebir deseni. // ⚠️ KÖK NEDEN (kanal 1 ms sonra 12030 ile kopuyordu): polling ile alınmış sid'i // websocket'e yükseltirken engine.io v3 "probe" el sıkışması ZORUNLUDUR: // C->S "2probe", S->C "3probe", C->S "5" (upgrade). Eskiden burada "0" gönderiliyordu; // "0" sunucudan istemciye giden "open" tipidir, istemciden gelmesi geçersiz // olduğundan sunucu bağlantıyı anında kapatıyordu. Varsayılan "/" namespace'ine // sunucu polling aşamasında zaten bağlamıştır (ayrı CONNECT gerekmez). { bool probeTamam = false; const char* p1 = "2probe"; if (WinHttpWebSocketSend(ws, WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE, (PVOID)p1, 6) == NO_ERROR) { char pb[64] = {0}; DWORD pn = 0; WINHTTP_WEB_SOCKET_BUFFER_TYPE pt = WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE; if (WinHttpWebSocketReceive(ws, pb, sizeof(pb), &pn, &pt) == NO_ERROR && pt == WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE && pn == 6 && memcmp(pb, "3probe", 6) == 0) { const char* p2 = "5"; probeTamam = WinHttpWebSocketSend(ws, WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE, (PVOID)p2, 1) == NO_ERROR; } } if (!probeTamam) { gunluk::uyari("uzak: engine.io upgrade probe basarisiz - 5 sn sonra tekrar"); { std::lock_guard lk(g_mtx); g_kanal.durum = UzakKanalDurum::Kopuk; g_kanal.hata = "upgrade probe basarisiz"; } WinHttpCloseHandle(ws); if (baglantiWs) WinHttpCloseHandle(baglantiWs); WinHttpCloseHandle(oturum); return YENIDEN_MIN_MS; } gunluk::bilgi("uzak: engine.io upgrade tamam (probe)"); } { std::lock_guard lk(g_mtx); g_kanal.durum = UzakKanalDurum::Bagli; g_kanal.hata.clear(); // Sunucudaki soketCihazMap kaydini doldur — hedefli emit BUNA gore gider. // (FikoYT.sistem.io.cihaz.baglandi; gonderilemezse komut cihaza ULASMAZ.) if (baglantiKaydiGonder(ws, u)) gunluk::bilgi("uzak: baglanti kaydi gonderildi (cihaz={})", cihazKimlikUret()); else gunluk::uyari("uzak: baglanti kaydi gonderilemedi - hedefli komut gelmeyebilir"); g_kanal.sunucu = u.host; } gunluk::bilgi("uzak komut kanali BAGLI: {}", u.host); // Dinle dongusu std::string birikim; char tampon[4096]; // ⚠️ Engine.IO PING: sunucu pingInterval (varsayilan 25 sn) boyunca paket // gelmezse bağlantıyı KENDİSİ kapatır. Komut kanalı doğal olarak sessiz // olabildiği için periyodik "2" (ping) göndermek şart; yoksa kanal // saniyede bir kopup yeniden handshake yapar. auto sonPing = std::chrono::steady_clock::now(); const DWORD pingAralikMs = 20000; // sunucudan biraz erken gönder for (;;) { if (g_kanal.durduruldu.load()) break; if (std::chrono::steady_clock::now() - sonPing > std::chrono::milliseconds(pingAralikMs)) { sonPing = std::chrono::steady_clock::now(); if (WinHttpWebSocketSend(ws, WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE, (PVOID)"2", 1) != NO_ERROR) { std::lock_guard lk(g_mtx); g_kanal.hata = "ping gonderilemedi"; g_kanal.durum = UzakKanalDurum::Kopuk; break; } } DWORD okunan = 0; WINHTTP_WEB_SOCKET_BUFFER_TYPE tur = WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE; const DWORD rc = WinHttpWebSocketReceive(ws, tampon, sizeof(tampon), &okunan, &tur); if (rc != NO_ERROR) { std::lock_guard lk(g_mtx); g_kanal.hata = "okuma hatasi"; g_kanal.durum = UzakKanalDurum::Kopuk; gunluk::uyari("uzak: okuma hatasi GLE {} (WinHttp {}) - 5 sn sonra tekrar", GetLastError(), (unsigned)rc); break; } if (tur == WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE) { // Sunucu neden kapattı? Status kodunu sor (kendi kapanış kodu değil, // sunucunun gönderdiği close frame kodu) — teşhis için gerekli. // ⚠️ İMZA: (ws, USHORT* kod, PVOID sebepBuf, DWORD sebepLen, DWORD* tüketilen) // pvReason PVOID'dur (std::string* DEĞİL) ve dwReasonLength tamponun // BAYT cinsinden boyutudur. Tüketilen en fazla 123 bayt olur. USHORT kapKod = 0; char sebepBuf[128] = {0}; DWORD sebepUz = (DWORD)sizeof(sebepBuf); DWORD tüketilen = 0; const DWORD cs = WinHttpWebSocketQueryCloseStatus(ws, &kapKod, sebepBuf, sebepUz, &tüketilen); if (cs == NO_ERROR) { gunluk::uyari("uzak: sunucu kapatti - kod={} sebep='{}'", (unsigned)kapKod, sebepBuf); } else { gunluk::uyari("uzak: sunucu kapatti (close sorgusu hata {}, kod={})", (unsigned)cs, (unsigned)kapKod); } std::lock_guard lk(g_mtx); g_kanal.hata = "sunucu baglantiyi kapatti"; g_kanal.durum = UzakKanalDurum::Kopuk; break; } birikim.append(tampon, okunan); if (tur != WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE) continue; // parca std::string ev, json; if (payloadCoz(birikim, ev, json)) { birikim.clear(); // ⚠️ YEREL KOPYAYA AL: kapanış sırasında g_cekirdek nullptr yapılabilir, // doğrudan *g_cekirdek kullanmak dangling pointer olurdu. if (Cekirdek* cek = g_cekirdek.load()) { if (ev == "FikoYT.sistem.io.cihaz.komut") { std::string cevap; const bool ok = uzakKomutMesajiIsle(*cek, json, cevap); { std::lock_guard lk(g_mtx); ++g_kanal.komutSayisi; } // Cevabi geri gonder. WinHttpWebSocketSend 4 arguman alir; gonderilen // bayt sayisi dondurulmez (yoksa hata durumunda cevap sessizce gider). const std::string paket = eventUret("FikoYT.sistem.io.cihaz.komut-cevap", cevap); WinHttpWebSocketSend(ws, WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE, (PVOID)paket.data(), (DWORD)paket.size()); gunluk::bilgi("uzak komut cevabi: {}", ok ? "OK" : "RED"); } } } } WinHttpWebSocketClose(ws, WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS, nullptr, 0); WinHttpCloseHandle(ws); if (baglantiWs) WinHttpCloseHandle(baglantiWs); WinHttpCloseHandle(oturum); return YENIDEN_MIN_MS; } void kanalIsParcacigi() { DWORD beklemeMs = 0; while (!g_kanal.durduruldu.load()) { if (g_kanal.yenidenBaglanIste.exchange(false)) beklemeMs = 0; UrlParca u; if (!urlCoz(urlOku(), u)) { std::lock_guard lk(g_mtx); g_kanal.hata = "gecersiz URL"; g_kanal.durum = UzakKanalDurum::Kopuk; for (int i = 0; i < 30 && !g_kanal.durduruldu.load(); ++i) std::this_thread::sleep_for(std::chrono::seconds(1)); continue; } { std::lock_guard lk(g_mtx); g_kanal.durum = UzakKanalDurum::Baglanir; } beklemeMs = oturumAc(u); beklemeMs = std::min(beklemeMs * 2, YENIDEN_MAX_MS); for (DWORD b = 0; b < beklemeMs && !g_kanal.durduruldu.load(); b += 250) { if (g_kanal.yenidenBaglanIste.load()) break; std::this_thread::sleep_for(std::chrono::milliseconds(250)); } } g_kanal.calisiyor = false; // Durdurmayı bekleyen tarafa haber ver (uzakKomutKanalDurdur beklemesin). { std::lock_guard lk(g_durmus_mtx); g_durdu = true; } g_durmus_cv.notify_all(); } } // namespace bool lisansTlsSunucuDogrula(void* istek) { return tlsSunucuDogrula(static_cast(istek)); } const char* uzakKanalDurumAdi(UzakKanalDurum d) { switch (d) { case UzakKanalDurum::Baglanir: return "baglaniyor"; case UzakKanalDurum::Bagli: return "BAGLI"; case UzakKanalDurum::Kopuk: return "kopuk"; default: return "kapali"; } } UzakKanalDurum uzakKanalDurumu() { std::lock_guard lk(g_mtx); return g_kanal.durum; } std::string uzakKanalOzet() { std::lock_guard lk(g_mtx); std::string s = std::string("Uzak komut kanali: ") + uzakKanalDurumAdi(g_kanal.durum); if (!g_kanal.sunucu.empty()) s += " - " + g_kanal.sunucu; if (g_kanal.komutSayisi > 0) s += " · " + std::to_string(g_kanal.komutSayisi) + " komut"; if (!g_kanal.hata.empty()) s += " (" + g_kanal.hata + ")"; return s; } void uzakKomutKanalBaslat(Cekirdek& cekirdek) { g_cekirdek.store(&cekirdek, std::memory_order_release); g_kanal.durduruldu = false; { std::lock_guard lk(g_durmus_mtx); g_durdu = false; } if (g_kanal.calisiyor.exchange(true)) return; std::thread(kanalIsParcacigi).detach(); } void uzakKomutKanalDurdur() { // ⚠️ SıralAMA KRİTİK: önce çekirdek işaretçisini düşür (thread dangling // pointer görmesin), sonra durdur bayrağını kaldır, sonra GERÇEKTEN // durmasını bekle. Eskiden sadece bayrak kalkıyordu; kapanışta // Cekirdek yok edilirken thread hâlâ *g_cekirdek okuyordu. g_cekirdek.store(nullptr, std::memory_order_release); g_kanal.durduruldu = true; g_kanal.calisiyor = false; // Thread bloklanmış WinHttpWebSocketReceive içinde olabilir; 3 sn bekle, // sonra zorla bırak (kapanışı kilitlememek için). std::unique_lock lk(g_durmus_mtx); g_durmus_cv.wait_for(lk, std::chrono::seconds(3), [] { return g_durdu; }); g_durdu = true; } } // namespace fiko