-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add cache issuer directory #15
Changes from 5 commits
ceb85bd
1f927bd
92ef2a9
e193ae5
6482ab3
6249350
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -13,23 +13,33 @@ export class MetricsRegistry { | |||||
registry: RegistryType; | ||||||
options: RegistryOptions; | ||||||
|
||||||
requestsTotal: CounterType; | ||||||
directoryCacheMissTotal: CounterType; | ||||||
erroredRequestsTotal: CounterType; | ||||||
issuanceRequestTotal: CounterType; | ||||||
keyRotationTotal: CounterType; | ||||||
keyClearTotal: CounterType; | ||||||
issuanceRequestTotal: CounterType; | ||||||
requestsTotal: CounterType; | ||||||
signedTokenTotal: CounterType; | ||||||
|
||||||
constructor(options: RegistryOptions) { | ||||||
this.options = options; | ||||||
this.registry = new Registry(); | ||||||
|
||||||
this.requestsTotal = this.registry.create('counter', 'requests_total', 'total requests'); | ||||||
this.directoryCacheMissTotal = this.registry.create( | ||||||
'counter', | ||||||
'directory_cache_miss_total', | ||||||
'Number of requests for private token issuer directory which are not served by the cache.' | ||||||
); | ||||||
this.erroredRequestsTotal = this.registry.create( | ||||||
'counter', | ||||||
'errored_requests_total', | ||||||
'Errored requests served to eyeball' | ||||||
); | ||||||
this.issuanceRequestTotal = this.registry.create( | ||||||
'counter', | ||||||
'issuance_request_total', | ||||||
'Number of requests for private token issuance.' | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. successful?
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not necessary successful. the metric is increased at the start of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. side: this is also not a new metrics. The metrics have been sorted by alphabetical orders, and it's part of this change. |
||||||
); | ||||||
this.keyRotationTotal = this.registry.create( | ||||||
'counter', | ||||||
'key_rotation_total', | ||||||
|
@@ -40,11 +50,7 @@ export class MetricsRegistry { | |||||
'key_clear_total', | ||||||
'Number of key clear performed.' | ||||||
); | ||||||
this.issuanceRequestTotal = this.registry.create( | ||||||
'counter', | ||||||
'issuance_request_total', | ||||||
'Number of requests for private token issuance.' | ||||||
); | ||||||
this.requestsTotal = this.registry.create('counter', 'requests_total', 'total requests'); | ||||||
this.signedTokenTotal = this.registry.create( | ||||||
'counter', | ||||||
'signed_token_total', | ||||||
|
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -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> => { | ||||||||
|
@@ -79,7 +80,39 @@ export const handleTokenRequest = async (ctx: Context, request: Request) => { | |||||||
}); | ||||||||
}; | ||||||||
|
||||||||
export const handleTokenDirectory = async (ctx: Context, request?: Request) => { | ||||||||
const getDirectoryCache = async (): Promise<Cache> => { | ||||||||
return caches.open('response/issuer-directory'); | ||||||||
}; | ||||||||
|
||||||||
const FAKE_DOMAIN_CACHE = 'cache.local'; | ||||||||
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) => { | ||||||||
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 }); | ||||||||
|
||||||||
const keys = await ctx.env.ISSUANCE_KEYS.list({ include: ['customMetadata'] }); | ||||||||
|
||||||||
if (keys.objects.length === 0) { | ||||||||
|
@@ -95,14 +128,32 @@ export const handleTokenDirectory = async (ctx: Context, request?: Request) => { | |||||||
})), | ||||||||
}; | ||||||||
|
||||||||
return 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=86400', | ||||||||
'cache-control': `public, max-age=${ctx.env.DIRECTORY_CACHE_MAX_AGE_SECONDS}`, | ||||||||
'content-length': body.length.toString(), | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: Isn't content-length automatically set? Or is it missing due to chunked encoding being used? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when I first tested, it did not seem to be set automatically. therefore adding it here. The question I have is if the request is compressed, I'm not sure if it's modified automatically. |
||||||||
'date': new Date().toUTCString(), | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Date is not that important for caching, has this been added for debugging purposes? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's been added to help debugging if there are caching issues in the future. |
||||||||
etag, | ||||||||
}, | ||||||||
}); | ||||||||
ctx.waitUntil(cache.put(DIRECTORY_CACHE_REQUEST, response.clone())); | ||||||||
|
||||||||
return response; | ||||||||
}; | ||||||||
|
||||||||
const clearDirectoryCache = async (): Promise<boolean> => { | ||||||||
const cache = await getDirectoryCache(); | ||||||||
return cache.delete(DIRECTORY_CACHE_REQUEST); | ||||||||
}; | ||||||||
|
||||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||||||
export const handleRotateKey = async (ctx: Context, request?: Request) => { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can remove the linter exception and prepend variable's name with
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the rule triggered a warning in both case. I've added a rule configuration to ignore |
||||||||
ctx.metrics.keyRotationTotal.inc({ env: ctx.env.ENVIRONMENT }); | ||||||||
|
||||||||
|
@@ -148,9 +199,12 @@ export const handleRotateKey = async (ctx: Context, request?: Request) => { | |||||||
customMetadata: metadata, | ||||||||
}); | ||||||||
|
||||||||
ctx.waitUntil(clearDirectoryCache()); | ||||||||
|
||||||||
return new Response(`New key ${publicKeyEnc}`, { status: 201 }); | ||||||||
}; | ||||||||
|
||||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||||||
const handleClearKey = async (ctx: Context, request?: Request) => { | ||||||||
ctx.metrics.keyClearTotal.inc({ env: ctx.env.ENVIRONMENT }); | ||||||||
const keys = await ctx.env.ISSUANCE_KEYS.list(); | ||||||||
|
@@ -169,6 +223,9 @@ const handleClearKey = async (ctx: Context, request?: Request) => { | |||||||
} | ||||||||
const toDeleteArray = [...toDelete]; | ||||||||
await ctx.env.ISSUANCE_KEYS.delete(toDeleteArray); | ||||||||
|
||||||||
ctx.waitUntil(clearDirectoryCache()); | ||||||||
|
||||||||
return new Response(`Keys cleared: ${toDeleteArray.join('\n')}`, { status: 201 }); | ||||||||
}; | ||||||||
|
||||||||
|
@@ -179,6 +236,7 @@ export default { | |||||||
const router = new Router(); | ||||||||
|
||||||||
router | ||||||||
.head(PRIVATE_TOKEN_ISSUER_DIRECTORY, handleHeadTokenDirectory) | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that You do not have to go our of your way to omit the body in the Response constructor. The underlying Workers runtime will skip inclusion of the the body for HEAD requests. The advantage of using the same implementation is that your response (headers) will be the same with no extra effort. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updated |
||||||||
.get(PRIVATE_TOKEN_ISSUER_DIRECTORY, handleTokenDirectory) | ||||||||
.post('/token-request', handleTokenRequest) | ||||||||
.post('/admin/rotate', handleRotateKey) | ||||||||
|
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(''); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the metric counts cache miss. It is also possible to devise a metric
directory_cache_request_total{status="hit|miss"}
. However, I feel this would add uneccessary weight.