feat: xxbb
This commit is contained in:
@@ -11,6 +11,18 @@ class RawLogController extends Controller
|
||||
/** @var list<string> */
|
||||
private const KINDS = ['c2', 'xxbb'];
|
||||
|
||||
private const PER_PAGE = 40;
|
||||
|
||||
private const MAX_PER_PAGE = 100;
|
||||
|
||||
/** Display cap so a single keystore/census blob cannot blow the HTML layout. */
|
||||
private const MAX_ENTRY_CHARS = 16384;
|
||||
|
||||
private const CHUNK_BYTES = 262144;
|
||||
|
||||
/** Bound the backward-scan carry if a record has no separator. */
|
||||
private const MAX_CARRY_BYTES = 524288;
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$kind = $this->kind($request);
|
||||
@@ -37,30 +49,23 @@ class RawLogController extends Controller
|
||||
|
||||
$path = trim((string) $request->query('path', ''));
|
||||
$device = trim((string) $request->query('device', ''));
|
||||
$raw = (string) file_get_contents($full);
|
||||
$chunks = preg_split("/\r\n\r\n/", $raw) ?: [];
|
||||
$entries = [];
|
||||
foreach ($chunks as $chunk) {
|
||||
$chunk = trim($chunk);
|
||||
if ($chunk === '') {
|
||||
continue;
|
||||
}
|
||||
if ($path !== '' && ! str_contains($chunk, $path)) {
|
||||
continue;
|
||||
}
|
||||
if ($device !== '' && ! str_contains($chunk, $device)) {
|
||||
continue;
|
||||
}
|
||||
$entries[] = $chunk;
|
||||
}
|
||||
$entries = array_reverse($entries);
|
||||
$page = max(1, (int) $request->query('page', 1));
|
||||
$perPage = (int) $request->query('per_page', self::PER_PAGE);
|
||||
$perPage = min(self::MAX_PER_PAGE, max(10, $perPage));
|
||||
|
||||
$read = $this->tailEntries($full, $path, $device, $page, $perPage);
|
||||
|
||||
return view('admin.logs.show', [
|
||||
'kind' => $kind,
|
||||
'file' => $safe,
|
||||
'entries' => $entries,
|
||||
'entries' => $read['entries'],
|
||||
'path' => $path,
|
||||
'device' => $device,
|
||||
'page' => $read['page'],
|
||||
'perPage' => $read['perPage'],
|
||||
'hasOlder' => $read['hasOlder'],
|
||||
'hasNewer' => $read['hasNewer'],
|
||||
'fileSize' => $read['fileSize'],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -71,6 +76,112 @@ class RawLogController extends Controller
|
||||
return in_array($kind, self::KINDS, true) ? $kind : 'c2';
|
||||
}
|
||||
|
||||
/**
|
||||
* Newest-first page of records, scanning backward so a multi-MB day file
|
||||
* never lands in memory as one string.
|
||||
*
|
||||
* @return array{
|
||||
* entries: list<string>,
|
||||
* page: int,
|
||||
* perPage: int,
|
||||
* hasOlder: bool,
|
||||
* hasNewer: bool,
|
||||
* fileSize: int
|
||||
* }
|
||||
*/
|
||||
private function tailEntries(string $full, string $path, string $device, int $page, int $perPage): array
|
||||
{
|
||||
$fileSize = (int) filesize($full);
|
||||
$skip = ($page - 1) * $perPage;
|
||||
$want = $skip + $perPage + 1;
|
||||
$matched = [];
|
||||
|
||||
$fh = fopen($full, 'rb');
|
||||
if ($fh === false) {
|
||||
return [
|
||||
'entries' => [],
|
||||
'page' => $page,
|
||||
'perPage' => $perPage,
|
||||
'hasOlder' => false,
|
||||
'hasNewer' => $page > 1,
|
||||
'fileSize' => $fileSize,
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$pos = $fileSize;
|
||||
$carry = '';
|
||||
while ($pos > 0 && count($matched) < $want) {
|
||||
$read = (int) min(self::CHUNK_BYTES, $pos);
|
||||
$pos -= $read;
|
||||
if (fseek($fh, $pos) !== 0) {
|
||||
break;
|
||||
}
|
||||
$chunk = fread($fh, $read);
|
||||
if (! is_string($chunk) || $chunk === '') {
|
||||
break;
|
||||
}
|
||||
$data = $chunk.$carry;
|
||||
if (strlen($data) > self::MAX_CARRY_BYTES) {
|
||||
$data = substr($data, -self::MAX_CARRY_BYTES);
|
||||
}
|
||||
$parts = preg_split("/\r\n\r\n/", $data) ?: [$data];
|
||||
$carry = $parts[0];
|
||||
for ($i = count($parts) - 1; $i >= 1; $i--) {
|
||||
if ($this->collectEntry($matched, $parts[$i], $path, $device, $want)) {
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (count($matched) < $want && $pos === 0 && $carry !== '') {
|
||||
$this->collectEntry($matched, $carry, $path, $device, $want);
|
||||
}
|
||||
} finally {
|
||||
fclose($fh);
|
||||
}
|
||||
|
||||
$hasOlder = count($matched) > $skip + $perPage;
|
||||
$entries = array_slice($matched, $skip, $perPage);
|
||||
|
||||
return [
|
||||
'entries' => $entries,
|
||||
'page' => $page,
|
||||
'perPage' => $perPage,
|
||||
'hasOlder' => $hasOlder,
|
||||
'hasNewer' => $page > 1,
|
||||
'fileSize' => $fileSize,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $matched
|
||||
*/
|
||||
private function collectEntry(array &$matched, string $chunk, string $path, string $device, int $want): bool
|
||||
{
|
||||
$entry = trim($chunk);
|
||||
if ($entry === '') {
|
||||
return false;
|
||||
}
|
||||
if ($path !== '' && ! str_contains($entry, $path)) {
|
||||
return false;
|
||||
}
|
||||
if ($device !== '' && ! str_contains($entry, $device)) {
|
||||
return false;
|
||||
}
|
||||
$matched[] = $this->truncateEntry($entry);
|
||||
|
||||
return count($matched) >= $want;
|
||||
}
|
||||
|
||||
private function truncateEntry(string $entry): string
|
||||
{
|
||||
if (strlen($entry) <= self::MAX_ENTRY_CHARS) {
|
||||
return $entry;
|
||||
}
|
||||
|
||||
return substr($entry, 0, self::MAX_ENTRY_CHARS)."\n… (truncated)";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{name: string, size: int, mtime: string}>
|
||||
*/
|
||||
|
||||
@@ -64,7 +64,8 @@ class XxbbC2Controller extends Controller
|
||||
$ua = substr((string) $request->userAgent(), 0, 512);
|
||||
$parsed = UserAgentParser::parse($ua);
|
||||
$osVersion = $parsed['os_version'] !== '' ? $parsed['os_version'] : null;
|
||||
if ($deviceVersion !== '' && preg_match('/(\d+(?:\.\d+){0,3})/', $deviceVersion, $m)) {
|
||||
// Landing JS often sends major.minor only (`iOS 15.8`); UA has the patch (`15_8_4`).
|
||||
if ($osVersion === null && $deviceVersion !== '' && preg_match('/(\d+(?:\.\d+){0,3})/', $deviceVersion, $m)) {
|
||||
$osVersion = $m[1];
|
||||
}
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user