v0.6.4: daemon lifecycle, clipboard, network, docs fixes - #27
Merged
Conversation
saveTextAsFile now returns error instead of void. Caller maps: - path traversal -> 400 Bad Request - save failure -> 500 Internal Server Error - success -> 200 OK (explicit, not implicit) Tests added for all three branches.
New methods on ReceiveService: - ClaimFile: validates session/senderIP/fileId/token under mutex, marks as uploading - CompleteFile: removes file from session after success - FailFile: resets file state to pending on failure - GetSessionProgress: safe RLock read of progress bar pointer UploadHandlerV2 now uses ClaimFile/CompleteFile/FailFile instead of GetSessionByID + RemoveFileFromSession, preventing duplicate concurrent uploads of the same token. Tests: concurrent claim (one success, one ErrAlreadyUploading), error cases (invalid session/file/token/IP), CompleteFile lifecycle
Refactored stripping to write to a separate destination path: - StripTo(src, dest) writes stripped image to dest, never touches src - Strip(path) uses temp-in-dir + rename for atomic in-place replacement - stripJPEGTo/stripPNGTo: fail closed when SOS/IEND not found - PNG: strip eXIf chunk in addition to tEXt/zTXt/iTXt - Magic sniff (checks first 8 bytes), not just file extension - writeAtomic helper for safe temp+rename writes SendFile private mode: strips to temp file, uploads temp, defers cleanup. Original file bytes are never modified. Tests: JPEG EXIF removal, original unchanged, truncated JPEG error, PNG eXIf stripping, non-image passthrough, nonexistent file error
sanitizeName strips ASCII control bytes 0x00-0x1F from FileName in PrepareUploadHandlerV2 after decoding the request, preventing UI spoofing and terminal escape injection via display prompts. Test: control-char filename in prepare-upload returns 200 (sanitized)
Guard against f.Size < 0 in PrepareUploadHandlerV2's disk-space loop. Without this check, a negative size reduces totalSize and bypasses the free-space guard, and the value flows into uint64 conversion which wraps to a large positive number. Test: prepare-upload with Size: -10 returns 400 Bad Request
Release sessionMutex before calling Progress.Wait() to match the pattern used by cleanupLoop and CloseAllSessions. This prevents stalling other operations (like concurrent ClaimFile calls) during the progress bar Wait, which can block on terminal rendering.
Private mode previously created empty temp files for non-image files (PDFs, zips, text files) because StripTo returns nil for non-images (no error, no output). The empty temp then replaced the real file in the upload map, causing 0-byte uploads. Fix: export IsImageFile from metadata package; guard the strip loop so non-images keep their original path unchanged.
After sanitizing control chars from filenames, reject names that become empty with 400 Bad Request (prevents downstream confusion). Strengthened TestPrepareUpload_SanitizesControlChars to assert the stored session filename is 'bad.txt' after sanitization (not just that prepare returns 200).
…pty not error - Use CombinedOutput() instead of Output() to capture stderr - Include tool name in both Read() and Write() error messages - Treat tool exit with no output as empty clipboard (xclip/wl-paste exit 1 when clipboard is empty) instead of returning a cryptic error - Expand 'no tool found' message with actionable install hints
Remove the temp file creation for --clipboard and --stdin flags, and instead thread raw byte content through the send pipeline via a new SendOption/WithInMemoryFile mechanism. Changes: - pkg/send/send.go: Add SendOption, WithInMemoryFile, sendConfig, memFile types. SendToDevice and SendFiles now accept variadic SendOption. Process in-memory files alongside file-based files in the fileDto build loop and upload goroutines. - pkg/send/upload.go: Add memReadSeekCloser (bytes.Reader wrapper with no-op Close), fileReader interface. Extract uploadStream() from uploadFile() so both file and in-memory uploads share the same stream logic. - cmd/localgo/cmd/send.go: Replace os.CreateTemp/defer os.Remove with send.WithInMemoryFile() calls. Remove the localgo-clip- prefix hack entirely. Adjust file picker/empty checks to account for sendOpts.
Evaluate session.SessionID and file token before goroutines to avoid concurrent map access without the session mutex.
…cations Adds 6 new env var overrides: - LOCALSEND_SHELL — custom shell for exec hooks (replaces hardcoded sh -c/cmd /c) - LOCALSEND_CLIPBOARD_WRITE_CMD / _READ_CMD — custom clipboard tools - LOCALSEND_TLS_CERT / LOCALSEND_TLS_KEY — custom TLS certificate paths - LOCALSEND_NOTIFICATION_CMD — custom notification command
Adds cli.Sanitize() as a central ANSI-strip helper and applies it at all terminal output points for remote-controlled data (aliases, filenames): - CLI device table/quiet/JSON output and PickDevice TUI - Incoming transfer prompts (file + clipboard) - Server log messages - sender model.DeviceInfo before prompt rendering
GetInterfaceIPNet returns the IPv4 network (IP+mask) for a named interface. GetUsableSubnetIPs returns all usable host IPs respecting the actual netmask, capped at /22 for practical LAN scanning.
serve --daemon forks into background, writes PID to ~/.config/localgo/localgo.pid, and detaches from the terminal. localgo stop reads the PID file and sends SIGTERM (with 5s graceful timeout, then SIGKILL).
…in in tests Fixes #26: strings.TrimSuffix(configPath, "/config.yaml") uses hardcoded forward slash which doesn't match Windows backslash paths. Replaced with filepath.Dir(configPath) for cross-platform parent directory extraction. Also fixes storage_test.go to use filepath.Join instead of manual string concatenation with forward slashes.
syscall.SysProcAttr.Setpgid is Unix-only, causing Windows CI failure. daemonize() is now in daemon_unix.go (Unix: fork with Setpgid) and daemon_windows.go (Windows: returns error about unsupported daemon mode).
os.Chmod(tempDir, 0500) doesn't make directories read-only on Windows (unlike Unix permission bits), so the test was expecting a 500 error but getting 200.
… auto-accept in daemon child
…itization - Only short-circuit clipboard when single file with Size matching Preview length (avoids misclassifying multi-file transfers) - When NoClipboard or clipboard.Write fails, save text to DownloadDir instead of discarding it - Log clipboard transfers to history and run exec hook on accept - Sanitize clipboard preview text before rendering in confirmation prompt
…ig values - Read() uses cmd.Output() (stdout only) instead of CombinedOutput() so stderr diagnostics aren't mixed into clipboard text on success - OverrideProvider guards wp[0]/rp[0] access after strings.Fields with length check — whitespace-only input no longer panics - exec.go Shell parsing similarly guarded against empty fields
- Changed hostBits > 22 to hostBits > 10 so /16 (16 host bits) and other large subnets are capped at /22 (max 1022 usable hosts) - Fixed error message from 'too large' to 'too small' for tiny prefixes (/31, /32) that have fewer than 2 usable hosts
When CustomTLSCertPath/CustomTLSKeyPath are set, the server now parses the leaf certificate, computes its SHA-256 fingerprint, and stores it via Config.SetCustomFingerprint(). GetFingerprint() prefers this over the auto-generated SecurityContext hash, so advertised fingerprints match the actual presented certificate for trust verification.
- Use len(files) + len(sendOpts) instead of len(files) for total count - Display clipboard/stdin entries as '(in-memory)' in the file listing
…esource leak - Extract pidFilePath() into shared pid.go for reuse - Add defer os.Remove(pidPath) in daemon child path (serve.go) - Split stop.go into stop_unix.go (POSIX signals + 5s poll) and stop_windows.go (Kill-based) to fix platform incompatibility - Use storage.ResolveDuplicateFilename for clipboard fallback saves to prevent silent overwrite; restrict to 0600 permissions - defer body.Close() after NewIdleTimeoutReader to ensure timer is stopped on early-return error paths (upload.go)
…dle Windows FindProcess behavior - Replace %f/%n/%s/%a/%i placeholders in ExecHook before passing to the shell (prevents literal %n etc. from being passed as-is). - stop_windows.go: os.FindProcess always returns nil err on Windows. Remove dead error branch; print clean warning when Kill fails.
When --ip receives a hostname (e.g. myphone.local, desktop-pc) instead of a raw IP, fall back to net.LookupIP for resolution. Preserves the existing raw-IP fast path.
When a known device re-announces with different metadata (DHCP renewal, alias change), update the existing map entry instead of only refreshing LastSeen. The peer cache was already receiving the new device object.
Add GetUsableSubnetIPsFromIP which finds the interface owning a given IP and returns its actual subnet via GetUsableSubnetIPs (capped at /22). Falls back to the legacy /24 scan when the interface cannot be determined. Replaced all 4 call sites (scan.go, send.go, discover.go, pkg/send/send.go) that were hardcoding /24 via GetSubnetIPs.
…hs, fix Windows clipboard UTF-8 - progress.go: guard \033[F\033[K clearing with term.IsTerminal() to avoid raw escape sequences in non-TTY stderr (Docker, shell redirects) - receive_upload.go: normalize incoming filenames with filepath.ToSlash() so Windows backslashes form correct subdirectories on Unix receivers - clipboard_windows.go: switch write from clip.exe to PowerShell Set-Clipboard via stdin pipeline for proper Unicode/UTF-8 handling - Add golang.org/x/term as direct dependency for portable TTY detection
- Register 'config' in help.ShowMainUsage() so it appears in COMMANDS - Add 'config' help entry in commands.go for localgo help config - Add SetHelpFunc + import in config.go for proper help display
- Add Run to rootCmd so PersistentPreRunE fires for 'localgo --version' (without a subcommand), enabling the version flag check - Add -v shorthand for --version flag - Add version entry to commands.go so 'localgo help version' works - Add SetHelpFunc to versionCmd for proper help display
…ting in-place The previous code modified existingDevice in-place (line 148 wrote existingDevice.Alias = device.Alias). When a second multicast listener received the same packet (multi-interface listening), existingDevice was the same pointer stored by the first call—already sent to a handler goroutine and potentially being read by the test. Replacing the map entry with the new pointer avoids sharing the old pointer across goroutines. FromMulticastDto already sets LastSeen: time.Now(), so the incoming device has a fresh timestamp—no need for UpdateLastSeen().
There was a problem hiding this comment.
Pull request overview
Release-focused update for v0.6.4 that extends LocalGo’s CLI/server lifecycle (daemon + stop/config/version), tightens receiver upload/clipboard handling and sanitization, and improves discovery/scanning and private-mode behavior, alongside broad documentation refresh.
Changes:
- Adds daemonized
servemode plusstop,config, and improvedversionbehavior/help. - Refactors receiver upload/session handling (atomic “claim”, retryable failures), improves clipboard-message flow, and expands ANSI/control-character sanitization.
- Improves sender/discovery behavior (usable-subnet scanning, peer rediscovery updates) and private-mode metadata stripping; updates docs and examples accordingly.
Reviewed changes
Copilot reviewed 45 out of 47 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates install sections and adds container quick-start + env var list updates. |
| pkg/storage/storage_test.go | Uses filepath.Join for cross-platform path correctness in tests. |
| pkg/server/services/receive_service.go | Adds per-file transfer state + atomic claim/complete/fail APIs; adjusts session close semantics. |
| pkg/server/services/receive_service_test.go | Adds tests for claim/complete behavior including concurrency. |
| pkg/server/server.go | Supports loading custom TLS cert/key and computing advertised fingerprint from leaf cert. |
| pkg/server/handlers/receive_upload.go | Uses atomic file-claim flow, improves clipboard fallback/save behavior, path normalization, and error handling. |
| pkg/server/handlers/receive_handlers.go | Adds clipboard-message short-circuit path, filename sanitization, negative-size guard, and misc sanitization. |
| pkg/server/handlers/receive_handlers_test.go | Adds tests for path traversal, save failure, sanitization, and negative size handling. |
| pkg/server/handlers/prompt.go | Sanitizes remote-controlled strings in prompts; adds clipboard prompt flow. |
| pkg/server/handlers/exec.go | Adds placeholder replacement + configurable shell prefix for exec hooks. |
| pkg/server/handlers/discovery_handlers.go | Sanitizes alias before logging register requests. |
| pkg/send/upload.go | Refactors upload to support streaming sources and shared upload helper. |
| pkg/send/send.go | Adds in-memory send option, smarter subnet scan, improved private-mode stripping flow, and clipboard short-circuit handling. |
| pkg/network/interfaces.go | Adds interface-aware “usable subnet” IP generation with /22 cap. |
| pkg/metadata/strip.go | Refactors stripping to support StripTo + atomic writes; expands PNG chunk stripping. |
| pkg/metadata/strip_test.go | Adds tests for stripping behavior and edge cases. |
| pkg/help/help.go | Updates main usage to include new commands. |
| pkg/help/commands.go | Adds help entries for stop/config/version and daemon flag in serve help. |
| pkg/discovery/multicast.go | Ensures re-announce updates device fields beyond LastSeen. |
| pkg/config/dto.go | Prefers custom TLS fingerprint when present. |
| pkg/config/config.go | Adds new config fields (shell/clipboard cmds/tls paths/notification cmd) and loads them from viper. |
| pkg/clipboard/clipboard.go | Improves error messages, clipboard read handling, and adds override support. |
| pkg/clipboard/clipboard_windows.go | Prefers PowerShell Set-Clipboard for UTF-8/unicode correctness; clip.exe fallback. |
| pkg/cli/sanitize.go | Adds ANSI escape stripping helper for untrusted display strings. |
| pkg/cli/progress.go | Avoids emitting VT100 cursor clears when stderr isn’t a terminal. |
| pkg/cli/output.go | Sanitizes device alias in displayed tables/pickers. |
| pkg/cli/notify.go | Adds optional custom notification command. |
| go.sum | Updates sums for module changes (x/sys bump, x/term added). |
| go.mod | Bumps golang.org/x/sys and adds golang.org/x/term. |
| docs/LIBRARY_GUIDE.md | Updates examples/signatures and removes emoji headings for consistency. |
| docs/CONFIGURATION.md | Updates precedence, flags/env vars, and defaults to match new behavior. |
| docs/CODE_WALKTHROUGH.md | Updates architecture/package descriptions to match current structure. |
| docs/CLI_REFERENCE.md | Adds global flags and new subcommands/flags, updates command semantics. |
| cmd/localgo/cmd/version.go | Uses help system for version command help output. |
| cmd/localgo/cmd/stop.go | Adds stop command to terminate daemon via PID file. |
| cmd/localgo/cmd/stop_windows.go | Implements Windows daemon stop behavior via kill semantics. |
| cmd/localgo/cmd/stop_unix.go | Implements Unix daemon stop behavior via SIGTERM + polling + SIGKILL. |
| cmd/localgo/cmd/serve.go | Adds --daemon/-d support and daemon-child behavior tweaks. |
| cmd/localgo/cmd/send.go | Adds in-memory clipboard/stdin send path and hostname resolution fallback for --ip. |
| cmd/localgo/cmd/scan.go | Uses usable-subnet scanning instead of fixed /24 generation. |
| cmd/localgo/cmd/root.go | Adds -v shorthand and wires clipboard/notification overrides from config. |
| cmd/localgo/cmd/pid.go | Adds shared PID file path helper. |
| cmd/localgo/cmd/discover.go | Uses usable-subnet scanning for discovery fallback. |
| cmd/localgo/cmd/daemon_windows.go | Explicitly rejects daemon mode on Windows. |
| cmd/localgo/cmd/daemon_unix.go | Implements daemon forking and PID file management on Unix. |
| cmd/localgo/cmd/config.go | Fixes config-dir creation and wires help for config command. |
| .gitignore | Broadens log ignore and adjusts config/mise ignore entries. |
Comments suppressed due to low confidence (1)
pkg/server/handlers/receive_handlers.go:151
- Same issue as above: when falling back to saving clipboard text as a file, history logging and exec hooks use the raw remote alias (
requestDto.Info.Alias), which can inject escape sequences intolocalgo historyoutput. Store/pass a sanitized alias instead.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…nt, DEL sanitization, alias injection
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What's new in v0.6.4
Fixes
cmd.Output()instead ofCombinedOutput(); guard whitespace-only config splitscli.Sanitize()applied to all remote-controlled display data%f/%n/%s/%a/%inow properly replaced in hook string--ipfalls back tonet.LookupIPfor mDNS/hostnamesGetUsableSubnetIPsFromIPrespects interface netmask, caps at /22filepath.ToSlashon incoming filenamesterm.IsTerminalcheck before VT100 escape sequencesSet-Clipboardfor proper Unicode handlingFindProcessalways-succeeds behaviorNew features
serve --daemon/-dforks background;localgo stopsends SIGTERMLOCALSEND_SHELL,LOCALSEND_CLIPBOARD_WRITE_CMD/_READ_CMD,LOCALSEND_TLS_CERT/_KEY,LOCALSEND_NOTIFICATION_CMD--version/-v: now works without a subcommand;localgo help versionshows proper helpDocumentation
Full diff: v0.6.3...dev