Compare commits
5 Commits
828adcfcc2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b9e01b107 | ||
|
|
af7d8a0792 | ||
| be2c3b749c | |||
|
|
b106683712 | ||
|
|
6a27663e72 |
279
AUDIT.md
Normal file
279
AUDIT.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# EMLy Security Audit
|
||||
|
||||
**Date:** 2026-02-16
|
||||
|
||||
**Scope:** Main EMLy desktop application (Go backend + SvelteKit frontend). Server directory excluded.
|
||||
|
||||
---
|
||||
|
||||
## Critical (2)
|
||||
|
||||
### CRIT-1: API Key Committed to Repository
|
||||
**File:** `config.ini:11`
|
||||
|
||||
`BUGREPORT_API_KEY` is in a tracked file and distributed with the binary. It is also returned to the frontend via `GetConfig()` and included in every bug report's `configData` field. Anyone who inspects the installed application directory, the repository, or the binary can extract this key.
|
||||
|
||||
**Risk:** Unauthorized access to the bug report API; potential abuse of any API endpoints authenticated by this key.
|
||||
|
||||
**Recommendation:** Rotate the key immediately. Stop distributing it in `config.ini`. Source it from an encrypted credential store or per-user environment variable. Strip it from the `GetConfig()` response to the frontend.
|
||||
|
||||
---
|
||||
|
||||
### CRIT-2: Path Traversal via Attachment Filename
|
||||
**Files:** `app_viewer.go:83,153,223,285,321`
|
||||
|
||||
Email attachment filenames are used unsanitized in temp file paths. A malicious email could craft filenames like `../../malicious.exe` or absolute paths. `OpenPDFWindow` (line 223) is the worst offender: `filepath.Join(tempDir, filename)` with no timestamp prefix at all.
|
||||
|
||||
```go
|
||||
// OpenPDFWindow — bare filename, no prefix
|
||||
tempFile := filepath.Join(tempDir, filename)
|
||||
|
||||
// OpenImageWindow — timestamp prefix but still unsanitized
|
||||
tempFile := filepath.Join(tempDir, fmt.Sprintf("%s_%s", timestamp, filename))
|
||||
```
|
||||
|
||||
**Risk:** Overwriting arbitrary temp files; potential privilege escalation if a writable autorun target path can be hit.
|
||||
|
||||
**Recommendation:** Sanitize attachment filenames with `filepath.Base()` + a character allowlist `[a-zA-Z0-9._-]` before using them in temp paths.
|
||||
|
||||
---
|
||||
|
||||
## High (5)
|
||||
|
||||
### HIGH-1: Command Injection in `OpenURLInBrowser`
|
||||
**File:** `app_system.go:156-159`
|
||||
|
||||
```go
|
||||
func (a *App) OpenURLInBrowser(url string) error {
|
||||
cmd := exec.Command("cmd", "/c", "start", "", url)
|
||||
return cmd.Start()
|
||||
}
|
||||
```
|
||||
|
||||
Passes unsanitized URL to `cmd /c start`. A `file:///` URL or shell metacharacters (`&`, `|`) can execute arbitrary commands.
|
||||
|
||||
**Risk:** Arbitrary local file execution; command injection via crafted URL.
|
||||
|
||||
**Recommendation:** Validate that the URL uses `https://` scheme before passing it. Consider using `rundll32.exe url.dll,FileProtocolHandler` instead of `cmd /c start`.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-2: Unsafe Path in `OpenFolderInExplorer`
|
||||
**File:** `app_system.go:143-146`
|
||||
|
||||
```go
|
||||
func (a *App) OpenFolderInExplorer(folderPath string) error {
|
||||
cmd := exec.Command("explorer", folderPath)
|
||||
return cmd.Start()
|
||||
}
|
||||
```
|
||||
|
||||
Raw frontend string passed to `explorer.exe` with no validation. This is a public Wails method callable from any frontend code.
|
||||
|
||||
**Risk:** Unexpected explorer behavior with crafted paths or UNC paths.
|
||||
|
||||
**Recommendation:** Validate that `folderPath` is a local directory path and exists before passing to explorer.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-3: Iframe Sandbox Escape — Email Body XSS
|
||||
**File:** `frontend/src/lib/components/MailViewer.svelte:387`
|
||||
|
||||
```svelte
|
||||
<iframe
|
||||
srcdoc={mailState.currentEmail.body + iframeUtilHtml}
|
||||
sandbox="allow-same-origin allow-scripts"
|
||||
/>
|
||||
```
|
||||
|
||||
`allow-scripts` + `allow-same-origin` together allow embedded email scripts to remove the sandbox attribute entirely and access the parent Wails window + all Go backend bindings. MDN explicitly warns against this combination.
|
||||
|
||||
**Risk:** Full XSS in the Wails WebView context; arbitrary Go backend method invocation from a malicious email.
|
||||
|
||||
**Recommendation:** Remove `allow-same-origin` from the iframe sandbox. Replace `iframeUtilHtml` script injection with a `postMessage`-based approach from the parent so `allow-scripts` can also be removed entirely.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-4: Arbitrary Code Execution via `InstallUpdateSilentFromPath`
|
||||
**File:** `app_update.go:573-643`
|
||||
|
||||
This exposed Wails method accepts an arbitrary UNC/local path from the frontend, copies the binary to temp, and executes it with UAC elevation (`/ALLUSERS`). There is no signature verification, no path allowlist, and no checksum validation.
|
||||
|
||||
**Risk:** Any attacker who can call this method (e.g., via XSS from HIGH-3) can execute any binary with administrator rights.
|
||||
|
||||
**Recommendation:** Restrict to validated inputs — check that installer paths match a known allowlist or have a valid code signature before execution.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-5: Race Condition on `updateStatus`
|
||||
**File:** `app_update.go:55-65`
|
||||
|
||||
```go
|
||||
var updateStatus = UpdateStatus{ ... }
|
||||
```
|
||||
|
||||
Global mutable variable accessed from multiple goroutines (startup check goroutine, frontend calls to `CheckForUpdates`, `DownloadUpdate`, `GetUpdateStatus`, `InstallUpdateSilent`) without any mutex protection. TOCTOU races possible on `Ready`/`InstallerPath` fields.
|
||||
|
||||
**Risk:** Installing from an empty path; checking stale ready status; data corruption.
|
||||
|
||||
**Recommendation:** Protect `updateStatus` with a `sync.RWMutex` or replace with an atomic struct/channel-based state machine.
|
||||
|
||||
---
|
||||
|
||||
## Medium (7)
|
||||
|
||||
### MED-1: API Key Leaked in Bug Reports
|
||||
**Files:** `frontend/src/lib/components/BugReportDialog.svelte:92-101`, `logger.go:72-81`
|
||||
|
||||
`captureConfig()` calls `GetConfig()` and serializes the entire config including `BUGREPORT_API_KEY` into `configData`. This is sent to the remote API, written to `config.json` in the temp folder, and included in the zip. The `FrontendLog` function also logs all frontend output verbatim — any accidental `console.log(config)` would write the key to the log file.
|
||||
|
||||
**Recommendation:** Filter out `BUGREPORT_API_KEY` before serializing config data. Redact sensitive fields in `FrontendLog`.
|
||||
|
||||
---
|
||||
|
||||
### MED-2: No TLS Validation on API Requests
|
||||
**Files:** `app_heartbeat.go:28-37`, `app_bugreport.go:376-384`
|
||||
|
||||
Both HTTP clients use the default transport with no certificate pinning and no enforcement of minimum TLS versions. The API URL from `config.ini` is not validated to be HTTPS before making requests. Bug report uploads contain PII (name, email, hostname, HWID, screenshot, email file) and the API key header.
|
||||
|
||||
**Recommendation:** Validate that `apiURL` starts with `https://`. Consider certificate pinning for the bug report API.
|
||||
|
||||
---
|
||||
|
||||
### MED-3: Raw Frontend String Written to Disk
|
||||
**File:** `app_settings.go:31-63`
|
||||
|
||||
`ExportSettings` writes the raw `settingsJSON` string from the frontend to any user-chosen path with no content validation. A compromised frontend (e.g., via HIGH-3 XSS) could write arbitrary content.
|
||||
|
||||
**Recommendation:** Validate that `settingsJSON` is well-formed JSON matching the expected settings schema before writing.
|
||||
|
||||
---
|
||||
|
||||
### MED-4: Imported Settings Not Schema-Validated
|
||||
**Files:** `app_settings.go:73-100`, `frontend/src/lib/stores/settings.svelte.ts:37`
|
||||
|
||||
Imported settings JSON is merged into the settings store via spread operator without schema validation. An attacker-supplied settings file could manipulate `enableAttachedDebuggerProtection` or inject unexpected values.
|
||||
|
||||
**Recommendation:** Validate imported JSON against the `EMLy_GUI_Settings` schema. Reject unknown keys.
|
||||
|
||||
---
|
||||
|
||||
### MED-5: `isEmailFile` Accepts Any String
|
||||
**File:** `frontend/src/lib/utils/mail/email-loader.ts:42-44`
|
||||
|
||||
```typescript
|
||||
export function isEmailFile(filePath: string): boolean {
|
||||
return filePath.trim().length > 0;
|
||||
}
|
||||
```
|
||||
|
||||
Any non-empty path passes validation and is sent to the Go backend for parsing, including paths to executables or sensitive files.
|
||||
|
||||
**Recommendation:** Check file extension against `EMAIL_EXTENSIONS` before passing to backend.
|
||||
|
||||
---
|
||||
|
||||
### MED-6: PATH Hijacking via `wmic` and `reg`
|
||||
**File:** `backend/utils/machine-identifier.go:75-99`
|
||||
|
||||
`wmic` and `reg.exe` are resolved via PATH. If PATH is manipulated, a malicious binary could be executed instead. `wmic` is also deprecated since Windows 10 21H1.
|
||||
|
||||
**Recommendation:** Use full paths (`C:\Windows\System32\wbem\wmic.exe`, `C:\Windows\System32\reg.exe`) or replace with native Go syscalls/WMI COM interfaces.
|
||||
|
||||
---
|
||||
|
||||
### MED-7: Log File Grows Unboundedly
|
||||
**File:** `logger.go:35`
|
||||
|
||||
The log file is opened in append mode with no size limit, rotation, or truncation. Frontend console output is forwarded to the logger, so verbose emails or a tight log loop can fill disk.
|
||||
|
||||
**Recommendation:** Implement log rotation (e.g., max 10MB, keep 3 rotated files) or use a library like `lumberjack`.
|
||||
|
||||
---
|
||||
|
||||
## Low (7)
|
||||
|
||||
### LOW-1: Temp Files Written with `0644` Permissions
|
||||
**Files:** `app_bugreport.go`, `app_viewer.go`, `app_screenshot.go`
|
||||
|
||||
All temp files (screenshots, mail copies, attachments) are written with `0644`. Sensitive email content in predictable temp paths (`emly_bugreport_<timestamp>`) could be read by other processes.
|
||||
|
||||
**Recommendation:** Use `0600` for temp files containing sensitive content.
|
||||
|
||||
---
|
||||
|
||||
### LOW-2: Log Injection via `FrontendLog`
|
||||
**File:** `logger.go:72-81`
|
||||
|
||||
`level` and `message` are user-supplied with no sanitization. Newlines in `message` can inject fake log entries. No rate limiting.
|
||||
|
||||
**Recommendation:** Strip newlines from `message`. Consider rate-limiting frontend log calls.
|
||||
|
||||
---
|
||||
|
||||
### LOW-3: `OpenPDFWindow` File Collision
|
||||
**File:** `app_viewer.go:223`
|
||||
|
||||
Unlike other viewer methods, `OpenPDFWindow` uses the bare filename with no timestamp prefix. Two PDFs with the same name silently overwrite each other.
|
||||
|
||||
**Recommendation:** Add a timestamp prefix consistent with the other viewer methods.
|
||||
|
||||
---
|
||||
|
||||
### LOW-4: Single-Instance Lock Exposes File Path
|
||||
**File:** `main.go:46-50`
|
||||
|
||||
Lock ID includes the full file path, which becomes a named mutex visible system-wide. Other processes can enumerate it to discover what files are being viewed.
|
||||
|
||||
**Recommendation:** Hash the file path before using it in the lock ID.
|
||||
|
||||
---
|
||||
|
||||
### LOW-5: External IP via Unauthenticated HTTP
|
||||
**File:** `backend/utils/machine-identifier.go:134-147`
|
||||
|
||||
External IP fetched from `api.ipify.org` without certificate pinning. A MITM can spoof the IP. The request also reveals EMLy usage to the third-party service.
|
||||
|
||||
**Recommendation:** Consider making external IP lookup optional or using multiple sources.
|
||||
|
||||
---
|
||||
|
||||
### LOW-6: `GetConfig()` Exposes API Key to Frontend
|
||||
**File:** `app.go:150-158`
|
||||
|
||||
Public Wails method returns the full `Config` struct including `BugReportAPIKey`. Any frontend JavaScript can retrieve it.
|
||||
|
||||
**Recommendation:** Create a `GetSafeConfig()` that omits sensitive fields, or strip the API key from the returned struct.
|
||||
|
||||
---
|
||||
|
||||
### LOW-7: Attachment Filenames Not Sanitized in Zip
|
||||
**File:** `app_bugreport.go:422-465`
|
||||
|
||||
Email attachment filenames copied into the bug report folder retain their original names (possibly containing traversal sequences). These appear in the zip archive sent to the server.
|
||||
|
||||
**Recommendation:** Sanitize filenames with `filepath.Base()` before copying into the bug report folder.
|
||||
|
||||
---
|
||||
|
||||
## Info (4)
|
||||
|
||||
### INFO-1: `allow-same-origin` Could Be Removed from Iframe
|
||||
**File:** `frontend/src/lib/components/MailViewer.svelte:387`
|
||||
|
||||
If `iframeUtilHtml` script injection were replaced with `postMessage`, both `allow-scripts` and `allow-same-origin` could be removed entirely.
|
||||
|
||||
### INFO-2: Unnecessary `cmd.exe` Shell Invocation
|
||||
**File:** `app_system.go:92-94`
|
||||
|
||||
`ms-settings:` URIs can be launched via `rundll32.exe url.dll,FileProtocolHandler` without invoking `cmd.exe`, reducing shell attack surface.
|
||||
|
||||
### INFO-3: `unsafe.Pointer` Without Size Guards
|
||||
**Files:** `backend/utils/file-metadata.go:115`, `backend/utils/screenshot_windows.go:94-213`
|
||||
|
||||
Cast to `[1 << 20]uint16` array and slicing by `valLen` is potentially out-of-bounds if the Windows API returns an unexpected length.
|
||||
|
||||
### INFO-4: No Memory Limits on Email Parsing
|
||||
**Files:** `backend/utils/mail/mailparser.go`, `eml_reader.go`
|
||||
|
||||
All email parts read into memory via `io.ReadAll` with no size limit. A malicious `.eml` with a gigabyte-sized attachment would exhaust process memory. Consider `io.LimitReader`.
|
||||
@@ -1,5 +1,8 @@
|
||||
# Changelog EMLy
|
||||
|
||||
## 1.6.0 (2026-02-17)
|
||||
1) Implementazione in sviluppo del sistema di aggiornamento automatico e manuale, con supporto per canali di rilascio (stable/beta) e gestione delle versioni. (Ancora non attivo di default, in fase di test)
|
||||
|
||||
## 1.5.5 (2026-02-14)
|
||||
1) Aggiunto il supporto al caricamento dei bug report su un server esterno, per facilitare la raccolta e gestione dei report da parte degli sviluppatori. (Con fallback locale in caso di errore)
|
||||
2) Aggiunto il supporto alle mail con formato TNEF/winmail.dat, per estrarre gli allegati correttamente.
|
||||
|
||||
8
TODO.md
8
TODO.md
@@ -10,3 +10,11 @@
|
||||
|
||||
# Bugs
|
||||
- [ ] Missing i18n for Toast notifications (to investigate)
|
||||
|
||||
|
||||
# Security
|
||||
- [ ] Fix HIGH-1
|
||||
- [ ] Fix HIGH-2
|
||||
- [ ] Fix MED-3
|
||||
- [ ] Fix MED-4
|
||||
- [ ] Fix MED-7
|
||||
|
||||
27
app.go
27
app.go
@@ -4,8 +4,10 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -141,6 +143,31 @@ func (a *App) QuitApp() {
|
||||
os.Exit(133)
|
||||
}
|
||||
|
||||
// RestartApp performs a full application restart, including the Go backend.
|
||||
// It schedules a new process via PowerShell with a short delay to ensure the
|
||||
// single-instance lock is released before the new instance starts, then exits.
|
||||
func (a *App) RestartApp() error {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
Log("RestartApp: failed to get executable path:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Escape single quotes in the path for PowerShell string literal
|
||||
safePath := strings.ReplaceAll(exe, "'", "''")
|
||||
script := fmt.Sprintf(`Start-Sleep -Seconds 1; Start-Process '%s'`, safePath)
|
||||
|
||||
cmd := exec.Command("powershell", "-WindowStyle", "Hidden", "-Command", script)
|
||||
if err := cmd.Start(); err != nil {
|
||||
Log("RestartApp: failed to schedule restart:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
Log("RestartApp: scheduled restart, quitting current instance...")
|
||||
runtime.Quit(a.ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Configuration Management
|
||||
// =============================================================================
|
||||
|
||||
@@ -81,15 +81,11 @@ func (a *App) CheckForUpdates() (UpdateStatus, error) {
|
||||
updateStatus.LastCheckTime = time.Now().Format("2006-01-02 15:04:05")
|
||||
runtime.EventsEmit(a.ctx, "update:status", updateStatus)
|
||||
|
||||
defer func() {
|
||||
updateStatus.Checking = false
|
||||
runtime.EventsEmit(a.ctx, "update:status", updateStatus)
|
||||
}()
|
||||
|
||||
// Get current version from config
|
||||
config := a.GetConfig()
|
||||
if config == nil {
|
||||
updateStatus.ErrorMessage = "Failed to load configuration"
|
||||
updateStatus.Checking = false
|
||||
return updateStatus, fmt.Errorf("failed to load config")
|
||||
}
|
||||
|
||||
@@ -99,6 +95,7 @@ func (a *App) CheckForUpdates() (UpdateStatus, error) {
|
||||
// Check if updates are enabled
|
||||
if config.EMLy.UpdateCheckEnabled != "true" {
|
||||
updateStatus.ErrorMessage = "Update checking is disabled"
|
||||
updateStatus.Checking = false
|
||||
return updateStatus, fmt.Errorf("update checking is disabled in config")
|
||||
}
|
||||
|
||||
@@ -106,6 +103,7 @@ func (a *App) CheckForUpdates() (UpdateStatus, error) {
|
||||
updatePath := strings.TrimSpace(config.EMLy.UpdatePath)
|
||||
if updatePath == "" {
|
||||
updateStatus.ErrorMessage = "Update path not configured"
|
||||
updateStatus.Checking = false
|
||||
return updateStatus, fmt.Errorf("UPDATE_PATH is empty in config.ini")
|
||||
}
|
||||
|
||||
@@ -113,6 +111,7 @@ func (a *App) CheckForUpdates() (UpdateStatus, error) {
|
||||
manifest, err := a.loadUpdateManifest(updatePath)
|
||||
if err != nil {
|
||||
updateStatus.ErrorMessage = fmt.Sprintf("Failed to load manifest: %v", err)
|
||||
updateStatus.Checking = false
|
||||
return updateStatus, err
|
||||
}
|
||||
|
||||
@@ -150,6 +149,7 @@ func (a *App) CheckForUpdates() (UpdateStatus, error) {
|
||||
updateStatus.CurrentVersion, currentChannel)
|
||||
}
|
||||
|
||||
updateStatus.Checking = false
|
||||
return updateStatus, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -8,32 +8,18 @@ import (
|
||||
"image/png"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/kbinani/screenshot"
|
||||
)
|
||||
|
||||
var (
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
gdi32 = syscall.NewLazyDLL("gdi32.dll")
|
||||
dwmapi = syscall.NewLazyDLL("dwmapi.dll")
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
dwmapi = syscall.NewLazyDLL("dwmapi.dll")
|
||||
|
||||
// user32 functions
|
||||
getForegroundWindow = user32.NewProc("GetForegroundWindow")
|
||||
getWindowRect = user32.NewProc("GetWindowRect")
|
||||
getClientRect = user32.NewProc("GetClientRect")
|
||||
getDC = user32.NewProc("GetDC")
|
||||
releaseDC = user32.NewProc("ReleaseDC")
|
||||
findWindowW = user32.NewProc("FindWindowW")
|
||||
getWindowDC = user32.NewProc("GetWindowDC")
|
||||
printWindow = user32.NewProc("PrintWindow")
|
||||
clientToScreen = user32.NewProc("ClientToScreen")
|
||||
|
||||
// gdi32 functions
|
||||
createCompatibleDC = gdi32.NewProc("CreateCompatibleDC")
|
||||
createCompatibleBitmap = gdi32.NewProc("CreateCompatibleBitmap")
|
||||
selectObject = gdi32.NewProc("SelectObject")
|
||||
bitBlt = gdi32.NewProc("BitBlt")
|
||||
deleteDC = gdi32.NewProc("DeleteDC")
|
||||
deleteObject = gdi32.NewProc("DeleteObject")
|
||||
getDIBits = gdi32.NewProc("GetDIBits")
|
||||
getForegroundWindow = user32.NewProc("GetForegroundWindow")
|
||||
getWindowRect = user32.NewProc("GetWindowRect")
|
||||
findWindowW = user32.NewProc("FindWindowW")
|
||||
|
||||
// dwmapi functions
|
||||
dwmGetWindowAttribute = dwmapi.NewProc("DwmGetWindowAttribute")
|
||||
@@ -47,39 +33,7 @@ type RECT struct {
|
||||
Bottom int32
|
||||
}
|
||||
|
||||
// POINT structure for Windows API
|
||||
type POINT struct {
|
||||
X int32
|
||||
Y int32
|
||||
}
|
||||
|
||||
// BITMAPINFOHEADER structure
|
||||
type BITMAPINFOHEADER struct {
|
||||
BiSize uint32
|
||||
BiWidth int32
|
||||
BiHeight int32
|
||||
BiPlanes uint16
|
||||
BiBitCount uint16
|
||||
BiCompression uint32
|
||||
BiSizeImage uint32
|
||||
BiXPelsPerMeter int32
|
||||
BiYPelsPerMeter int32
|
||||
BiClrUsed uint32
|
||||
BiClrImportant uint32
|
||||
}
|
||||
|
||||
// BITMAPINFO structure
|
||||
type BITMAPINFO struct {
|
||||
BmiHeader BITMAPINFOHEADER
|
||||
BmiColors [1]uint32
|
||||
}
|
||||
|
||||
const (
|
||||
SRCCOPY = 0x00CC0020
|
||||
DIB_RGB_COLORS = 0
|
||||
BI_RGB = 0
|
||||
PW_CLIENTONLY = 1
|
||||
PW_RENDERFULLCONTENT = 2
|
||||
DWMWA_EXTENDED_FRAME_BOUNDS = 9
|
||||
)
|
||||
|
||||
@@ -113,82 +67,10 @@ func CaptureWindowByHandle(hwnd uintptr) (*image.RGBA, error) {
|
||||
return nil, fmt.Errorf("invalid window dimensions: %dx%d", width, height)
|
||||
}
|
||||
|
||||
// Get window DC
|
||||
hdcWindow, _, err := getWindowDC.Call(hwnd)
|
||||
if hdcWindow == 0 {
|
||||
return nil, fmt.Errorf("GetWindowDC failed: %v", err)
|
||||
}
|
||||
defer releaseDC.Call(hwnd, hdcWindow)
|
||||
|
||||
// Create compatible DC
|
||||
hdcMem, _, err := createCompatibleDC.Call(hdcWindow)
|
||||
if hdcMem == 0 {
|
||||
return nil, fmt.Errorf("CreateCompatibleDC failed: %v", err)
|
||||
}
|
||||
defer deleteDC.Call(hdcMem)
|
||||
|
||||
// Create compatible bitmap
|
||||
hBitmap, _, err := createCompatibleBitmap.Call(hdcWindow, uintptr(width), uintptr(height))
|
||||
if hBitmap == 0 {
|
||||
return nil, fmt.Errorf("CreateCompatibleBitmap failed: %v", err)
|
||||
}
|
||||
defer deleteObject.Call(hBitmap)
|
||||
|
||||
// Select bitmap into DC
|
||||
oldBitmap, _, _ := selectObject.Call(hdcMem, hBitmap)
|
||||
defer selectObject.Call(hdcMem, oldBitmap)
|
||||
|
||||
// Try PrintWindow first (works better with layered/composited windows)
|
||||
ret, _, _ = printWindow.Call(hwnd, hdcMem, PW_RENDERFULLCONTENT)
|
||||
if ret == 0 {
|
||||
// Fallback to BitBlt
|
||||
ret, _, err = bitBlt.Call(
|
||||
hdcMem, 0, 0, uintptr(width), uintptr(height),
|
||||
hdcWindow, 0, 0,
|
||||
SRCCOPY,
|
||||
)
|
||||
if ret == 0 {
|
||||
return nil, fmt.Errorf("BitBlt failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare BITMAPINFO
|
||||
bmi := BITMAPINFO{
|
||||
BmiHeader: BITMAPINFOHEADER{
|
||||
BiSize: uint32(unsafe.Sizeof(BITMAPINFOHEADER{})),
|
||||
BiWidth: int32(width),
|
||||
BiHeight: -int32(height), // Negative for top-down DIB
|
||||
BiPlanes: 1,
|
||||
BiBitCount: 32,
|
||||
BiCompression: BI_RGB,
|
||||
},
|
||||
}
|
||||
|
||||
// Allocate buffer for pixel data
|
||||
pixelDataSize := width * height * 4
|
||||
pixelData := make([]byte, pixelDataSize)
|
||||
|
||||
// Get the bitmap bits
|
||||
ret, _, err = getDIBits.Call(
|
||||
hdcMem,
|
||||
hBitmap,
|
||||
0,
|
||||
uintptr(height),
|
||||
uintptr(unsafe.Pointer(&pixelData[0])),
|
||||
uintptr(unsafe.Pointer(&bmi)),
|
||||
DIB_RGB_COLORS,
|
||||
)
|
||||
if ret == 0 {
|
||||
return nil, fmt.Errorf("GetDIBits failed: %v", err)
|
||||
}
|
||||
|
||||
// Convert BGRA to RGBA
|
||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
for i := 0; i < len(pixelData); i += 4 {
|
||||
img.Pix[i+0] = pixelData[i+2] // R <- B
|
||||
img.Pix[i+1] = pixelData[i+1] // G <- G
|
||||
img.Pix[i+2] = pixelData[i+0] // B <- R
|
||||
img.Pix[i+3] = pixelData[i+3] // A <- A
|
||||
// Using kbinani/screenshot to capture the rectangle on screen
|
||||
img, err := screenshot.CaptureRect(image.Rect(int(rect.Left), int(rect.Top), int(rect.Right), int(rect.Bottom)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("screenshot.CaptureRect failed: %v", err)
|
||||
}
|
||||
|
||||
return img, nil
|
||||
|
||||
10
config.ini
10
config.ini
@@ -1,11 +1,11 @@
|
||||
[EMLy]
|
||||
SDK_DECODER_SEMVER = 1.4.1
|
||||
SDK_DECODER_SEMVER = 1.4.2
|
||||
SDK_DECODER_RELEASE_CHANNEL = beta
|
||||
GUI_SEMVER = 1.5.5
|
||||
GUI_SEMVER = 1.6.1
|
||||
GUI_RELEASE_CHANNEL = beta
|
||||
LANGUAGE = it
|
||||
UPDATE_CHECK_ENABLED = false
|
||||
UPDATE_PATH =
|
||||
UPDATE_CHECK_ENABLED = true
|
||||
UPDATE_PATH = "\\dc-rm2\logo\update"
|
||||
UPDATE_AUTO_CHECK = false
|
||||
BUGREPORT_API_URL = "https://emly-api.lyzcoote.cloud"
|
||||
BUGREPORT_API_URL = "https://api.emly.ffois.it"
|
||||
BUGREPORT_API_KEY = "emly_1BaQdBknsMGcY5DynSby71JnWOKXtJvnuUprkgWT0pujpLFxj5HaTXP9vtJAMk63"
|
||||
@@ -41,9 +41,12 @@
|
||||
"settings_danger_reset_label": "Reset App Data",
|
||||
"settings_danger_reset_hint": "This will clear all your settings and return the app to its default state.",
|
||||
"settings_danger_reset_button": "Reset data",
|
||||
"settings_danger_reload_label": "Reload App",
|
||||
"settings_danger_reload_hint": "Reloads the application interface. Useful if the UI becomes unresponsive.",
|
||||
"settings_danger_reload_button": "Reload",
|
||||
"settings_danger_reload_ui_label": "Reload UI",
|
||||
"settings_danger_reload_app_label": "Reload App",
|
||||
"settings_danger_reload_all_label": "Reload UI/App",
|
||||
"settings_danger_reload_hint": "Reloads the application interface. Useful if it becomes unresponsive.",
|
||||
"settings_danger_reload_button_ui": "Reload UI",
|
||||
"settings_danger_reload_button_app": "Reload app",
|
||||
"settings_danger_reset_dialog_title": "Are you absolutely sure?",
|
||||
"settings_danger_reset_dialog_description": "This action cannot be undone. This will permanently delete your current settings and return the app to its default state.",
|
||||
"settings_danger_reset_dialog_cancel": "Cancel",
|
||||
|
||||
@@ -41,9 +41,12 @@
|
||||
"settings_danger_reset_label": "Reimposta Dati App",
|
||||
"settings_danger_reset_hint": "Questo cancellerà tutte le tue impostazioni e riporterà l'app allo stato predefinito.",
|
||||
"settings_danger_reset_button": "Reimposta dati",
|
||||
"settings_danger_reload_label": "Ricarica App",
|
||||
"settings_danger_reload_hint": "Ricarica l'interfaccia dell'applicazione. Utile se l'UI non risponde.",
|
||||
"settings_danger_reload_button": "Ricarica",
|
||||
"settings_danger_reload_ui_label": "Ricarica UI",
|
||||
"settings_danger_reload_app_label": "Ricarica App",
|
||||
"settings_danger_reload_all_label": "Ricarica UI/App",
|
||||
"settings_danger_reload_hint": "Ricarica l'interfaccia dell'applicazione. Utile se non risponde.",
|
||||
"settings_danger_reload_button_ui": "Ricarica UI",
|
||||
"settings_danger_reload_button_app": "Ricarica app",
|
||||
"settings_danger_reset_dialog_title": "Sei assolutamente sicuro?",
|
||||
"settings_danger_reset_dialog_description": "Questa azione non può essere annullata. Questo eliminerà permanentemente le tue impostazioni attuali e riporterà l'app allo stato predefinito.",
|
||||
"settings_danger_reset_dialog_cancel": "Annulla",
|
||||
|
||||
@@ -298,8 +298,8 @@
|
||||
href="/"
|
||||
class={`${buttonVariants({ variant: "destructive" })} cursor-pointer hover:cursor-pointer`}
|
||||
style="text-decoration: none; margin-left: auto; height: 24px; font-size: 12px; padding: 0 8px;"
|
||||
aria-label={m.settings_danger_reload_button()}
|
||||
title={m.settings_danger_reload_button() + " app"}
|
||||
aria-label={m.settings_danger_reload_button_ui()}
|
||||
title={m.settings_danger_reload_button_ui()}
|
||||
>
|
||||
<RefreshCcwDot />
|
||||
</a>
|
||||
@@ -308,8 +308,8 @@
|
||||
href="#"
|
||||
class={`${buttonVariants({ variant: "destructive" })} cursor-pointer hover:cursor-pointer`}
|
||||
style="text-decoration: none; height: 24px; font-size: 12px; padding: 0 8px;"
|
||||
aria-label={m.settings_danger_reload_button()}
|
||||
title={m.settings_danger_reload_button() + " app"}
|
||||
aria-label={m.settings_danger_reload_button_ui()}
|
||||
title={m.settings_danger_reload_button_ui() }
|
||||
onclick={() => {
|
||||
$bugReportDialogOpen = !$bugReportDialogOpen;
|
||||
}}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import { Switch } from "$lib/components/ui/switch";
|
||||
import { ChevronLeft, Flame, Download, Upload, RefreshCw, CheckCircle2, AlertCircle, Sun, Moon } from "@lucide/svelte";
|
||||
import { ChevronLeft, Flame, Download, Upload, RefreshCw, CheckCircle2, AlertCircle, Sun, Moon, RefreshCcw } from "@lucide/svelte";
|
||||
import type { EMLy_GUI_Settings } from "$lib/types";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { It, Us } from "svelte-flags";
|
||||
@@ -25,7 +25,7 @@
|
||||
import { setLocale } from "$lib/paraglide/runtime";
|
||||
import { mailState } from "$lib/stores/mail-state.svelte.js";
|
||||
import { dev } from '$app/environment';
|
||||
import { ExportSettings, ImportSettings, CheckForUpdates, DownloadUpdate, InstallUpdate, GetUpdateStatus, SetUpdateCheckerEnabled } from "$lib/wailsjs/go/main/App";
|
||||
import { ExportSettings, ImportSettings, CheckForUpdates, DownloadUpdate, InstallUpdate, GetUpdateStatus, SetUpdateCheckerEnabled, RestartApp } from "$lib/wailsjs/go/main/App";
|
||||
import { EventsOn, EventsOff } from "$lib/wailsjs/runtime/runtime";
|
||||
|
||||
let { data } = $props();
|
||||
@@ -195,10 +195,14 @@
|
||||
});
|
||||
|
||||
// Sync update checker setting to backend config.ini
|
||||
let previousUpdateCheckerEnabled = form.enableUpdateChecker;
|
||||
let previousUpdateCheckerEnabled = $state<boolean | undefined>(undefined);
|
||||
$effect(() => {
|
||||
(async () => {
|
||||
if (!browser) return;
|
||||
if (previousUpdateCheckerEnabled === undefined) {
|
||||
previousUpdateCheckerEnabled = form.enableUpdateChecker;
|
||||
return;
|
||||
}
|
||||
if (form.enableUpdateChecker !== previousUpdateCheckerEnabled) {
|
||||
try {
|
||||
await SetUpdateCheckerEnabled(form.enableUpdateChecker ?? true);
|
||||
@@ -257,6 +261,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function restartEntireApp() {
|
||||
try {
|
||||
await RestartApp();
|
||||
} catch(e) {
|
||||
toast.error("Error while trying to reload the app");
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Update System State
|
||||
type UpdateStatus = {
|
||||
currentVersion: string;
|
||||
@@ -344,7 +358,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="min-h-[calc(100vh-1rem)] bg-gradient-to-b from-background to-muted/30">
|
||||
<div class="min-h-[calc(100vh-1rem)] bg-linear-to-b from-background to-muted/30">
|
||||
<div
|
||||
class="mx-auto flex max-w-3xl flex-col gap-4 px-4 py-6 sm:px-6 sm:py-10 opacity-80"
|
||||
>
|
||||
@@ -858,21 +872,31 @@
|
||||
class="flex items-center justify-between gap-4 rounded-lg border border-destructive/30 bg-card p-4"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<Label class="text-sm">{m.settings_danger_reload_label()}</Label>
|
||||
<Label class="text-sm">{m.settings_danger_reload_all_label()}</Label>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{m.settings_danger_reload_hint()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
data-sveltekit-reload
|
||||
href="/"
|
||||
class={`${buttonVariants({ variant: "destructive" })} cursor-pointer hover:cursor-pointer`}
|
||||
style="text-decoration: none;"
|
||||
>
|
||||
{m.settings_danger_reload_button()}
|
||||
</a>
|
||||
<div class="flex items-center gap-2">
|
||||
<a
|
||||
data-sveltekit-reload
|
||||
href="/"
|
||||
class={`${buttonVariants({ variant: "destructive" })} cursor-pointer hover:cursor-pointer`}
|
||||
style="text-decoration: none;"
|
||||
>
|
||||
{m.settings_danger_reload_button_ui()}
|
||||
</a>
|
||||
<Button
|
||||
class={`${buttonVariants({ variant: "destructive" })} cursor-pointer hover:cursor-pointer`}
|
||||
onclick={restartEntireApp}
|
||||
style="text-decoration: none;"
|
||||
>
|
||||
{m.settings_danger_reload_button_app()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div
|
||||
|
||||
4
go.mod
4
go.mod
@@ -4,6 +4,7 @@ go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/jaypipes/ghw v0.21.2
|
||||
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018
|
||||
github.com/teamwork/tnef v0.0.0-20200108124832-7deabccfdb32
|
||||
github.com/wailsapp/wails/v2 v2.11.0
|
||||
golang.org/x/sys v0.40.0
|
||||
@@ -13,18 +14,21 @@ require (
|
||||
|
||||
require (
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/gen2brain/shm v0.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/jaypipes/pcidb v1.1.1 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/jezek/xgb v1.1.1 // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
|
||||
9
go.sum
9
go.sum
@@ -4,6 +4,8 @@ github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3IS
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gen2brain/shm v0.1.0 h1:MwPeg+zJQXN0RM9o+HqaSFypNoNEcNpeoGp0BTSx2YY=
|
||||
github.com/gen2brain/shm v0.1.0/go.mod h1:UgIcVtvmOu+aCJpqJX7GOtiN7X2ct+TKLg4RTxwPIUA=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
@@ -20,6 +22,10 @@ github.com/jaypipes/pcidb v1.1.1/go.mod h1:x27LT2krrUgjf875KxQXKB0Ha/YXLdZRVmw6h
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
|
||||
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
|
||||
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWrS+E/7xMVpydsI48PMHcc7SfR4OxkDF4=
|
||||
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
@@ -36,6 +42,8 @@ github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/
|
||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
@@ -95,6 +103,7 @@ golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#define ApplicationName 'EMLy'
|
||||
#define ApplicationVersion GetVersionNumbersString('EMLy.exe')
|
||||
#define ApplicationVersion '1.5.4_beta'
|
||||
#define ApplicationVersion '1.6.0_beta'
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
Reference in New Issue
Block a user