Qdon은 DigitalOcean의 오브젝트 스토리지를 사용합니다. DigitalOcean Space(이하 DO Space)는 CDN을 제공하지만 이 CDN에 원하는 도메인을 넣어 사용하려면 네임서버를 DigitalOcean의 것을 사용해야 한다는 이상한 제약이 걸려 있습니다. 보통은 그냥 CNAME만 지정해 주면 알아서 하는데 아무리 말해도 고쳐지지 않더라구요.

그래서 임시 방편으로 마스토돈이 권장하는 방법인, 로컬에 프록시를 두는 방법을 사용해 왔습니다. 하지만 큐돈은 클라우드플레어까지 사용하고 있었기에 이런 방법을 사용하면 “클라이언트 - 클라우드플레어 - 로컬 프록시 - 오브젝트 스토리지"를 거쳐가기 때문에 속도 저하가 심각했습니다.

그래서 사용하던 두 번째 방법은 클라우드 플레어의 페이지 규칙을 통해 302 리다이렉트를 주는 것이었습니다. 이러면 ActivityPub상의 URL은 바뀌지 않지만 내부적으로 리다이렉트 된 URL을 통해 파일을 가져오게 되니 CDN의 속도를 어느 정도 누릴 수 있었습니다. (다만 CF의 TTFB는 높은 편이므로 그래도 좀 느립니다) 하지만 이 방법은 DO Space의 URL이 노출된다는 사소한 단점이 있습니다. 별로 취약한 건 아니지만 리다이렉트가 있다는 점과 함께 좀 껄끄럽긴 하죠.

또 다른 방법은 클라우드 플레서 유료 플랜에서만 사용 가능한 건데 페이지 규칙을 이용해 Host 헤더를 변경해 요청을 날리는 방법이 있습니다. 하지만 큐돈은 그럴 여력이 되지 않으므로 넘어갑시다.

오늘 사용할 방법으로는 클라우드플레어의 워커 기능을 이용해서 아예 투명하게 프록싱을 하되 큐돈의 서버는 거치지 않는 방법을 사용해 보겠습니다.

※이 방법은 마스토돈 신기능인 Cache Buster에 영향이 갈 수 있습니다.

우선 클라우드플레어 워커 설정에 들어가서 다음과 같은 코드를 작성합니다. 당연하겠지만 newUrl.hostname에 들어갈 부분은 각자 환경에 맞게 수정 하셔야 합니다. 캐시버스터 설정을 하셨다면 cacheBypassHeader, cacheBypassValue도 맞춰서 지정해 놓아야 합니다.

const cacheBypassHeader = 'Cache-Bypass-Qdon';
const cacheBypassValue = 'XXXX';
const mediaHost = 'xxx.xxx.cdn.digitaloceanspaces.com'

const removeHeaders = [
  'Set-Cookie',
  'x-amz-id-2',
  'x-amz-request-id',
  'x-amz-meta-server-side-encryption',
  'x-amz-server-side-encryption',
  'x-amz-bucket-region',
  'x-amzn-requestid',
  'x-amz-meta-s3cmd-attrs',
  'x-rgw-object-type',
  'x-hw',
];

const overrideHeaders = {
  'Cache-Control': `max-age=${60 * 60 * 8}`,
};

const cache = caches.default;

async function fetchMedia(event) {
  const req = event.request;
  const headers = req.headers;
  let newUrl = new URL(req.url);
  newUrl.hostname = mediaHost;
  const cacheKey = new Request(newUrl.toString(), req);

  let ttl = 60 * 60 * 8;
  if (headers.get(cacheBypassHeader) === cacheBypassValue) {
    ttl = -1;
    await cache.delete(cacheKey);
  }

  let response = await cache.match(cacheKey);

  if (!response) {
    response = await fetch(newUrl, {
      method: req.method,
      headers: req.headers,
      cf: {
        cacheTtl: ttl,
        cacheEverything: true,
        cacheKey: cacheKey,
      }
    });
    response = new Response(response.body, response);
    removeHeaders.forEach(name => response.headers.delete(name));
    for (let [key, val] of Object.entries(overrideHeaders)) {
      response.headers.set(key, val);
    }

    event.waitUntil(cache.put(cacheKey, response.clone()));

    response.headers.set('X-Fresh', 'Jes');
  }

  return response;
}

addEventListener("fetch", event => event.respondWith(fetchMedia(event)));

이후, 해당 워커를 오브젝트 스토리지 경로 (bucket.qdon.space/*)에 맞게 추가하면 됩니다. 무료 플랜에서는 하루 10만 건까지 사용 가능하니 초과분에 대해서는 아예 차단을 할 지 아니면 원본 경로를 사용할 지 선택할 수 있습니다. 원본 경로를 선택하면 무료 사용량 초과시 예전처럼 302 리다이렉트가 되어 문제 없이 이용할 수 있습니다.