Logic Code check_domain_type.ts (Check mainsite/satellite)
Tài liệu logic code: check_domain_type.ts#
1. Mục tiêu của hệ thống#
Dùng Puppeteer (trình duyệt tự động) để mở một domain, phân tích trang web và phân loại nó thành một trong 3 loại:| Loại | Ý nghĩa | Ví dụ thực tế |
|---|
mainsite | Website chính — có form đăng ký / đăng nhập thật, hoặc là game canvas thật | game88.com (có form login), game-pixi.com (canvas game) |
satellite | Website vệ tinh — landing page có nút "mồi" dẫn người dùng sang mainsite | landing123.com (chỉ có nút "Đăng ký ngay") |
unknown | Không xác định được — nước ngoài, social, lỗi | facebook.com, casino-en.eu |
Hàm entry point duy nhất:
2. Toàn bộ luồng xử lý — 6 bước#
URL đầu vào
│
▼
[BƯỚC 1] Kiểm tra blacklist nhanh
│ Domain thuộc facebook/google/cloudflare/... → return "unknown" NGAY (không mở browser)
│
▼
[BƯỚC 2] Mở browser Puppeteer + visitPage (retry tối đa 2 lần)
│ Thất bại hoàn toàn → return Error
│
▼
[BƯỚC 3] Dynamic Polling — chờ trang render (tối đa 15s, poll mỗi 500ms)
│ Dừng sớm khi đạt "Double-Gate": Keywords + (Inputs hoặc Canvas)
│ Thu thập localRoots từ iframe (dùng để phân biệt redirect nội bộ sau này)
│
▼
[BƯỚC 4] Kiểm tra tiếng Việt (3 tín hiệu)
│ Không phải tiếng Việt → return "unknown" NGAY
│
▼
[BƯỚC 5] CTA Scan Loop (vòng lặp chính, tối đa 4 lần nhảy domain)
│ ├─ Đánh giá trang A ban đầu (aStatus)
│ ├─ Đóng popup tự động
│ ├─ Tìm Auth CTA ưu tiên → click → theo dõi redirect
│ ├─ Nếu không có Auth CTA → quét tất cả CTA → click lần lượt
│ └─ Nếu redirect sang domain mới → nhảy sang domain đó (hopCount++)
│
▼
[BƯỚC 6] Rule A/B — phán loại cuối cùng
│
▼
return { domainType, domainRedirect, statusCode }
3. Bước 1 — Kiểm tra blacklist nhanh#
Trước khi tốn tài nguyên mở browser, kiểm tra ngay xem domain có thuộc danh sách bị bỏ qua không.CTA_EXTERNAL_IGNORE_HOSTS — IP/hostname bị ignore hoàn toàn:CTA_EXTERNAL_IGNORE_ROOTS — Root domain bị ignore:facebook.com, fb.com, instagram.com, twitter.com, x.com,
youtube.com, youtu.be, t.me, telegram.org, zalo.me,
line.me, linkedin.com, tiktok.com, discord.com, discord.gg,
whatsapp.com, wa.me, google.com, apple.com, cloudflare.com,
lc.chat, snapchat.com
Muốn thêm domain mới vào blacklist: Bổ sung vào CTA_EXTERNAL_IGNORE_ROOTS trong file.
Helper functions liên quan:normalizeDomainForType(input) — Làm sạch URL về hostname thuần:"https://www.GAME88.com/path?q=1" → "game88.com"
Loại bỏ: https://, www., /path, ?query, #hash, :port → lowercase
getRegistrableDomain(host) — Lấy "root domain" để so sánh:"sub.game88.com" → "game88.com" (lấy 2 phần cuối)
"sub.game.com.vn" → "game.com.vn" (com.vn là ccSLD → lấy 3 phần)
"192.168.1.1" → "192.168.1.1" (IP giữ nguyên)
Lưu ý COMMON_SLD_SUFFIXES: Set chứa các ccSLD như com.vn, co.uk, com.au... Nếu thiếu một ccSLD, root sẽ bị tính sai và phán loại nhầm. Muốn thêm quốc gia mới → thêm vào Set này.
4. Bước 2 — Mở browser#
Retry tối đa 2 lần nếu visitPage() bị lỗi
Nếu URL sau visit là about:blank hoặc chrome-error:// → coi là thất bại, retry
Nếu visitPage() có throw exception nhưng URL đã load được (timeout nhẹ) → vẫn tiếp tục
5. Bước 3 — Dynamic Polling (chờ trang render)#
Sau khi trang load, đợi thêm 3 giây cơ bản rồi bắt đầu poll mỗi 500ms, tối đa 15 giây.Mỗi lần poll, đọc từ TOÀN BỘ frames (main + iframes):| Signal | Cách lấy |
|---|
hasKeywords | Body text chứa "dang ky" / "dang ki" / "dang nhap" (đã normalize tiếng Việt) |
visibleInputCount | Số <input> thực sự hiển thị (loại hidden, submit, button, radio, checkbox) |
hasCanvas | Có <canvas> hoặc #Cocos2dGameContainer / #GameCanvas |
Điều kiện dừng sớm (Double-Gate):CÓ keywords VÀ (inputs > 0 HOẶC canvas) → BREAK ngay lập tức
CÓ keywords VÀ đã chờ >= 4s (Grace Period) → BREAK (SPA có thể chậm)
Hết 15s → BREAK dù chưa thoả điều kiện
Thu thập localRoots: Trong lúc poll, nếu iframe nào có domain khác root chính → add vào localRoots. Dùng sau này để biết redirect sang domain đó có phải "nội bộ" không.
6. Bước 4 — Kiểm tra tiếng Việt#
Chạy trong browser context, 3 tín hiệu theo thứ tự từ nhanh đến chậm:Tín hiệu 1 (O(1), nhanh nhất):
html[lang="vi"] HOẶC <meta http-equiv="content-language" content="vi">
→ Có → tiếng Việt, tiếp tục
Tín hiệu 2 (metadata):
document.title + meta[description] + meta[keywords]
→ Có ký tự tiếng Việt (àáảã...) HOẶC chứa "dang ky"/"dang nhap"
→ Có → tiếng Việt, tiếp tục
Tín hiệu 3 (body text):
body.textContent < 50 ký tự → coi là tiếng Việt (page chưa load đủ)
body có "dang ky"/"dang nhap"/"tai khoan" → tiếng Việt
body có >= 3 ký tự tiếng Việt (pattern regex) → tiếng Việt
Không đủ → return "unknown", kết thúc
7. Bước 5 — CTA Scan Loop (phần phức tạp nhất)#
7a. Biến trạng thái quan trọng#
| Biến | Kiểu | Ý nghĩa |
|---|
originalRoot | string | Root domain của URL input ban đầu |
postRedirectRoot | string | Root domain sau khi trang tự redirect (nếu có) |
activeRoot | string | Root domain của trang đang xem hiện tại |
aStatus | "mainsite"|"satellite" | Đánh giá ban đầu trang A trước khi click |
resolvedDomainType | DomainType | Kết quả tạm thời sau khi click |
jumpedRoot | string|null | Root domain đích nếu CTA dẫn sang domain khác |
jumpedUrl | string|null | URL đích đầy đủ |
hopCount | number | Số lần đã nhảy domain (tối đa 4) |
authCheckCompleted | boolean | Flag để thoát vòng lặp |
skippedExternalCtaSignatures | Set<string> | Signature các CTA đã bỏ qua (dẫn đến social) |
7b. Phát hiện game canvas (getCanvasLoaderInfo)#
Hàm này quét main frame rồi các iframe, trả về thông tin game engine:Cocos2D → window.cc / window.CocosEngine / #Cocos2dGameContainer / #GameCanvas
Unity → window.UnityLoader / canvas#unity-canvas
Pixi.js → window.PIXI
Script → <script src> chứa "cocos2d*.js" / "unity*.js" / "pixi*.js" / "phaser*.js"
Canvas → <canvas> width > 300 AND height > 300
Splash → #splash element đang hiển thị
waitForLoaderProgress: Nếu loader đang ở "0%":Chờ thêm tối đa 25s (poll 1s)
→ splash biến mất / text đổi / hết 0% → dừng
→ Vẫn 0% sau 25s → set isCanvasLoaderStuck = true
isCanvasLoaderStuck = true → isCanvasLikelyMainsite() sẽ trả về true → coi là mainsite.7c. Đánh giá trang A ban đầu (evaluateCurrentPage)#
Có Auth CTA VÀ inputs > 0 → mainsite
Có game engine mạnh (Cocos/Unity/Pixi) VÀ Auth CTA → mainsite
Có game engine mạnh VÀ KHÔNG có Auth CTA:
Body KHÔNG có auth keywords → "Pure Game Canvas" → mainsite
Body CÓ auth keywords → Hybrid site → để CTA scan quyết định
isCanvasLoaderStuck = true → mainsite
Không đủ điều kiện → satellite (tạm thời, chưa phán cuối)
Kết quả lưu vào aStatus — dùng sau ở Rule A/B.Pure Game Canvas: Cocos2D/Unity/Pixi render toàn bộ UI trong canvas. Không có DOM auth, không có body keywords. Phán mainsite ngay, không cần click CTA.
Chạy một lần trước vòng lặp:2.
Tìm button Close theo CSS selector (.close-btn, .btn-close, .modal-close, [aria-label="Close"], .ant-modal-close...)
3.
Tìm theo text: "đóng", "dong", "close", "bỏ qua", "x"
4.
Tìm theo class .close trên button, a, i, img
5.
Chỉ click nếu element hoàn toàn nằm trong viewport
7e. Vòng lặp CTA chính#
Ưu tiên 1: Tìm Auth CTA chính xác (getExactAuthHandle)Tìm element có text khớp chính xác (sau normalize NFD) với:"dang ky" / "dang ki" / "dang nhap" / "choi ban web" / "choi ngay" / "vao game" / "web" / "choi"
Scoring để chọn element tốt nhất trong nhiều candidate:+12 isTopMost: element không bị che phủ tại tọa độ trung tâm
-5 bị element khác che phủ
+5 cursor: pointer
+4 tag là <a>, <button>, hoặc <input>
+3 class chứa "btn" hoặc "button"
+6 role="button"
+6 text khớp chính xác với auth keywords
Hỗ trợ Shadow DOM: Scan đệ quy vào shadowRoot của mọi element (Web Components, LitElement...).Hỗ trợ iframe: Kiểm tra từng iframe, bỏ qua iframe không visible trên viewport.skippedSignatures: Bỏ qua các Auth CTA đã từng bị skip (signature = tag|text|x|y|href).
Ưu tiên 2: Quét tất cả CTA trong viewport (nếu không có Auth CTA)Lấy tối đa 10 candidates, sort theo:1.
CTA có "play" / "playonweb" trong text/href → lên đầu
2.
Theo vị trí: từ trên xuống, từ trái sang phải
Điều kiện một element được coi là CTA:Tag: a, button, ion-button, input
HOẶC role="button"
HOẶC có onclick / ng-click / @click / v-on:click / ui-sref / routerlink
HOẶC class chứa "btn" / "button"
HOẶC cursor: pointer
VÀ phải visible (display≠none, visibility≠hidden, opacity>0, không disabled)
VÀ phải nằm trong viewport
VÀ href không phải mailto: / tel: / dẫn đến social
7f. Sau khi click một CTA — theo dõi redirect#
Trước khi click: Patch window.open, location.assign, location.replace để capture URL:Click element (triggerElementClick) — 3 tầng fallback:Tầng 1: JS dispatchEvent (mousedown → mouseup → click) với composed:true
→ Bypass z-index overlay, xuyên Shadow DOM
Tầng 2: elementHandle.click({ delay: 40ms })
→ Puppeteer chuẩn, có delay giả lập human
Tầng 3: Mouse coordinates tại bounding box center
→ Fallback cuối cùng
Thu thập URL sau click từ 3 nguồn (ưu tiên theo thứ tự):1. window.__ctaRedirect.url → JS redirect (assign/replace/open)
2. browser.getLastPopupUrl() → Tab/popup mới (target="_blank", window.open)
3. waitForStableUrl() → URL tab hiện tại sau khi ổn định
waitForStableUrl(initialUrl) — Chờ URL ổn định sau redirect:Tối đa 20s, poll 400ms
Phải quan sát ít nhất 4.5s (minObserveMs) trước khi kết luận
URL không đổi trong 1.2s liên tiếp → coi là stable
Kiểm tra popup URL mỗi lần poll (popup fires async, có thể đến trễ)
7g. Phán loại sau khi click — Decision Tree đầy đủ#
SAU KHI CLICK CTA:
CapturedRoot hoặc newUrlRoot KHÁC activeRoot?
├── Có
│ ├── Thuộc social/ignored? (isIgnoredExternalCtaDestination)
│ │ ├── Có → Add vào skippedExternalCtaSignatures
│ │ │ → restorePageIfNeeded (quay lại trang cũ)
│ │ │ → shouldRescanAfterRestore = true → tiếp tục scan
│ │ └── Không → set jumpedRoot, jumpedUrl → break khỏi CTA for-loop
│ │ → sau đó navigate sang jumpedRoot (xem mục 7h)
└── Không (cùng root hoặc không đổi)
URL tab thay đổi trong cùng root?
├── Có
│ ├── Redirect đến localRoots? → mainsite ngay
│ ├── Đợi 1.2s → đếm input và canvas sau navigate
│ │ ├── inputs tăng HOẶC canvas mới xuất hiện → MAINSITE
│ │ └── Không thay đổi → tiếp tục (Fake UI, thử CTA kế)
└── Không (URL không đổi)
Có popup url mới?
├── popup root khác activeRoot và không phải social → jumpedRoot
├── popup root khác activeRoot nhưng là social → skip, tiếp tục
├── popup root = activeRoot (cùng root) → MAINSITE
└── Không có popup
So sánh DOM trước và sau click:
├── inputs tăng VÀ >= 2 HOẶC canvas mới xuất hiện → MAINSITE
└── Không thay đổi → dead button → tiếp tục CTA kế tiếp
Nếu đã thử hết tất cả CTA, không có gì thay đổi:
→ totalCtasTested > 0: tất cả là fake/dead → SATELLITE
→ totalCtasTested = 0: kiểm tra lại Auth CTA + inputs
→ Có Auth CTA VÀ inputs > 0 → MAINSITE
→ Không đủ → SATELLITE
7h. Điều hướng sang domain mới (jumpedRoot)#
jumpedRoot đã từng visit? (visitedRoots)
├── Có → Phát hiện vòng lặp → SATELLITE, dừng lại
└── Không
→ Navigate đến jumpedUrl
→ Chờ Dynamic Polling thêm 15s (chờ trang mới render)
→ waitForLoaderProgress() (nếu có game loading)
→ activeRoot = jumpedRoot, hopCount++
→ autoClosePopups()
→ tiếp tục vòng lặp while (quét lại CTA trang mới)
Nếu hopCount >= maxHopCount (4) → vượt quá giới hạn → SATELLITE
Quan trọng: Trang B (domain mới) sẽ được scan theo đúng cùng quy trình — auth handle, CTA candidates, click & theo dõi — và cho ra resolvedDomainType (bStatus).
8. Bước 6 — Rule A/B (Phán loại cuối cùng)#
Sau khi vòng lặp kết thúc (authCheckCompleted = true):bStatus = resolvedDomainType (kết quả của trang B / trang cuối)
finalRoot = activeRoot (root domain của trang đang xem cuối cùng)
aStatus = đánh giá ban đầu của trang A trước khi click
originalRoot = root của URL input
postRedirectRoot = root sau khi trang A tự động redirect (nếu có)
─────────────────────────────────────────────────────────
RULE 1: bStatus = "mainsite"
─────────────────────────────────────────────────────────
finalRoot == originalRoot HOẶC finalRoot == postRedirectRoot
→ A là MAINSITE ("B cùng nhà với A, B là mainsite, vậy A cũng là mainsite")
finalRoot != originalRoot VÀ finalRoot != postRedirectRoot
→ A là SATELLITE ("B là mainsite nhưng ở nhà khác → A chỉ là vệ tinh dẫn đến B")
─────────────────────────────────────────────────────────
RULE 2: bStatus = "satellite"
─────────────────────────────────────────────────────────
finalRoot == originalRoot HOẶC finalRoot == postRedirectRoot:
aStatus == "mainsite" → A là MAINSITE ("B cùng nhà nhưng là satellite, A ban đầu là mainsite → tin aStatus")
aStatus == "satellite" → A là SATELLITE
finalRoot != originalRoot:
→ A là SATELLITE ("B là satellite ở nhà khác → A cũng là satellite")
| Tình huống | A.root | B.root | bStatus | Kết quả A |
|---|
| A dẫn đến chính mình (route nội bộ) | game88.com | game88.com | mainsite | mainsite |
| A là landing dẫn sang game | land.com | game88.com | mainsite | satellite |
| A nghi là game, trả về trang phụ | game88.com | game88.com | satellite | mainsite (dùng aStatus) |
| A → B → B cũng là landing | land.com | another.com | satellite | satellite |
9. Các hàm helper — Tóm tắt#
| Hàm | Vị trí | Mục đích |
|---|
normalizeDomainForType(input) | Top-level | Làm sạch URL → hostname thuần |
isIpAddress(value) | Top-level | Kiểm tra IPv4/IPv6 |
getRegistrableDomain(host) | Top-level | Lấy root domain (xử lý ccSLD) |
isIgnoredExternalCtaDestination(url) | Top-level | Kiểm tra có phải social/CDN không |
getCanvasLoaderInfo() | Trong run | Detect game engine (Cocos/Unity/Pixi) |
waitForLoaderProgress() | Trong run | Chờ loader 0% tiến triển, tối đa 25s |
getExactAuthHandle(skipped) | Trong loop | Tìm Auth CTA text chính xác + scoring |
getVisibleInputCount() | Trong loop | Đếm input field thực sự hiển thị |
isCanvasLikelyMainsite() | Trong loop | Quyết định nhanh canvas có phải mainsite |
evaluateCurrentPage() | Trong loop | Đánh giá trang A trước khi click |
triggerElementClick(handle) | Trong loop | Click với 3 tầng fallback |
waitForStableUrl(initial) | Trong loop | Chờ URL ổn định sau redirect (tối đa 20s) |
waitForPopupUrl() | Trong loop | Chờ popup tab mới (tối đa 20s) |
findViewportReplacementCta(x, y) | Trong loop | Tìm lại CTA sau re-render SPA |
autoClosePopups() | Trong loop | Đóng popup/quảng cáo tự động |
shouldSkipExternalHop(url, root) | Trong loop | Có nên bỏ qua redirect này không |
restorePageIfNeeded(before, after) | Trong loop | Quay lại trang cũ sau khi bị redirect sang social |
10. Output và xử lý lỗi#
Browser luôn được đóng trong finally block dù có lỗi hay không
try/catch bao toàn bộ → lỗi không unhandled, luôn trả về object
11. Các điểm cần chú ý khi maintain#
[!IMPORTANT]
localRoots có 2 instance: Một ở scope ngoài (dùng trong Dynamic Polling), một ở scope trong CTA loop. Nếu debug redirect bị nhận nhầm là "n ội bộ", kiểm tra cả 2 instance này.
[!WARNING]
skippedExternalCtaSignatures dùng signature tag|text|x|y|href. Nếu SPA re-render làm element dịch chuyển tọa độ, signature sẽ không khớp → CTA có thể bị click lại. Chỉ là rủi ro nhỏ vì đã có restorePageIfNeeded xử lý.
[!CAUTION]
maxHopCount = 4: Tăng lên sẽ tăng thời gian xử lý rất đáng kể (mỗi hop có thể tốn 20-40s).
[!NOTE]
shouldUseDesktopForDomainType = false: Flag tắt, chưa dùng. Khi bật, browser switch sang desktop viewport trước khi scan. Dùng cho trang chỉ show đúng trên desktop.
[!TIP]
Thêm từ khoá Auth CTA mới: Sửa hàm isAuthText() trong getExactAuthHandle. Thêm domain mới vào blacklist: sửa CTA_EXTERNAL_IGNORE_ROOTS. Thêm ccSLD mới: sửa COMMON_SLD_SUFFIXES.
12. Ví dụ thực tế — 6 case#
Case 1: Satellite điển hình#
URL: landing123.com
Bước 3: poll → thấy "Đăng ký ngay", không có input form
Bước 5: getExactAuthHandle → tìm thấy "dang ky"
Click → redirect sang game88.com (khác root)
jumpedRoot = "game88.com"
Navigate sang game88.com
game88.com có form đăng nhập thật, inputs > 0
resolvedDomainType = "mainsite"
Bước 6: bStatus=mainsite, finalRoot=game88.com ≠ originalRoot=landing123.com
→ A = SATELLITE ✓
Case 2: Mainsite game canvas Cocos2D#
URL: game88.com
Bước 3: poll → thấy canvas, hasCocos
Bước 5: getCanvasLoaderInfo → hasCocos=true, splashVisible=true
waitForLoaderProgress → loader tiến từ 0% → 45% → load xong
evaluateCurrentPage → Pure Game Canvas, không có DOM auth
→ aStatus = "mainsite", authCheckCompleted = true
Bước 6: bStatus=mainsite, finalRoot=game88.com = originalRoot
→ A = MAINSITE ✓
Case 3: Domain nước ngoài#
URL: casino-eu.com
Bước 4: check tiếng Việt
html[lang] = "en", no meta vi
title/keywords không có ký tự VI
body text < 3 ký tự VI
→ isVietnamesePage = false
→ return "unknown" NGAY ✓
Case 4: Fake UI (Satellite ngụy trang)#
URL: fakesite.com
Bước 5: getExactAuthHandle → tìm "Đăng nhập"
preClickInputCount = 0
Click → URL đổi sang fakesite.com/login (cùng root)
Đợi 1.2s → đếm inputs tại /login → 0
→ Fake UI (URL đổi nhưng không có form thật)
resolvedDomainType = "satellite"
Bước 6: finalRoot = originalRoot, bStatus = satellite, aStatus = satellite
→ A = SATELLITE ✓
Case 5: CTA dẫn sang Telegram — skip và tiếp tục#
URL: mixsite.com
Bước 5: getExactAuthHandle → tìm "Đăng ký"
Click → capturedUrl = "https://t.me/game88channel"
capturedRoot = "t.me" → isIgnoredExternalCtaDestination = true
shouldSkipExternalHop = true
→ Add signature vào skippedExternalCtaSignatures
→ restorePageIfNeeded (browser đã chuyển sang t.me → quay về mixsite.com)
→ shouldRescanAfterRestore = true → continue vòng lặp
Quét lại CTA (lần 2) → không có Auth CTA nào khác
→ Không có jumpedRoot, không có authCheckCompleted
→ totalCtasTested = 0 → fallback check: Auth + inputs → cả 2 = 0
→ resolvedDomainType = "satellite"
→ A = SATELLITE ✓
Case 6: Loader stuck ở 0%#
URL: gamestuck.com
Bước 5: getCanvasLoaderInfo → hasCanvas=true, splashVisible=true, rawText="0%"
waitForLoaderProgress → chờ 25s, text vẫn "0%"
→ isCanvasLoaderStuck = true
isCanvasLikelyMainsite() = true (vì isCanvasLoaderStuck)
evaluateCurrentPage → canvasLikely=true, nhưng không có auth CTA
hasStrongGameSignal = true (hasCocos)
→ không có DOM auth, không có body keywords → Pure Game Canvas
→ aStatus = "mainsite"
Bước 6: authCheckCompleted=true từ sớm
→ A = MAINSITE ✓
Modified at 2026-04-21 07:51:13