Initial commit

Made-with: Cursor
This commit is contained in:
Nils
2026-03-29 20:45:07 +02:00
commit 10be82cba5
30 changed files with 5085 additions and 0 deletions
+423
View File
@@ -0,0 +1,423 @@
#include <obs-module.h>
#include <obs-frontend-api.h>
#include <util/threading.h>
#include <string.h>
#include <stdio.h>
#include <curl/curl.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <ifaddrs.h>
#include <net/if.h>
#define closesocket close
#define SOCKET int
#define INVALID_SOCKET (-1)
#endif
#include "irl-source.h"
#include "obfuscation.h"
#include "translations.h"
OBS_DECLARE_MODULE()
/* ---- IP detection (global) ---- */
char g_local_ip[64] = "";
char g_external_ip[64] = "";
static void detect_local_ip(void)
{
#ifdef _WIN32
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
ULONG buf_size = 15000;
PIP_ADAPTER_ADDRESSES addrs = (PIP_ADAPTER_ADDRESSES)malloc(buf_size);
if (!addrs) {
snprintf(g_local_ip, sizeof(g_local_ip), "?.?.?.?");
return;
}
ULONG flags = GAA_FLAG_INCLUDE_GATEWAYS;
ULONG ret = GetAdaptersAddresses(AF_INET, flags, NULL, addrs,
&buf_size);
if (ret == ERROR_BUFFER_OVERFLOW) {
free(addrs);
addrs = (PIP_ADAPTER_ADDRESSES)malloc(buf_size);
if (!addrs) {
snprintf(g_local_ip, sizeof(g_local_ip), "?.?.?.?");
return;
}
ret = GetAdaptersAddresses(AF_INET, flags, NULL, addrs,
&buf_size);
}
if (ret != NO_ERROR) {
free(addrs);
snprintf(g_local_ip, sizeof(g_local_ip), "?.?.?.?");
return;
}
for (PIP_ADAPTER_ADDRESSES a = addrs; a; a = a->Next) {
if (a->OperStatus != IfOperStatusUp)
continue;
if (!a->FirstGatewayAddress)
continue;
if (a->IfType != IF_TYPE_ETHERNET_CSMACD &&
a->IfType != IF_TYPE_IEEE80211)
continue;
PIP_ADAPTER_UNICAST_ADDRESS ua = a->FirstUnicastAddress;
for (; ua; ua = ua->Next) {
struct sockaddr_in *sa =
(struct sockaddr_in *)ua->Address.lpSockaddr;
if (sa->sin_family == AF_INET) {
inet_ntop(AF_INET, &sa->sin_addr, g_local_ip,
sizeof(g_local_ip));
free(addrs);
return;
}
}
}
free(addrs);
snprintf(g_local_ip, sizeof(g_local_ip), "?.?.?.?");
#else
struct ifaddrs *ifas, *ifa;
if (getifaddrs(&ifas) == -1) {
snprintf(g_local_ip, sizeof(g_local_ip), "?.?.?.?");
return;
}
for (ifa = ifas; ifa; ifa = ifa->ifa_next) {
if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET)
continue;
if (ifa->ifa_flags & IFF_LOOPBACK)
continue;
if (!(ifa->ifa_flags & IFF_UP))
continue;
struct sockaddr_in *sa =
(struct sockaddr_in *)ifa->ifa_addr;
inet_ntop(AF_INET, &sa->sin_addr, g_local_ip,
sizeof(g_local_ip));
break;
}
freeifaddrs(ifas);
if (!g_local_ip[0])
snprintf(g_local_ip, sizeof(g_local_ip), "?.?.?.?");
#endif
}
static void http_get_body(const char *host, const char *path, char *out,
size_t out_sz)
{
struct addrinfo hints = {0}, *res = NULL;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo(host, "80", &hints, &res) != 0) {
snprintf(out, out_sz, "?");
return;
}
SOCKET s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (s == INVALID_SOCKET) {
freeaddrinfo(res);
snprintf(out, out_sz, "?");
return;
}
if (connect(s, res->ai_addr, (int)res->ai_addrlen) != 0) {
freeaddrinfo(res);
closesocket(s);
snprintf(out, out_sz, "?");
return;
}
freeaddrinfo(res);
char req[512];
snprintf(req, sizeof(req),
"GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n",
path, host);
send(s, req, (int)strlen(req), 0);
char response[2048] = {0};
int total = 0, n;
while ((n = recv(s, response + total,
(int)(sizeof(response) - total - 1), 0)) > 0)
total += n;
response[total] = '\0';
closesocket(s);
char *body = strstr(response, "\r\n\r\n");
if (body) {
body += 4;
while (*body == ' ' || *body == '\r' || *body == '\n')
body++;
char *end = body;
while (*end && *end != '\r' && *end != '\n' && *end != ' ')
end++;
*end = '\0';
snprintf(out, out_sz, "%s", body);
} else {
snprintf(out, out_sz, "?");
}
}
/* ---- DuckDNS update ---- */
void duckdns_update(const char *domain, const char *token)
{
if (!domain || !domain[0] || !token || !token[0])
return;
char path[512];
snprintf(path, sizeof(path), obf_duckdns_update_fmt(), domain, token);
char result[256] = {0};
http_get_body(obf_duckdns_host(), path, result, sizeof(result));
if (strncmp(result, "OK", 2) == 0 || strncmp(result, "KO", 2) == 0) {
blog(LOG_DEBUG, "[%s] DuckDNS update for %s.duckdns.org: %s",
PLUGIN_NAME, domain, result);
} else {
blog(LOG_WARNING, "[%s] DuckDNS update failed: %s",
PLUGIN_NAME, result);
}
}
/* ---- IP monitoring ---- */
static volatile bool g_ip_thread_active = false;
static pthread_t g_ip_thread;
static void trigger_duckdns_update(void)
{
for (int i = 0; i < g_irl_source_count && i < MAX_IRL_SOURCES; i++) {
struct irl_source_data *d = g_irl_sources[i];
if (!d) continue;
pthread_mutex_lock(&d->mutex);
bool has_dns = d->duckdns_domain && d->duckdns_domain[0] &&
d->duckdns_token && d->duckdns_token[0];
char *dd = has_dns ? bstrdup(d->duckdns_domain) : NULL;
char *dt = has_dns ? bstrdup(d->duckdns_token) : NULL;
pthread_mutex_unlock(&d->mutex);
if (dd && dt) {
duckdns_update(dd, dt);
bfree(dd);
bfree(dt);
break;
}
bfree(dd);
bfree(dt);
}
}
#define IP_CHECK_INTERVAL_SEC 60
static void *ip_detect_thread(void *arg)
{
UNUSED_PARAMETER(arg);
detect_local_ip();
http_get_body(obf_ipify_host(), "/", g_external_ip,
sizeof(g_external_ip));
blog(LOG_DEBUG, "[%s] Local IP: %s, External IP: %s", PLUGIN_NAME,
g_local_ip, g_external_ip);
while (g_ip_thread_active) {
for (int i = 0; i < IP_CHECK_INTERVAL_SEC * 10 &&
g_ip_thread_active; i++)
os_sleep_ms(100);
if (!g_ip_thread_active)
break;
char new_ip[64] = {0};
http_get_body(obf_ipify_host(), "/", new_ip, sizeof(new_ip));
if (new_ip[0] && strcmp(new_ip, "?") != 0 &&
strcmp(new_ip, g_external_ip) != 0) {
blog(LOG_DEBUG,
"[%s] External IP changed: %s -> %s",
PLUGIN_NAME, g_external_ip, new_ip);
snprintf(g_external_ip, sizeof(g_external_ip),
"%s", new_ip);
trigger_duckdns_update();
}
}
return NULL;
}
/* ---- Update check ---- */
struct update_mem_buf {
char *data;
size_t size;
};
static size_t update_write_cb(void *contents, size_t size, size_t nmemb,
void *userp)
{
size_t total = size * nmemb;
struct update_mem_buf *buf = (struct update_mem_buf *)userp;
char *tmp = realloc(buf->data, buf->size + total + 1);
if (!tmp) return 0;
buf->data = tmp;
memcpy(buf->data + buf->size, contents, total);
buf->size += total;
buf->data[buf->size] = '\0';
return total;
}
static int compare_versions(const char *a, const char *b)
{
int a1 = 0, a2 = 0, a3 = 0, b1 = 0, b2 = 0, b3 = 0;
sscanf(a, "%d.%d.%d", &a1, &a2, &a3);
sscanf(b, "%d.%d.%d", &b1, &b2, &b3);
if (a1 != b1) return a1 - b1;
if (a2 != b2) return a2 - b2;
return a3 - b3;
}
struct update_ctx {
char version[64];
};
#include "help-dialog.hpp"
#include "stats-dialog.hpp"
static void task_show_update_dialog(void *param)
{
struct update_ctx *ctx = param;
update_dialog_show(ctx->version, obs_get_locale());
free(ctx);
}
static void *update_check_thread(void *arg)
{
UNUSED_PARAMETER(arg);
os_sleep_ms(5000);
char url[256];
snprintf(url, sizeof(url), "%s%s%s",
obf_https_prefix(), obf_stools_host(),
obf_api_version_path());
char ua[128];
snprintf(ua, sizeof(ua), "%s%s", obf_ua_prefix(), PLUGIN_VERSION);
CURL *curl = curl_easy_init();
if (!curl) return NULL;
struct update_mem_buf buf = {NULL, 0};
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, update_write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, ua);
CURLcode res = curl_easy_perform(curl);
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
curl_easy_cleanup(curl);
if (res != CURLE_OK || http_code != 200 || !buf.data) {
free(buf.data);
return NULL;
}
/* Parse {"version":"x.y.z"} */
const char *vkey = strstr(buf.data, "\"version\"");
if (!vkey) { free(buf.data); return NULL; }
const char *vstart = strchr(vkey + 9, '"');
if (!vstart) { free(buf.data); return NULL; }
vstart++;
const char *vend = strchr(vstart, '"');
if (!vend || vend - vstart > 60) { free(buf.data); return NULL; }
char remote_ver[64];
size_t vlen = (size_t)(vend - vstart);
memcpy(remote_ver, vstart, vlen);
remote_ver[vlen] = '\0';
free(buf.data);
if (compare_versions(remote_ver, PLUGIN_VERSION) > 0) {
blog(LOG_DEBUG, "[%s] New version available: %s (current: %s)",
PLUGIN_NAME, remote_ver, PLUGIN_VERSION);
struct update_ctx *ctx = malloc(sizeof(*ctx));
if (ctx) {
snprintf(ctx->version, sizeof(ctx->version), "%s",
remote_ver);
obs_queue_task(OBS_TASK_UI, task_show_update_dialog,
ctx, false);
}
}
return NULL;
}
/* ---- Tools menu ---- */
static void tools_menu_cb(void *private_data)
{
UNUSED_PARAMETER(private_data);
help_dialog_show(g_local_ip, g_external_ip, PLUGIN_VERSION,
obs_get_locale());
}
static void tools_stats_cb(void *private_data)
{
UNUSED_PARAMETER(private_data);
stats_dialog_show(obs_get_locale());
}
/* ---- module lifecycle ---- */
bool obs_module_load(void)
{
curl_global_init(CURL_GLOBAL_DEFAULT);
obs_register_source(&irl_source_info);
g_ip_thread_active = true;
if (pthread_create(&g_ip_thread, NULL, ip_detect_thread, NULL) != 0)
g_ip_thread_active = false;
pthread_t update_thread;
if (pthread_create(&update_thread, NULL, update_check_thread, NULL) == 0)
pthread_detach(update_thread);
blog(LOG_INFO, "[%s] Plugin loaded (v%s)", PLUGIN_NAME, PLUGIN_VERSION);
return true;
}
void obs_module_post_load(void)
{
obs_frontend_add_tools_menu_item(tr_tools_menu_help(),
tools_menu_cb, NULL);
obs_frontend_add_tools_menu_item(tr_tools_menu_stats(),
tools_stats_cb, NULL);
blog(LOG_DEBUG, "[%s] Tools menu registered", PLUGIN_NAME);
}
void obs_module_unload(void)
{
if (g_ip_thread_active) {
g_ip_thread_active = false;
pthread_join(g_ip_thread, NULL);
}
curl_global_cleanup();
blog(LOG_INFO, "[%s] Plugin unloaded", PLUGIN_NAME);
}