fix(cloud): logout order, url normalization, length caps, package hygiene (P4-9/10/11, P5-6)
- P4-9 [L] logout() rotates the session id before wiping (was regenerate-after-destroy → warning). - P4-10 [L] EventValidator caps device_id/camera_id (64) + zone_id (32) → 400 not a MySQL 'Data too long' 500 the edge retries forever. - P4-11 [L] Request::pathFor collapses leading slashes so '//api/events' isn't misrouted to '/events'. - P5-6 [L] cafe24 build prunes dev caches (.ruff_cache/.impeccable/__pycache__/…) from the package. python + phpunit (86) green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
This commit is contained in:
parent
36b1f94571
commit
c66b034921
7 changed files with 52 additions and 5 deletions
|
|
@ -152,6 +152,13 @@ cp "$CLOUD/deploy/preflight.php" "$WEB/_preflight.php"
|
|||
cp "$CLOUD/deploy/user.ini" "$WEB/.user.ini"
|
||||
cp "$CLOUD/deploy/README-UPLOAD.md" "$STAGE/README-UPLOAD.md"
|
||||
|
||||
# `cp -r` above copies whatever is in cloud/src|public — defensively strip dev/tooling caches
|
||||
# so a stray one (e.g. a tool that ran in the tree) can never ship inside the web-served
|
||||
# package. Legit dotfiles (.htaccess/.user.ini/.gitkeep) are named explicitly, not globbed.
|
||||
find "$WEB" "$PRIV" -depth \( -name '.ruff_cache' -o -name '.impeccable' -o -name '__pycache__' \
|
||||
-o -name '.pytest_cache' -o -name '.mypy_cache' -o -name '.DS_Store' \
|
||||
-o -name '.phpunit.result.cache' -o -name 'Thumbs.db' \) -exec rm -rf {} + 2>/dev/null || true
|
||||
|
||||
# storage/snapshots must exist and be writable. Nothing in src/ ever creates it, and the
|
||||
# failure is silent rather than loud: IngestController does
|
||||
# `if (file_put_contents(...) === false) continue;`, so a missing dir means the snapshot is
|
||||
|
|
|
|||
|
|
@ -67,8 +67,11 @@ final class AuthController
|
|||
|
||||
public function logout(SessionStore $sess): Response
|
||||
{
|
||||
$sess->clear();
|
||||
// Rotate the id FIRST (invalidates the old cookie) while the session is still active,
|
||||
// then wipe. The old order called session_regenerate_id() AFTER session_destroy(),
|
||||
// which warns on an inactive session and leaves the old cookie unreplaced.
|
||||
$sess->regenerate();
|
||||
$sess->clear();
|
||||
return Response::json(['ok' => true], 200);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ final class Request
|
|||
*/
|
||||
public static function pathFor(string $requestUri, string $base): string
|
||||
{
|
||||
// Collapse leading slashes FIRST: parse_url('//api/events') reads 'api' as a
|
||||
// scheme-relative host and returns '/events', silently dropping the first segment.
|
||||
$requestUri = '/' . ltrim($requestUri, '/');
|
||||
$path = parse_url($requestUri, PHP_URL_PATH) ?: '/';
|
||||
if ($base !== '' && ($path === $base || str_starts_with($path, $base . '/'))) {
|
||||
// Match $base exactly or as a whole segment. A bare str_starts_with($path, $base)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,13 @@ final class EventValidator
|
|||
if (isset($p['zone_id']) && !is_string($p['zone_id'])) $errors[] = 'bad:zone_id';
|
||||
if (isset($p['duration_s']) && !is_numeric($p['duration_s'])) $errors[] = 'bad:duration_s';
|
||||
|
||||
// Length caps matching the DB columns (device_id/camera_id VARCHAR(64), zone_id
|
||||
// CHAR(32)). Without these an overlong id is a MySQL 'Data too long' 500 that the edge
|
||||
// re-queues and retries forever; a 400 lets it drop the poison event instead.
|
||||
foreach (['device_id' => 64, 'camera_id' => 64, 'zone_id' => 32] as $k => $max) {
|
||||
if (isset($p[$k]) && is_string($p[$k]) && mb_strlen($p[$k]) > $max) $errors[] = "too_long:$k";
|
||||
}
|
||||
|
||||
if ($errors) return ['ok' => false, 'errors' => $errors, 'clean' => []];
|
||||
|
||||
$clean = [
|
||||
|
|
|
|||
|
|
@ -75,6 +75,15 @@ final class BasePathTest extends TestCase
|
|||
$this->assertSame(['id' => '42'], json_decode($r->dispatch($detail)->body(), true));
|
||||
}
|
||||
|
||||
public function testDoubleSlashUriIsNotMisrouted(): void
|
||||
{
|
||||
// A URI like '//api/events' makes parse_url treat 'api' as a host (scheme-relative),
|
||||
// returning path '/events' — so '//api/events' would silently route to '/events'.
|
||||
// Leading slashes must collapse to a single one.
|
||||
$this->assertSame('/api/events', Request::pathFor('//api/events', ''));
|
||||
$this->assertSame('/api/events', Request::pathFor('///api/events?x=1', ''));
|
||||
}
|
||||
|
||||
public function testRouteParamsAreRawUrlDecoded(): void
|
||||
{
|
||||
// A camera_id with a space or Korean characters is %-encoded in the URL. Without
|
||||
|
|
|
|||
|
|
@ -15,6 +15,19 @@ final class EventValidatorTest extends TestCase
|
|||
$this->assertTrue($r['ok'], implode(',', $r['errors']));
|
||||
$this->assertSame('fall', $r['clean']['event_type']);
|
||||
}
|
||||
public function testRejectsOverlongIds(): void {
|
||||
// An overlong id must be a 400 (invalid), not a MySQL 'Data too long' 500 that the edge
|
||||
// then retries forever. Columns: device_id/camera_id VARCHAR(64), zone_id CHAR(32).
|
||||
$long = $this->valid(); $long['device_id'] = str_repeat('x', 65);
|
||||
$this->assertFalse(EventValidator::validate($long)['ok']);
|
||||
$cam = $this->valid(); $cam['camera_id'] = str_repeat('y', 65);
|
||||
$this->assertFalse(EventValidator::validate($cam)['ok']);
|
||||
$zone = $this->valid(); $zone['zone_id'] = str_repeat('z', 33);
|
||||
$this->assertFalse(EventValidator::validate($zone)['ok']);
|
||||
// a normal-length id still passes
|
||||
$ok = $this->valid(); $ok['device_id'] = str_repeat('x', 64);
|
||||
$this->assertTrue(EventValidator::validate($ok)['ok']);
|
||||
}
|
||||
public function testRejectsUnknownEventType(): void {
|
||||
$p = $this->valid(); $p['event_type'] = 'explode';
|
||||
$this->assertFalse(EventValidator::validate($p)['ok']);
|
||||
|
|
|
|||
|
|
@ -169,16 +169,19 @@ misconfig/degradation fail LOUD.
|
|||
order write→insert, log failures, reconcile orphans.
|
||||
- [ ] **P4-8 [L]** `cloud/src/Auth/AuthController.php:31` — login form emits a CSRF token that
|
||||
`login` never checks. **Fix:** verify it (or drop it).
|
||||
- [ ] **P4-9 [L]** `cloud/src/Auth/AuthController.php:71` — logout calls `regenerate()` after
|
||||
- [x] **P4-9 [L]** `cloud/src/Auth/AuthController.php:71` — logout calls `regenerate()` after
|
||||
`session_destroy()` → E_WARNING, old cookie not replaced. **Fix:** reorder.
|
||||
- [ ] **P4-10 [L]** `cloud/src/Ingest/EventValidator.php:26` — no string length caps → an
|
||||
DONE — logout() rotates the id then wipes (was regenerate-after-destroy).
|
||||
- [x] **P4-10 [L]** `cloud/src/Ingest/EventValidator.php:26` — no string length caps → an
|
||||
overlong id becomes a MySQL 'Data too long' 500 retry loop instead of a 400. **Fix:** cap
|
||||
lengths in the validator.
|
||||
- [ ] **P4-11 [L]** `cloud/src/Http/Request.php:45` — `pathFor` routes any `//...` URI to `/`
|
||||
DONE — EventValidator caps device_id/camera_id (64) + zone_id (32); EventValidatorTest.
|
||||
- [x] **P4-11 [L]** `cloud/src/Http/Request.php:45` — `pathFor` routes any `//...` URI to `/`
|
||||
(parse_url scheme-relative). **Fix:** collapse/normalize.
|
||||
|
||||
## Phase 5 — Install / bundle / supply-chain
|
||||
|
||||
DONE — pathFor collapses leading slashes before parse_url; BasePathTest locks it.
|
||||
- [x] **P5-1 [B]** `deploy/bundle/install_offline.sh:79` — the OFFLINE installer has no
|
||||
backend-usability gate (setup_online's [6/8] does): it installs `POSE_BACKEND=movenet`
|
||||
verbatim even when the bundle carries no working backend. **Fix:** mirror the [6/8] probe.
|
||||
|
|
@ -197,7 +200,7 @@ misconfig/degradation fail LOUD.
|
|||
`/dev/media*` and `/dev/dma_heap`; picamera2/libcamera on Pi 5 needs them, so the CSI camera
|
||||
is denied by the unit's own policy. **Fix:** add the device classes; verify on hardware.
|
||||
DONE — added `char-media` + `/dev/dma_heap/`. OFF-HW: confirm on the Pi with the CSI camera.
|
||||
- [ ] **P5-6 [L]** `cloud/deploy/build_cafe24_package.sh` — dev cache dirs
|
||||
- [x] **P5-6 [L]** `cloud/deploy/build_cafe24_package.sh` — dev cache dirs
|
||||
(`.impeccable/`, `.ruff_cache/`) can ship inside the web-served package. **Fix:** exclude.
|
||||
|
||||
---
|
||||
|
|
@ -212,3 +215,5 @@ misconfig/degradation fail LOUD.
|
|||
tuning — needs on-device validation), Phase 3 (privacy — P3-2 retention needs a policy
|
||||
decision: how many days), Phase 4 (cloud hardening, 11 items — mostly clear), Phase 5 (P5-2..6).
|
||||
Nothing committed yet.
|
||||
|
||||
DONE — build prunes .ruff_cache/.impeccable/__pycache__/etc from the staged package.
|
||||
Loading…
Reference in a new issue