Add OAuth login, proper API integration, ZIP extraction, UAC elevation, update notifications, CI/CD

This commit is contained in:
Nils
2026-05-04 20:26:01 +02:00
parent 3673cc41a8
commit 3ae72e447f
10 changed files with 1148 additions and 145 deletions
+254
View File
@@ -0,0 +1,254 @@
name: Build & Release
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g. 1.0.0)'
required: true
env:
OBS_VERSION: '32.1.0'
OBS_DEPS_VERSION: '2025-08-23'
PLUGIN_NAME: 'st-pluginmanager'
jobs:
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- name: Setup MSVC
uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64
- name: Determine version
id: version
shell: bash
run: |
if [ "${{ github.event_name }}" == "push" ]; then
echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
else
echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
fi
- name: Clone OBS headers
shell: bash
run: |
git clone --depth 1 --branch ${{ env.OBS_VERSION }} \
--filter=blob:none --sparse \
https://github.com/obsproject/obs-studio.git deps/obs-studio
cd deps/obs-studio
git sparse-checkout set libobs frontend/api deps/w32-pthreads
- name: Create obsconfig.h
shell: powershell
run: |
@"
#pragma once
#define OBS_DATA_PATH "data"
#define OBS_PLUGIN_PATH "obs-plugins/64bit"
#define OBS_PLUGIN_DESTINATION "obs-plugins/64bit"
#define OBS_INSTALL_PREFIX "C:/Program Files/obs-studio"
#define OBS_RELEASE_CANDIDATE 0
#define OBS_BETA 0
"@ | Out-File -Encoding ASCII "deps/obs-studio/libobs/obsconfig.h"
- name: Download OBS deps
shell: powershell
run: |
$url = "https://github.com/obsproject/obs-deps/releases/download/${{ env.OBS_DEPS_VERSION }}/windows-deps-${{ env.OBS_DEPS_VERSION }}-x64.zip"
Invoke-WebRequest -Uri $url -OutFile deps/obs-deps.zip -UseBasicParsing
Expand-Archive -Path deps/obs-deps.zip -DestinationPath deps/obs-deps -Force
- name: Download Qt6 deps
shell: powershell
run: |
$url = "https://github.com/obsproject/obs-deps/releases/download/${{ env.OBS_DEPS_VERSION }}/windows-deps-qt6-${{ env.OBS_DEPS_VERSION }}-x64.zip"
Invoke-WebRequest -Uri $url -OutFile deps/qt6.zip -UseBasicParsing
Expand-Archive -Path deps/qt6.zip -DestinationPath deps/qt6 -Force
- name: Download OBS binaries
shell: powershell
run: |
$url = "https://github.com/obsproject/obs-studio/releases/download/${{ env.OBS_VERSION }}/OBS-Studio-${{ env.OBS_VERSION }}-Windows-x64.zip"
Invoke-WebRequest -Uri $url -OutFile deps/obs-bin.zip -UseBasicParsing
Expand-Archive -Path deps/obs-bin.zip -DestinationPath deps/obs-bin -Force
- name: Generate import libraries
shell: powershell
run: |
$outDir = "deps/obs-libs"
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
foreach ($dllName in @("obs", "obs-frontend-api", "w32-pthreads")) {
$dll = Get-ChildItem -Path "deps/obs-bin" -Filter "$dllName.dll" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $dll) { throw "Could not find $dllName.dll in deps/obs-bin" }
Write-Host "Found: $($dll.FullName)"
$raw = (& dumpbin /exports $dll.FullName 2>&1) | Out-String
$lines = $raw -split "`r?`n"
$defLines = @("LIBRARY ""$dllName""", "EXPORTS")
$capture = $false
foreach ($line in $lines) {
if ($line -match "ordinal\s+hint\s+RVA\s+name") { $capture = $true; continue }
if ($capture -and $line -match "^\s*Summary") { break }
if ($capture -and $line -match "^\s+(\d+)\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]+\s+(\S+)") {
$defLines += (" " + $Matches[2])
}
}
$exportCount = $defLines.Count - 2
Write-Host " $dllName exports: $exportCount"
if ($exportCount -lt 1) { throw "No exports found in $dllName.dll" }
$defLines -join "`n" | Out-File -Encoding ASCII "$outDir/$dllName.def" -NoNewline
& lib /nologo /def:"$outDir/$dllName.def" /out:"$outDir/$dllName.lib" /machine:x64
if (-not (Test-Path "$outDir/$dllName.lib")) { throw "Failed to create $dllName.lib" }
}
- name: Build plugin
shell: powershell
run: |
cmake -S . -B build -G Ninja `
-DCMAKE_BUILD_TYPE=RelWithDebInfo `
-DCMAKE_C_COMPILER=cl `
-DCMAKE_CXX_COMPILER=cl `
-DOBS_SOURCE_DIR="deps/obs-studio" `
-DOBS_LIB_DIR="deps/obs-libs" `
-DQT6_DIR="deps/qt6" `
-DCURL_DIR="deps/obs-deps" `
-DPLUGIN_VERSION_OVERRIDE="${{ steps.version.outputs.version }}"
cmake --build build --config RelWithDebInfo
- name: Package
shell: powershell
run: |
$ver = "${{ steps.version.outputs.version }}"
New-Item -ItemType Directory -Force "release" | Out-Null
$dir = "staging/st-pluginmanager"
New-Item -ItemType Directory -Force "$dir/obs-plugins/64bit" | Out-Null
Copy-Item "build/st-pluginmanager.dll" "$dir/obs-plugins/64bit/"
Compress-Archive -Path "$dir/*" -DestinationPath "release/st-pluginmanager-$ver-windows-x64.zip"
- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: windows-release
path: release/*
build-linux:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Determine version
id: version
shell: bash
run: |
if [ "${{ github.event_name }}" == "push" ]; then
echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
else
echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
fi
- name: Install dependencies
run: |
sudo add-apt-repository ppa:obsproject/obs-studio -y
sudo apt-get update
sudo apt-get install -y \
libobs-dev \
qt6-base-dev \
libcurl4-openssl-dev libxkbcommon-dev \
cmake ninja-build pkg-config
- name: Setup OBS frontend headers
id: frontend
run: |
if pkg-config --cflags obs-frontend-api 2>/dev/null; then
echo "Found obs-frontend-api via pkg-config"
echo "cmake_arg=" >> $GITHUB_OUTPUT
elif [ -f /usr/include/obs/obs-frontend-api.h ]; then
echo "Found obs-frontend-api.h in system includes"
echo "cmake_arg=" >> $GITHUB_OUTPUT
else
SYS_VER=$(pkg-config --modversion libobs | cut -d. -f1-3)
echo "Cloning OBS $SYS_VER frontend headers..."
git clone --depth 1 --branch "$SYS_VER" \
--filter=blob:none --sparse \
https://github.com/obsproject/obs-studio.git deps/obs-studio
cd deps/obs-studio
git sparse-checkout set frontend/api
echo "cmake_arg=-DOBS_FRONTEND_INCLUDE_DIR=deps/obs-studio/frontend/api" >> $GITHUB_OUTPUT
fi
- name: Build plugin
run: |
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DPLUGIN_VERSION_OVERRIDE="${{ steps.version.outputs.version }}" \
${{ steps.frontend.outputs.cmake_arg }}
cmake --build build
- name: Package
run: |
ver="${{ steps.version.outputs.version }}"
mkdir -p release
mkdir -p staging/obs-plugins
cp build/st-pluginmanager.so staging/obs-plugins/
cd staging
tar czf ../release/st-pluginmanager-$ver-linux-x86_64.tar.gz *
- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: linux-release
path: release/*
release:
needs: [build-windows, build-linux]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- name: Determine version
id: version
shell: bash
run: |
if [ "${{ github.event_name }}" == "push" ]; then
echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
echo "tag=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
else
echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
echo "tag=v${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
fi
- name: Download all artifacts
uses: actions/download-artifact@v7
with:
path: artifacts
- name: Create release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.tag }}
name: stools Plugin Manager v${{ steps.version.outputs.version }}
draft: false
prerelease: false
generate_release_notes: true
body: |
## Installation
### Windows
1. Download **`st-pluginmanager-${{ steps.version.outputs.version }}-windows-x64.zip`**
2. Extract into your OBS Studio folder (e.g. `C:\Program Files\obs-studio\`)
3. Restart OBS
### Linux
1. Download **`st-pluginmanager-${{ steps.version.outputs.version }}-linux-x86_64.tar.gz`**
2. Extract to `/usr/lib/obs-plugins/`
3. Restart OBS
files: |
artifacts/windows-release/*
artifacts/linux-release/*
+2
View File
@@ -16,6 +16,7 @@ add_library(st-pluginmanager MODULE
src/plugin-main.c src/plugin-main.c
src/auth.c src/auth.c
src/downloader.c src/downloader.c
src/oauth.c
src/obfuscation.cpp src/obfuscation.cpp
src/manager-dialog.cpp src/manager-dialog.cpp
) )
@@ -71,6 +72,7 @@ if(WIN32)
obs_frontend obs_frontend
w32_pthreads w32_pthreads
shell32 shell32
ws2_32
Qt6::Core Qt6::Core
Qt6::Gui Qt6::Gui
Qt6::Widgets Qt6::Widgets
+541 -62
View File
@@ -6,10 +6,12 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <stdarg.h>
#include <curl/curl.h> #include <curl/curl.h>
#ifdef _WIN32 #ifdef _WIN32
#include <windows.h> #include <windows.h>
#include <shellapi.h>
#include <shlobj.h> #include <shlobj.h>
#include <direct.h> #include <direct.h>
#define PATH_SEP '\\' #define PATH_SEP '\\'
@@ -19,6 +21,23 @@
#define PATH_SEP '/' #define PATH_SEP '/'
#endif #endif
/* ---- Error reporting ---- */
static char s_last_error[512] = "";
static void set_error(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vsnprintf(s_last_error, sizeof(s_last_error), fmt, ap);
va_end(ap);
}
const char *downloader_last_error(void)
{
return s_last_error;
}
/* ---- cURL helpers ---- */ /* ---- cURL helpers ---- */
struct mem_buf { struct mem_buf {
@@ -70,9 +89,12 @@ static char *api_get(const char *path, const char *token)
obf_https_prefix(), obf_stools_host(), path); obf_https_prefix(), obf_stools_host(), path);
struct curl_slist *headers = NULL; struct curl_slist *headers = NULL;
char auth_header[512]; if (token && token[0]) {
snprintf(auth_header, sizeof(auth_header), obf_auth_bearer_fmt(), token); char auth_header[512];
headers = curl_slist_append(headers, auth_header); snprintf(auth_header, sizeof(auth_header),
obf_auth_bearer_fmt(), token);
headers = curl_slist_append(headers, auth_header);
}
char ua[128]; char ua[128];
snprintf(ua, sizeof(ua), "%s%s", obf_ua_prefix(), PLUGIN_VERSION); snprintf(ua, sizeof(ua), "%s%s", obf_ua_prefix(), PLUGIN_VERSION);
@@ -80,7 +102,8 @@ static char *api_get(const char *path, const char *token)
struct mem_buf buf = {NULL, 0}; struct mem_buf buf = {NULL, 0};
curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); if (headers)
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
@@ -90,7 +113,8 @@ static char *api_get(const char *path, const char *token)
CURLcode res = curl_easy_perform(curl); CURLcode res = curl_easy_perform(curl);
long http_code = 0; long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
curl_slist_free_all(headers); if (headers)
curl_slist_free_all(headers);
curl_easy_cleanup(curl); curl_easy_cleanup(curl);
if (res != CURLE_OK || http_code != 200) { if (res != CURLE_OK || http_code != 200) {
@@ -133,15 +157,51 @@ static void json_extract_string(const char *json, const char *key,
out[len] = '\0'; out[len] = '\0';
} }
static int json_extract_int(const char *json, const char *key)
{
const char *v = json_find_key(json, key);
if (!v) return -1;
return atoi(v);
}
/* ---- OBS plugin directory ---- */ /* ---- OBS plugin directory ---- */
static void ensure_dir_exists(const char *path)
{
#ifdef _WIN32
char tmp[512];
snprintf(tmp, sizeof(tmp), "%s", path);
for (char *p = tmp + 3; *p; p++) {
if (*p == '\\' || *p == '/') {
*p = '\0';
CreateDirectoryA(tmp, NULL);
*p = '\\';
}
}
CreateDirectoryA(tmp, NULL);
#else
char cmd[1024];
snprintf(cmd, sizeof(cmd), "mkdir -p '%s'", path);
system(cmd);
#endif
}
bool downloader_get_obs_plugin_dir(char *buf, size_t sz) bool downloader_get_obs_plugin_dir(char *buf, size_t sz)
{ {
#ifdef _WIN32 #ifdef _WIN32
char appdata[MAX_PATH]; char exe_path[MAX_PATH];
SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, appdata); GetModuleFileNameA(NULL, exe_path, MAX_PATH);
snprintf(buf, sz, "%s\\obs-studio\\obs-plugins\\64bit", appdata);
CreateDirectoryA(buf, NULL); /* Go up from 64bit to obs-studio root */
char *last_sep = strrchr(exe_path, '\\');
if (last_sep) *last_sep = '\0'; /* remove obs64.exe */
last_sep = strrchr(exe_path, '\\');
if (last_sep) *last_sep = '\0'; /* remove 64bit */
last_sep = strrchr(exe_path, '\\');
if (last_sep) *last_sep = '\0'; /* remove bin */
snprintf(buf, sz, "%s\\obs-plugins\\64bit", exe_path);
ensure_dir_exists(buf);
return true; return true;
#elif defined(__APPLE__) #elif defined(__APPLE__)
const char *home = getenv("HOME"); const char *home = getenv("HOME");
@@ -159,50 +219,145 @@ bool downloader_get_obs_plugin_dir(char *buf, size_t sz)
#endif #endif
} }
/* ---- Parse plugin list from API ---- */ /* ---- Parse plugin list from /api/products ---- */
static const char *platform_suffix(void)
{
#ifdef _WIN32
return "windows";
#elif defined(__APPLE__)
return "macos";
#else
return "linux";
#endif
}
bool downloader_fetch_plugin_list(const char *token, struct plugin_list *out) bool downloader_fetch_plugin_list(const char *token, struct plugin_list *out)
{ {
memset(out, 0, sizeof(*out)); memset(out, 0, sizeof(*out));
char *json = api_get(obf_api_plugins_path(), token); /* /api/products is public, but send token if available */
char *json = api_get(obf_api_products_path(), token);
if (!json) return false; if (!json) return false;
/* /*
* Expected JSON format: * Response: array of product objects
* [{"slug":"easy-irl-stream","name":"Easy IRL Stream", * [{"id":"easy-irl-stream","name":"Easy IRL Stream",
* "version":"1.1.4","platform":{"windows":".dll","linux":".so"}}, ...] * "type":"download","accessLevel":"free",...}, ...]
*
* We only show products of type "download"
*/ */
const char *pos = json; const char *pos = json;
while (out->count < MAX_PLUGINS) { while (out->count < MAX_PLUGINS) {
const char *obj_start = strchr(pos, '{'); const char *obj_start = strchr(pos, '{');
if (!obj_start) break; if (!obj_start) break;
const char *obj_end = strchr(obj_start, '}'); /* Find matching closing brace (simplified: first } works for
* flat objects; nested JSON could break this but products are flat) */
int depth = 0;
const char *p = obj_start;
const char *obj_end = NULL;
while (*p) {
if (*p == '{') depth++;
else if (*p == '}') {
depth--;
if (depth == 0) { obj_end = p; break; }
}
p++;
}
if (!obj_end) break; if (!obj_end) break;
size_t obj_len = (size_t)(obj_end - obj_start + 1); size_t obj_len = (size_t)(obj_end - obj_start + 1);
char obj_buf[2048]; char *obj_buf = (char *)malloc(obj_len + 1);
if (obj_len >= sizeof(obj_buf)) { if (!obj_buf) break;
pos = obj_end + 1;
continue;
}
memcpy(obj_buf, obj_start, obj_len); memcpy(obj_buf, obj_start, obj_len);
obj_buf[obj_len] = '\0'; obj_buf[obj_len] = '\0';
struct plugin_info *pi = &out->items[out->count]; char type[64] = "";
json_extract_string(obj_buf, "slug", pi->slug, sizeof(pi->slug)); json_extract_string(obj_buf, "type", type, sizeof(type));
json_extract_string(obj_buf, "name", pi->name, sizeof(pi->name));
json_extract_string(obj_buf, "version", pi->latest_version,
sizeof(pi->latest_version));
if (pi->slug[0] && pi->name[0]) /* Only include downloadable plugins */
out->count++; if (strcmp(type, "download") == 0) {
struct plugin_info *pi = &out->items[out->count];
json_extract_string(obj_buf, "id", pi->slug,
sizeof(pi->slug));
json_extract_string(obj_buf, "name", pi->name,
sizeof(pi->name));
if (pi->slug[0] && pi->name[0])
out->count++;
}
free(obj_buf);
pos = obj_end + 1; pos = obj_end + 1;
} }
free(json); free(json);
/* Now fetch latest version for each plugin from /api/releases/{slug} */
for (int i = 0; i < out->count; i++) {
struct plugin_info *pi = &out->items[i];
char path[256];
snprintf(path, sizeof(path), obf_api_releases_fmt(), pi->slug);
char *rel_json = api_get(path, token);
if (!rel_json) continue;
/*
* Response: {"product":{...},"releases":[{...},...]}}
* First release is latest. Has "version":"vX.Y.Z" and
* "assets":[{"id":123,"filename":"...","platform":"windows",...}]
*/
const char *releases_key = strstr(rel_json, "\"releases\"");
if (!releases_key) { free(rel_json); continue; }
/* Find first release object */
const char *first_rel = strchr(releases_key, '{');
if (first_rel) {
char version[64] = "";
json_extract_string(first_rel, "version", version,
sizeof(version));
/* Strip leading 'v' if present */
const char *ver = version;
if (ver[0] == 'v' || ver[0] == 'V') ver++;
snprintf(pi->latest_version,
sizeof(pi->latest_version), "%s", ver);
/* Find matching asset for our platform */
const char *assets_key = strstr(first_rel, "\"assets\"");
if (assets_key) {
const char *asset_pos = assets_key;
const char *plat = platform_suffix();
while ((asset_pos = strchr(asset_pos, '{')) != NULL) {
char asset_plat[64] = "";
char asset_fn[256] = "";
json_extract_string(asset_pos,
"platform",
asset_plat,
sizeof(asset_plat));
json_extract_string(asset_pos,
"filename",
asset_fn,
sizeof(asset_fn));
int asset_id = json_extract_int(
asset_pos, "id");
if (strcmp(asset_plat, plat) == 0 &&
asset_id > 0) {
pi->download_asset_id = asset_id;
break;
}
asset_pos++;
}
}
}
free(rel_json);
}
dbg_log(LOG_INFO, "[%s] Fetched %d plugins", PLUGIN_NAME, out->count); dbg_log(LOG_INFO, "[%s] Fetched %d plugins", PLUGIN_NAME, out->count);
return out->count > 0; return out->count > 0;
} }
@@ -223,7 +378,7 @@ static bool read_version_file(const char *dir, const char *slug,
char *ver, size_t ver_sz) char *ver, size_t ver_sz)
{ {
char path[512]; char path[512];
snprintf(path, sizeof(path), "%s%c%s.version", dir, PATH_SEP, slug); snprintf(path, sizeof(path), "%s%c.%s.version", dir, PATH_SEP, slug);
FILE *f = fopen(path, "r"); FILE *f = fopen(path, "r");
if (!f) return false; if (!f) return false;
if (!fgets(ver, (int)ver_sz, f)) { if (!fgets(ver, (int)ver_sz, f)) {
@@ -257,9 +412,6 @@ void downloader_detect_installed(struct plugin_list *list,
#ifdef _WIN32 #ifdef _WIN32
snprintf(dll_path, sizeof(dll_path), "%s\\%s.dll", snprintf(dll_path, sizeof(dll_path), "%s\\%s.dll",
obs_plugin_dir, pi->slug); obs_plugin_dir, pi->slug);
#elif defined(__APPLE__)
snprintf(dll_path, sizeof(dll_path), "%s/%s.so",
obs_plugin_dir, pi->slug);
#else #else
snprintf(dll_path, sizeof(dll_path), "%s/%s.so", snprintf(dll_path, sizeof(dll_path), "%s/%s.so",
obs_plugin_dir, pi->slug); obs_plugin_dir, pi->slug);
@@ -285,21 +437,242 @@ void downloader_detect_installed(struct plugin_list *list,
/* ---- Download and install a plugin ---- */ /* ---- Download and install a plugin ---- */
bool downloader_install_plugin(const char *token, const char *slug, /* ---- Archive extraction via system tools ---- */
const char *obs_plugin_dir)
static bool is_zip(const char *path)
{
FILE *f = fopen(path, "rb");
if (!f) return false;
unsigned char sig[4] = {0};
fread(sig, 1, 4, f);
fclose(f);
return sig[0] == 'P' && sig[1] == 'K' &&
sig[2] == 0x03 && sig[3] == 0x04;
}
static bool is_targz(const char *path)
{
FILE *f = fopen(path, "rb");
if (!f) return false;
unsigned char sig[2] = {0};
fread(sig, 1, 2, f);
fclose(f);
return sig[0] == 0x1f && sig[1] == 0x8b;
}
static bool get_temp_dir(char *buf, size_t sz)
{ {
const char *platform =
#ifdef _WIN32 #ifdef _WIN32
"windows"; GetTempPathA((DWORD)sz, buf);
#elif defined(__APPLE__) return true;
"macos";
#else #else
"linux"; snprintf(buf, sz, "/tmp");
return true;
#endif
}
/*
* Extract archive to a temp directory, then find the plugin binary.
*
* Release ZIP structure (Windows):
* easy-irl-stream/obs-plugins/64bit/easy-irl-stream.dll
*
* Release tar.gz structure (Linux):
* obs-plugins/easy-irl-stream.so
*/
static bool extract_archive(const char *archive_path, const char *extract_dir)
{
#ifdef _WIN32
char cmd[2048];
snprintf(cmd, sizeof(cmd),
"powershell -NoProfile -Command \"Expand-Archive -Force "
"-Path '%s' -DestinationPath '%s'\"",
archive_path, extract_dir);
int ret = system(cmd);
return ret == 0;
#else
char cmd[2048];
if (is_targz(archive_path)) {
snprintf(cmd, sizeof(cmd),
"mkdir -p '%s' && tar xzf '%s' -C '%s'",
extract_dir, archive_path, extract_dir);
} else {
snprintf(cmd, sizeof(cmd),
"mkdir -p '%s' && unzip -o '%s' -d '%s'",
extract_dir, archive_path, extract_dir);
}
int ret = system(cmd);
return ret == 0;
#endif
}
/*
* Recursively search for the plugin binary (slug.dll/.so) in extract_dir.
* This handles any nested folder structure.
*/
static bool find_plugin_binary(const char *dir, const char *filename,
char *result, size_t result_sz)
{
#ifdef _WIN32
char search[512];
snprintf(search, sizeof(search), "%s\\*", dir);
WIN32_FIND_DATAA fd;
HANDLE hFind = FindFirstFileA(search, &fd);
if (hFind == INVALID_HANDLE_VALUE) return false;
do {
if (fd.cFileName[0] == '.') continue;
char full[512];
snprintf(full, sizeof(full), "%s\\%s", dir, fd.cFileName);
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
if (find_plugin_binary(full, filename, result, result_sz))
{
FindClose(hFind);
return true;
}
} else if (_stricmp(fd.cFileName, filename) == 0) {
snprintf(result, result_sz, "%s", full);
FindClose(hFind);
return true;
}
} while (FindNextFileA(hFind, &fd));
FindClose(hFind);
return false;
#else
char cmd[1024];
snprintf(cmd, sizeof(cmd),
"find '%s' -name '%s' -type f 2>/dev/null | head -1",
dir, filename);
FILE *p = popen(cmd, "r");
if (!p) return false;
if (fgets(result, (int)result_sz, p)) {
size_t len = strlen(result);
while (len > 0 && (result[len-1] == '\n' || result[len-1] == '\r'))
result[--len] = '\0';
pclose(p);
return len > 0;
}
pclose(p);
return false;
#endif
}
static void remove_directory(const char *dir)
{
#ifdef _WIN32
char cmd[1024];
snprintf(cmd, sizeof(cmd), "rmdir /s /q \"%s\"", dir);
system(cmd);
#else
char cmd[1024];
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", dir);
system(cmd);
#endif
}
/*
* Copy a file with UAC elevation on Windows.
* Launches a hidden PowerShell process as Administrator.
*/
#ifdef _WIN32
static bool copy_elevated(const char *src, const char *dst)
{
char ps_args[2048];
snprintf(ps_args, sizeof(ps_args),
"-NoProfile -WindowStyle Hidden -Command \""
"Copy-Item -Force -Path '%s' -Destination '%s'\"",
src, dst);
SHELLEXECUTEINFOA sei;
memset(&sei, 0, sizeof(sei));
sei.cbSize = sizeof(sei);
sei.fMask = SEE_MASK_NOCLOSEPROCESS;
sei.lpVerb = "runas";
sei.lpFile = "powershell.exe";
sei.lpParameters = ps_args;
sei.nShow = SW_HIDE;
if (!ShellExecuteExA(&sei)) {
dbg_log(LOG_ERROR, "[%s] UAC elevation denied or failed",
PLUGIN_NAME);
return false;
}
HANDLE proc = sei.hProcess;
WaitForSingleObject(proc, 30000);
DWORD exit_code = 1;
GetExitCodeProcess(proc, &exit_code);
CloseHandle(proc);
return exit_code == 0;
}
#endif #endif
static void hide_file(const char *path)
{
#ifdef _WIN32
SetFileAttributesA(path, FILE_ATTRIBUTE_HIDDEN);
#else
(void)path;
#endif
}
bool downloader_write_version_file(const char *obs_plugin_dir,
const char *slug, const char *version)
{
char ver_path[512];
snprintf(ver_path, sizeof(ver_path), "%s%c.%s.version",
obs_plugin_dir, PATH_SEP, slug);
FILE *f = fopen(ver_path, "w");
if (f) {
fputs(version, f);
fclose(f);
hide_file(ver_path);
return true;
}
#ifdef _WIN32
/* Try elevated write */
char tmp_ver[512];
char tmp_d[512];
get_temp_dir(tmp_d, sizeof(tmp_d));
snprintf(tmp_ver, sizeof(tmp_ver), "%s%cst_pm_%s.version",
tmp_d, PATH_SEP, slug);
f = fopen(tmp_ver, "w");
if (!f) return false;
fputs(version, f);
fclose(f);
bool ok = copy_elevated(tmp_ver, ver_path);
remove(tmp_ver);
return ok;
#else
return false;
#endif
}
/* ---- Download and install a plugin ---- */
bool downloader_install_plugin(const char *token, const char *slug,
int asset_id, const char *obs_plugin_dir)
{
s_last_error[0] = '\0';
if (asset_id <= 0) {
set_error("No download asset found for %s", slug);
dbg_log(LOG_ERROR, "[%s] No asset ID for %s", PLUGIN_NAME, slug);
return false;
}
char path[512]; char path[512];
snprintf(path, sizeof(path), obf_api_plugin_download_fmt(), snprintf(path, sizeof(path), obf_api_downloads_fmt(), slug, asset_id);
slug, platform);
char url[512]; char url[512];
snprintf(url, sizeof(url), "%s%s%s", snprintf(url, sizeof(url), "%s%s%s",
@@ -312,25 +685,25 @@ bool downloader_install_plugin(const char *token, const char *slug,
".so"; ".so";
#endif #endif
char tmp_path[512], final_path[512], ver_path[512]; /* Download to temp file */
snprintf(tmp_path, sizeof(tmp_path), "%s%c%s%s.tmp", char tmp_dir[512];
obs_plugin_dir, PATH_SEP, slug, ext); get_temp_dir(tmp_dir, sizeof(tmp_dir));
snprintf(final_path, sizeof(final_path), "%s%c%s%s",
obs_plugin_dir, PATH_SEP, slug, ext);
snprintf(ver_path, sizeof(ver_path), "%s%c%s.version",
obs_plugin_dir, PATH_SEP, slug);
FILE *f = fopen(tmp_path, "wb"); char archive_path[512];
snprintf(archive_path, sizeof(archive_path),
"%s%cst_pm_%s_download", tmp_dir, PATH_SEP, slug);
FILE *f = fopen(archive_path, "wb");
if (!f) { if (!f) {
dbg_log(LOG_ERROR, "[%s] Cannot open %s for writing", dbg_log(LOG_ERROR, "[%s] Cannot open %s for writing",
PLUGIN_NAME, tmp_path); PLUGIN_NAME, archive_path);
return false; return false;
} }
CURL *curl = curl_easy_init(); CURL *curl = curl_easy_init();
if (!curl) { if (!curl) {
fclose(f); fclose(f);
remove(tmp_path); remove(archive_path);
return false; return false;
} }
@@ -359,23 +732,129 @@ bool downloader_install_plugin(const char *token, const char *slug,
fclose(f); fclose(f);
if (res != CURLE_OK || http_code != 200) { if (res != CURLE_OK || http_code != 200) {
set_error("Download failed: %s (HTTP %ld)",
res != CURLE_OK ? curl_easy_strerror(res) : "server error",
http_code);
dbg_log(LOG_ERROR, "[%s] Download %s failed: %s (HTTP %ld)", dbg_log(LOG_ERROR, "[%s] Download %s failed: %s (HTTP %ld)",
PLUGIN_NAME, slug, curl_easy_strerror(res), http_code); PLUGIN_NAME, slug, curl_easy_strerror(res), http_code);
remove(tmp_path); remove(archive_path);
return false; return false;
} }
/* Atomic replace: delete old, rename tmp */ /* Check if it's an archive or a raw binary */
remove(final_path); bool success = false;
if (rename(tmp_path, final_path) != 0) { char final_path[512];
dbg_log(LOG_ERROR, "[%s] Failed to rename %s -> %s", snprintf(final_path, sizeof(final_path), "%s%c%s%s",
PLUGIN_NAME, tmp_path, final_path); obs_plugin_dir, PATH_SEP, slug, ext);
remove(tmp_path);
return false; if (is_zip(archive_path) || is_targz(archive_path)) {
/* Extract to temp, then find the binary */
char extract_dir[512];
snprintf(extract_dir, sizeof(extract_dir),
"%s%cst_pm_%s_extract", tmp_dir, PATH_SEP, slug);
remove_directory(extract_dir);
dbg_log(LOG_INFO, "[%s] Extracting archive for %s",
PLUGIN_NAME, slug);
if (!extract_archive(archive_path, extract_dir)) {
set_error("Failed to extract archive for %s", slug);
dbg_log(LOG_ERROR, "[%s] Failed to extract archive for %s",
PLUGIN_NAME, slug);
remove(archive_path);
remove_directory(extract_dir);
return false;
}
/* Find the DLL/SO inside the extracted tree */
char dll_filename[128];
snprintf(dll_filename, sizeof(dll_filename), "%s%s", slug, ext);
char found_path[512] = "";
if (!find_plugin_binary(extract_dir, dll_filename,
found_path, sizeof(found_path))) {
set_error("Could not find %s in archive", dll_filename);
dbg_log(LOG_ERROR,
"[%s] Could not find %s in extracted archive",
PLUGIN_NAME, dll_filename);
remove(archive_path);
remove_directory(extract_dir);
return false;
}
dbg_log(LOG_INFO, "[%s] Found binary at: %s",
PLUGIN_NAME, found_path);
/* Ensure target dir exists */
ensure_dir_exists(obs_plugin_dir);
/* Copy to OBS plugin dir (try normal, then elevated) */
remove(final_path);
#ifdef _WIN32
success = CopyFileA(found_path, final_path, FALSE);
if (!success) {
DWORD copy_err = GetLastError();
dbg_log(LOG_INFO,
"[%s] Normal copy failed (err %lu), trying elevated",
PLUGIN_NAME, copy_err);
success = copy_elevated(found_path, final_path);
}
#else
{
char cp_cmd[1024];
snprintf(cp_cmd, sizeof(cp_cmd),
"cp '%s' '%s'", found_path, final_path);
success = system(cp_cmd) == 0;
if (!success) {
snprintf(cp_cmd, sizeof(cp_cmd),
"pkexec cp '%s' '%s'",
found_path, final_path);
success = system(cp_cmd) == 0;
}
}
#endif
remove_directory(extract_dir);
} else {
/* Raw binary - just move to final location */
remove(final_path);
#ifdef _WIN32
success = CopyFileA(archive_path, final_path, FALSE);
if (!success) {
dbg_log(LOG_INFO,
"[%s] Normal copy failed, trying elevated",
PLUGIN_NAME);
success = copy_elevated(archive_path, final_path);
}
#else
success = rename(archive_path, final_path) == 0;
if (!success) {
char cp_cmd[1024];
snprintf(cp_cmd, sizeof(cp_cmd),
"pkexec cp '%s' '%s'",
archive_path, final_path);
success = system(cp_cmd) == 0;
}
#endif
} }
/* Write version file from server response header or plugin list */ remove(archive_path);
dbg_log(LOG_INFO, "[%s] Installed %s to %s", PLUGIN_NAME, slug,
final_path); if (success) {
return true; dbg_log(LOG_INFO, "[%s] Installed %s to %s",
PLUGIN_NAME, slug, final_path);
} else {
#ifdef _WIN32
DWORD err = GetLastError();
set_error("Failed to copy to %s (error %lu)", final_path, err);
#else
set_error("Failed to copy to %s", final_path);
#endif
dbg_log(LOG_ERROR, "[%s] Failed to install %s to %s",
PLUGIN_NAME, slug, final_path);
}
return success;
} }
+5 -1
View File
@@ -17,6 +17,7 @@ struct plugin_info {
char name[MAX_NAME_LEN]; char name[MAX_NAME_LEN];
char latest_version[MAX_VER_LEN]; char latest_version[MAX_VER_LEN];
char installed_version[MAX_VER_LEN]; char installed_version[MAX_VER_LEN];
int download_asset_id;
bool installed; bool installed;
bool update_available; bool update_available;
}; };
@@ -28,10 +29,13 @@ struct plugin_list {
bool downloader_fetch_plugin_list(const char *token, struct plugin_list *out); bool downloader_fetch_plugin_list(const char *token, struct plugin_list *out);
bool downloader_install_plugin(const char *token, const char *slug, bool downloader_install_plugin(const char *token, const char *slug,
const char *obs_plugin_dir); int asset_id, const char *obs_plugin_dir);
void downloader_detect_installed(struct plugin_list *list, void downloader_detect_installed(struct plugin_list *list,
const char *obs_plugin_dir); const char *obs_plugin_dir);
bool downloader_get_obs_plugin_dir(char *buf, size_t sz); bool downloader_get_obs_plugin_dir(char *buf, size_t sz);
bool downloader_write_version_file(const char *obs_plugin_dir,
const char *slug, const char *version);
const char *downloader_last_error(void);
#ifdef __cplusplus #ifdef __cplusplus
} }
+64 -51
View File
@@ -20,6 +20,7 @@
extern "C" { extern "C" {
#include "auth.h" #include "auth.h"
#include "downloader.h" #include "downloader.h"
#include "oauth.h"
} }
#include "manager-dialog.hpp" #include "manager-dialog.hpp"
@@ -36,7 +37,8 @@ public:
{ {
bool de = is_de(locale); bool de = is_de(locale);
setWindowTitle("stools Plugin Manager"); setWindowTitle("stools Plugin Manager");
setMinimumSize(600, 450); setMinimumSize(680, 450);
resize(700, 480);
auto *mainLayout = new QVBoxLayout(this); auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(16, 16, 16, 16); mainLayout->setContentsMargins(16, 16, 16, 16);
@@ -58,13 +60,8 @@ public:
m_statusLabel = new QLabel(); m_statusLabel = new QLabel();
authLayout->addWidget(m_statusLabel, 1); authLayout->addWidget(m_statusLabel, 1);
m_tokenInput = new QLineEdit(); m_loginBtn = new QPushButton(
m_tokenInput->setEchoMode(QLineEdit::Password); de ? "Mit stools.cc anmelden" : "Login with stools.cc");
m_tokenInput->setPlaceholderText("API Token");
m_tokenInput->setMinimumWidth(200);
authLayout->addWidget(m_tokenInput);
m_loginBtn = new QPushButton(de ? "Anmelden" : "Login");
connect(m_loginBtn, &QPushButton::clicked, this, connect(m_loginBtn, &QPushButton::clicked, this,
&ManagerDialog::onLogin); &ManagerDialog::onLogin);
authLayout->addWidget(m_loginBtn); authLayout->addWidget(m_loginBtn);
@@ -91,14 +88,19 @@ public:
m_table->horizontalHeader()->setSectionResizeMode( m_table->horizontalHeader()->setSectionResizeMode(
0, QHeaderView::Stretch); 0, QHeaderView::Stretch);
m_table->horizontalHeader()->setSectionResizeMode( m_table->horizontalHeader()->setSectionResizeMode(
1, QHeaderView::ResizeToContents); 1, QHeaderView::Fixed);
m_table->horizontalHeader()->setSectionResizeMode( m_table->horizontalHeader()->setSectionResizeMode(
2, QHeaderView::ResizeToContents); 2, QHeaderView::Fixed);
m_table->horizontalHeader()->setSectionResizeMode( m_table->horizontalHeader()->setSectionResizeMode(
3, QHeaderView::ResizeToContents); 3, QHeaderView::Fixed);
m_table->setColumnWidth(1, 120);
m_table->setColumnWidth(2, 90);
m_table->setColumnWidth(3, 110);
m_table->verticalHeader()->setVisible(false); m_table->verticalHeader()->setVisible(false);
m_table->setSelectionMode(QAbstractItemView::NoSelection); m_table->setSelectionMode(QAbstractItemView::NoSelection);
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers); m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_table->setShowGrid(false);
m_table->setAlternatingRowColors(true);
mainLayout->addWidget(m_table, 1); mainLayout->addWidget(m_table, 1);
/* ---- Refresh button ---- */ /* ---- Refresh button ---- */
@@ -131,7 +133,6 @@ private:
QString m_locale; QString m_locale;
QFrame *m_authFrame; QFrame *m_authFrame;
QLabel *m_statusLabel; QLabel *m_statusLabel;
QLineEdit *m_tokenInput;
QPushButton *m_loginBtn; QPushButton *m_loginBtn;
QPushButton *m_logoutBtn; QPushButton *m_logoutBtn;
QTableWidget *m_table; QTableWidget *m_table;
@@ -144,7 +145,6 @@ private:
bool de = is_de(m_locale.toUtf8().constData()); bool de = is_de(m_locale.toUtf8().constData());
bool logged_in = auth_is_logged_in(); bool logged_in = auth_is_logged_in();
m_tokenInput->setVisible(!logged_in);
m_loginBtn->setVisible(!logged_in); m_loginBtn->setVisible(!logged_in);
m_logoutBtn->setVisible(logged_in); m_logoutBtn->setVisible(logged_in);
m_refreshBtn->setEnabled(logged_in); m_refreshBtn->setEnabled(logged_in);
@@ -166,28 +166,36 @@ private:
void onLogin() void onLogin()
{ {
QString token = m_tokenInput->text().trimmed();
if (token.isEmpty()) return;
m_loginBtn->setEnabled(false);
m_loginBtn->setText("...");
QApplication::processEvents();
bool ok = auth_login(token.toUtf8().constData());
bool de = is_de(m_locale.toUtf8().constData()); bool de = is_de(m_locale.toUtf8().constData());
m_loginBtn->setEnabled(true); m_loginBtn->setEnabled(false);
m_loginBtn->setText(de ? "Anmelden" : "Login"); m_loginBtn->setText(de ? "Browser öffnet..." : "Opening browser...");
QApplication::processEvents();
if (ok) { char token[512] = "";
m_tokenInput->clear(); bool got_token = oauth_start_flow(token, sizeof(token));
updateAuthUI();
onRefresh(); if (got_token && token[0]) {
bool ok = auth_login(token);
if (ok) {
updateAuthUI();
onRefresh();
} else {
QMessageBox::warning(
this, "stools Plugin Manager",
de ? "Token-Validierung fehlgeschlagen."
: "Token validation failed.");
}
} else { } else {
QMessageBox::warning(this, "stools Plugin Manager", QMessageBox::warning(
de ? "Ungültiger Token." this, "stools Plugin Manager",
: "Invalid token."); de ? "Login abgebrochen oder fehlgeschlagen."
: "Login cancelled or failed.");
} }
m_loginBtn->setEnabled(true);
m_loginBtn->setText(
de ? "Mit Twitch anmelden" : "Login with Twitch");
} }
void onLogout() void onLogout()
@@ -291,25 +299,22 @@ private:
return; return;
} }
int asset_id = 0;
for (int i = 0; i < m_plugins.count; i++) {
if (slug == m_plugins.items[i].slug) {
asset_id = m_plugins.items[i].download_asset_id;
break;
}
}
bool ok = downloader_install_plugin( bool ok = downloader_install_plugin(
auth_get_token(), slug.toUtf8().constData(), obs_dir); auth_get_token(), slug.toUtf8().constData(),
asset_id, obs_dir);
if (ok) { if (ok) {
/* Write version file */ downloader_write_version_file(
char ver_path[512]; obs_dir, slug.toUtf8().constData(),
snprintf(ver_path, sizeof(ver_path), "%s%c%s.version", version.toUtf8().constData());
obs_dir,
#ifdef _WIN32
'\\',
#else
'/',
#endif
slug.toUtf8().constData());
FILE *f = fopen(ver_path, "w");
if (f) {
fputs(version.toUtf8().constData(), f);
fclose(f);
}
btn->setText(de ? "Aktuell" : "Up to date"); btn->setText(de ? "Aktuell" : "Up to date");
btn->setEnabled(false); btn->setEnabled(false);
@@ -322,11 +327,19 @@ private:
onRefresh(); onRefresh();
} else { } else {
btn->setText(de ? "Fehlgeschlagen" : "Failed"); const char *err = downloader_last_error();
QTimer::singleShot(2000, [btn, de]() { QString detail = err && err[0]
btn->setText(de ? "Erneut versuchen" : "Retry"); ? QString::fromUtf8(err)
btn->setEnabled(true); : (de ? "Unbekannter Fehler" : "Unknown error");
});
QMessageBox::critical(
this, "stools Plugin Manager",
QString(de ? "Installation von %1 fehlgeschlagen:\n%2"
: "Installation of %1 failed:\n%2")
.arg(slug, detail));
btn->setText(de ? "Erneut versuchen" : "Retry");
btn->setEnabled(true);
} }
} }
}; };
+227
View File
@@ -0,0 +1,227 @@
#include "oauth.h"
#include "obfuscation.h"
#include "debug-log.h"
#include "compat.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <shellapi.h>
#pragma comment(lib, "ws2_32.lib")
#define OAUTH_SOCKET SOCKET
#define OAUTH_INVALID INVALID_SOCKET
#define oauth_close closesocket
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#define OAUTH_SOCKET int
#define OAUTH_INVALID (-1)
#define oauth_close close
#endif
#define LISTEN_PORT_START 19850
#define LISTEN_PORT_END 19860
static const char *HTML_SUCCESS =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Connection: close\r\n\r\n"
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
"<link href=\"https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600&display=swap\" rel=\"stylesheet\">"
"<style>"
"*{margin:0;padding:0;box-sizing:border-box}"
"body{font-family:'Plus Jakarta Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;"
"background:#08090e;color:rgba(255,255,255,0.85);"
"display:flex;align-items:center;justify-content:center;min-height:100vh}"
"::selection{background:rgba(59,126,228,0.3)}"
".card{background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.08);"
"backdrop-filter:blur(20px);border-radius:16px;padding:48px;text-align:center;"
"max-width:380px;width:100%}"
".icon{width:56px;height:56px;border-radius:50%;display:flex;align-items:center;"
"justify-content:center;margin:0 auto 20px;"
"background:linear-gradient(135deg,rgba(72,187,120,0.15),rgba(72,187,120,0.08));"
"border:1px solid rgba(72,187,120,0.2)}"
".icon svg{width:28px;height:28px;color:#48bb78}"
"h2{font-size:1.125rem;font-weight:600;color:#fff;margin-bottom:6px}"
"p{font-size:0.875rem;color:rgba(255,255,255,0.5);line-height:1.5}"
"</style></head><body>"
"<div class=\"card\">"
"<div class=\"icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5 13l4 4L19 7\"/></svg></div>"
"<h2>Login erfolgreich</h2>"
"<p>Du kannst dieses Fenster schlie&szlig;en und zu OBS zur&uuml;ckkehren.</p>"
"</div></body></html>";
static const char *HTML_ERROR =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Connection: close\r\n\r\n"
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
"<link href=\"https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600&display=swap\" rel=\"stylesheet\">"
"<style>"
"*{margin:0;padding:0;box-sizing:border-box}"
"body{font-family:'Plus Jakarta Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;"
"background:#08090e;color:rgba(255,255,255,0.85);"
"display:flex;align-items:center;justify-content:center;min-height:100vh}"
"::selection{background:rgba(59,126,228,0.3)}"
".card{background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.08);"
"backdrop-filter:blur(20px);border-radius:16px;padding:48px;text-align:center;"
"max-width:380px;width:100%}"
".icon{width:56px;height:56px;border-radius:50%;display:flex;align-items:center;"
"justify-content:center;margin:0 auto 20px;"
"background:linear-gradient(135deg,rgba(239,68,68,0.15),rgba(239,68,68,0.08));"
"border:1px solid rgba(239,68,68,0.2)}"
".icon svg{width:28px;height:28px;color:#ef4444}"
"h2{font-size:1.125rem;font-weight:600;color:#fff;margin-bottom:6px}"
"p{font-size:0.875rem;color:rgba(255,255,255,0.5);line-height:1.5}"
"</style></head><body>"
"<div class=\"card\">"
"<div class=\"icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\"/></svg></div>"
"<h2>Login fehlgeschlagen</h2>"
"<p>Bitte versuche es erneut.</p>"
"</div></body></html>";
static OAUTH_SOCKET start_server(int *port_out)
{
for (int port = LISTEN_PORT_START; port <= LISTEN_PORT_END; port++) {
OAUTH_SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == OAUTH_INVALID)
continue;
int reuse = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,
(const char *)&reuse, sizeof(reuse));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons((uint16_t)port);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) == 0 &&
listen(sock, 1) == 0) {
*port_out = port;
return sock;
}
oauth_close(sock);
}
return OAUTH_INVALID;
}
static bool extract_param(const char *request, const char *key,
char *out, size_t out_sz)
{
char search[64];
snprintf(search, sizeof(search), "%s=", key);
const char *pos = strstr(request, search);
if (!pos) return false;
pos += strlen(search);
size_t i = 0;
while (pos[i] && pos[i] != '&' && pos[i] != ' ' &&
pos[i] != '\r' && pos[i] != '\n' && i < out_sz - 1) {
out[i] = pos[i];
i++;
}
out[i] = '\0';
return i > 0;
}
bool oauth_start_flow(char *token_out, int token_out_sz)
{
#ifdef _WIN32
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
#endif
int port = 0;
OAUTH_SOCKET server = start_server(&port);
if (server == OAUTH_INVALID) {
dbg_log(LOG_ERROR, "[%s] OAuth: failed to start local server",
PLUGIN_NAME);
return false;
}
char redirect_uri[128];
snprintf(redirect_uri, sizeof(redirect_uri),
"http://localhost:%d/callback", port);
char url[512];
snprintf(url, sizeof(url),
"%s%s/auth/connect?app=plugin-manager&redirect_uri=%s",
obf_https_prefix(), obf_stools_host(), redirect_uri);
#ifdef _WIN32
ShellExecuteA(NULL, "open", url, NULL, NULL, SW_SHOWNORMAL);
#elif defined(__APPLE__)
char cmd[1024];
snprintf(cmd, sizeof(cmd), "open \"%s\"", url);
system(cmd);
#else
char cmd[1024];
snprintf(cmd, sizeof(cmd), "xdg-open \"%s\"", url);
system(cmd);
#endif
dbg_log(LOG_INFO, "[%s] OAuth: waiting for callback on port %d",
PLUGIN_NAME, port);
/* Set timeout on accept: 120 seconds */
#ifdef _WIN32
int timeout_ms = 120000;
setsockopt(server, SOL_SOCKET, SO_RCVTIMEO,
(const char *)&timeout_ms, sizeof(timeout_ms));
#else
struct timeval tv = {120, 0};
setsockopt(server, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
#endif
struct sockaddr_in client_addr;
int client_len = sizeof(client_addr);
OAUTH_SOCKET client = accept(server, (struct sockaddr *)&client_addr,
#ifdef _WIN32
&client_len
#else
(socklen_t *)&client_len
#endif
);
bool success = false;
if (client != OAUTH_INVALID) {
char request[4096] = {0};
recv(client, request, sizeof(request) - 1, 0);
char token[512] = "";
char error[64] = "";
extract_param(request, "token", token, sizeof(token));
extract_param(request, "error", error, sizeof(error));
if (token[0] && !error[0]) {
send(client, HTML_SUCCESS, (int)strlen(HTML_SUCCESS), 0);
snprintf(token_out, token_out_sz, "%s", token);
success = true;
dbg_log(LOG_INFO, "[%s] OAuth: token received",
PLUGIN_NAME);
} else {
send(client, HTML_ERROR, (int)strlen(HTML_ERROR), 0);
dbg_log(LOG_WARNING,
"[%s] OAuth: denied or error (%s)",
PLUGIN_NAME, error[0] ? error : "no token");
}
oauth_close(client);
} else {
dbg_log(LOG_WARNING, "[%s] OAuth: timeout waiting for callback",
PLUGIN_NAME);
}
oauth_close(server);
return success;
}
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
bool oauth_start_flow(char *token_out, int token_out_sz);
#ifdef __cplusplus
}
#endif
+3 -2
View File
@@ -17,5 +17,6 @@ OBF_FUNC(obf_stools_host, "stools.cc")
OBF_FUNC(obf_auth_bearer_fmt, "Authorization: Bearer %s") OBF_FUNC(obf_auth_bearer_fmt, "Authorization: Bearer %s")
OBF_FUNC(obf_ua_prefix, "stools-pluginmanager/") OBF_FUNC(obf_ua_prefix, "stools-pluginmanager/")
OBF_FUNC(obf_api_me_path, "/api/me") OBF_FUNC(obf_api_me_path, "/api/me")
OBF_FUNC(obf_api_plugins_path, "/api/plugins") OBF_FUNC(obf_api_products_path, "/api/products")
OBF_FUNC(obf_api_plugin_download_fmt, "/api/plugins/%s/download?platform=%s") OBF_FUNC(obf_api_releases_fmt, "/api/releases/%s")
OBF_FUNC(obf_api_downloads_fmt, "/api/downloads/%s/%d")
+3 -2
View File
@@ -9,8 +9,9 @@ const char *obf_stools_host(void);
const char *obf_auth_bearer_fmt(void); const char *obf_auth_bearer_fmt(void);
const char *obf_ua_prefix(void); const char *obf_ua_prefix(void);
const char *obf_api_me_path(void); const char *obf_api_me_path(void);
const char *obf_api_plugins_path(void); const char *obf_api_products_path(void);
const char *obf_api_plugin_download_fmt(void); const char *obf_api_releases_fmt(void);
const char *obf_api_downloads_fmt(void);
#ifdef __cplusplus #ifdef __cplusplus
} }
+36 -27
View File
@@ -22,6 +22,15 @@ static const char *g_locale = NULL;
static pthread_t g_init_thread; static pthread_t g_init_thread;
static volatile bool g_init_done = false; static volatile bool g_init_done = false;
static char g_update_msg[1024] = "";
static void show_update_alert(void *param)
{
(void)param;
if (g_update_msg[0])
manager_dialog_show(g_locale);
}
static void *init_thread_func(void *arg) static void *init_thread_func(void *arg)
{ {
(void)arg; (void)arg;
@@ -38,41 +47,41 @@ static void *init_thread_func(void *arg)
sizeof(obs_dir))) { sizeof(obs_dir))) {
downloader_detect_installed(&plugins, obs_dir); downloader_detect_installed(&plugins, obs_dir);
int updates = 0;
for (int i = 0; i < plugins.count; i++) { for (int i = 0; i < plugins.count; i++) {
struct plugin_info *pi = struct plugin_info *pi =
&plugins.items[i]; &plugins.items[i];
if (!pi->installed || if (pi->installed &&
pi->update_available) { pi->update_available) {
updates++;
dbg_log(LOG_INFO, dbg_log(LOG_INFO,
"[%s] Auto-installing %s v%s", "[%s] Update available: %s %s -> %s",
PLUGIN_NAME, pi->slug, PLUGIN_NAME, pi->name,
pi->installed_version,
pi->latest_version); pi->latest_version);
if (downloader_install_plugin(
auth_get_token(),
pi->slug,
obs_dir)) {
char vp[512];
snprintf(vp,
sizeof(vp),
"%s"
#ifdef _WIN32
"\\"
#else
"/"
#endif
"%s.version",
obs_dir,
pi->slug);
FILE *f = fopen(vp,
"w");
if (f) {
fputs(pi->latest_version,
f);
fclose(f);
}
}
} }
} }
if (updates > 0) {
bool de = g_locale &&
g_locale[0] == 'd' &&
g_locale[1] == 'e';
if (updates == 1)
snprintf(g_update_msg,
sizeof(g_update_msg),
de ? "1 Plugin-Update verfügbar."
: "1 plugin update available.");
else
snprintf(g_update_msg,
sizeof(g_update_msg),
de ? "%d Plugin-Updates verfügbar."
: "%d plugin updates available.",
updates);
obs_queue_task(OBS_TASK_UI,
show_update_alert,
NULL, false);
}
} }
} }
} }