Web Totals

Học từ Facebook #80: DPR vào cookie cho ảnh @2x, che khoá trong log

Facebook ghi devicePixelRatio vào cookie để server chọn sẵn ảnh đúng độ phân giải, và chặn log có khoá SRTP bằng một regex. Bản Comet nghe nhầm tên sự kiện; regex lọt 94% khoá AES-256.

hunghg255hunghg25522 min read
#facebook#cookie#images#security#logging
Học từ Facebook #80: DPR vào cookie cho ảnh @2x, che khoá trong log

Bài này gồm hai module rất nhỏ của hạ tầng web Facebook. Mỗi module chỉ mười mấy dòng, và cả hai đều có lỗi.

Ảnh @2x. HTML của News Feed được render ở server, kèm sẵn URL ảnh. Server không biết màn hình của bạn là 1x, 2x hay 3x, nên không biết gửi ảnh nào. Gửi ảnh 3x cho mọi người thì tốn băng thông; gửi ảnh 1x thì màn Retina thấy mờ. srcset giải được cho thẻ <img>, nhưng không giải được cho ảnh nền CSS, sprite, hay URL ảnh mà JavaScript dựng. Cách của Facebook: client ghi devicePixelRatio vào cookie dpr, lần tải sau server đọc cookie.

Khoá trong log. Gọi video/thoại qua WebRTC có một đoạn SDP. Với SDES (cách trao khoá cũ), SDP chứa dòng a=crypto:… inline:<khoá base64>. Đó là khoá giải mã âm thanh của cuộc gọi. Log nguyên SDP lên server là làm lộ khoá. Facebook chặn bằng ZenonSDESKeyDetector: một regex.

Tip

Tóm tắt

  • WebPixelRatioDetector ghi cookie dpr lúc rời trang (pagehide/beforeunload), chỉ ghi khi DPR khác 1. WebPixelRatio.get() ưu tiên giá trị server đã dùng (SiteData.pr) hơn DPR thật, để JavaScript chọn ảnh khớp với HTML.
  • Bản Comet (CometPixelRatioUpdater) nghe "pageHide" (chữ H hoa). Tên sự kiện phân biệt hoa thường, nên listener này không bao giờ chạy.
  • Regex SDES /inline:\s*[0-9a-zA-z+/]{40}(\b|\s|$)/ chỉ nhận đúng 40 ký tự. Thử 10.000 khoá ngẫu nhiên: lọt 2,8% khoá AES-128 (khoá kết thúc bằng + hoặc /) và 94% khoá AES-256 (64 ký tự).
  • Bản viết lại ghi cookie ngay và mỗi khi DPR đổi (theo dõi bằng matchMedia), và che khoá thay vì chỉ phát hiện.

#Facebook làm thế nào?

#WebPixelRatioDetector (bundle 278.js)

Phần chính, đổi tên biến (bản đầy đủ trong spoiler):

function onLeave() {
  if (written) return;
  written = true;
  dpr() !== 1 ? Cookie.set('dpr', String(dpr())) : Cookie.clear('dpr');
}
// startDetecting(): nghe 'pagehide' (nếu có) và Run.onBeforeUnload(onLeave)
js
Code gốc đã đổi tên biến: WebPixelRatioDetector
const COOKIE = PixelRatioConst.cookieName;   // 'dpr'
let started = false, written = false, clearFirst = false;

const dpr = () => window.devicePixelRatio || 1;

function onLeave() {
  if (written) return;
  written = true;
  if (clearFirst) Cookie.clear(COOKIE);
  dpr() !== 1 ? Cookie.set(COOKIE, String(dpr())) : Cookie.clear(COOKIE);
}

export function startDetecting(clear) {
  if (clear) clearFirst = true;
  if (started) return;
  started = true;
  if ('onpagehide' in window) DOMEventListener.add(window, 'pagehide', onLeave);
  Run.onBeforeUnload(onLeave, false);
}
js

Cookie được ghi lúc rời trang, không phải lúc tải. Code không giải thích vì sao. Có lẽ để lấy giá trị DPR cuối cùng: người dùng có thể zoom trong lúc xem (zoom 110% trên màn 1x cho DPR 1,1), hoặc kéo cửa sổ sang màn khác. Hệ quả là lượt tải đầu tiên trên một trình duyệt mới luôn không có cookie, và lượt tải thứ hai mới có ảnh đúng.

