add linux
This commit is contained in:
@@ -5,7 +5,7 @@ set -e
|
|||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
echo "============================================="
|
echo "============================================="
|
||||||
echo " BUILDING SECURE WAILS CLIENT FOR MACOS "
|
echo " BUILDING SECURE WAILS CLIENT "
|
||||||
echo "============================================="
|
echo "============================================="
|
||||||
|
|
||||||
# Setup Go path for tools like garble and wails
|
# Setup Go path for tools like garble and wails
|
||||||
@@ -46,10 +46,21 @@ else
|
|||||||
fi
|
fi
|
||||||
BUILD_FLAGS+=(-ldflags "-s -w")
|
BUILD_FLAGS+=(-ldflags "-s -w")
|
||||||
|
|
||||||
echo "[*] Building macOS Apple Silicon (arm64)..."
|
# Detect host OS
|
||||||
"$WAILS_CMD" build -platform darwin/arm64 "${BUILD_FLAGS[@]}"
|
HOST_OS=$(go env GOOS)
|
||||||
|
|
||||||
echo "[*] Building macOS Intel (amd64)..."
|
if [ "$HOST_OS" = "darwin" ]; then
|
||||||
"$WAILS_CMD" build -platform darwin/amd64 "${BUILD_FLAGS[@]}"
|
echo "[*] Building macOS Apple Silicon (arm64)..."
|
||||||
|
"$WAILS_CMD" build -platform darwin/arm64 "${BUILD_FLAGS[@]}"
|
||||||
|
|
||||||
echo "[+] macOS arm64 and amd64 builds completed successfully!"
|
echo "[*] Building macOS Intel (amd64)..."
|
||||||
|
"$WAILS_CMD" build -platform darwin/amd64 "${BUILD_FLAGS[@]}"
|
||||||
|
echo "[+] macOS arm64 and amd64 builds completed successfully!"
|
||||||
|
elif [ "$HOST_OS" = "linux" ]; then
|
||||||
|
echo "[*] Building Linux Intel/AMD64 (amd64)..."
|
||||||
|
"$WAILS_CMD" build -platform linux/amd64 -tags webkit2_41 "${BUILD_FLAGS[@]}"
|
||||||
|
echo "[+] Linux amd64 build completed successfully!"
|
||||||
|
else
|
||||||
|
echo "[!] Unsupported host OS: $HOST_OS. Please build manually using 'wails build'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|||||||
1
client/build/trim.txt
Normal file
1
client/build/trim.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1782971860
|
||||||
214
client/internal/blocker/blocker_linux.go
Normal file
214
client/internal/blocker/blocker_linux.go
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package blocker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var systemAllowed = map[string]bool{
|
||||||
|
"gnome-shell": true,
|
||||||
|
"mutter": true,
|
||||||
|
"xterm": true,
|
||||||
|
"gnome-terminal": true,
|
||||||
|
"konsole": true,
|
||||||
|
"ptyxis": true, // Ubuntu terminal
|
||||||
|
"bash": true,
|
||||||
|
"zsh": true,
|
||||||
|
"sh": true,
|
||||||
|
"wails": true,
|
||||||
|
"code": true,
|
||||||
|
"cursor": true,
|
||||||
|
"windsurf": true,
|
||||||
|
"goland": true,
|
||||||
|
"idea": true,
|
||||||
|
"client": true,
|
||||||
|
"simple_care_v1.0": true,
|
||||||
|
"xwayland": true,
|
||||||
|
"mutter-x11-fram": true,
|
||||||
|
"ibus-x11": true,
|
||||||
|
"ibus-daemon": true,
|
||||||
|
"gsd-": true, // prefix
|
||||||
|
"ibus-": true, // prefix
|
||||||
|
"xdg-": true, // prefix
|
||||||
|
"at-spi": true, // prefix
|
||||||
|
"evolution-": true, // prefix
|
||||||
|
"goa-daemon": true,
|
||||||
|
"gjs": true,
|
||||||
|
"snapd-": true, // prefix
|
||||||
|
"systemd": true,
|
||||||
|
"webkit": true, // WebKit subprocesses
|
||||||
|
"glycin": true, // GNOME image helper
|
||||||
|
"zenity": true, // Dialog utility
|
||||||
|
"rustdesk": true, // Remote support safety
|
||||||
|
"rustdesk-bin": true,
|
||||||
|
"anydesk": true,
|
||||||
|
"teamviewer": true,
|
||||||
|
"remmina": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSystemAllowed(name string) bool {
|
||||||
|
name = strings.ToLower(name)
|
||||||
|
if systemAllowed[name] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for pattern := range systemAllowed {
|
||||||
|
if strings.HasPrefix(name, pattern) && pattern != name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPPID(pid uint32) uint32 {
|
||||||
|
statusBytes, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid))
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(statusBytes), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.HasPrefix(line, "PPid:") {
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
if ppid, err := strconv.ParseUint(parts[1], 10, 32); err == nil {
|
||||||
|
return uint32(ppid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDescendantOf(pid, targetPid uint32) bool {
|
||||||
|
curr := pid
|
||||||
|
for i := 0; i < 10; i++ { // limits lookup to 10 ancestor levels
|
||||||
|
ppid := getPPID(curr)
|
||||||
|
if ppid == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if ppid == targetPid {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
curr = ppid
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProcessInfo struct {
|
||||||
|
PID uint32
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVisibleProcesses() (map[uint32]ProcessInfo, error) {
|
||||||
|
files, err := os.ReadDir("/proc")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
procs := make(map[uint32]ProcessInfo)
|
||||||
|
for _, f := range files {
|
||||||
|
if !f.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pid, err := strconv.ParseUint(f.Name(), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mapsPath := fmt.Sprintf("/proc/%d/maps", pid)
|
||||||
|
mapsBytes, err := os.ReadFile(mapsPath)
|
||||||
|
if err != nil {
|
||||||
|
// Skip processes we don't own (permission denied)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mapsStr := string(mapsBytes)
|
||||||
|
isGUI := strings.Contains(mapsStr, "libgtk") ||
|
||||||
|
strings.Contains(mapsStr, "libQt") ||
|
||||||
|
strings.Contains(mapsStr, "libX11") ||
|
||||||
|
strings.Contains(mapsStr, "libwayland-client")
|
||||||
|
|
||||||
|
if !isGUI {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read process name from /proc/PID/comm
|
||||||
|
commBytes, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
procName := strings.TrimSpace(string(commBytes))
|
||||||
|
|
||||||
|
if procName != "" {
|
||||||
|
procs[uint32(pid)] = ProcessInfo{
|
||||||
|
PID: uint32(pid),
|
||||||
|
Name: procName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return procs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Blocker) checkAndKill() {
|
||||||
|
b.mu.Lock()
|
||||||
|
keywords := make([]string, len(b.allowedKeywords))
|
||||||
|
copy(keywords, b.allowedKeywords)
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
if len(keywords) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentExec := ""
|
||||||
|
if execPath, err := os.Executable(); err == nil {
|
||||||
|
currentExec = strings.ToLower(filepath.Base(execPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
procs, err := getVisibleProcesses()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[BLOCKER] Failed to get visible processes: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
myPid := uint32(os.Getpid())
|
||||||
|
for pid, info := range procs {
|
||||||
|
pNameLower := strings.ToLower(info.Name)
|
||||||
|
|
||||||
|
// 1. Always allow our app, our sub-processes, system/critical developer tools, or agent helpers
|
||||||
|
isOurSubprocess := pid == myPid || isDescendantOf(pid, myPid)
|
||||||
|
isAntigravity := strings.Contains(pNameLower, "antigravity")
|
||||||
|
if isOurSubprocess || (currentExec != "" && pNameLower == currentExec) || isSystemAllowed(pNameLower) || isAntigravity {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check if the process name contains any allowed keywords
|
||||||
|
allowed := false
|
||||||
|
for _, kw := range keywords {
|
||||||
|
if matchesAllowedKeyword(kw, pNameLower, pNameLower) {
|
||||||
|
allowed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. If not allowed, kill the application
|
||||||
|
if !allowed {
|
||||||
|
if b.OnBlocked != nil {
|
||||||
|
b.OnBlocked(info.Name, info.Name)
|
||||||
|
}
|
||||||
|
log.Printf("[BLOCKER] KILLED unauthorized application: %s (PID: %d)", info.Name, pid)
|
||||||
|
proc, err := os.FindProcess(int(pid))
|
||||||
|
if err == nil {
|
||||||
|
errKill := proc.Kill()
|
||||||
|
if errKill == nil && b.OnKill != nil {
|
||||||
|
b.OnKill(info.Name, info.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !windows && !darwin
|
//go:build !windows && !darwin && !linux
|
||||||
|
|
||||||
package blocker
|
package blocker
|
||||||
|
|
||||||
|
|||||||
90
client/internal/guard/guard_linux.go
Normal file
90
client/internal/guard/guard_linux.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package guard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kbinani/screenshot"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
guardOnce sync.Once
|
||||||
|
guardViolation func(string)
|
||||||
|
guardStop chan struct{}
|
||||||
|
suppressMu sync.Mutex
|
||||||
|
suppressViolationsUntil time.Time
|
||||||
|
)
|
||||||
|
|
||||||
|
// SuppressFor temporarily suspends environment violation checks (e.g. when transition screen)
|
||||||
|
func SuppressFor(d time.Duration) {
|
||||||
|
if d <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
suppressMu.Lock()
|
||||||
|
next := time.Now().Add(d)
|
||||||
|
if next.After(suppressViolationsUntil) {
|
||||||
|
suppressViolationsUntil = next
|
||||||
|
}
|
||||||
|
suppressMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func violationsSuppressed() bool {
|
||||||
|
suppressMu.Lock()
|
||||||
|
defer suppressMu.Unlock()
|
||||||
|
return time.Now().Before(suppressViolationsUntil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start monitors the Linux desktop environment (multi-display check)
|
||||||
|
func Start(onViolation func(reason string)) {
|
||||||
|
guardOnce.Do(func() {
|
||||||
|
if onViolation == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guardViolation = onViolation
|
||||||
|
guardStop = make(chan struct{})
|
||||||
|
|
||||||
|
if screenshot.NumActiveDisplays() > 1 {
|
||||||
|
onViolation("Phát hiện nhiều hơn 1 màn hình. Vui lòng chỉ dùng một màn hình khi chạy Simple Care.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go pollLoop()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the environment guard
|
||||||
|
func Stop() {
|
||||||
|
if guardStop != nil {
|
||||||
|
select {
|
||||||
|
case <-guardStop:
|
||||||
|
// already closed
|
||||||
|
default:
|
||||||
|
close(guardStop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pollLoop() {
|
||||||
|
ticker := time.NewTicker(3 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-guardStop:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
checkEnvironment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkEnvironment() {
|
||||||
|
if guardViolation == nil || violationsSuppressed() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n := screenshot.NumActiveDisplays(); n > 1 {
|
||||||
|
guardViolation("Phát hiện nhiều hơn 1 màn hình. Vui lòng rút/bật tắt màn hình phụ.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !windows && !darwin
|
//go:build !windows && !darwin && !linux
|
||||||
|
|
||||||
package guard
|
package guard
|
||||||
|
|
||||||
|
|||||||
40
client/internal/winapi/window_linux.go
Normal file
40
client/internal/winapi/window_linux.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package winapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ActivateAppWindow attempts to focus the application window.
|
||||||
|
// On Linux standard X11/Wayland desktop, focus is managed by the WM.
|
||||||
|
func ActivateAppWindow(titleHint string) {
|
||||||
|
log.Printf("[WINDOW] ActivateAppWindow requested for: %s", titleHint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlayNotifySound plays a system notification sound using canberra-gtk-play, falling back to pw-play or aplay.
|
||||||
|
func PlayNotifySound() {
|
||||||
|
log.Println("[WINDOW] Playing notification sound...")
|
||||||
|
go func() {
|
||||||
|
// Try canberra-gtk-play first
|
||||||
|
cmd := exec.Command("canberra-gtk-play", "-i", "bell")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
// Fallback to pw-play (Pipewire)
|
||||||
|
cmd = exec.Command("pw-play", "/usr/share/sounds/freedesktop/stereo/bell.oga")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
// Fallback to aplay (ALSA)
|
||||||
|
_ = exec.Command("aplay", "/usr/share/sounds/alsa/Front_Center.wav").Run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowWarningMessageBox displays an asynchronous GUI warning dialog using zenity.
|
||||||
|
func ShowWarningMessageBox(title, message string) {
|
||||||
|
log.Printf("[WINDOW] Warning Message Box: %s - %s", title, message)
|
||||||
|
go func() {
|
||||||
|
cmd := exec.Command("zenity", "--warning", "--title="+title, "--text="+message, "--no-wrap")
|
||||||
|
_ = cmd.Run()
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !windows && !darwin
|
//go:build !windows && !darwin && !linux
|
||||||
|
|
||||||
package winapi
|
package winapi
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user