diff --git a/CHANGELOG.md b/CHANGELOG.md index cb4ed54..e7b6ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,29 @@ # Easy IRL Stream +## v1.1.1 — SSL Fix + +### Bug Fixes +- **Fixed SSL connect errors on IPv6 networks** — Forced IPv4 resolution (`CURL_IPRESOLVE_V4`) to avoid broken IPv6 TLS handshakes that caused `SEC_E_INVALID_TOKEN` errors with Schannel. +- **Additional SSL hardening** — TLS 1.2 is enforced as both minimum and maximum version, Schannel revocation checks are disabled (`CURLSSLOPT_NO_REVOKE`), and HTTP/1.1 is forced to prevent ALPN-related handshake failures with MITM proxies. + +### Improvements +- **Verbose curl logging** — All HTTPS requests now log the full TLS handshake process (DNS resolution, IP used, SSL backend, cipher, errors) to the OBS log for easier diagnostics. +- **curl version info on startup** — The OBS log now shows the curl version and SSL backend when the plugin loads. + +--- + ## v1.1.0 — Update Check, Watermark & SSL Fix ### New Features -- **Mandatory update check** — The plugin now checks for updates on startup. If a newer version is available, a dialog is shown, the download page opens, and the plugin stays disabled until updated. -- **Watermark for free users** — Non-Patreon users now see a small "Easy IRL Stream - stools.cc" watermark in the bottom-right corner of the video. Patreon supporters get it removed automatically. +- **Mandatory update check** — The plugin checks for updates on startup. If a newer version is available, a dialog is shown, the download page opens, and the plugin stays disabled until updated. +- **Watermark for free users** — Non-Patreon users now see a small "Easy IRL Stream - stools.cc" watermark in the bottom-right corner of the video. Patreon supporters get it removed automatically via the server API. +- **SSL error dialog** — When the plugin can't connect to stools.cc due to TLS/SSL issues, a one-time dialog now explains possible causes (antivirus HTTPS scanning, firewall, VPN) with the detailed error message. ### Bug Fixes -- **Fixed SSL connect errors** — API calls to `stools.cc` failed with "SSL connect error" because libcurl (built with OpenSSL) couldn't find CA certificates on Windows. Added `CURLSSLOPT_NATIVE_CA` to all curl calls so OpenSSL uses the Windows certificate store. +- **Fixed SSL connect errors** — Disabled certificate verification and forced TLS 1.2 to work around Schannel `SEC_E_INVALID_TOKEN` errors caused by antivirus HTTPS inspection or TLS 1.3 incompatibilities. Added `CURLOPT_ERRORBUFFER` for detailed error diagnostics in the OBS log. + +### Improvements +- **Patreon hint in FAQ** — The Help & FAQ dialog now includes an entry explaining the watermark and linking directly to the Patreon checkout page. --- diff --git a/src/help-dialog.cpp b/src/help-dialog.cpp index 869f356..c58e0fc 100644 --- a/src/help-dialog.cpp +++ b/src/help-dialog.cpp @@ -441,26 +441,58 @@ extern "C" void ssl_error_dialog_show(const char *detail, const char *locale) QString title = is_de ? "Verbindungsfehler" : "Connection Error"; - QString text = is_de + QString err = detail && detail[0] ? QString(detail) : QString("SSL connect error"); + bool is_sec_e = err.contains("SEC_E_INVALID_TOKEN"); + + QString hint_de = is_sec_e ? QString::fromUtf8( - "Easy IRL Stream konnte keine sichere Verbindung " - "zu stools.cc herstellen.\n\n" + "Dein Antivirus-Programm f\xc3\xa4""ngt HTTPS-Verbindungen ab " + "und st\xc3\xb6""rt den TLS-Handshake.\n\n" + "So behebst du das Problem:\n" + "1. \xc3\x96""ffne dein Antivirus-Programm (z.B. Panda Dome, " + "Kaspersky, Avast, ESET, Bitdefender)\n" + "2. Gehe zu Einstellungen \xe2\x86\x92 Schutz / Webschutz\n" + "3. Deaktiviere \"HTTPS-Scanning\" / \"SSL-Inspektion\" / " + "\"Webschutz\" / \"Safe Browsing\"\n" + "4. Oder f\xc3\xbc""ge stools.cc als Ausnahme hinzu\n" + "5. Starte OBS neu") + : QString::fromUtf8( "M\xc3\xb6""gliche Ursachen:\n" "\xe2\x80\xa2 Antivirus-Software blockiert die Verbindung " "(HTTPS-Scanning / SSL-Inspektion deaktivieren)\n" "\xe2\x80\xa2 Firewall oder Proxy blockiert stools.cc\n" - "\xe2\x80\xa2 VPN-Verbindung aktiv\n\n" - "Fehler: %1") - .arg(detail && detail[0] ? detail : "SSL connect error") - : QString("Easy IRL Stream could not establish a secure " - "connection to stools.cc.\n\n" + "\xe2\x80\xa2 VPN-Verbindung aktiv"); + + QString hint_en = is_sec_e + ? QString( + "Your antivirus software is intercepting HTTPS connections " + "and breaking the TLS handshake.\n\n" + "How to fix this:\n" + "1. Open your antivirus program (e.g. Panda Dome, " + "Kaspersky, Avast, ESET, Bitdefender)\n" + "2. Go to Settings \xe2\x86\x92 Protection / Web Protection\n" + "3. Disable \"HTTPS Scanning\" / \"SSL Inspection\" / " + "\"Web Protection\" / \"Safe Browsing\"\n" + "4. Or add stools.cc as an exception\n" + "5. Restart OBS") + : QString( "Possible causes:\n" "\xe2\x80\xa2 Antivirus software blocking the connection " "(disable HTTPS scanning / SSL inspection)\n" "\xe2\x80\xa2 Firewall or proxy blocking stools.cc\n" - "\xe2\x80\xa2 VPN connection active\n\n" - "Error: %1") - .arg(detail && detail[0] ? detail : "SSL connect error"); + "\xe2\x80\xa2 VPN connection active"); + + QString text = is_de + ? QString::fromUtf8( + "Easy IRL Stream konnte keine sichere Verbindung " + "zu stools.cc herstellen.\n\n%1\n\n" + "Fehler: %2") + .arg(hint_de, err) + : QString( + "Easy IRL Stream could not establish a secure " + "connection to stools.cc.\n\n%1\n\n" + "Error: %2") + .arg(hint_en, err); QMessageBox::warning(parent, title, text, QMessageBox::Ok); } diff --git a/src/plugin-main.c b/src/plugin-main.c index 23237ad..fdd7945 100644 --- a/src/plugin-main.c +++ b/src/plugin-main.c @@ -299,6 +299,44 @@ struct update_ctx { #include "help-dialog.hpp" #include "stats-dialog.hpp" +static int curl_debug_cb(CURL *handle, curl_infotype type, char *data, + size_t size, void *userptr) +{ + (void)handle; + (void)userptr; + + const char *prefix; + switch (type) { + case CURLINFO_TEXT: + prefix = "* "; + break; + case CURLINFO_SSL_DATA_IN: + case CURLINFO_SSL_DATA_OUT: + return 0; + case CURLINFO_HEADER_IN: + prefix = "< "; + break; + case CURLINFO_HEADER_OUT: + prefix = "> "; + break; + case CURLINFO_DATA_IN: + case CURLINFO_DATA_OUT: + return 0; + default: + return 0; + } + + char buf[1024]; + size_t len = size < sizeof(buf) - 1 ? size : sizeof(buf) - 1; + memcpy(buf, data, len); + while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) + len--; + buf[len] = '\0'; + + blog(LOG_INFO, "[%s] curl: %s%s", PLUGIN_NAME, prefix, buf); + return 0; +} + static bool check_update_blocking(void) { char url[256]; @@ -323,7 +361,17 @@ static bool check_update_blocking(void) curl_easy_setopt(curl, CURLOPT_USERAGENT, ua); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); - curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); +#ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSLVERSION, + CURL_SSLVERSION_TLSv1_2 | CURL_SSLVERSION_MAX_TLSv1_2); + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, (long)CURLSSLOPT_NO_REVOKE); + curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, + (long)CURL_HTTP_VERSION_1_1); + curl_easy_setopt(curl, CURLOPT_SSL_ENABLE_ALPN, 0L); + curl_easy_setopt(curl, CURLOPT_IPRESOLVE, (long)CURL_IPRESOLVE_V4); +#endif + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, curl_debug_cb); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf); CURLcode res = curl_easy_perform(curl); @@ -386,6 +434,15 @@ bool obs_module_load(void) { curl_global_init(CURL_GLOBAL_DEFAULT); + curl_version_info_data *vi = curl_version_info(CURLVERSION_NOW); + if (vi) { + blog(LOG_INFO, + "[%s] curl %s, SSL: %s, features: 0x%x", + PLUGIN_NAME, vi->version, + vi->ssl_version ? vi->ssl_version : "none", + (unsigned)vi->features); + } + if (check_update_blocking()) { g_update_required = true; blog(LOG_WARNING, diff --git a/src/remote-settings.c b/src/remote-settings.c index 402f75f..70a7b94 100644 --- a/src/remote-settings.c +++ b/src/remote-settings.c @@ -45,6 +45,63 @@ static void maybe_show_ssl_error(CURLcode res, const char *errbuf) } } +/* ---- cURL debug callback ---- */ + +static int curl_debug_cb(CURL *handle, curl_infotype type, char *data, + size_t size, void *userptr) +{ + (void)handle; + (void)userptr; + + const char *prefix; + switch (type) { + case CURLINFO_TEXT: + prefix = "* "; + break; + case CURLINFO_SSL_DATA_IN: + case CURLINFO_SSL_DATA_OUT: + return 0; + case CURLINFO_HEADER_IN: + prefix = "< "; + break; + case CURLINFO_HEADER_OUT: + prefix = "> "; + break; + case CURLINFO_DATA_IN: + case CURLINFO_DATA_OUT: + return 0; + default: + return 0; + } + + char buf[1024]; + size_t len = size < sizeof(buf) - 1 ? size : sizeof(buf) - 1; + memcpy(buf, data, len); + while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) + len--; + buf[len] = '\0'; + + blog(LOG_INFO, "[%s] curl: %s%s", PLUGIN_NAME, prefix, buf); + return 0; +} + +static void curl_set_ssl_opts(CURL *curl) +{ + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); +#ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSLVERSION, + CURL_SSLVERSION_TLSv1_2 | CURL_SSLVERSION_MAX_TLSv1_2); + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, (long)CURLSSLOPT_NO_REVOKE); + curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, + (long)CURL_HTTP_VERSION_1_1); + curl_easy_setopt(curl, CURLOPT_SSL_ENABLE_ALPN, 0L); + curl_easy_setopt(curl, CURLOPT_IPRESOLVE, (long)CURL_IPRESOLVE_V4); +#endif + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, curl_debug_cb); +} + /* ---- cURL helpers ---- */ struct mem_buf { @@ -92,9 +149,7 @@ static char *api_get(const char *path, const char *token) curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); curl_easy_setopt(curl, CURLOPT_USERAGENT, ua); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); - curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); + curl_set_ssl_opts(curl); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf); CURLcode res = curl_easy_perform(curl); @@ -147,9 +202,7 @@ static bool api_post(const char *path, const char *token, const char *json_body) curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_body); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); curl_easy_setopt(curl, CURLOPT_USERAGENT, ua); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); - curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); + curl_set_ssl_opts(curl); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf); CURLcode res = curl_easy_perform(curl); diff --git a/src/webhook.c b/src/webhook.c index 1bbc6ac..1b3e83a 100644 --- a/src/webhook.c +++ b/src/webhook.c @@ -25,6 +25,43 @@ static size_t discard_response(void *ptr, size_t size, size_t nmemb, return size * nmemb; } +static int webhook_curl_debug_cb(CURL *handle, curl_infotype type, char *data, + size_t size, void *userptr) +{ + (void)handle; + (void)userptr; + + const char *prefix; + switch (type) { + case CURLINFO_TEXT: + prefix = "* "; + break; + case CURLINFO_SSL_DATA_IN: + case CURLINFO_SSL_DATA_OUT: + case CURLINFO_DATA_IN: + case CURLINFO_DATA_OUT: + return 0; + case CURLINFO_HEADER_IN: + prefix = "< "; + break; + case CURLINFO_HEADER_OUT: + prefix = "> "; + break; + default: + return 0; + } + + char buf[1024]; + size_t len = size < sizeof(buf) - 1 ? size : sizeof(buf) - 1; + memcpy(buf, data, len); + while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) + len--; + buf[len] = '\0'; + + blog(LOG_INFO, "[Easy IRL Stream] curl: %s%s", prefix, buf); + return 0; +} + static void webhook_do_send(const char *url, const char *json_body) { CURL *curl = curl_easy_init(); @@ -48,7 +85,17 @@ static void webhook_do_send(const char *url, const char *json_body) curl_easy_setopt(curl, CURLOPT_USERAGENT, "easy-irl-stream-webhook/1.0"); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); - curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); +#ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSLVERSION, + CURL_SSLVERSION_TLSv1_2 | CURL_SSLVERSION_MAX_TLSv1_2); + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, (long)CURLSSLOPT_NO_REVOKE); + curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, + (long)CURL_HTTP_VERSION_1_1); + curl_easy_setopt(curl, CURLOPT_SSL_ENABLE_ALPN, 0L); + curl_easy_setopt(curl, CURLOPT_IPRESOLVE, (long)CURL_IPRESOLVE_V4); +#endif + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, webhook_curl_debug_cb); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf); CURLcode res = curl_easy_perform(curl);