DPR = 1 thì xoá cookie thay vì ghi dpr=1, tiết kiệm vài byte cho mọi request của phần đông người dùng máy bàn.

#WebPixelRatio (bundle 26.js)

function get() {
  return SiteData.pr != null && SiteData.pr > 0 ? SiteData.pr : window.devicePixelRatio || 1;
}
js

SiteData.pr là DPR mà server đã dùng khi render HTML (đọc từ cookie). JavaScript hỏi "dùng ảnh tỉ lệ nào" thì nhận giá trị của server, không phải DPR thật. Nhờ vậy ảnh do JavaScript chèn sau cùng độ phân giải với ảnh trong HTML. Nếu bạn vừa kéo cửa sổ sang màn 2x, trang vẫn dùng ảnh 1x cho tới lần tải sau.

#CometPixelRatioUpdater (bundle 16.js)

Bản cho giao diện mới (Comet), viết lại gọn hơn:

function update() {
  window.devicePixelRatio !== 1 ? Cookie.set('dpr', String(window.devicePixelRatio)) : Cookie.clear('dpr');
}

export function startDetecting() {
  Run.onBeforeUnload(update, false);
  'onpagehide' in window && window.addEventListener('pageHide', update);   // ← "pageHide"
}
js

Kiểm tra 'onpagehide' in window dùng tên đúng, nhưng addEventListener dùng 'pageHide'. addEventListener không báo lỗi với tên lạ (bạn có thể nghe sự kiện tuỳ ý), nên lỗi này im lặng. Cookie vẫn được ghi nhờ beforeunload, nhưng beforeunload không chạy đáng tin trên di động (đóng tab bằng cách vuốt, hệ điều hành giết app nền). Trên di động, pagehide mới là sự kiện đáng tin, và đó đúng là cái bị gõ sai. Demo có nút phát pagehide vào hai listener để thấy listener 'pageHide' chạy 0 lần.

sequenceDiagram
  participant B as Trình duyệt
  participant S as Server
  B->>S: GET / (chưa có cookie dpr)
  S-->>B: HTML với ảnh 1x, SiteData.pr = 1
  Note over B: pagehide: ghi cookie dpr=2
  B->>S: GET / (cookie dpr=2)
  S-->>B: HTML với ảnh @2x, SiteData.pr = 2
  Note over B: JS chèn ảnh: WebPixelRatio.get() = 2

#ZenonSDESKeyDetector (bundle 15.js, 17.js, 105.js, 145.js)

const SDES = /inline:\s*[0-9a-zA-z+/]{40}(\b|\s|$)/;
export default function containsSdesKey(event) {
  return SDES.test(JSON.stringify(event));
}

// ScotchRTWebUserActionLogger.logEvent (17.js), tương tự ở RTWebUserActionLogger, ZenonUserActionLogger
logEvent(event) {
  if (ZenonSDESKeyDetector(event))
    throw unrecoverableViolation('ScotchRTWebUserAction event contains SDES crypto key! This log entry must be removed!', 'rtc_www');
  …
}
js

Có log chứa khoá thì ném lỗi, không gửi. Cách này đúng tinh thần: lỗi làm đội phát hiện và sửa chỗ log. Nhưng regex có ba chỗ hở.

#Bên trong: khoá SDES trông thế nào và regex hở ở đâu

RFC 4568 định nghĩa inline: theo sau là base64 của key ‖ salt:

Bộ mã Key + salt Base64
AES_CM_128_HMAC_SHA1_80 16 + 14 = 30 byte 40 ký tự
AES_256_CM_HMAC_SHA1_80 (RFC 6188) 32 + 14 = 46 byte 64 ký tự, có ==

Sau khoá thường là |2^31|1:1 (thời hạn, chỉ số khoá) hoặc xuống dòng.

  1. (\b|\s|$) sau 40 ký tự. \b là ranh giới giữa ký tự "từ" ([A-Za-z0-9_]) và ký tự không phải từ. Khoá kết thúc bằng + hoặc / (không phải ký tự từ) mà theo sau là | (cũng không phải) thì không có ranh giới. Xác suất ký tự cuối là + hoặc / là 2/64, khoảng 3%. Đo được 2,8%.
  2. Khoá 64 ký tự. Sau 40 ký tự đầu là ký tự thứ 41, thường cũng là chữ hoặc số, nên không có ranh giới; regex chỉ khớp được khi ký tự 40 hoặc 41 tình cờ là +//. Đo được 94% khoá AES-256 lọt.
  3. A-z thay vì A-Z. Khoảng A-z trong bảng ASCII gồm cả [ \ ] ^ _ và dấu backtick. Không làm lọt khoá, nhưng khớp nhầm chuỗi như inline: + 40 dấu _. Lỗi gõ kinh điển.

