Skip to content

Commit 589923a

Browse files
JusterZhuclaude
andauthored
feat: 统一配置模块 + 补丁自动上传 + 多语言框架 (#84)
* feat: add encryption file scanning before patch packaging - Add EncryptionDetectionService with multi-layered detection: - Extension blacklist matching (40+ known encrypted extensions) - PE file deep analysis (section name fingerprinting for 50+ protectors like .NET Reactor, ConfuserEx, VMProtect, Themida, UPX, etc.) - PE CLR/COR20 header integrity check for .NET assemblies - ELF file section analysis (, ) - JAR/Class CAFEBABE magic number validation - Python .pyc magic number range check - Full-file Shannon entropy analysis (threshold 7.8, auto-skip compressed/media formats like .zip, .png, .mp4) - Risk level classification: High / Medium / Low - Toggle switch in PatchView UI, enabled by default - Dialog prompt with grouped results when suspicious files found (Skip / Include All / Cancel) - Full i18n support (zh-CN / en-US) Closes #TBD * fix: address Copilot review suggestions - Fix dialog hang when user closes window via X button (set TCS result before Close, register Closed event as fallback) - Replace hardcoded 'cancelled by user' with localized string - Add ConfigureAwait(false) in scan loop to avoid UI thread context switching on every file iteration Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: add config persistence, HTTP upload, and Lingua i18n framework - Add unified configuration module (AppConfig) persisted to %APPDATA% - Implement ConfigService with JSON serialization, auto-backup, and schema migration - Add AuthCredential with DPAPI encryption for sensitive fields (passwords, tokens, API keys) - Add Settings page with upload server config, auth scheme selection, feature toggles - Implement HttpUploadService with multipart upload, progress tracking, exponential backoff retry - Add auto-upload toggle in PatchView with upload progress bar - Integrate Irihi.Lingua 0.2.0 for i18n - Extract ~80 translation strings to zh-CN.json and en-US.json files - Refactor LocalizationService to load from embedded JSON with hardcoded fallback - Persist UI preferences: theme, language, window size/position - Persist path memory across all pages (Patch, Simulate, Config, Extension) - Add IntEqualsConverter for conditional visibility in SettingsView Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: address Copilot review suggestions - Fix 1: Persist AutoUploadEnabled to AppConfig on toggle change - Fix 2: Bind hard-coded 'Upload' header to i18n (new Patch.Upload key) - Fix 3: Localize hard-coded 'Uploading...' string (new Upload.Uploading key) - Fix 4: Always consult FallbackStrings in LocalizationService, not just when cache empty - Fix 5: ValidateConnectionAsync now returns false for 401/403/404 (auth-aware) - Fix 6: Add TryBuildUrl with early validation, return clear error for invalid URLs - Fix 7: Return empty string on CryptographicException instead of leaking ciphertext - Fix 8: Replace fragile index-based auth visibility with IsBasicAuthVisible etc. properties - Fix 9: Add SemaphoreSlim guard to ConfigService.SaveAsync for concurrent write safety Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c6e04f4 commit 589923a

27 files changed

Lines changed: 2355 additions & 408 deletions

src/App.axaml.cs

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,83 @@
1-
using Avalonia;
1+
using System;
2+
using System.Threading.Tasks;
3+
using Avalonia;
4+
using Avalonia.Controls;
25
using Avalonia.Controls.ApplicationLifetimes;
36
using Avalonia.Markup.Xaml;
7+
using GeneralUpdate.Tools.Configuration;
48
using GeneralUpdate.Tools.ViewModels;
59
using GeneralUpdate.Tools.Views;
610

711
namespace GeneralUpdate.Tools;
812

913
public partial class App : Application
1014
{
11-
public override void Initialize() { AvaloniaXamlLoader.Load(this); }
12-
public override void OnFrameworkInitializationCompleted()
15+
public override void Initialize()
16+
{
17+
AvaloniaXamlLoader.Load(this);
18+
19+
// Load translations from embedded JSON files (falls back to built-in dictionaries)
20+
Services.LocalizationService.Instance.LoadFromResources();
21+
}
22+
23+
public override async void OnFrameworkInitializationCompleted()
1324
{
1425
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
15-
desktop.MainWindow = new MainWindow { DataContext = new MainWindowViewModel() };
26+
{
27+
// Initialize configuration
28+
var configService = ConfigServiceSingleton.Instance;
29+
await configService.LoadAsync();
30+
var config = configService.Config;
31+
32+
// Apply saved theme
33+
RequestedThemeVariant = config.Theme == "Dark"
34+
? Avalonia.Styling.ThemeVariant.Dark
35+
: Avalonia.Styling.ThemeVariant.Light;
36+
37+
// Apply saved locale
38+
Services.LocalizationService.Instance.Locale = config.Language;
39+
40+
var mainWindow = new MainWindow
41+
{
42+
DataContext = new MainWindowViewModel(config)
43+
};
44+
45+
// Restore window state
46+
mainWindow.Width = config.WindowWidth;
47+
mainWindow.Height = config.WindowHeight;
48+
if (config.WindowMaximized)
49+
mainWindow.WindowState = WindowState.Maximized;
50+
51+
// Save window state on close
52+
mainWindow.Closing += (_, _) =>
53+
{
54+
if (mainWindow.WindowState == WindowState.Maximized)
55+
{
56+
config.WindowMaximized = true;
57+
}
58+
else
59+
{
60+
config.WindowMaximized = false;
61+
config.WindowWidth = mainWindow.Width;
62+
config.WindowHeight = mainWindow.Height;
63+
}
64+
65+
// Fire-and-forget save
66+
_ = configService.SaveAsync();
67+
};
68+
69+
desktop.MainWindow = mainWindow;
70+
}
71+
1672
base.OnFrameworkInitializationCompleted();
1773
}
1874
}
75+
76+
/// <summary>
77+
/// Singleton accessor for <see cref="ConfigService"/>.
78+
/// Mirrors the pattern used by <see cref="Services.LocalizationService"/>.
79+
/// </summary>
80+
public static class ConfigServiceSingleton
81+
{
82+
public static ConfigService Instance { get; } = new();
83+
}

src/Configuration/AppConfig.cs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System.Collections.Generic;
2+
using Newtonsoft.Json;
3+
4+
namespace GeneralUpdate.Tools.Configuration;
5+
6+
/// <summary>
7+
/// Top-level application configuration model.
8+
/// Persisted to <c>%APPDATA%/GeneralUpdate.Tools/config.json</c>.
9+
/// </summary>
10+
public class AppConfig
11+
{
12+
/// <summary>Configuration schema version, for migration support.</summary>
13+
[JsonProperty("_schemaVersion")]
14+
public int SchemaVersion { get; set; } = 1;
15+
16+
// ── UI Preferences ────────────────────────────────────────
17+
18+
[JsonProperty("language")]
19+
public string Language { get; set; } = "zh-CN";
20+
21+
[JsonProperty("theme")]
22+
public string Theme { get; set; } = "Light";
23+
24+
[JsonProperty("windowWidth")]
25+
public double WindowWidth { get; set; } = 960;
26+
27+
[JsonProperty("windowHeight")]
28+
public double WindowHeight { get; set; } = 640;
29+
30+
[JsonProperty("windowMaximized")]
31+
public bool WindowMaximized { get; set; }
32+
33+
// ── Path Memory ───────────────────────────────────────────
34+
35+
[JsonProperty("lastPatchOldDir")]
36+
public string LastPatchOldDir { get; set; } = string.Empty;
37+
38+
[JsonProperty("lastPatchNewDir")]
39+
public string LastPatchNewDir { get; set; } = string.Empty;
40+
41+
[JsonProperty("lastPatchOutputDir")]
42+
public string LastPatchOutputDir { get; set; } = string.Empty;
43+
44+
[JsonProperty("lastSimulateAppDir")]
45+
public string LastSimulateAppDir { get; set; } = string.Empty;
46+
47+
[JsonProperty("lastSimulatePatchFile")]
48+
public string LastSimulatePatchFile { get; set; } = string.Empty;
49+
50+
[JsonProperty("lastSimulateOutputDir")]
51+
public string LastSimulateOutputDir { get; set; } = string.Empty;
52+
53+
[JsonProperty("lastConfigClientPath")]
54+
public string LastConfigClientPath { get; set; } = string.Empty;
55+
56+
[JsonProperty("lastConfigUpgradePath")]
57+
public string LastConfigUpgradePath { get; set; } = string.Empty;
58+
59+
[JsonProperty("lastOssOutputDir")]
60+
public string LastOssOutputDir { get; set; } = string.Empty;
61+
62+
[JsonProperty("lastExtensionDir")]
63+
public string LastExtensionDir { get; set; } = string.Empty;
64+
65+
[JsonProperty("lastExtensionOutputDir")]
66+
public string LastExtensionOutputDir { get; set; } = string.Empty;
67+
68+
// ── Upload Configuration ──────────────────────────────────
69+
70+
[JsonProperty("uploadServerUrl")]
71+
public string UploadServerUrl { get; set; } = string.Empty;
72+
73+
[JsonProperty("uploadEndpoint")]
74+
public string UploadEndpoint { get; set; } = "/api/v1/packages/upload";
75+
76+
[JsonProperty("uploadTimeoutSeconds")]
77+
public int UploadTimeoutSeconds { get; set; } = 300;
78+
79+
[JsonProperty("uploadRetryCount")]
80+
public int UploadRetryCount { get; set; } = 3;
81+
82+
[JsonProperty("autoUploadEnabled")]
83+
public bool AutoUploadEnabled { get; set; }
84+
85+
[JsonProperty("uploadAuth")]
86+
public AuthCredential UploadAuth { get; set; } = new();
87+
88+
// ── Simulation Configuration ──────────────────────────────
89+
90+
[JsonProperty("simulationServerPort")]
91+
public string SimulationServerPort { get; set; } = "5000";
92+
93+
[JsonProperty("simulationRequireAuth")]
94+
public bool SimulationRequireAuth { get; set; }
95+
96+
[JsonProperty("simulationAuth")]
97+
public AuthCredential SimulationAuth { get; set; } = new();
98+
99+
[JsonProperty("simulationPlatformType")]
100+
public string SimulationPlatformType { get; set; } = "Windows";
101+
102+
[JsonProperty("simulationAppType")]
103+
public string SimulationAppType { get; set; } = "Client";
104+
105+
// ── Feature Switches ──────────────────────────────────────
106+
107+
[JsonProperty("encryptionScanEnabled")]
108+
public bool EncryptionScanEnabled { get; set; } = true;
109+
110+
[JsonProperty("autoValidateSemver")]
111+
public bool AutoValidateSemver { get; set; } = true;
112+
113+
[JsonProperty("showJsonPreview")]
114+
public bool ShowJsonPreview { get; set; } = true;
115+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
namespace GeneralUpdate.Tools.Configuration;
2+
3+
/// <summary>
4+
/// Generic authentication credential used for both upload and simulation scenarios.
5+
/// Sensitive fields (passwords, tokens, API keys) are stored encrypted via DPAPI.
6+
/// </summary>
7+
public class AuthCredential
8+
{
9+
/// <summary>Authentication scheme to use.</summary>
10+
public AuthScheme Scheme { get; set; } = AuthScheme.None;
11+
12+
// ── Basic Auth ────────────────────────────────────────────
13+
14+
/// <summary>Username for Basic Authentication.</summary>
15+
public string Username { get; set; } = string.Empty;
16+
17+
/// <summary>DPAPI-encrypted password for Basic Authentication.</summary>
18+
public string EncryptedPassword { get; set; } = string.Empty;
19+
20+
// ── Bearer Token ──────────────────────────────────────────
21+
22+
/// <summary>DPAPI-encrypted bearer token / JWT.</summary>
23+
public string EncryptedToken { get; set; } = string.Empty;
24+
25+
/// <summary>
26+
/// Optional login endpoint URL. When set, the upload service can
27+
/// automatically obtain a fresh token by POSTing credentials to this endpoint.
28+
/// </summary>
29+
public string LoginUrl { get; set; } = string.Empty;
30+
31+
// ── API Key ───────────────────────────────────────────────
32+
33+
/// <summary>Custom HTTP header name for API Key (e.g. "X-API-Key").</summary>
34+
public string ApiKeyHeaderName { get; set; } = "X-API-Key";
35+
36+
/// <summary>DPAPI-encrypted API key value.</summary>
37+
public string EncryptedApiKey { get; set; } = string.Empty;
38+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System;
2+
using System.Security.Cryptography;
3+
using System.Text;
4+
5+
namespace GeneralUpdate.Tools.Configuration;
6+
7+
/// <summary>
8+
/// Encrypts and decrypts sensitive credential fields using Windows Data Protection API (DPAPI).
9+
/// On non-Windows platforms, falls back to Base64 encoding (not cryptographically secure).
10+
/// </summary>
11+
public static class AuthCredentialEncryptor
12+
{
13+
private static readonly DataProtectionScope Scope = DataProtectionScope.CurrentUser;
14+
15+
/// <summary>Encrypt a plain-text secret. Returns Base64-encoded ciphertext.</summary>
16+
public static string Protect(string plainText)
17+
{
18+
if (string.IsNullOrEmpty(plainText))
19+
return string.Empty;
20+
21+
try
22+
{
23+
var plainBytes = Encoding.UTF8.GetBytes(plainText);
24+
var cipherBytes = ProtectedData.Protect(plainBytes, null, Scope);
25+
return Convert.ToBase64String(cipherBytes);
26+
}
27+
catch (PlatformNotSupportedException)
28+
{
29+
// Fallback for non-Windows: Base64 (reversible, NOT secure)
30+
return Convert.ToBase64String(Encoding.UTF8.GetBytes(plainText));
31+
}
32+
}
33+
34+
/// <summary>Decrypt a protected secret. Returns the original plain text.</summary>
35+
public static string Unprotect(string cipherText)
36+
{
37+
if (string.IsNullOrEmpty(cipherText))
38+
return string.Empty;
39+
40+
try
41+
{
42+
var cipherBytes = Convert.FromBase64String(cipherText);
43+
var plainBytes = ProtectedData.Unprotect(cipherBytes, null, Scope);
44+
return Encoding.UTF8.GetString(plainBytes);
45+
}
46+
catch (PlatformNotSupportedException)
47+
{
48+
// Fallback for non-Windows
49+
return Encoding.UTF8.GetString(Convert.FromBase64String(cipherText));
50+
}
51+
catch (FormatException)
52+
{
53+
// Data was stored in plain text before encryption was introduced
54+
return cipherText;
55+
}
56+
catch (CryptographicException)
57+
{
58+
// Data was encrypted under a different user context — cannot decrypt.
59+
// Return empty to avoid leaking ciphertext into UI.
60+
return string.Empty;
61+
}
62+
}
63+
64+
/// <summary>
65+
/// Convenience: decrypts all sensitive fields in an <see cref="AuthCredential"/> in-place
66+
/// and returns a copy with plain-text values for use in HTTP clients.
67+
/// </summary>
68+
public static AuthCredentialPlain Decrypt(AuthCredential credential)
69+
{
70+
return new AuthCredentialPlain
71+
{
72+
Scheme = credential.Scheme,
73+
Username = credential.Username,
74+
Password = Unprotect(credential.EncryptedPassword),
75+
Token = Unprotect(credential.EncryptedToken),
76+
LoginUrl = credential.LoginUrl,
77+
ApiKeyHeaderName = credential.ApiKeyHeaderName,
78+
ApiKey = Unprotect(credential.EncryptedApiKey),
79+
};
80+
}
81+
82+
/// <summary>
83+
/// Convenience: encrypts plain-text values back into an <see cref="AuthCredential"/>.
84+
/// </summary>
85+
public static AuthCredential Encrypt(AuthCredentialPlain plain)
86+
{
87+
return new AuthCredential
88+
{
89+
Scheme = plain.Scheme,
90+
Username = plain.Username,
91+
EncryptedPassword = Protect(plain.Password),
92+
EncryptedToken = Protect(plain.Token),
93+
LoginUrl = plain.LoginUrl,
94+
ApiKeyHeaderName = plain.ApiKeyHeaderName,
95+
EncryptedApiKey = Protect(plain.ApiKey),
96+
};
97+
}
98+
}
99+
100+
/// <summary>
101+
/// Plain-text version of <see cref="AuthCredential"/> for use at runtime.
102+
/// Never persisted to disk.
103+
/// </summary>
104+
public class AuthCredentialPlain
105+
{
106+
public AuthScheme Scheme { get; set; }
107+
public string Username { get; set; } = string.Empty;
108+
public string Password { get; set; } = string.Empty;
109+
public string Token { get; set; } = string.Empty;
110+
public string LoginUrl { get; set; } = string.Empty;
111+
public string ApiKeyHeaderName { get; set; } = "X-API-Key";
112+
public string ApiKey { get; set; } = string.Empty;
113+
}

src/Configuration/AuthScheme.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace GeneralUpdate.Tools.Configuration;
2+
3+
/// <summary>
4+
/// Authentication scheme for server communication.
5+
/// </summary>
6+
public enum AuthScheme
7+
{
8+
/// <summary>No authentication required.</summary>
9+
None,
10+
11+
/// <summary>HTTP Basic Authentication (username + password encoded in Authorization header).</summary>
12+
Basic,
13+
14+
/// <summary>Bearer Token authentication (JWT or opaque token in Authorization header).</summary>
15+
BearerToken,
16+
17+
/// <summary>Custom API Key in a configurable HTTP header (e.g. X-API-Key).</summary>
18+
ApiKey,
19+
}

0 commit comments

Comments
 (0)