mirror of
https://github.com/gnh1201/welsonjs.git
synced 2026-02-15 23:28:27 +00:00
Remove telemetry, add curl fallback and integrity hash
Removed all telemetry-related code and configuration from WelsonJS.Launcher, including source files, resource strings, and app.config keys. Enhanced AssemblyLoader to support fallback to curl.exe for downloads on legacy Windows, with integrity hash verification. Updated documentation and resource files to reflect the new curl fallback mechanism and added the required integrity hash for curl.exe.
This commit is contained in:
parent
f6b32d3c88
commit
e50a966b89
|
|
@ -180,6 +180,12 @@ namespace Catswords.Phantomizer
|
|||
if (_registered)
|
||||
return;
|
||||
|
||||
// Fix TLS connectivity issues
|
||||
EnsureSecurityProtocols(SecurityProtocolType.Tls12);
|
||||
EnsureSecurityProtocolByName("Tls13"); // Add if available
|
||||
// EnsureSecurityProtocols(SecurityProtocolType.Tls11, SecurityProtocolType.Tls); // Optional legacy compatibility (uncomment if needed)
|
||||
|
||||
// Load integrity manifest
|
||||
try
|
||||
{
|
||||
if (!_integrityLoaded)
|
||||
|
|
@ -198,10 +204,6 @@ namespace Catswords.Phantomizer
|
|||
throw;
|
||||
}
|
||||
|
||||
EnsureSecurityProtocols(SecurityProtocolType.Tls12);
|
||||
EnsureSecurityProtocolByName("Tls13"); // Add if available
|
||||
// EnsureSecurityProtocols(SecurityProtocolType.Tls11, SecurityProtocolType.Tls); // Optional legacy compatibility (uncomment if needed)
|
||||
|
||||
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
|
||||
_registered = true;
|
||||
|
||||
|
|
@ -209,7 +211,6 @@ namespace Catswords.Phantomizer
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Loads native modules associated with an assembly (explicit).
|
||||
/// </summary>
|
||||
|
|
@ -333,6 +334,19 @@ namespace Catswords.Phantomizer
|
|||
LoadNativeModules(an.Name, an.Version, fileNames);
|
||||
}
|
||||
|
||||
public static void AddIntegrityHash(string hash)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hash))
|
||||
throw new ArgumentNullException(nameof(hash));
|
||||
|
||||
lock (IntegritySyncRoot)
|
||||
{
|
||||
if (_integrityHashes == null)
|
||||
_integrityHashes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
_integrityHashes.Add(hash.Trim().ToLower());
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// ASSEMBLY RESOLVE HANDLER (MANAGED)
|
||||
|
|
@ -399,8 +413,6 @@ namespace Catswords.Phantomizer
|
|||
|
||||
private static void DownloadFile(string url, string dest)
|
||||
{
|
||||
HttpResponseMessage res = null;
|
||||
|
||||
try
|
||||
{
|
||||
string gzUrl = url + ".gz";
|
||||
|
|
@ -416,13 +428,11 @@ namespace Catswords.Phantomizer
|
|||
if (!downloaded)
|
||||
{
|
||||
Trace.TraceInformation("Downloading file from: {0}", url);
|
||||
res = Http.GetAsync(url).GetAwaiter().GetResult();
|
||||
res.EnsureSuccessStatusCode();
|
||||
|
||||
using (Stream s = res.Content.ReadAsStreamAsync().GetAwaiter().GetResult())
|
||||
using (var stream = GetStreamFromUrl(url))
|
||||
using (var fs = new FileStream(dest, FileMode.Create, FileAccess.Write))
|
||||
{
|
||||
s.CopyTo(fs);
|
||||
stream.CopyTo(fs);
|
||||
}
|
||||
|
||||
Trace.TraceInformation("Downloaded file to: {0}", dest);
|
||||
|
|
@ -443,10 +453,6 @@ namespace Catswords.Phantomizer
|
|||
Trace.TraceError("Unexpected error downloading {0}: {1}", url, ex.Message);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
res?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -527,8 +533,6 @@ namespace Catswords.Phantomizer
|
|||
if (_integrityLoaded)
|
||||
return;
|
||||
|
||||
_integrityHashes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(IntegrityUrl))
|
||||
{
|
||||
_integrityLoaded = true;
|
||||
|
|
@ -543,18 +547,24 @@ namespace Catswords.Phantomizer
|
|||
if (!verified)
|
||||
throw new InvalidOperationException("IntegrityUrl verification failed.");
|
||||
|
||||
using (var res = Http.GetAsync(IntegrityUrl).GetAwaiter().GetResult())
|
||||
using (var stream = GetStreamFromUrl(IntegrityUrl))
|
||||
{
|
||||
res.EnsureSuccessStatusCode();
|
||||
using (var stream = res.Content.ReadAsStreamAsync().GetAwaiter().GetResult())
|
||||
{
|
||||
doc = XDocument.Load(stream);
|
||||
}
|
||||
doc = XDocument.Load(stream);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.TraceError("AssemblyIntegrity: failed to load manifest: {0}", ex.Message);
|
||||
Trace.TraceError("AssemblyIntegrity: failed to load manifest.\n{0}", ex.ToString());
|
||||
|
||||
Exception inner = ex.InnerException;
|
||||
int depth = 0;
|
||||
while (inner != null && depth < 8)
|
||||
{
|
||||
Trace.TraceError("AssemblyIntegrity: inner[{0}]\n{1}", depth, inner.ToString());
|
||||
inner = inner.InnerException;
|
||||
depth++;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Failed to load AssemblyIntegrity manifest.", ex);
|
||||
}
|
||||
|
||||
|
|
@ -577,7 +587,7 @@ namespace Catswords.Phantomizer
|
|||
if (string.IsNullOrWhiteSpace(val))
|
||||
continue;
|
||||
|
||||
_integrityHashes.Add(val);
|
||||
AddIntegrityHash(val);
|
||||
}
|
||||
|
||||
_integrityLoaded = true;
|
||||
|
|
@ -760,7 +770,7 @@ namespace Catswords.Phantomizer
|
|||
}
|
||||
|
||||
// Adds protocol by enum name when available (e.g., "Tls13"), otherwise no-op.
|
||||
public static void EnsureSecurityProtocolByName(string protocolName)
|
||||
private static void EnsureSecurityProtocolByName(string protocolName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(protocolName))
|
||||
return;
|
||||
|
|
@ -818,5 +828,123 @@ namespace Catswords.Phantomizer
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static Stream CurlGetAsStream(string url)
|
||||
{
|
||||
Trace.TraceInformation("Trying curl.exe to get URL: {0}", url);
|
||||
|
||||
// Resolve curl.exe only from the application base directory
|
||||
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||
string curlExePath = Path.Combine(baseDir, "curl.exe");
|
||||
|
||||
// Check existence of curl.exe
|
||||
if (!File.Exists(curlExePath))
|
||||
throw new FileNotFoundException("curl.exe was not found in the application directory.", curlExePath);
|
||||
|
||||
// Check integrity of curl.exe
|
||||
byte[] bytes = File.ReadAllBytes(curlExePath);
|
||||
string sha256 = ComputeHashHex(bytes, SHA256.Create());
|
||||
if (_integrityHashes == null || !_integrityHashes.Contains(sha256))
|
||||
throw new InvalidOperationException("curl.exe integrity check failed.");
|
||||
|
||||
// Prepare process start info
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = curlExePath,
|
||||
Arguments =
|
||||
"-f -sS -L --retry 3 --retry-delay 1 " +
|
||||
"--connect-timeout 10 --max-time 30 " +
|
||||
"\"" + url + "\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
|
||||
var process = new Process { StartInfo = psi };
|
||||
if (!process.Start())
|
||||
throw new InvalidOperationException("Failed to start curl.exe process.");
|
||||
|
||||
// Drain stderr asynchronously.
|
||||
// If any error line is received, log it immediately.
|
||||
process.ErrorDataReceived += (s, e) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(e.Data))
|
||||
{
|
||||
Trace.TraceError("curl stderr: {0}", e.Data);
|
||||
}
|
||||
};
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
var memory = new MemoryStream();
|
||||
|
||||
// Read stdout fully; this completes when stdout is closed (EOF)
|
||||
process.StandardOutput.BaseStream.CopyTo(memory);
|
||||
|
||||
// Enforce a hard timeout so we never wait forever
|
||||
if (!process.WaitForExit(60000))
|
||||
{
|
||||
try { process.Kill(); } catch { }
|
||||
throw new TimeoutException("curl.exe did not exit within the hard timeout.");
|
||||
}
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
throw new InvalidOperationException("curl.exe failed with exit code " + process.ExitCode + ".");
|
||||
|
||||
memory.Position = 0;
|
||||
return memory; // Caller must dispose the stream
|
||||
}
|
||||
|
||||
private static T ExecuteWithFallback<T>(Func<T> primaryAction, Func<Exception, bool> shouldFallback, Func<T> fallbackAction)
|
||||
{
|
||||
try
|
||||
{
|
||||
return primaryAction();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (shouldFallback != null && shouldFallback(ex))
|
||||
return fallbackAction();
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream GetStreamFromUrl(string url)
|
||||
{
|
||||
Trace.TraceInformation("Getting stream from URL: {0}", url);
|
||||
|
||||
return ExecuteWithFallback(
|
||||
primaryAction: () =>
|
||||
{
|
||||
var res = Http.GetAsync(url).GetAwaiter().GetResult();
|
||||
res.EnsureSuccessStatusCode();
|
||||
return res.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
|
||||
},
|
||||
shouldFallback: IsTlsHandshakeFailure,
|
||||
fallbackAction: () =>
|
||||
{
|
||||
return CurlGetAsStream(url);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static bool IsTlsHandshakeFailure(Exception ex)
|
||||
{
|
||||
bool isTlsException = ex is HttpRequestException httpEx &&
|
||||
httpEx.InnerException is WebException webEx &&
|
||||
webEx.Status == WebExceptionStatus.SecureChannelFailure;
|
||||
|
||||
if (isTlsException)
|
||||
{
|
||||
Trace.TraceWarning("TLS handshake failure: {0}", ex.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.TraceInformation("HttpRequestException is not a TLS handshake failure: {0}", ex.Message);
|
||||
}
|
||||
|
||||
return isTlsException;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,6 +178,25 @@ Once uploaded and pinned, the file cannot be silently modified without changing
|
|||
|
||||
---
|
||||
|
||||
### 🔄 curl.exe Fallback on Legacy Windows
|
||||
|
||||
If TLS connectivity issues occur on older versions of Windows (earlier than Windows 10), it is possible to fall back to using `curl.exe`. In this case, the `curl.exe` binary must pass an integrity check.
|
||||
|
||||
```csharp
|
||||
// curl.exe integrity hash can be added here if needed
|
||||
// e.g., 23b24c6a2dc39dbfd83522968d99096fc6076130a6de7a489bc0380cce89143d (curl-8.17.0-win-x86-full.2025-11-09, Muldersoft)
|
||||
loaderType.GetMethod("AddIntegrityHash")?.Invoke(null, new object[] { GetAppConfig("IntegrityHashCurl") });
|
||||
|
||||
/*
|
||||
// if use non-reflective call
|
||||
// curl.exe integrity hash can be added here if needed
|
||||
// e.g., 23b24c6a2dc39dbfd83522968d99096fc6076130a6de7a489bc0380cce89143d (curl-8.17.0-win-x86-full.2025-11-09, Muldersoft)
|
||||
AssemblyLoader.AddIntegrityHash(GetAppConfig("IntegrityHashCurl"));
|
||||
*/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📥 Download the pre-compiled file
|
||||
|
||||
* [Download Catswords.Phantomizer.dll.gz (catswords.blob.core.windows.net)](https://catswords.blob.core.windows.net/welsonjs/packages/managed/Catswords.Phantomizer/1.0.0.1/Catswords.Phantomizer.dll.gz)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ using System.Linq;
|
|||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
using WelsonJS.Launcher.Telemetry;
|
||||
|
||||
namespace WelsonJS.Launcher
|
||||
{
|
||||
|
|
@ -24,7 +23,6 @@ namespace WelsonJS.Launcher
|
|||
public static Mutex _mutex;
|
||||
public static ResourceServer _resourceServer;
|
||||
public static string _dateTimeFormat;
|
||||
public static TelemetryClient _telemetryClient;
|
||||
|
||||
static Program()
|
||||
{
|
||||
|
|
@ -36,35 +34,6 @@ namespace WelsonJS.Launcher
|
|||
|
||||
// load external assemblies
|
||||
InitializeAssemblyLoader();
|
||||
|
||||
// telemetry
|
||||
try
|
||||
{
|
||||
var telemetryProvider = GetAppConfig("TelemetryProvider");
|
||||
var telemetryOptions = new TelemetryOptions
|
||||
{
|
||||
ApiKey = GetAppConfig("TelemetryApiKey"),
|
||||
BaseUrl = GetAppConfig("TelemetryBaseUrl"),
|
||||
DistinctId = TelemetryIdentity.GetDistinctId(),
|
||||
Disabled = !string.Equals(
|
||||
GetAppConfig("TelemetryEnabled"),
|
||||
"true",
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
};
|
||||
|
||||
if (!telemetryOptions.Disabled &&
|
||||
!string.IsNullOrWhiteSpace(telemetryProvider) &&
|
||||
!string.IsNullOrWhiteSpace(telemetryOptions.ApiKey) &&
|
||||
!string.IsNullOrWhiteSpace(telemetryOptions.BaseUrl))
|
||||
{
|
||||
_telemetryClient = new TelemetryClient(telemetryProvider, telemetryOptions, _logger);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Telemetry initialization failed: {ex}");
|
||||
_telemetryClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
[STAThread]
|
||||
|
|
@ -92,13 +61,6 @@ namespace WelsonJS.Launcher
|
|||
return;
|
||||
}
|
||||
|
||||
// send event to the telemetry server
|
||||
if (_telemetryClient != null)
|
||||
{
|
||||
var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown";
|
||||
_ = _telemetryClient.TrackAppStartedAsync("WelsonJS.Launcher", version);
|
||||
}
|
||||
|
||||
// draw the main form
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
|
|
@ -133,6 +95,9 @@ namespace WelsonJS.Launcher
|
|||
loaderType.GetProperty("LoaderNamespace")?.SetValue(null, typeof(Program).Namespace);
|
||||
loaderType.GetProperty("AppName")?.SetValue(null, "WelsonJS");
|
||||
loaderType.GetProperty("IntegrityUrl")?.SetValue(null, GetAppConfig("AssemblyIntegrityUrl"));
|
||||
// curl.exe integrity hash can be added here if needed
|
||||
// e.g., 23b24c6a2dc39dbfd83522968d99096fc6076130a6de7a489bc0380cce89143d (curl-8.17.0-win-x86-full.2025-11-09, Muldersoft)
|
||||
loaderType.GetMethod("AddIntegrityHash")?.Invoke(null, new object[] { GetAppConfig("IntegrityHashCurl") });
|
||||
loaderType.GetMethod("Register")?.Invoke(null, null);
|
||||
|
||||
var loadNativeModulesMethod = loaderType.GetMethod(
|
||||
|
|
@ -161,6 +126,9 @@ namespace WelsonJS.Launcher
|
|||
AssemblyLoader.IntegrityUrl = GetAppConfig("AssemblyIntegrityUrl"); // (Optional) Set the integrity URL
|
||||
AssemblyLoader.LoaderNamespace = typeof(Program).Namespace;
|
||||
AssemblyLoader.AppName = "WelsonJS";
|
||||
// curl.exe integrity hash can be added here if needed
|
||||
// e.g., 23b24c6a2dc39dbfd83522968d99096fc6076130a6de7a489bc0380cce89143d (curl-8.17.0-win-x86-full.2025-11-09, Muldersoft)
|
||||
AssemblyLoader.AddIntegrityHash(GetAppConfig("IntegrityHashCurl"));
|
||||
AssemblyLoader.Register();
|
||||
|
||||
AssemblyLoader.LoadNativeModules(
|
||||
|
|
@ -171,7 +139,6 @@ namespace WelsonJS.Launcher
|
|||
*/
|
||||
}
|
||||
|
||||
|
||||
public static void RecordFirstDeployTime(string directory, string instanceId)
|
||||
{
|
||||
// get current time
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ namespace WelsonJS.Launcher.Properties {
|
|||
// 클래스에서 자동으로 생성되었습니다.
|
||||
// 멤버를 추가하거나 제거하려면 .ResX 파일을 편집한 다음 /str 옵션을 사용하여 ResGen을
|
||||
// 다시 실행하거나 VS 프로젝트를 다시 빌드하십시오.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
|
|
@ -315,6 +315,15 @@ namespace WelsonJS.Launcher.Properties {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 23b24c6a2dc39dbfd83522968d99096fc6076130a6de7a489bc0380cce89143d과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||
/// </summary>
|
||||
internal static string IntegrityHashCurl {
|
||||
get {
|
||||
return ResourceManager.GetString("IntegrityHashCurl", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||
/// </summary>
|
||||
|
|
@ -397,42 +406,6 @@ namespace WelsonJS.Launcher.Properties {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// phc_pmRHJ0aVEhtULRT4ilexwCjYpGtE9VYRhlA05fwiYt8과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||
/// </summary>
|
||||
internal static string TelemetryApiKey {
|
||||
get {
|
||||
return ResourceManager.GetString("TelemetryApiKey", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// https://us.i.posthog.com과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||
/// </summary>
|
||||
internal static string TelemetryBaseUrl {
|
||||
get {
|
||||
return ResourceManager.GetString("TelemetryBaseUrl", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// true과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||
/// </summary>
|
||||
internal static string TelemetryEnabled {
|
||||
get {
|
||||
return ResourceManager.GetString("TelemetryEnabled", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// posthog과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||
/// </summary>
|
||||
internal static string TelemetryProvider {
|
||||
get {
|
||||
return ResourceManager.GetString("TelemetryProvider", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 141.101.82.1과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -226,18 +226,6 @@
|
|||
<data name="icon_editor_32" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\icon_editor_32.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
<data name="TelemetryApiKey" xml:space="preserve">
|
||||
<value>phc_pmRHJ0aVEhtULRT4ilexwCjYpGtE9VYRhlA05fwiYt8</value>
|
||||
</data>
|
||||
<data name="TelemetryBaseUrl" xml:space="preserve">
|
||||
<value>https://us.i.posthog.com</value>
|
||||
</data>
|
||||
<data name="TelemetryProvider" xml:space="preserve">
|
||||
<value>posthog</value>
|
||||
</data>
|
||||
<data name="TelemetryEnabled" xml:space="preserve">
|
||||
<value>true</value>
|
||||
</data>
|
||||
<data name="AssemblyBaseUrl" xml:space="preserve">
|
||||
<value>https://catswords.blob.core.windows.net/welsonjs/packages</value>
|
||||
</data>
|
||||
|
|
@ -247,4 +235,7 @@
|
|||
<data name="AssemblyIntegrityUrl" xml:space="preserve">
|
||||
<value>https://spare-yellow-cicada.myfilebase.com/ipfs/QmYL29Z7BRvE6dnL4HPKeNvJyNJrh2LnVhzo3k9bp3wBZf</value>
|
||||
</data>
|
||||
<data name="IntegrityHashCurl" xml:space="preserve">
|
||||
<value>23b24c6a2dc39dbfd83522968d99096fc6076130a6de7a489bc0380cce89143d</value>
|
||||
</data>
|
||||
</root>
|
||||
Binary file not shown.
|
|
@ -1,20 +0,0 @@
|
|||
// ITelemetryProvider.cs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors
|
||||
// https://github.com/gnh1201/welsonjs
|
||||
//
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WelsonJS.Launcher.Telemetry
|
||||
{
|
||||
public interface ITelemetryProvider
|
||||
{
|
||||
Task TrackEventAsync(
|
||||
string eventName,
|
||||
IDictionary<string, object> properties = null,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
// PosthogTelemetryProvider.cs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors
|
||||
// https://github.com/gnh1201/welsonjs
|
||||
//
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WelsonJS.Launcher.Telemetry
|
||||
{
|
||||
public sealed class PosthogTelemetryProvider : ITelemetryProvider, IDisposable
|
||||
{
|
||||
private readonly TelemetryOptions _options;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ICompatibleLogger _logger;
|
||||
private bool _disposed;
|
||||
|
||||
public PosthogTelemetryProvider(TelemetryOptions options, ICompatibleLogger logger = null)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
|
||||
if (!_options.Disabled)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_options.ApiKey))
|
||||
throw new ArgumentException("PostHog API key is missing.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_options.BaseUrl))
|
||||
throw new ArgumentException("PostHog BaseUrl is missing.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_options.DistinctId))
|
||||
_options.DistinctId = $"anon-{Guid.NewGuid():N}";
|
||||
|
||||
_httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task TrackEventAsync(
|
||||
string eventName,
|
||||
IDictionary<string, object> properties = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(PosthogTelemetryProvider));
|
||||
if (_options.Disabled) return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(eventName))
|
||||
return;
|
||||
|
||||
string json;
|
||||
using (var ser = new JsSerializer())
|
||||
{
|
||||
var payload = new Dictionary<string, object>
|
||||
{
|
||||
["api_key"] = _options.ApiKey,
|
||||
["distinct_id"] = _options.DistinctId,
|
||||
["event"] = eventName,
|
||||
["properties"] = properties ?? new Dictionary<string, object>()
|
||||
};
|
||||
|
||||
json = ser.Serialize(payload, 0);
|
||||
}
|
||||
|
||||
var url = $"{_options.BaseUrl.TrimEnd('/')}/i/v0/e";
|
||||
|
||||
try
|
||||
{
|
||||
using (var response = await _httpClient.PostAsync(
|
||||
url,
|
||||
new StringContent(json, Encoding.UTF8, "application/json"),
|
||||
cancellationToken
|
||||
))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log and swallow for fire-and-forget telemetry
|
||||
_logger?.Error($"Failed to send telemetry event '{eventName}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
// TelemetryClient.cs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors
|
||||
// https://github.com/gnh1201/welsonjs
|
||||
//
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WelsonJS.Launcher.Telemetry
|
||||
{
|
||||
public sealed class TelemetryClient : IDisposable
|
||||
{
|
||||
private readonly ITelemetryProvider _provider;
|
||||
|
||||
public TelemetryClient(string providerName, TelemetryOptions options, ICompatibleLogger logger = null)
|
||||
{
|
||||
_provider = TelemetryProviderFactory.Create(providerName, options, logger);
|
||||
}
|
||||
|
||||
public Task TrackEventAsync(
|
||||
string eventName,
|
||||
IDictionary<string, object> properties = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _provider.TrackEventAsync(eventName, properties, cancellationToken);
|
||||
}
|
||||
|
||||
public Task TrackAppStartedAsync(string appName, string appVersion)
|
||||
{
|
||||
var props = new Dictionary<string, object>
|
||||
{
|
||||
{ "app_name", appName },
|
||||
{ "app_version", appVersion },
|
||||
{ "os_platform", Environment.OSVersion.Platform.ToString() },
|
||||
{ "os_version", Environment.OSVersion.Version.ToString() },
|
||||
{ "timestamp_utc", DateTime.UtcNow.ToString("o") }
|
||||
};
|
||||
|
||||
return TrackEventAsync("app_started", props);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
(_provider as IDisposable)?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
// TelemetryIdentity.cs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors
|
||||
// https://github.com/gnh1201/welsonjs
|
||||
//
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Management;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace WelsonJS.Launcher.Telemetry
|
||||
{
|
||||
public static class TelemetryIdentity
|
||||
{
|
||||
/// <summary>
|
||||
/// Collects multiple hardware/OS identity sources (BIOS UUID, OS SerialNumber, MachineName),
|
||||
/// joins them with ',' and computes SHA-256 hash, then returns the first 16 hex characters
|
||||
/// as a compact Distinct ID.
|
||||
/// </summary>
|
||||
public static string GetDistinctId()
|
||||
{
|
||||
var sources = new List<string>();
|
||||
|
||||
string biosUuid = GetBiosUuid();
|
||||
if (!string.IsNullOrEmpty(biosUuid))
|
||||
sources.Add(biosUuid);
|
||||
|
||||
string osUuid = GetOsUuid();
|
||||
if (!string.IsNullOrEmpty(osUuid))
|
||||
sources.Add(osUuid);
|
||||
|
||||
string machineName = GetMachineName();
|
||||
if (!string.IsNullOrEmpty(machineName))
|
||||
sources.Add(machineName);
|
||||
|
||||
string raw = string.Join(",", sources);
|
||||
string hash = ComputeSha256(raw);
|
||||
|
||||
if (!string.IsNullOrEmpty(hash) && hash.Length >= 16)
|
||||
return hash.Substring(0, 16);
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
private static string GetMachineName()
|
||||
{
|
||||
try { return Environment.MachineName ?? ""; }
|
||||
catch { return ""; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves BIOS UUID from Win32_ComputerSystemProduct.
|
||||
/// Filters out invalid values (all zeros or all 'F').
|
||||
/// </summary>
|
||||
private static string GetBiosUuid()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var searcher =
|
||||
new ManagementObjectSearcher("SELECT UUID FROM Win32_ComputerSystemProduct"))
|
||||
{
|
||||
foreach (ManagementObject obj in searcher.Get())
|
||||
{
|
||||
string uuid = obj["UUID"] as string;
|
||||
if (string.IsNullOrEmpty(uuid))
|
||||
return null;
|
||||
|
||||
uuid = uuid.Trim();
|
||||
string compact = uuid.Replace("-", "").ToUpperInvariant();
|
||||
|
||||
// Exclude invalid dummy UUIDs such as all 0s or all Fs
|
||||
if (IsAllChar(compact, '0') || IsAllChar(compact, 'F'))
|
||||
return null;
|
||||
|
||||
return uuid;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves OS-level UUID equivalent using Win32_OperatingSystem.SerialNumber.
|
||||
/// This value is unique per Windows installation.
|
||||
/// </summary>
|
||||
private static string GetOsUuid()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var searcher =
|
||||
new ManagementObjectSearcher("SELECT SerialNumber FROM Win32_OperatingSystem"))
|
||||
{
|
||||
foreach (ManagementObject obj in searcher.Get())
|
||||
{
|
||||
string serial = obj["SerialNumber"] as string;
|
||||
if (!string.IsNullOrEmpty(serial))
|
||||
return serial.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsAllChar(string s, char c)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s)) return false;
|
||||
for (int i = 0; i < s.Length; i++)
|
||||
if (s[i] != c) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes SHA-256 hex string for the given input text.
|
||||
/// Returns null if hashing fails.
|
||||
/// </summary>
|
||||
private static string ComputeSha256(string input)
|
||||
{
|
||||
if (input == null) input = "";
|
||||
|
||||
try
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var data = Encoding.UTF8.GetBytes(input);
|
||||
var digest = sha.ComputeHash(data);
|
||||
|
||||
var sb = new StringBuilder(digest.Length * 2);
|
||||
for (int i = 0; i < digest.Length; i++)
|
||||
sb.Append(digest[i].ToString("x2"));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
// TelemetryOptions.cs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors
|
||||
// https://github.com/gnh1201/welsonjs
|
||||
//
|
||||
namespace WelsonJS.Launcher.Telemetry
|
||||
{
|
||||
public class TelemetryOptions
|
||||
{
|
||||
public string ApiKey { get; set; }
|
||||
public string BaseUrl { get; set; }
|
||||
public string DistinctId { get; set; }
|
||||
public bool Disabled { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
// TelemetryProviderFactory.cs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors
|
||||
// https://github.com/gnh1201/welsonjs
|
||||
//
|
||||
using System;
|
||||
|
||||
namespace WelsonJS.Launcher.Telemetry
|
||||
{
|
||||
public static class TelemetryProviderFactory
|
||||
{
|
||||
public static ITelemetryProvider Create(string provider, TelemetryOptions options, ICompatibleLogger logger = null)
|
||||
{
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
if (provider == null)
|
||||
throw new ArgumentNullException(nameof(provider));
|
||||
|
||||
provider = provider.ToLowerInvariant();
|
||||
|
||||
switch (provider)
|
||||
{
|
||||
case "posthog":
|
||||
return new PosthogTelemetryProvider(options, logger);
|
||||
|
||||
default:
|
||||
throw new NotSupportedException(
|
||||
"Unknown telemetry provider: " + provider
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -52,8 +52,11 @@ namespace WelsonJS.Launcher
|
|||
Trace.Listeners.Add(new TextWriterTraceListener(writer)
|
||||
{
|
||||
Name = "FileTraceListener",
|
||||
TraceOutputOptions = TraceOptions.DateTime
|
||||
TraceOutputOptions = TraceOptions.DateTime,
|
||||
Filter = new EventTypeFilter(SourceLevels.Information)
|
||||
});
|
||||
|
||||
Trace.Listeners.Add(new ConsoleTraceListener());
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
|
|
@ -128,12 +128,6 @@
|
|||
<DependentUpon>GlobalSettingsForm.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="ResourceServer.cs" />
|
||||
<Compile Include="Telemetry\ITelemetryProvider.cs" />
|
||||
<Compile Include="Telemetry\PosthogTelemetryProvider.cs" />
|
||||
<Compile Include="Telemetry\TelemetryClient.cs" />
|
||||
<Compile Include="Telemetry\TelemetryIdentity.cs" />
|
||||
<Compile Include="Telemetry\TelemetryOptions.cs" />
|
||||
<Compile Include="Telemetry\TelemetryProviderFactory.cs" />
|
||||
<Compile Include="TraceLogger.cs" />
|
||||
<Compile Include="WebSocketManager.cs" />
|
||||
<EmbeddedResource Include="EnvForm.resx">
|
||||
|
|
|
|||
|
|
@ -25,12 +25,9 @@
|
|||
<add key="IpQueryApiKey2" value=""/>
|
||||
<add key="IpQueryApiPrefix2" value="https://api.abuseipdb.com/api/v2/"/>
|
||||
<add key="DateTimeFormat" value="yyyy-MM-dd HH:mm:ss"/>
|
||||
<add key="TelemetryProvider" value="posthog"/>
|
||||
<add key="TelemetryApiKey" value="phc_pmRHJ0aVEhtULRT4ilexwCjYpGtE9VYRhlA05fwiYt8"/>
|
||||
<add key="TelemetryBaseUrl" value="https://us.i.posthog.com"/>
|
||||
<add key="TelemetryEnabled" value="true"/>
|
||||
<add key="AssemblyBaseUrl" value="https://catswords.blob.core.windows.net/welsonjs/packages"/>
|
||||
<add key="AssemblyIntegrityUrl" value="https://spare-yellow-cicada.myfilebase.com/ipfs/QmYL29Z7BRvE6dnL4HPKeNvJyNJrh2LnVhzo3k9bp3wBZf"/>
|
||||
<add key="IntegrityHashCurl" value="23b24c6a2dc39dbfd83522968d99096fc6076130a6de7a489bc0380cce89143d"/>
|
||||
</appSettings>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user