Có lẽ khi viết, mọi cuộc gọi đều dùng AES-128 nên lỗi 2 không xuất hiện; còn lỗi 1 lọt khoảng 1 trên 35 khoá, đủ hiếm để không ai thấy.

#Viết lại

Cookie DPR: ghi ngay, ghi lại khi rời trang hoặc khi DPR đổi.

src/lib/dprCookie.ts
  const write = () => {
    const value = formatDpr(getDpr());
    if (value === last) return; // không ghi cookie thừa
    last = value;
    setCookie(name, value);
  };

  // `matchMedia` chỉ báo khi ĐÚNG giá trị DPR hiện tại hết khớp, nên mỗi lần đổi phải đăng ký lại.
  let query: MediaQueryList | null = null;
  const watch = () => {
    query?.removeEventListener('change', onDprChange);
    query = window.matchMedia?.(`(resolution: ${getDpr()}dppx)`) ?? null;
    query?.addEventListener('change', onDprChange);
  };
  function onDprChange() {
    write();
    options.onChange?.(getDpr());
    watch();
  }
  const onHide = () => {
    if (document.visibilityState === 'hidden') write();
  };

  write(); // ghi ngay: bản gốc chỉ ghi lúc rời trang nên lượt tải thứ hai vẫn chưa có cookie
  watch();
  window.addEventListener('pagehide', write);
  document.addEventListener('visibilitychange', onHide);
ts

Phát hiện và che khoá:

src/lib/dprCookie.ts
/**
 * Khoá SDES (RFC 4568): base64 của key‖salt. AES_CM_128: 30 byte → 40 ký tự;
 * AES_256_CM: 46 byte → 64 ký tự (có `==`). Sau khoá là `|`, khoảng trắng hoặc hết chuỗi.
 */
const SDES_KEY = /(inline:\s*)[A-Za-z0-9+/]{40,}={0,2}(?![A-Za-z0-9+/=])/g;

export function containsSdesKey(text: string) {
  SDES_KEY.lastIndex = 0;
  return SDES_KEY.test(text);
}

export function redactSdesKeys(text: string) {
  let count = 0;
  const redacted = text.replace(SDES_KEY, (_, prefix: string) => {
    count++;
    return `${prefix}[đã che]`;
  });
  return { text: redacted, count };
}
ts

#Đi qua code

  • write so giá trị mới với last trước khi ghi: pagehide, visibilitychange và matchMedia có thể cùng bắn, nhưng cookie chỉ đổi khi DPR thật sự đổi.
  • matchMedia('(resolution: 2dppx)') chỉ báo change khi điều kiện "DPR đúng bằng 2" đổi từ đúng sang sai. Sau mỗi lần đổi phải tạo truy vấn mới với giá trị mới; đây là cách MDN hướng dẫn theo dõi devicePixelRatio.
  • formatDpr làm tròn 2 chữ số và trả null cho DPR 1, để setCookie xoá cookie thay vì ghi dpr=1.
  • Regex khoá dùng (?![A-Za-z0-9+/=]) (ký tự sau không thuộc bảng base64) thay cho \b. Điều kiện này đúng với mọi ký tự cuối, kể cả + và /, và buộc regex nuốt hết khoá dài thay vì dừng ở ký tự thứ 40.
  • redactSdesKeys giữ lại tiền tố inline: và thay phần khoá: người đọc log vẫn thấy cuộc gọi dùng SDES, chỉ không thấy khoá.

Khác bản gốc:

  • Ghi cookie ngay lúc tải, không đợi rời trang; lượt tải thứ hai đã có ảnh đúng.
  • Theo dõi DPR đổi bằng matchMedia('(resolution: Xdppx)'), đăng ký lại sau mỗi lần đổi (truy vấn chỉ báo khi giá trị hiện tại hết khớp).
  • Dùng pagehide và visibilitychange (tên đúng), bỏ beforeunload (làm mất bfcache ở một số trình duyệt).
  • Làm tròn 2 chữ số: DPR Android như 2.625 hay zoom 1.100000023841858 không làm cookie dài vô ích.
  • Regex khoá nhận 40 ký tự trở lên, có =, và kiểm tra "ký tự sau không phải base64" thay cho \b. redactSecrets che khoá trong mọi chuỗi của object log (xử lý cả vòng tham chiếu), thay vì ném lỗi và mất cả dòng log.
