diff --git a/acquisition/acquisition.go b/acquisition/acquisition.go index 09e1d90..a6b8dd2 100644 --- a/acquisition/acquisition.go +++ b/acquisition/acquisition.go @@ -20,7 +20,6 @@ import ( rt "github.com/botherder/go-savetime/runtime" "github.com/google/uuid" "github.com/mvt-project/androidqf/adb" - "github.com/mvt-project/androidqf/assets" "github.com/mvt-project/androidqf/log" "github.com/mvt-project/androidqf/utils" ) @@ -30,6 +29,7 @@ type Acquisition struct { UUID string `json:"uuid"` AndroidQFVersion string `json:"androidqf_version"` StoragePath string `json:"storage_path"` + BaseDir string `json:"base_dir"` Started time.Time `json:"started"` Completed time.Time `json:"completed"` Collector *adb.Collector `json:"collector"` @@ -56,6 +56,7 @@ func New(path string) (*Acquisition, error) { } else { acq.StoragePath = path } + acq.BaseDir = filepath.Dir(acq.StoragePath) // Check if the path exist stat, err := os.Stat(acq.StoragePath) if os.IsNotExist(err) { @@ -83,7 +84,7 @@ func New(path string) (*Acquisition, error) { acq.Collector = coll // Try to initialize encrypted streaming mode - encWriter, err := NewEncryptedZipWriter(acq.UUID) + encWriter, err := NewEncryptedZipWriter(acq.UUID, acq.BaseDir) if err != nil { // No key file or encryption setup failed, use normal mode log.Debug("Encrypted streaming not available, using normal mode") @@ -175,9 +176,9 @@ func (a *Acquisition) Complete() { a.Collector.Clean() } - // Stop ADB server before trying to remove extracted assets + // Stop ADB server, then clean up any temp directory used for bundled assets. adb.Client.KillServer() - assets.CleanAssets() + adb.Client.Cleanup() } func (a *Acquisition) GetSystemInformation() error { diff --git a/acquisition/encrypted_stream.go b/acquisition/encrypted_stream.go index 16ab47e..5f9fe00 100644 --- a/acquisition/encrypted_stream.go +++ b/acquisition/encrypted_stream.go @@ -16,7 +16,6 @@ import ( "time" "filippo.io/age" - saveRuntime "github.com/botherder/go-savetime/runtime" "github.com/mvt-project/androidqf/log" ) @@ -30,9 +29,8 @@ type EncryptedZipWriter struct { } // NewEncryptedZipWriter creates a new encrypted zip writer if key.txt exists -func NewEncryptedZipWriter(uuid string) (*EncryptedZipWriter, error) { - cwd := saveRuntime.GetExecutableDirectory() - keyFilePath := filepath.Join(cwd, "key.txt") +func NewEncryptedZipWriter(uuid, baseDir string) (*EncryptedZipWriter, error) { + keyFilePath := filepath.Join(baseDir, "key.txt") // Check if key file exists if _, err := os.Stat(keyFilePath); os.IsNotExist(err) { @@ -55,7 +53,7 @@ func NewEncryptedZipWriter(uuid string) (*EncryptedZipWriter, error) { // Create output file encFileName := fmt.Sprintf("%s.zip.age", uuid) - outputPath := filepath.Join(cwd, encFileName) + outputPath := filepath.Join(baseDir, encFileName) file, err := os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { diff --git a/acquisition/secure.go b/acquisition/secure.go index 9c4b9ff..cf75215 100644 --- a/acquisition/secure.go +++ b/acquisition/secure.go @@ -14,7 +14,6 @@ import ( "strings" "filippo.io/age" - saveRuntime "github.com/botherder/go-savetime/runtime" "github.com/mvt-project/androidqf/log" ) @@ -44,9 +43,7 @@ func (a *Acquisition) StoreSecurely() error { return nil } - cwd := saveRuntime.GetExecutableDirectory() - - keyFilePath := filepath.Join(cwd, "key.txt") + keyFilePath := filepath.Join(a.BaseDir, "key.txt") if _, err := os.Stat(keyFilePath); os.IsNotExist(err) { return nil } @@ -54,7 +51,7 @@ func (a *Acquisition) StoreSecurely() error { log.Info("You provided an age public key, storing the acquisition securely.") zipFileName := fmt.Sprintf("%s.zip", a.UUID) - zipFilePath := filepath.Join(cwd, zipFileName) + zipFilePath := filepath.Join(a.BaseDir, zipFileName) log.Info("Compressing the acquisition folder. This might take a while...") @@ -83,7 +80,7 @@ func (a *Acquisition) StoreSecurely() error { defer zipFile.Close() encFileName := fmt.Sprintf("%s.age", zipFileName) - encFilePath := filepath.Join(cwd, encFileName) + encFilePath := filepath.Join(a.BaseDir, encFileName) encFile, err := os.OpenFile(encFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600) if err != nil { return fmt.Errorf("unable to create encrypted file: %v", err) diff --git a/adb/adb.go b/adb/adb.go index f12076c..05b0902 100644 --- a/adb/adb.go +++ b/adb/adb.go @@ -8,6 +8,7 @@ package adb import ( "errors" "fmt" + "os" "os/exec" "strings" @@ -16,8 +17,18 @@ import ( ) type ADB struct { - ExePath string - Serial string + ExePath string + Serial string + TmpAssetsDir string +} + +// Cleanup removes the temporary directory used to store extracted adb assets, +// if one was created. It is a no-op when the system adb was used instead. +func (a *ADB) Cleanup() { + if a.TmpAssetsDir != "" { + os.RemoveAll(a.TmpAssetsDir) + a.TmpAssetsDir = "" + } } var Client *ADB diff --git a/adb/adb_darwin.go b/adb/adb_darwin.go index aff7a33..9566d1e 100644 --- a/adb/adb_darwin.go +++ b/adb/adb_darwin.go @@ -6,25 +6,35 @@ package adb import ( + "fmt" + "os" "os/exec" "path/filepath" - saveRuntime "github.com/botherder/go-savetime/runtime" "github.com/mvt-project/androidqf/assets" ) func (a *ADB) findExe() error { - err := assets.DeployAssets() + // Prefer a system-installed adb (covers distro packages where adb is on PATH). + if path, err := exec.LookPath("adb"); err == nil { + a.ExePath = path + return nil + } + + // Fall back to the bundled binary. Extract it into a temp directory so we + // never try to write next to the executable (which may be /usr/bin or + // another read-only system path). + tmpDir, err := os.MkdirTemp("", "androidqf-adb-*") if err != nil { - return err + return fmt.Errorf("failed to create temp dir for adb: %v", err) } - adbPath, err := exec.LookPath("adb") - if err == nil { - a.ExePath = adbPath - return nil - } else { - a.ExePath = filepath.Join(saveRuntime.GetExecutableDirectory(), "adb") + if err := assets.DeployAssetsToDir(tmpDir); err != nil { + os.RemoveAll(tmpDir) + return fmt.Errorf("failed to deploy bundled adb: %v", err) } + + a.ExePath = filepath.Join(tmpDir, "adb") + a.TmpAssetsDir = tmpDir return nil } diff --git a/adb/adb_linux.go b/adb/adb_linux.go index 8d2a9e7..9566d1e 100644 --- a/adb/adb_linux.go +++ b/adb/adb_linux.go @@ -6,24 +6,35 @@ package adb import ( + "fmt" + "os" "os/exec" "path/filepath" - saveRuntime "github.com/botherder/go-savetime/runtime" "github.com/mvt-project/androidqf/assets" ) func (a *ADB) findExe() error { - err := assets.DeployAssets() + // Prefer a system-installed adb (covers distro packages where adb is on PATH). + if path, err := exec.LookPath("adb"); err == nil { + a.ExePath = path + return nil + } + + // Fall back to the bundled binary. Extract it into a temp directory so we + // never try to write next to the executable (which may be /usr/bin or + // another read-only system path). + tmpDir, err := os.MkdirTemp("", "androidqf-adb-*") if err != nil { - return err + return fmt.Errorf("failed to create temp dir for adb: %v", err) } - adbPath, err := exec.LookPath("adb") - if err == nil { - a.ExePath = adbPath - } else { - a.ExePath = filepath.Join(saveRuntime.GetExecutableDirectory(), "adb") + if err := assets.DeployAssetsToDir(tmpDir); err != nil { + os.RemoveAll(tmpDir) + return fmt.Errorf("failed to deploy bundled adb: %v", err) } + + a.ExePath = filepath.Join(tmpDir, "adb") + a.TmpAssetsDir = tmpDir return nil } diff --git a/adb/adb_windows.go b/adb/adb_windows.go index 3fc861f..4ecd9a1 100644 --- a/adb/adb_windows.go +++ b/adb/adb_windows.go @@ -7,6 +7,7 @@ package adb import ( "errors" + "fmt" "os" "os/exec" "path/filepath" @@ -16,28 +17,34 @@ import ( ) func (a *ADB) findExe() error { - // TODO: only deploy assets when needed - err := assets.DeployAssets() + // Prefer a system-installed adb (covers distro packages where adb is on PATH). + if path, err := exec.LookPath("adb.exe"); err == nil { + a.ExePath = path + return nil + } + + // Fall back to the bundled binary. Extract it (and the required DLLs) into + // a temp directory so we never try to write next to the executable (which + // may be a read-only system path). + tmpDir, err := os.MkdirTemp("", "androidqf-adb-*") if err != nil { - return err + return fmt.Errorf("failed to create temp dir for adb: %v", err) } - adbPath, err := exec.LookPath("adb.exe") - if err == nil { - a.ExePath = adbPath - } else { - // Get path of the current directory - ex, err := os.Executable() - if err != nil { - return err - } - // Need full path to bypass go 1.19 restrictions about local path - a.ExePath = filepath.Join(filepath.Dir(ex), "adb.exe") - _, err = os.Stat(a.ExePath) - if err != nil { - log.Debugf("ADB doesn't exist at %s", a.ExePath) - return errors.New("Impossible to find ADB") - } + if err := assets.DeployAssetsToDir(tmpDir); err != nil { + os.RemoveAll(tmpDir) + return fmt.Errorf("failed to deploy bundled adb: %v", err) } + + // Need full path to bypass Go 1.19+ restrictions about relative executable paths. + exePath := filepath.Join(tmpDir, "adb.exe") + if _, err := os.Stat(exePath); err != nil { + os.RemoveAll(tmpDir) + log.Debugf("ADB doesn't exist at %s", exePath) + return errors.New("impossible to find ADB") + } + + a.ExePath = exePath + a.TmpAssetsDir = tmpDir return nil } diff --git a/adb/collector.go b/adb/collector.go index 7baedd3..874e48a 100644 --- a/adb/collector.go +++ b/adb/collector.go @@ -14,7 +14,6 @@ import ( "strings" "github.com/mvt-project/androidqf/log" - "github.com/mvt-project/androidqf/assets" ) @@ -112,13 +111,32 @@ func (c *Collector) Install() error { } log.Debugf("Deploying collector binary '%s' for architecture '%s'.", collectorName, c.Architecture) - collectorBinary, err := assets.Collector.ReadFile(collectorName) - if err != nil { - // Somehow the file doesn't exist - return errors.New("couldn't find the collector binary") + + // If the caller has pointed us at a directory of pre-built collector + // binaries (e.g. a distro package placing them under + // /usr/lib/androidqf/android-collector/), use those in preference to the + // embedded assets. This lets packagers ship the collectors separately + // without patching the source, while portable-binary users get the + // embedded fallback automatically. + var collectorBinary []byte + if collectorDir := os.Getenv("ANDROIDQF_COLLECTOR_DIR"); collectorDir != "" { + data, readErr := os.ReadFile(filepath.Join(collectorDir, collectorName)) + if readErr == nil { + collectorBinary = data + log.Debugf("Using collector from ANDROIDQF_COLLECTOR_DIR: %s", collectorDir) + } else { + log.Debugf("ANDROIDQF_COLLECTOR_DIR set but could not read collector: %v — falling back to embedded", readErr) + } + } + if len(collectorBinary) == 0 { + var err error + collectorBinary, err = assets.Collector.ReadFile(collectorName) + if err != nil { + return errors.New("couldn't find the collector binary") + } } - collectorTemp, _ := os.CreateTemp("", "collector_") + collectorTemp, err := os.CreateTemp("", "collector_") if err != nil { return err } diff --git a/assets/assets.go b/assets/assets.go index 637991a..bcc98f4 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -10,8 +10,6 @@ import ( "errors" "os" "path/filepath" - - saveRuntime "github.com/botherder/go-savetime/runtime" ) //go:embed collector_* @@ -22,55 +20,35 @@ type Asset struct { Data []byte } -// DeployAssets is used to retrieve the embedded adb binaries and store them. -func DeployAssets() error { - cwd := saveRuntime.GetExecutableDirectory() - +// DeployAssetsToDir extracts the embedded adb binaries into the given directory. +// If a file already exists there it is silently skipped, so calling this +// function more than once (or concurrently) is safe. +func DeployAssetsToDir(dir string) error { for _, asset := range getAssets() { - assetPath := filepath.Join(cwd, asset.Name) + assetPath := filepath.Join(dir, asset.Name) - // If the file already exists, skip it. This avoids failing when adb - // is already deployed or in use by another process. + // Already present – skip without error. if _, err := os.Stat(assetPath); err == nil { continue } else if !os.IsNotExist(err) { - // Can't determine file existence (e.g., permission error); skip deploying this asset. + // Permission or other stat error – skip this asset rather than abort. continue } - // Try to create the asset file. If creation fails (for example because - // the file was created between the Stat and OpenFile calls, or because - // the file is locked by another process), skip the asset instead of failing. - assetFile, err := os.OpenFile(assetPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o755) + // O_EXCL ensures we don't clobber a file created between Stat and here. + f, err := os.OpenFile(assetPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o755) if err != nil { - // If the file exists now, just continue; otherwise skip this asset. if errors.Is(err, os.ErrExist) { continue } - // Could be locked or another transient error — do not fail the whole deployment. + // Transient error (e.g. locked) – skip rather than abort. continue } - // Write and close immediately (avoid defer in a loop). - _, err = assetFile.Write(asset.Data) - assetFile.Close() - if err != nil { - return err - } - } - - return nil -} - -// Remove assets from the local disk -func CleanAssets() error { - cwd := saveRuntime.GetExecutableDirectory() - - for _, asset := range getAssets() { - assetPath := filepath.Join(cwd, asset.Name) - err := os.Remove(assetPath) - if err != nil { - return err + _, writeErr := f.Write(asset.Data) + f.Close() + if writeErr != nil { + return writeErr } }