Add OAuth login, proper API integration, ZIP extraction, UAC elevation, update notifications, CI/CD
This commit is contained in:
@@ -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/*
|
||||||
@@ -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
|
||||||
|
|||||||
+533
-54
@@ -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;
|
||||||
|
if (token && token[0]) {
|
||||||
char auth_header[512];
|
char auth_header[512];
|
||||||
snprintf(auth_header, sizeof(auth_header), obf_auth_bearer_fmt(), token);
|
snprintf(auth_header, sizeof(auth_header),
|
||||||
|
obf_auth_bearer_fmt(), token);
|
||||||
headers = curl_slist_append(headers, auth_header);
|
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,6 +102,7 @@ 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);
|
||||||
|
if (headers)
|
||||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, 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);
|
||||||
@@ -90,6 +113,7 @@ 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);
|
||||||
|
if (headers)
|
||||||
curl_slist_free_all(headers);
|
curl_slist_free_all(headers);
|
||||||
curl_easy_cleanup(curl);
|
curl_easy_cleanup(curl);
|
||||||
|
|
||||||
@@ -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';
|
||||||
|
|
||||||
|
char type[64] = "";
|
||||||
|
json_extract_string(obj_buf, "type", type, sizeof(type));
|
||||||
|
|
||||||
|
/* Only include downloadable plugins */
|
||||||
|
if (strcmp(type, "download") == 0) {
|
||||||
struct plugin_info *pi = &out->items[out->count];
|
struct plugin_info *pi = &out->items[out->count];
|
||||||
json_extract_string(obj_buf, "slug", pi->slug, sizeof(pi->slug));
|
json_extract_string(obj_buf, "id", pi->slug,
|
||||||
json_extract_string(obj_buf, "name", pi->name, sizeof(pi->name));
|
sizeof(pi->slug));
|
||||||
json_extract_string(obj_buf, "version", pi->latest_version,
|
json_extract_string(obj_buf, "name", pi->name,
|
||||||
sizeof(pi->latest_version));
|
sizeof(pi->name));
|
||||||
|
|
||||||
if (pi->slug[0] && pi->name[0])
|
if (pi->slug[0] && pi->name[0])
|
||||||
out->count++;
|
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 */
|
||||||
|
bool success = false;
|
||||||
|
char final_path[512];
|
||||||
|
snprintf(final_path, sizeof(final_path), "%s%c%s%s",
|
||||||
|
obs_plugin_dir, PATH_SEP, slug, ext);
|
||||||
|
|
||||||
|
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);
|
remove(final_path);
|
||||||
if (rename(tmp_path, final_path) != 0) {
|
|
||||||
dbg_log(LOG_ERROR, "[%s] Failed to rename %s -> %s",
|
#ifdef _WIN32
|
||||||
PLUGIN_NAME, tmp_path, final_path);
|
success = CopyFileA(found_path, final_path, FALSE);
|
||||||
remove(tmp_path);
|
if (!success) {
|
||||||
return false;
|
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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-46
@@ -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();
|
||||||
|
|
||||||
|
char token[512] = "";
|
||||||
|
bool got_token = oauth_start_flow(token, sizeof(token));
|
||||||
|
|
||||||
|
if (got_token && token[0]) {
|
||||||
|
bool ok = auth_login(token);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
m_tokenInput->clear();
|
|
||||||
updateAuthUI();
|
updateAuthUI();
|
||||||
onRefresh();
|
onRefresh();
|
||||||
} else {
|
} else {
|
||||||
QMessageBox::warning(this, "stools Plugin Manager",
|
QMessageBox::warning(
|
||||||
de ? "Ungültiger Token."
|
this, "stools Plugin Manager",
|
||||||
: "Invalid token.");
|
de ? "Token-Validierung fehlgeschlagen."
|
||||||
|
: "Token validation failed.");
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
QMessageBox::warning(
|
||||||
|
this, "stools Plugin Manager",
|
||||||
|
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]
|
||||||
|
? QString::fromUtf8(err)
|
||||||
|
: (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->setText(de ? "Erneut versuchen" : "Retry");
|
||||||
btn->setEnabled(true);
|
btn->setEnabled(true);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+227
@@ -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ßen und zu OBS zurü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
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-26
@@ -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,40 +47,40 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user