Xem toàn bộ src/lib/dprCookie.ts
src/lib/dprCookie.ts
/**
 * Hai việc nhỏ của hạ tầng Facebook web:
 * 1. Ghi devicePixelRatio vào cookie `dpr` để lần tải sau server chọn sẵn ảnh @2x/@3x
 *    (ý tưởng từ WebPixelRatioDetector / CometPixelRatioUpdater).
 * 2. Không để khoá mã hoá SRTP (SDES `inline:`) lọt vào log (ý tưởng từ ZenonSDESKeyDetector).
 * Khác bản gốc: tên sự kiện đúng (`pagehide`, bản Comet viết nhầm `pageHide`), cập nhật ngay khi
 * DPR đổi (zoom, kéo cửa sổ sang màn khác), và che khoá thay vì chỉ phát hiện; regex nhận cả khoá
 * 64 ký tự và khoá kết thúc bằng `+`, `/`.
 */

export interface DprCookieOptions {
  name?: string;
  /** mặc định ghi vào document.cookie, path=/, 1 năm */
  setCookie?: (name: string, value: string | null) => void;
  getDpr?: () => number;
  /** báo khi DPR đổi trong lúc trang đang mở */
  onChange?: (dpr: number) => void;
}

/** 2 chữ số thập phân là đủ: 1.25, 1.5, 2.625 (Android) → '2.63'; DPR = 1 thì xoá cookie */
export function formatDpr(dpr: number): string | null {
  if (!Number.isFinite(dpr) || dpr <= 0) return null;
  const rounded = Math.round(dpr * 100) / 100;
  return rounded === 1 ? null : String(rounded);
}

function defaultSetCookie(name: string, value: string | null) {
  document.cookie =
    value == null
      ? `${name}=; path=/; max-age=0; samesite=lax`
      : `${name}=${value}; path=/; max-age=31536000; samesite=lax`;
}

export function syncDprCookie(options: DprCookieOptions = {}) {
  const name = options.name ?? 'dpr';
  const setCookie = options.setCookie ?? defaultSetCookie;
  const getDpr = options.getDpr ?? (() => window.devicePixelRatio || 1);
  let last: string | null | undefined;

  const write = () => {
    const value = formatDpr(getDpr());
    if (value === last) return; // không ghi cookie thừa
    last = value;
    setCookie(name, value);
  };

  // `matchMedia` chỉ báo khi ĐÚNG giá trị DPR hiện tại hết khớp, nên mỗi lần đổi phải đăng ký lại.
  let query: MediaQueryList | null = null;
  const watch = () => {
    query?.removeEventListener('change', onDprChange);
    query = window.matchMedia?.(`(resolution: ${getDpr()}dppx)`) ?? null;
    query?.addEventListener('change', onDprChange);
  };
  function onDprChange() {
    write();
    options.onChange?.(getDpr());
    watch();
  }
  const onHide = () => {
    if (document.visibilityState === 'hidden') write();
  };

  write(); // ghi ngay: bản gốc chỉ ghi lúc rời trang nên lượt tải thứ hai vẫn chưa có cookie
  watch();
  window.addEventListener('pagehide', write);
  document.addEventListener('visibilitychange', onHide);
  return () => {
    query?.removeEventListener('change', onDprChange);
    window.removeEventListener('pagehide', write);
    document.removeEventListener('visibilitychange', onHide);
  };
}

/** Bản của Facebook, giữ lại để demo và test so sánh. */
export const FB_SDES_PATTERN = /inline:\s*[0-9a-zA-z+/]{40}(\b|\s|$)/;

/**
 * Khoá SDES (RFC 4568): base64 của key‖salt. AES_CM_128: 30 byte → 40 ký tự;
 * AES_256_CM: 46 byte → 64 ký tự (có `==`). Sau khoá là `|`, khoảng trắng hoặc hết chuỗi.
 */
const SDES_KEY = /(inline:\s*)[A-Za-z0-9+/]{40,}={0,2}(?![A-Za-z0-9+/=])/g;

export function containsSdesKey(text: string) {
  SDES_KEY.lastIndex = 0;
  return SDES_KEY.test(text);
}

