redirect-unwrap.ts raw

   1  type RedirectExtractor = {
   2    host: string;
   3    exact?: boolean;
   4    path: string;
   5    param: string;
   6    decode?: 'none' | 'uri' | 'form' | 'b64url';
   7  };
   8  
   9  const REDIRECT_EXTRACTORS: RedirectExtractor[] = [
  10    { host: 'l.facebook.com', exact: true, path: '/l.php', param: 'u', decode: 'uri' },
  11    { host: 'lm.facebook.com', exact: true, path: '/l.php', param: 'u', decode: 'uri' },
  12    { host: 'linkedin.com', exact: false, path: '/safety/go', param: 'url', decode: 'uri' },
  13    { host: 'youtube.com', exact: false, path: '/redirect', param: 'q', decode: 'uri' },
  14    { host: 'go.bsky.app', exact: true, path: '/redirect', param: 'u', decode: 'uri' },
  15    { host: 'googleadservices.com', exact: false, path: '/pagead/aclk', param: 'adurl', decode: 'uri' },
  16    { host: 'target.georiot.com', exact: true, path: '/Proxy.ashx', param: 'GR_URL', decode: 'uri' },
  17    { host: 'click.linksynergy.com', exact: true, path: '/link', param: 'murl', decode: 'uri' },
  18  ];
  19  
  20  function decodeParam(value: string, mode: 'none' | 'uri' | 'form' | 'b64url'): string | null {
  21    try {
  22      switch (mode) {
  23        case 'none': return value;
  24        case 'uri': return decodeURIComponent(value);
  25        case 'form': return decodeURIComponent(value.replace(/\+/g, ' '));
  26        case 'b64url':
  27          return new TextDecoder().decode(
  28            Uint8Array.from(atob(value.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0)),
  29          );
  30      }
  31    } catch { return null; }
  32  }
  33  
  34  function isHttp(s: string): boolean { return /^https?:\/\//i.test(s); }
  35  
  36  export function unwrapRedirect(url: string): string | null {
  37    try {
  38      const u = new URL(url);
  39      if (u.hostname === 'out.reddit.com') {
  40        const target = u.searchParams.get('url');
  41        if (target && isHttp(target)) {
  42          const d = decodeURIComponent(target);
  43          if (isHttp(d)) return d;
  44        }
  45        return null;
  46      }
  47      for (const ex of REDIRECT_EXTRACTORS) {
  48        if (ex.exact ? u.hostname !== ex.host : (u.hostname !== ex.host && !u.hostname.endsWith('.' + ex.host))) continue;
  49        if (!u.pathname.startsWith(ex.path)) continue;
  50        const raw = u.searchParams.get(ex.param);
  51        if (!raw) continue;
  52        const d = decodeParam(raw, ex.decode ?? 'uri');
  53        if (d && isHttp(d)) return d;
  54      }
  55      return null;
  56    } catch { return null; }
  57  }
  58  
  59  export function fullyUnwrapRedirect(url: string, maxHops: number = 5): { url: string; hops: string[] } {
  60    const hops: string[] = [];
  61    let cur = url;
  62    for (let i = 0; i < maxHops; i++) {
  63      const next = unwrapRedirect(cur);
  64      if (!next || next === cur) break;
  65      try { hops.push(new URL(cur).hostname); } catch {}
  66      cur = next;
  67    }
  68    return { url: cur, hops };
  69  }
  70