// -- 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 #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; // sabit varsayılan (aşağıda); ayar dosyasından okunmaz 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 paket yardimcilari (engine.io v3, WEBSOCKET frame'leri) ------------ // Websocket'te her frame TEK engine.io paketidir. "N:" uzunluk oneki YALNIZ polling // govdesinde (handshake) vardir. Websocket'e tasinirsa sunucu frame'in ilk rakamini // engine.io paket tipi sanir (1=close, 2=ping ...) ve olay HIC islenmez. // EVENT frame'i: 42["olay",{...}] // '4' = engine.io MESSAGE, '2' = socket.io EVENT, varsayilan "/" namespace'i yazilmaz. // Kaynak (sunucu kutuphanesi): socket.io-go-parser/v2 v2.5.0 encoder.go:82 / decoder.go:218. // ws'e giden TUM frame'ler bu mutex ile serilesir: ping parcacigi ve dinleme dongusu // ayni handle'a ayni anda WinHttpWebSocketSend cagirmamali. std::mutex g_gonderMtx; DWORD wsGonder(HINTERNET ws, const std::string& s) { std::lock_guard lk(g_gonderMtx); return WinHttpWebSocketSend(ws, WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE, (PVOID)s.data(), (DWORD)s.size()); } // EIO=3: kalp atisini ISTEMCI baslatir (engine.io socket.go:183-189, 204-211): istemci "2" // yollar, sunucu "3" ile cevaplar. pingInterval(25 sn)+pingTimeout(20 sn) = 45 sn icinde // ping gelmezse sunucu baglantiyi kapatir. Sunucudan once davranmak icin 20 sn. constexpr DWORD PING_ARALIK_MS = 20000; 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 + "\""; // Üretici makinesi: lisans dosyası OLMAMASI normaldir — satıcı panelinde // "LISANS YOK" yerine "URETICI" gösterilsin diye bayrak taşınır. if (lisansUreticiMakinesiMi()) args += ",\"uretici\":\"1\""; 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 wsGonder(ws, paket) == NO_ERROR; } // (yardimcilar dosya sirasi: eventUret ileri bildirim -> baglantiKaydiGonder // -> eventUret TANIMI) // EVENT frame'i uret: 42["",] (uzunluk oneki YOK, namespace "/" yazilmaz) // Eski bicim ":2," sunucuda cozulemiyordu (bkz. yukaridaki not). // 'event' sabit ASCII adlardir (kacis gerekmez); 'args' hazir JSON metnidir. std::string eventUret(const std::string& event, const std::string& args) { std::string p = "42["; p += TIRNAK; p += event; p += TIRNAK; if (!args.empty()) { p += ','; p += args; } p += ']'; return p; } // Gelen engine.io v3 websocket frame'ini socket.io EVENT olarak ayristir. // Kabul edilen bicim: 42["olay",{...}] (42/ns,["olay",..] ve ack kimlikli // 42["olay",..] bicimleri de tolere edilir). 'json' = olayin ilk argumani (ham JSON). // false = EVENT degil / bozuk. Cagiran SESSIZ gecmez: ham metni gunluge yazar. bool payloadCoz(const std::string& frame, std::string& ev, std::string& json) { ev.clear(); json.clear(); if (frame.size() < 5 || frame[0] != '4' || frame[1] != '2') return false; std::size_t i = 2; if (frame[i] == '/') { // 42/ns,[...] const std::size_t v = frame.find(',', i); if (v == std::string::npos) return false; i = v + 1; } while (i < frame.size() && frame[i] >= '0' && frame[i] <= '9') ++i; // ack kimligi if (i + 1 >= frame.size() || frame[i] != '[' || frame[i + 1] != TIRNAK) return false; i += 2; while (i < frame.size() && frame[i] != TIRNAK) { if (frame[i] == TERS && i + 1 < frame.size()) ++i; ev += frame[i]; ++i; } if (i >= frame.size() || ev.empty()) return false; ++i; // olay adinin kapanis tirnagi if (i < frame.size() && frame[i] == ',') { ++i; const std::size_t son = frame.find_last_of(']'); if (son == std::string::npos || son < i) return false; json = frame.substr(i, son - i); } else if (i >= frame.size() || frame[i] != ']') { return false; } 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; } // engine.io MESSAGE ('4') frame'indeki socket.io paketini isle. false = oturumu kapat. bool socketioIsle(HINTERNET ws, const std::string& frame) { const char tip = frame.size() > 1 ? frame[1] : '?'; switch (tip) { case '0': // CONNECT (EIO=3'te bos paket: "40") gunluk::bilgi("uzak: socket.io CONNECT alindi: {}", frame.substr(0, 80)); return true; case '1': // DISCONNECT gunluk::uyari("uzak: sunucu socket.io DISCONNECT gonderdi: {}", frame.substr(0, 80)); return false; case '4': // CONNECT_ERROR gunluk::uyari("uzak: socket.io hata paketi: {}", frame.substr(0, 160)); return true; case '2': // EVENT break; default: gunluk::bilgi("uzak: islenmeyen socket.io paketi: {}", frame.substr(0, 80)); return true; } // Ham metin HER ZAMAN izlenir: paket sessizce dusmesin (komut imzasi ve mesaj dahil). gunluk::bilgi("uzak: gelen olay ({} bayt): {}", frame.size(), frame.substr(0, 400)); std::string ev, json; if (!payloadCoz(frame, ev, json)) { gunluk::uyari("uzak: olay paketi COZULEMEDI (dusuruldu): {}", frame.substr(0, 200)); return true; } if (ev != "FikoYT.sistem.io.cihaz.komut") { gunluk::bilgi("uzak: ilgilenilmeyen olay: {}", ev); return true; } // YEREL KOPYAYA AL: kapanis sirasinda g_cekirdek nullptr yapilabilir. Cekirdek* cek = g_cekirdek.load(); if (!cek) { gunluk::uyari("uzak: komut geldi ama cekirdek hazir degil - cevaplanmadi"); return true; } // Imza dogrulamasi, beyaz liste ve saglik kontrolu uzakKomutMesajiIsle -> uzakKomutUygula // icindedir; imzasiz / bozuk imzali komut orada REDDEDILIR (burada gevsetme YOK). std::string cevap; const bool ok = uzakKomutMesajiIsle(*cek, json, cevap); { std::lock_guard lk(g_mtx); ++g_kanal.komutSayisi; } const DWORD src = wsGonder(ws, eventUret("FikoYT.sistem.io.cihaz.komut-cevap", cevap)); if (src != NO_ERROR) gunluk::uyari("uzak: komut cevabi gonderilemedi (WinHttp {})", (unsigned)src); gunluk::bilgi("uzak komut cevabi: {}", ok ? "OK" : "RED"); 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; } // -- engine.io v3 upgrade + socket.io oturumu ------------------------------------ // 1) Polling handshake sid verdi. Sunucu (EIO=3) socket.io "/" namespace'ine KENDISI // baglar: socket.io server.go:519-521 `if conn.Protocol() == 3 { client.connect("/") }` // ve socket.go:515-518 (bos CONNECT paketi). Bu yuzden istemci "40" GONDERMEZ. // Gondermek ZARARLIDIR: client.go:212-219 namespace'i zaten bagli olan bir CONNECT'i // "invalid state" sayip baglantiyi KAPATIR. // 2) websocket'e yukseltme icin probe ZORUNLU: C->S "2probe", S->C "3probe", C->S "5". // (Eskiden "0" yollaniyordu; "0" sunucudan istemciye giden open tipidir, sunucu // baglantiyi aninda kapatiyordu - WinHttp 12030, 1 ms sonra.) // 3) Olaylar 42["olay",{...}] biciminde, uzunluk oneksiz gider/gelir (eventUret/payloadCoz). { 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 // Kalp atisi: EIO=3'te ISTEMCI baslatir (bkz. PING_ARALIK_MS). Eskiden "2" yalniz bir // frame GELDIKTEN sonra, dongu basinda kontrol ediliyordu; Receive bloklayici oldugundan // sessiz kanalda ping hic gitmiyor, sunucu 45 sn sonra baglantiyi kapatiyordu. // Ping parcacigi yalniz ws DEGERINI ve paylasilan durum nesnesini (shared_ptr kopyasi) // kullanir, yerel degiskene referans tutmaz; ws kapanmadan ONCE join edilir. struct PingDurum { std::mutex mtx; std::condition_variable cv; bool dur = false; }; const std::shared_ptr pingDurum = std::make_shared(); std::thread pingIs([ws, pingDurum] { try { std::unique_lock lk(pingDurum->mtx); while (!pingDurum->dur) { if (pingDurum->cv.wait_for(lk, std::chrono::milliseconds(PING_ARALIK_MS), [&] { return pingDurum->dur; })) break; // durdurma istendi lk.unlock(); const DWORD prc = wsGonder(ws, "2"); // engine.io ping lk.lock(); if (prc != NO_ERROR) break; } } catch (...) { } }); std::string birikim; char tampon[4096]; bool bitir = false; while (!bitir && !g_kanal.durduruldu.load()) { 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 kapatti? Close frame durum kodunu sor (teshis icin). // Imza: (ws, USHORT* kod, PVOID sebepBuf, DWORD sebepLen, DWORD* tuketilen) USHORT kapKod = 0; char sebepBuf[128] = {0}; DWORD tuketilen = 0; const DWORD cs = WinHttpWebSocketQueryCloseStatus(ws, &kapKod, sebepBuf, (DWORD)sizeof(sebepBuf) - 1, &tuketilen); 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 (birikim.size() > (1u << 20)) { // 1 MB ust sinir: bellek korumasi gunluk::uyari("uzak: ws mesaji cok buyuk ({} bayt) - baglanti kapatiliyor", birikim.size()); std::lock_guard lk(g_mtx); g_kanal.hata = "ws mesaji cok buyuk"; g_kanal.durum = UzakKanalDurum::Kopuk; break; } if (tur != WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE) continue; // parca: devam // Tam bir frame geldi. Ayristirma basarisiz olsa da birikim HER ZAMAN bosaltilir. // (Eskiden yalniz basarida bosaltiliyordu: tek bir cozulemeyen frame - or. "3" pong // ya da "42[...]" - sonrakilerin basina yapisip HER SEYI sessizce zehirliyordu.) const std::string frame = std::move(birikim); birikim.clear(); if (frame.empty()) continue; switch (frame[0]) { case '1': { // engine.io close gunluk::uyari("uzak: sunucu engine.io CLOSE gonderdi"); std::lock_guard lk(g_mtx); g_kanal.hata = "sunucu oturumu kapatti"; g_kanal.durum = UzakKanalDurum::Kopuk; bitir = true; break; } case '2': // sunucu ping (yalniz EIO=4'te gelir) -> pong wsGonder(ws, "3"); break; case '4': // engine.io MESSAGE -> socket.io paketi if (!socketioIsle(ws, frame)) { std::lock_guard lk(g_mtx); g_kanal.hata = "sunucu socket.io oturumunu kapatti"; g_kanal.durum = UzakKanalDurum::Kopuk; bitir = true; } break; default: // '0' open, '3' pong, '5' upgrade, '6' noop: islem gerekmez break; } } // Ping parcacigini durdur ve BEKLE (ws kapanmadan once). { std::lock_guard lk(pingDurum->mtx); pingDurum->dur = true; } pingDurum->cv.notify_all(); if (pingIs.joinable()) pingIs.join(); 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