export function redactSdesKeys(text: string) {
  let count = 0;
  const redacted = text.replace(SDES_KEY, (_, prefix: string) => {
    count++;
    return `${prefix}[đã che]`;
  });
  return { text: redacted, count };
}

/** Đi qua mọi chuỗi trong object log (không sửa object gốc), che khoá, trả số chỗ đã che. */
export function redactSecrets<T>(value: T): { value: T; count: number } {
  let count = 0;
  const seen = new WeakMap<object, unknown>();
  const walk = (v: unknown): unknown => {
    if (typeof v === 'string') {
      const r = redactSdesKeys(v);
      count += r.count;
      return r.text;
    }
    if (v === null || typeof v !== 'object') return v;
    if (seen.has(v)) return seen.get(v); // vòng tham chiếu
    const out: Record<string, unknown> | unknown[] = Array.isArray(v) ? [] : {};
    seen.set(v, out);
    for (const [k, child] of Object.entries(v)) (out as Record<string, unknown>)[k] = walk(child);
    return out;
  };
  return { value: walk(value) as T, count };
}
ts

#Tại sao không làm cách khác?

  • Chỉ dùng srcset/<picture>: đúng cách cho <img>, trình duyệt tự chọn. Nhưng không dùng được cho URL ảnh trong JSON, ảnh nền CSS render ở server, hay khi server muốn cắt ảnh theo đúng kích thước pixel.
  • Client Hints (Sec-CH-DPR, header Accept-CH: Sec-CH-DPR): trình duyệt tự gửi DPR trong request, không cần cookie. Chỉ Chromium hỗ trợ (tính tới 10/2026), và request đầu tiên vẫn không có (trừ khi dùng Critical-CH, tốn một lượt khứ hồi).
  • Chỉ phát hiện rồi ném lỗi (như bản gốc): tốt trong lúc phát triển, nhưng ở production thì mất dòng log và lỗi có khi bị nuốt bởi chỗ gọi. Che rồi gửi, kèm số lượng đã che, vừa an toàn vừa giữ được dữ liệu. Có thể kết hợp: che ở production, ném ở môi trường dev.
  • Không log SDP: tốt nhất. Nhưng SDP là thứ đầu tiên người ta muốn xem khi gỡ lỗi cuộc gọi. Và DTLS-SRTP (cách trao khoá hiện đại của WebRTC) không đưa khoá vào SDP, chỉ có fingerprint; regex này chủ yếu cho SDES còn sót lại.

#Edge case và cạm bẫy

  • Tên sự kiện phân biệt hoa thường: pagehide, visibilitychange, DOMContentLoaded. TypeScript bắt được lỗi này nếu bạn dùng window.addEventListener với overload có kiểu (tên lạ vẫn được chấp nhận, nhưng tham số handler thành Event chung). Một lint rule danh sách trắng tên sự kiện là cách chắc nhất.
  • Zoom thay đổi DPR: trên máy bàn, Ctrl + làm devicePixelRatio tăng. Người zoom 150% trên màn 1x nhận ảnh @2x. Đó thường là điều bạn muốn (ảnh nét hơn khi phóng to).
  • Cookie gửi theo mọi request: kể cả request ảnh, CSS. Giữ cookie ngắn (dpr=2), đặt path=/ một lần, đừng ghi thừa (bản viết lại so với giá trị cũ trước khi ghi).
  • JSON.stringify trước khi kiểm tra: object có vòng tham chiếu làm JSON.stringify ném lỗi; bản gốc sẽ ném TypeError thay vì kiểm tra. redactSecrets tự đi qua object nên không gặp.
  • Khoá bị escape: nếu SDP đã qua encodeURIComponent (inline%3A…, %2B, %2F), cả hai regex đều lọt. Che ở chỗ gần nguồn nhất, trước khi dữ liệu bị mã hoá thêm.

#Dùng trong dự án

import { redactSecrets, syncDprCookie } from './dprCookie';

// một lần lúc khởi động app
syncDprCookie({ onChange: (dpr) => imageCache.invalidate(dpr) });

// logger: che khoá trước khi gửi
function logCallEvent(event: Record<string, unknown>) {
  const { value, count } = redactSecrets(event);
  if (count && import.meta.env.DEV) throw new Error('Log chứa khoá SDES, sửa chỗ log này');
  navigator.sendBeacon('/log/rtc', JSON.stringify({ ...value, redacted: count }));
}
ts

