'HTTPS required']); exit; } header('Content-Type: application/json'); // This endpoint is for native clients, never a browser page: no CORS, no cache. header('Cache-Control: no-store'); header('X-Content-Type-Options: nosniff'); // --------------------------------------------------------------------------- // Authentication // --------------------------------------------------------------------------- /** Bearer token, compared in constant time so a wrong guess leaks no timing. */ function client_token(): string { $h = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? ''; if (stripos($h, 'Bearer ') === 0) return substr($h, 7); return ''; } if (!hash_equals(SYNC_TOKEN, client_token())) { // 404, not 401: an unauthenticated caller learns nothing about what lives // here — same reasoning as the desktop server's loopback lockdown. http_response_code(404); echo json_encode(['error' => 'Not found']); exit; } // --------------------------------------------------------------------------- // Routing // --------------------------------------------------------------------------- $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; if ($method === 'HEAD' || $method === 'GET') { if (!is_file(SYNC_FILE)) { // 404 = "no snapshot yet". The desktop client already treats this as // "first sync, remote is empty" rather than an error. http_response_code(404); echo json_encode(['error' => 'No snapshot yet']); exit; } $blob = file_get_contents(SYNC_FILE); if ($blob === false) { http_response_code(500); echo json_encode(['error' => 'Read failed']); exit; } // Strong ETag over the bytes. The write path will require If-Match against // it, which is what stops two devices from clobbering each other. $etag = '"' . hash('sha256', $blob) . '"'; header('ETag: ' . $etag); header('Last-Modified: ' . gmdate('D, d M Y H:i:s', (int)filemtime(SYNC_FILE)) . ' GMT'); // Save the round trip when the client already holds this version. if (trim($_SERVER['HTTP_IF_NONE_MATCH'] ?? '') === $etag) { http_response_code(304); exit; } header('Content-Length: ' . strlen($blob)); if ($method === 'HEAD') exit; echo $blob; exit; } // Everything else — including PUT — is not implemented yet. http_response_code(405); header('Allow: GET, HEAD'); echo json_encode(['error' => 'Read-only endpoint']);