This page is generated from the following source files:
This document provides a comprehensive architectural analysis of the res-downloader project, a cross-platform desktop application designed for network resource sniffing and high-speed downloading. The application combines a Go-based backend with a Vue 3 frontend, leveraging the Wails framework to deliver native performance across Windows, macOS, and Linux.
The project adopts a modern hybrid architecture that bridges native Go performance with contemporary web development practices.
| Layer | Technology | Purpose | Selection Rationale |
|---|---|---|---|
| Backend Framework | Wails v2.10.1 | Go-to-WebAssembly bridge | Lightweight alternative to Electron; native performance; smaller binary size |
| Frontend Framework | Vue 3 | UI component system | Composition API for reactive state management; mature ecosystem |
| State Management | Pinia | Global state store | TypeScript-native; simpler than Vuex; devtools integration |
| Routing | Vue Router 4 | SPA navigation | Lazy-loaded routes; hash-based history for local files |
| Proxy Engine | goproxy v1.7.2 | HTTP/HTTPS interception | Extensible middleware; MITM support; mature codebase |
| Logging | zerolog v1.33.0 | Structured logging | Zero-allocation JSON output; performance-focused |
| Cryptography | golang.org/x/crypto | AES-CBC encryption | Standard library extension; FIPS-compliant algorithms |
| UI Runtime | WebView2 / WebKit | Native rendering | OS-native rendering; no Chromium bundling required |
The main entry point (main.go:1-101) demonstrates the Wails application initialization with platform-specific configurations. The Go module dependencies (go.mod:1-42) confirm the use of goproxy for traffic interception and zerolog for structured logging.
The frontend entry point (frontend/src/main.ts:1-14) shows the Vue 3 application setup with Pinia state management and i18n internationalization support.
The backend architecture follows a singleton-based service registry pattern, where core services are initialized once and accessed globally throughout the application lifecycle.
正在加载图表渲染器...
The App struct serves as the central orchestrator, managing application lifecycle events and maintaining global state. Key responsibilities include:
wails.json and initializes all singleton services (core/app.go:42-172)OpenSystemProxy() and UnsetSystemProxy() methods for traffic interception controlThe singleton pattern ensures only one App instance exists throughout the application lifetime, with thread-safe initialization via sync.Once.
The Config struct manages all user-configurable settings with automatic persistence to disk. Default values are established at initialization (core/config.go:46-221):
go1defaultConfig := &Config{ 2 Theme: "lightTheme", 3 Locale: "zh", 4 Host: "127.0.0.1", 5 Port: "8899", 6 TaskNumber: runtime.NumCPU() * 2, 7 DownNumber: 3, 8 UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...", 9 Rule: "*", 10}
The configuration service delegates file operations to the Storage abstraction, enabling transparent persistence without coupling to filesystem details.
Structured logging is provided through a custom Logger wrapper around zerolog (core/logger.go:1-68). The logger supports:
.Stack().Err(err){UserDir}/logs/app.logThe network layer is the most architecturally significant component, implementing a dual-purpose HTTP server that handles both API requests and proxy traffic.
The HttpServer (core/http.go:21-408) listens on a configurable host/port combination and routes incoming requests based on the Host header:
go1http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2 if r.Host == "127.0.0.1:"+globalConfig.Port && HandleApi(w, r) { 3 // API request handled 4 } else { 5 proxyOnce.Proxy.ServeHTTP(w, r) // Proxy request 6 } 7}))
This design allows a single port to serve both the frontend API and proxy traffic, simplifying deployment and reducing port conflicts.
The proxy system uses a plugin registry pattern to enable extensible request/response modification. Each domain can register a custom plugin implementing the shared.Plugin interface:
go1func (p *Proxy) httpRequestEvent(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { 2 plugin := p.matchPlugin(r.Host) 3 if plugin != nil { 4 newReq, newResp := plugin.OnRequest(r, ctx) 5 if newResp != nil { 6 return newReq, newResp 7 } 8 if newReq != nil { 9 return newReq, nil 10 } 11 } 12 return pluginRegistry["default"].OnRequest(r, ctx) 13}
The plugin matching logic (core/proxy.go:135-173) extracts the top-level domain and looks up registered handlers, falling back to a default handler for unmatched domains.
Not all traffic should be intercepted and decrypted. The RuleSet engine (core/rule.go:22-126) provides flexible filtering through a custom rule syntax:
| Rule Pattern | Meaning | Example |
|---|---|---|
* | Match all domains | Intercept everything |
*.example.com | Wildcard subdomain | Match api.example.com, cdn.example.com |
example.com | Exact match | Only match example.com |
!pattern | Negation | Exclude from interception |
The rule evaluation logic processes rules in order, with later rules potentially overriding earlier ones:
go1func (r *RuleSet) shouldMitm(host string) bool { 2 action := false 3 for _, rule := range r.rules { 4 if rule.isAll { 5 action = !rule.isNeg 6 continue 7 } 8 if rule.isWildcard { 9 if h == rule.domain || strings.HasSuffix(h, "."+rule.domain) { 10 action = !rule.isNeg 11 } 12 continue 13 } 14 if h == rule.domain { 15 action = !rule.isNeg 16 } 17 } 18 return action 19}
The communication between Vue frontend and Go backend follows a type-safe binding pattern enforced by Wails code generation.
The Bind struct (core/bind.go:9-25) exposes methods that the frontend can invoke:
go1func (b *Bind) Config() *ResponseData { 2 return httpServerOnce.buildResp(1, "ok", globalConfig) 3} 4 5func (b *Bind) AppInfo() *ResponseData { 6 return httpServerOnce.buildResp(1, "ok", appOnce) 7} 8 9func (b *Bind) ResetApp() { 10 appOnce.IsReset = true 11 runtime.Quit(appOnce.ctx) 12}
These methods are automatically wrapped by Wails and exposed to JavaScript through generated bindings (frontend/wailsjs/go/core/Bind.js:1-15):
javascript1export function AppInfo() { 2 return window['go']['core']['Bind']['AppInfo'](); 3} 4 5export function Config() { 6 return window['go']['core']['Bind']['Config'](); 7}
The frontend uses Pinia for centralized state management (frontend/src/stores/index.ts:1-99). The useIndexStore manages:
The initialization flow demonstrates the async coordination between Wails bindings and Pinia:
typescript1const init = async () => { 2 Environment().then((res) => { 3 envInfo.value = res 4 }) 5 6 await bind.AppInfo().then((res: core.ResponseData)=>{ 7 appInfo.value = Object.assign({}, appInfo.value, res.data) 8 isProxy.value = res.data.IsProxy 9 }) 10 11 await bind.Config().then((res: core.ResponseData)=>{ 12 globalConfig.value = Object.assign({}, globalConfig.value, res.data) 13 }) 14 15 baseUrl.value = "http://127.0.0.1:" + globalConfig.value.Port 16}
The SPA navigation is handled by Vue Router with lazy-loaded components (frontend/src/router/index.ts:1-31):
typescript1const routes = [ 2 { 3 path: "/", 4 name: "layout", 5 component: () => import("@/components/layout/Index.vue"), 6 redirect: "/index", 7 children: [ 8 { 9 path: "/index", 10 name: "index", 11 meta: {keepAlive: true}, 12 component: () => import("@/views/index.vue"), 13 }, 14 { 15 path: "/setting", 16 name: "setting", 17 meta: {keepAlive: false}, 18 component: () => import("@/views/setting.vue"), 19 }, 20 ] 21 }, 22]
The hash-based history mode (createWebHashHistory()) is essential for local file serving, as it doesn't require server-side URL rewriting.
The following sequence diagram illustrates the end-to-end flow from application startup through a proxied network request:
正在加载图表渲染器...
Startup Sequence: The Wails runtime calls main() which creates the App singleton, triggering a cascade of service initializations (Config → Logger → System → HttpServer → Proxy → RuleSet)
Request Routing: Every incoming HTTP request is inspected for its Host header. Requests to 127.0.0.1:{Port} are treated as API calls; all others are proxied
MITM Decision: The rule engine evaluates the target domain against configured patterns to determine whether HTTPS decryption should be attempted
Plugin Interception: If a domain-specific plugin exists, it can modify both requests and responses, enabling custom resource extraction logic
The AESCipher (core/aes.go:16-72) provides AES-CBC encryption for sensitive data like cached passwords:
go1func (a *AESCipher) Encrypt(plainText string) (string, error) { 2 block, err := aes.NewCipher(a.key) 3 // ... PKCS7 padding 4 iv := cipherText[:aes.BlockSize] 5 mode := cipher.NewCBCEncrypter(block, iv) 6 mode.CryptBlocks(cipherText[aes.BlockSize:], []byte(plainText)) 7 return base64.StdEncoding.EncodeToString(cipherText), nil 8}
The decryption routine validates padding to prevent oracle attacks:
go1padding := int(cipherTextBytes[len(cipherTextBytes)-1]) 2if padding > len(cipherTextBytes) || padding > aes.BlockSize { 3 return "", errors.New("padding size error") 4}
The Storage struct (core/storage.go:1-41) provides a simple abstraction for file persistence:
go1func (l *Storage) Load() ([]byte, error) { 2 if !shared.FileExist(l.fileName) { 3 err := os.WriteFile(l.fileName, l.def, 0644) 4 return l.def, nil 5 } 6 return os.ReadFile(l.fileName) 7}
This pattern is used by Config for settings persistence and by SystemSetup for credential caching.
The FileDownloader supports parallel downloads through range requests (core/downloader.go:200-239):
go1func (fd *FileDownloader) createDownloadTasks() { 2 if fd.IsMultiPart { 3 eachSize := fd.TotalSize / int64(fd.totalTasks) 4 if eachSize < MinPartSize { 5 fd.totalTasks = int(fd.TotalSize / MinPartSize) 6 // ... adjust task count 7 } 8 for i := 0; i < fd.totalTasks; i++ { 9 start := eachSize * int64(i) 10 end := eachSize*int64(i+1) - 1 11 if i == fd.totalTasks-1 { 12 end = fd.TotalSize - 1 13 } 14 fd.DownloadTaskList = append(fd.DownloadTaskList, &DownloadTask{ 15 taskID: i, 16 rangeStart: start, 17 rangeEnd: end, 18 }) 19 } 20 } 21}
The downloader automatically adjusts the number of parallel tasks based on file size, ensuring each part is at least MinPartSize bytes.
The SystemSetup service (core/system.go:16-83) handles:
go1func (s *SystemSetup) checkPasswordFile() { 2 fileInfo, err := os.Stat(s.CacheFile) 3 lastModified := fileInfo.ModTime() 4 oneMonthAgo := time.Now().AddDate(0, -1, 0) 5 if lastModified.Before(oneMonthAgo) { 6 os.Remove(s.CacheFile) // Expired cache 7 return 8 } 9 // ... decrypt and load password 10}
The following diagram illustrates the dependency relationships between core modules:
正在加载图表渲染器...
main.go depends only on core/app, maintaining clean separationcore/shared package provides cross-cutting utilities used by multiple modulesThe dependency graph shows a well-structured layered architecture with no circular dependencies.
| Decision | Rationale | Trade-off |
|---|---|---|
| Wails over Electron | Smaller binary size (~15MB vs ~150MB), native performance, lower memory footprint | Smaller ecosystem, fewer third-party integrations |
| Singleton Services | Simplified state management, no dependency injection framework needed | Harder to test in isolation, global state mutations |
| Single-Port Architecture | Simplifies deployment, reduces port conflicts, easier firewall configuration | More complex request routing logic |
| Plugin-Based Proxy | Extensible without core modifications, domain-specific extraction logic | Performance overhead from interface dispatch |
| Rule-Based MITM | Fine-grained control over which domains to intercept | Rule evaluation on every request adds latency |
| File-Based Storage | Simple implementation, no database dependency | Not suitable for high-frequency writes, no transactions |
| AES-CBC Encryption | Standard algorithm, adequate for local credential storage | Not authenticated encryption (no GCM), vulnerable to padding oracle if IV reused |
| Hash-Based Routing | Works with local file:// protocol, no server configuration | URLs less readable, harder to share deep links |
The application ships with sensible defaults (core/config.go:46-221):
| Setting | Default | Description |
|---|---|---|
Host | 127.0.0.1 | HTTP server bind address |
Port | 8899 | HTTP server listen port |
TaskNumber | runtime.NumCPU() * 2 | Concurrent download tasks |
DownNumber | 3 | Parallel connections per download |
SaveDirectory | ~/Downloads | Default download location |
Rule | * | MITM rule (intercept all) |
Theme | lightTheme | UI theme selection |
Locale | zh | Interface language |
OnShutdown callback invokedIsReset flag set, application data is cleared