Ở server, đọc req.cookies.dpr, kẹp trong khoảng 1–3, chọn ảnh @${Math.ceil(dpr)}x và nhúng giá trị đó vào HTML (như SiteData.pr) để JavaScript chọn ảnh cùng độ phân giải.

#Kiểm thử

Ví dụ test (bản phác, không có trong repo)
import { describe, expect, it, vi } from 'vitest';
import { containsSdesKey, FB_SDES_PATTERN, formatDpr, redactSecrets, syncDprCookie } from './dprCookie';

const key = (bytes: number) => btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(bytes))));
const sdp = (k: string) => `a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:${k}|2^31|1:1\r\n`;

describe('SDES', () => {
  it('bản gốc lọt khoá kết thúc bằng /, bản viết lại thì không', () => {
    const k = 'S2cNZYehpoL+qjcULEZAkm7vcztYC5zcDmjYx3n/';
    expect(FB_SDES_PATTERN.test(sdp(k))).toBe(false);
    expect(containsSdesKey(sdp(k))).toBe(true);
  });

  it('nhận khoá 30 và 46 byte ngẫu nhiên', () => {
    for (let i = 0; i < 1000; i++) {
      expect(containsSdesKey(sdp(key(30)))).toBe(true);
      expect(containsSdesKey(sdp(key(46)))).toBe(true);
    }
  });

  it('không khớp nhầm', () => {
    expect(containsSdesKey('inline:' + '_'.repeat(40))).toBe(false);
    expect(containsSdesKey('inline: abc')).toBe(false);
  });

  it('che trong object lồng nhau, có vòng tham chiếu', () => {
    const event: Record<string, unknown> = { a: { sdp: sdp(key(30)) }, list: [sdp(key(46))] };
    event.self = event;
    const { value, count } = redactSecrets(event);
    expect(count).toBe(2);
    expect(JSON.stringify({ ...value, self: undefined })).not.toMatch(/inline:[A-Za-z0-9+/]{40}/);
    expect(event.a).toEqual({ sdp: expect.stringContaining('inline:') }); // object gốc không đổi
  });
});

describe('DPR cookie', () => {
  it('làm tròn và bỏ DPR = 1', () => {
    expect([1, 2, 2.625, 1.100000023841858].map(formatDpr)).toEqual([null, '2', '2.63', '1.1']);
  });

  it('ghi ngay, không ghi trùng, ghi lại khi pagehide', () => {
    const setCookie = vi.fn();
    let dpr = 2;
    const stop = syncDprCookie({ setCookie, getDpr: () => dpr });
    expect(setCookie).toHaveBeenCalledWith('dpr', '2');
    window.dispatchEvent(new Event('pagehide'));
    expect(setCookie).toHaveBeenCalledTimes(1);
    dpr = 3;
    window.dispatchEvent(new Event('pagehide'));
    expect(setCookie).toHaveBeenLastCalledWith('dpr', '3');
    stop();
  });
});
ts

#So với thư viện và chuẩn web

  • srcset + sizes, image-set() trong CSS: trình duyệt tự chọn theo DPR, mọi trình duyệt chính đều hỗ trợ. Nên là lựa chọn đầu tiên.
  • Client Hints (Sec-CH-DPR, Sec-CH-Viewport-Width): chuẩn hoá đúng việc mà cookie dpr làm thủ công. Chỉ Chromium (tính tới 10/2026); Safari và Firefox chưa gửi.
  • CDN ảnh (Cloudinary, imgix, Cloudflare Images): nhận tham số dpr trong URL hoặc đọc Client Hints, trả ảnh đúng kích thước.
  • Che dữ liệu nhạy cảm trong log: Sentry có beforeSend và danh sách trường bị che phía server; OpenTelemetry Collector có processor redaction. Đều dựa trên tên trường hoặc regex, nên regex phải được test với dữ liệu thật như ở đây.
  • WebRTC hiện đại dùng DTLS-SRTP: Chrome đã bỏ SDES từ lâu; khoá không còn xuất hiện trong SDP. Nhưng hệ thống gọi nội bộ, SIP gateway vẫn có thể dùng SDES.

Bài này thuộc series Học từ mã nguồn Facebook (106 bài, đọc theo thứ tự).

Xem nhanh danh sách bài trong series

Phần 1

Phần 2

Phần 3

Phần 4–6

Phần 7–10

Phần 11–15

Code + demo: examples/facebook-patterns · Mở trên StackBlitz