Skip to main content

ลิงก์แคมเปญและ Proxy รูปภาพ

ภาพรวม

endpoint เดียวใน service นี้ที่ ไม่ตอบเป็น JSON envelope เนื่องจากผู้เรียกไม่ใช่ client-web แต่เป็น LINE client หรือ in-app browser เอง (ตอนผู้ใช้กดลิงก์) หรือเป็น image proxy ของ LINE (ตอน render รูปในข้อความ) จึงต้องตอบเป็น 302 redirect หรือ stream bytes ของรูปออกไปโดยตรง

token ของ endpoint นี้เป็นแบบเข้ารหัสที่พา destination และบริบทการติดตามมาในตัว การคลิกหนึ่งครั้งจึง resolve ได้โดยไม่ต้องอ่านฐานข้อมูลเลย มีเพียงการเขียน tracking_log แบบ async best-effort เท่านั้น

Business Flow

GET /api/c/:token

  1. ถอดรหัส token ด้วย keyring AES-256-GCM (campaigntoken.Ring) หากล้มเหลวตอบ 400 "Invalid token" ในรูปแบบ text/html
  2. ตรวจวันหมดอายุจาก payload.E
  3. บันทึกก่อนแต่ไม่รอผล หาก token ยังไม่หมดอายุ ระบบจะ spawn goroutine เพื่อเขียน tracking_log โดย detach จาก request context เพื่อให้ client ที่ตัดการเชื่อมต่อไปไม่ยกเลิกการเขียน
    • ค่า enum ต่างกันตามชนิดของการกระทำ: การคลิก URL ใช้ action_type='click' กับ type='uri' ส่วนการเปิดรูปใช้ action_type='read' กับ type='asset'
    • content_type เป็น campaign เสมอ ส่วน line_uid เป็น NULL เมื่อค่า u ว่างหรือเป็น "-" ซึ่งหมายถึง token แบบ broadcast และหมายความว่าไม่มี trigger รายบุคคลโดยเจตนา
    • raw_data ถูกส่งเป็น string ด้วยเหตุผลเดียวกับ tracking-redirect คือ pgx simple protocol กับคอลัมน์ jsonb
  4. ตั้ง header Referrer-Policy: no-referrer ในทุกกรณี
  5. ชนิด url ตอบ 302
    • หาก token ถูก mint แบบ liff-entry (payload.F หมายถึงการกดที่จะเปิดหน้าต่าง LIFF เพราะลิงก์ อยู่ใต้โดเมน liff.line.me) ระบบจะ redirect ไปยัง path แบบ same-origin แทน URL absolute ของ liff.line.me โดย liffRelativePath แปลง URL รูปแบบ https://liff.line.me/{liffId}/foo?x=1 ให้เหลือ /foo?x=1
    • เหตุผลคือ Location ที่ชี้ไปยัง liff.line.me ทำให้ LINE เปิดหน้าต่าง LIFF ที่สอง และในกรณีลิงก์ ธรรมดาจะทิ้ง in-app browser ไว้เป็นหน้าต่างว่างที่ปิดไม่ได้
    • ลิงก์เก่าที่ไม่ใช่ liff-entry ยังคง redirect แบบเดิม เพื่อไม่ให้ความสามารถของ LIFF เช่นการเรียกใช้ กล้องเสียไป
    • URL ที่ไม่ได้อยู่บน liff.line.me หรือไม่มี sub-path หลัง liffId จะใช้ URL เดิมตามที่ได้รับมา
  6. ชนิด asset ทำหน้าที่ proxy รูป ผ่าน streamAsset
    • SSRF guard (isPrivateURL) บล็อก localhost และโดเมนย่อยของมัน, IPv4 ในช่วง reserved (10.x, 127.x, 0.x, 169.254.x ซึ่งเป็น cloud metadata, 192.168.x, 172.16–31.x) รวมถึง IPv6 ::1, fd* และ fe80* ทั้งนี้ URL ที่ parse ไม่ได้ถือว่าไม่ปลอดภัย และตอบ 403 "Blocked: private URL"
    • ตรวจ cache ก่อน โดยใช้ key CAMPAIGN_ASSET:{sha256(originURL)} ซึ่งเก็บค่าในรูปแบบ "{contentType}|{base64}" เมื่อ hit จะตอบพร้อม header X-Cache: HIT และ Cache-Control: public, max-age=86400
    • เมื่อ miss จะ fetch จาก origin ด้วย timeout 15 วินาที และ response ที่ไม่ใช่ 2xx ตอบ 502 "Failed to fetch asset"
    • อ่านได้สูงสุด 50MB (assetMaxBytes) หากเกินตอบ 502 "Asset too large"
    • cache เฉพาะไฟล์ที่ไม่เกิน 2MB (assetCacheMaxBytes) ด้วย TTL 24 ชั่วโมง แบบ fire-and-forget
    • ตอบด้วย Content-Type จาก origin (ค่าเริ่มต้น application/octet-stream), Content-Length, Cache-Control: public, max-age=86400 และ X-Cache: MISS

ไฟล์และฟังก์ชันหลัก

รายการค่า
RouteGET /api/c/:token
Registerinternal/campaignredirect/register.goRegister(r, deps) (group /c)
Handlerinternal/campaignredirect/handler.go(*Handler).Handle ซึ่งเขียนลง c.Writer โดยตรงแล้วเรียก c.Abort()
Serviceinternal/campaignredirect/service.goNewService, Handle, recordEvent, streamAsset, liffRelativePath, campaignID, writePlain
SSRFinternal/campaignredirect/ssrf.goisPrivateURL, ipv4Literal
Token ringinternal/campaigntoken/token.goNewRing, Decrypt, Payload
ค่าคงที่assetCacheMaxBytes=2MB, assetCacheTTL=86400, assetFetchTimeout=15s, assetMaxBytes=50MB

จุดเชื่อมต่อกับ Service อื่น

  • ตารางฐานข้อมูล tracking_log (เขียนอย่างเดียว)
  • Redis ผ่าน internal/cache สำหรับ byte cache ของรูป โดย key อ้างอิงจาก origin URL
  • Config CAMPAIGN_LINK_KEYS ซึ่งเป็น keyring ที่ใช้ร่วมกับ tracking-redirect แต่แยกจากกันด้วย AAD รวมถึง deps.Config.RedisCache.Namespace และ TTL
  • token ถูกสร้างโดย cms-api-go ในขั้นตอนสร้าง campaign หรือ rich message
  • ตรงกับ client-web feature: campaign-redirect-proxy โดย client-web มี Route Handler ฝั่ง server ที่ proxy มายัง endpoint นี้ เพื่อให้ลิงก์อยู่บนโดเมนสาธารณะ