Skip to content

Commit

Permalink
Add HEAD request handling and etag to issuer directory
Browse files Browse the repository at this point in the history
Browsers might send `HEAD` request to understand if they should do a `GET` request.
This commit adds an ETag on response, allowing `If-None-Match` to be
handled, as well as `HEAD` request to be correctly processed.
  • Loading branch information
thibmeu committed Feb 9, 2024
1 parent e193ae5 commit 6482ab3
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 3 deletions.
30 changes: 28 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '@cloudflare/privacypass-ts';
import { ConsoleLogger } from './context/logging';
import { MetricsRegistry } from './context/metrics';
import { hexEncode } from './utils/hex';
const { BlindRSAMode, Issuer, TokenRequest } = publicVerif;

const keyToTokenKeyID = async (key: Uint8Array): Promise<number> => {
Expand Down Expand Up @@ -88,11 +89,26 @@ const DIRECTORY_CACHE_REQUEST = new Request(
`https://${FAKE_DOMAIN_CACHE}${PRIVATE_TOKEN_ISSUER_DIRECTORY}`
);

export const handleHeadTokenDirectory = async (ctx: Context, request: Request) => {
const getResponse = await handleTokenDirectory(ctx, request);

return new Response(undefined, {
status: getResponse.status,
headers: getResponse.headers,
});
};

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const handleTokenDirectory = async (ctx: Context, request?: Request) => {
export const handleTokenDirectory = async (ctx: Context, request: Request) => {
const cache = await getDirectoryCache();
const cachedResponse = await cache.match(DIRECTORY_CACHE_REQUEST);
if (cachedResponse) {
if (request.headers.get('if-none-match') === cachedResponse.headers.get('etag')) {
return new Response(undefined, {
status: 304,
headers: cachedResponse.headers,
});
}
return cachedResponse;
}
ctx.metrics.directoryCacheMissTotal.inc({ env: ctx.env.ENVIRONMENT });
Expand All @@ -112,10 +128,19 @@ export const handleTokenDirectory = async (ctx: Context, request?: Request) => {
})),
};

const response = new Response(JSON.stringify(directory), {
const body = JSON.stringify(directory);
const digest = new Uint8Array(
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(body))
);
const etag = `"${hexEncode(digest)}"`;

const response = new Response(body, {
headers: {
'content-type': MediaType.PRIVATE_TOKEN_ISSUER_DIRECTORY,
'cache-control': `public, max-age=${ctx.env.DIRECTORY_CACHE_MAX_AGE_SECONDS}`,
'content-length': body.length.toString(),
'date': new Date().toUTCString(),
etag,
},
});
ctx.waitUntil(cache.put(DIRECTORY_CACHE_REQUEST, response.clone()));
Expand Down Expand Up @@ -211,6 +236,7 @@ export default {
const router = new Router();

router
.head(PRIVATE_TOKEN_ISSUER_DIRECTORY, handleHeadTokenDirectory)
.get(PRIVATE_TOKEN_ISSUER_DIRECTORY, handleTokenDirectory)
.post('/token-request', handleTokenRequest)
.post('/admin/rotate', handleRotateKey)
Expand Down
12 changes: 12 additions & 0 deletions src/utils/hex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const hexDecode = (s: string) => {
const bytes = s.match(/.{1,2}/g);
if (!bytes) {
return new Uint8Array(0);
}
return Uint8Array.from(bytes.map(b => parseInt(b, 16)));
};

export const hexEncode = (u: Uint8Array) =>
Array.from(u)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
25 changes: 24 additions & 1 deletion test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,35 @@ describe('cache directory response', () => {
expect(Object.entries(mockCache.cache)).toHaveLength(1);

const [cachedURL, _] = Object.entries(mockCache.cache)[0];
const cachedResponse = new Response('cached response');
const sampleEtag = '"sampleEtag"';
const cachedResponse = new Response('cached response', { headers: { etag: sampleEtag } });
mockCache.cache[cachedURL] = cachedResponse;

response = await workerObject.fetch(directoryRequest, getEnv(), new ExecutionContextMock());
expect(response.ok).toBe(true);
expect(response).toBe(cachedResponse);

const cachedDirectoryRequest = new Request(directoryURL, {
headers: { 'if-none-match': sampleEtag },
});
response = await workerObject.fetch(
cachedDirectoryRequest,
getEnv(),
new ExecutionContextMock()
);
expect(response.status).toBe(304);

const headCachedDirectoryRequest = new Request(directoryURL, {
method: 'HEAD',
headers: { 'if-none-match': sampleEtag },
});
response = await workerObject.fetch(
headCachedDirectoryRequest,
getEnv(),
new ExecutionContextMock()
);
expect(response.status).toBe(304);

spy.mockClear();
});
});

0 comments on commit 6482ab3

Please sign in to comment.