mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
turn off image signing to save CPU
This commit is contained in:
+16
-3
@@ -15,12 +15,25 @@ key:
|
|||||||
honours `?sig=p1` and returns a `Content-Signature` header.
|
honours `?sig=p1` and returns a `Content-Signature` header.
|
||||||
- `GET /<key>?sig=p1` — same, but the response body is RSA-SHA1 signed and the
|
- `GET /<key>?sig=p1` — same, but the response body is RSA-SHA1 signed and the
|
||||||
signature returned in a `Content-Signature: key-id=KEY:RSA:p1.rec.net; data=<base64>`
|
signature returned in a `Content-Signature: key-id=KEY:RSA:p1.rec.net; data=<base64>`
|
||||||
header. The client uses this to verify image integrity. Signing buffers the
|
header. The client uses this to verify image integrity. **Off by default** —
|
||||||
whole object.
|
see below.
|
||||||
|
|
||||||
The bucket is bound in the Worker as `env.IMAGES` (see `wrangler.jsonc`).
|
The bucket is bound in the Worker as `env.IMAGES` (see `wrangler.jsonc`).
|
||||||
|
|
||||||
## Response signing key
|
## Response signing
|
||||||
|
|
||||||
|
Signing is behind the `IMG_SIGNING_ENABLED` var in `wrangler.jsonc`, and it is
|
||||||
|
**`false`**. It is the worker's dominant CPU cost: signing has to buffer the
|
||||||
|
whole object into the isolate rather than streaming it out of R2, then hash the
|
||||||
|
full body with SHA-1 and run an RSA-2048 private-key operation — on every request
|
||||||
|
the edge cache misses. Nothing verifies the header today, so with the flag off
|
||||||
|
`?sig=p1` is accepted and ignored, and untransformed images stream through
|
||||||
|
untouched.
|
||||||
|
|
||||||
|
Flip the var to `true` to turn it back on. (Resizes still buffer regardless — the
|
||||||
|
Photon codec needs the whole image.)
|
||||||
|
|
||||||
|
### Signing key
|
||||||
|
|
||||||
`?sig=p1` signs with the RSA-2048 key in `env.IMG_SIGNING_KEY` (PKCS8 DER,
|
`?sig=p1` signs with the RSA-2048 key in `env.IMG_SIGNING_KEY` (PKCS8 DER,
|
||||||
base64). `wrangler.jsonc` ships an **insecure dev key** for local dev / tests;
|
base64). `wrangler.jsonc` ships an **insecure dev key** for local dev / tests;
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ export type Env = SharedHonoEnv & {
|
|||||||
* requested with `?sig=p1`. Optional — when absent, responses are unsigned.
|
* requested with `?sig=p1`. Optional — when absent, responses are unsigned.
|
||||||
*/
|
*/
|
||||||
IMG_SIGNING_KEY?: string
|
IMG_SIGNING_KEY?: string
|
||||||
|
/**
|
||||||
|
* Feature flag for response signing. `?sig=p1` is honoured only when this is
|
||||||
|
* true; when false (the default) the query param is ignored and the response
|
||||||
|
* streams unsigned. Off by default because signing is this worker's dominant
|
||||||
|
* CPU cost — see the `?sig=p1` handling in `img.app.ts`.
|
||||||
|
*/
|
||||||
|
IMG_SIGNING_ENABLED?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Variables can be extended */
|
/** Variables can be extended */
|
||||||
|
|||||||
+12
-4
@@ -257,7 +257,8 @@ app.get(
|
|||||||
'`recflare-img` bucket; extensionless ones are `storage` uploads and come from the',
|
'`recflare-img` bucket; extensionless ones are `storage` uploads and come from the',
|
||||||
'shared `recflare-cdn` bucket under its `image/` prefix. Optional center-crop and resize',
|
'shared `recflare-cdn` bucket under its `image/` prefix. Optional center-crop and resize',
|
||||||
'run through the Photon WASM codec; `?sig=p1` adds the RSA-SHA1 `Content-Signature`',
|
'run through the Photon WASM codec; `?sig=p1` adds the RSA-SHA1 `Content-Signature`',
|
||||||
'header the client verifies against `KEY:RSA:p1.rec.net`.',
|
'header the client verifies against `KEY:RSA:p1.rec.net`, when the',
|
||||||
|
'`IMG_SIGNING_ENABLED` flag is on (it is off by default).',
|
||||||
'',
|
'',
|
||||||
'Note that this worker only serves bytes: the image metadata the client lists (the',
|
'Note that this worker only serves bytes: the image metadata the client lists (the',
|
||||||
'`SavedImage` records behind `/api/images/...`) lives in the `api` worker, which',
|
'`SavedImage` records behind `/api/images/...`) lives in the `api` worker, which',
|
||||||
@@ -345,8 +346,10 @@ app.get(
|
|||||||
description: [
|
description: [
|
||||||
'`p1` RSA-SHA1 signs the response body and returns it as',
|
'`p1` RSA-SHA1 signs the response body and returns it as',
|
||||||
'`Content-Signature: key-id=KEY:RSA:p1.rec.net; data=<base64>`. Signed over the',
|
'`Content-Signature: key-id=KEY:RSA:p1.rec.net; data=<base64>`. Signed over the',
|
||||||
'bytes actually returned, i.e. the resized body when a transform applies. Omitted',
|
'bytes actually returned, i.e. the resized body when a transform applies.',
|
||||||
'when the worker has no `IMG_SIGNING_KEY`.',
|
'Signing is off by default (it costs the streaming fast path): the param is',
|
||||||
|
'ignored, and no header returned, unless the worker sets `IMG_SIGNING_ENABLED`',
|
||||||
|
'and has an `IMG_SIGNING_KEY`.',
|
||||||
].join(' '),
|
].join(' '),
|
||||||
schema: { type: 'string', enum: ['p1'] },
|
schema: { type: 'string', enum: ['p1'] },
|
||||||
},
|
},
|
||||||
@@ -369,7 +372,12 @@ app.get(
|
|||||||
const key = c.req.param('key')
|
const key = c.req.param('key')
|
||||||
if (key.includes('..')) return c.body(null, 400)
|
if (key.includes('..')) return c.body(null, 400)
|
||||||
|
|
||||||
const wantsSignature = c.req.query('sig') === 'p1'
|
// Signing is this worker's dominant CPU cost: it forces the whole object
|
||||||
|
// through the isolate (`arrayBuffer()` instead of streaming `object.body`)
|
||||||
|
// and pays a SHA-1 over the full body plus an RSA-2048 private-key operation
|
||||||
|
// on every edge-cache miss. Nothing verifies the header today, so `?sig=p1`
|
||||||
|
// is ignored unless IMG_SIGNING_ENABLED turns it back on.
|
||||||
|
const wantsSignature = c.env.IMG_SIGNING_ENABLED === true && c.req.query('sig') === 'p1'
|
||||||
const transform = parseTransform(
|
const transform = parseTransform(
|
||||||
c.req.query('width'),
|
c.req.query('width'),
|
||||||
c.req.query('height'),
|
c.req.query('height'),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { PhotonImage } from '@cf-wasm/photon'
|
import { PhotonImage } from '@cf-wasm/photon'
|
||||||
import { env, SELF } from 'cloudflare:test'
|
import { createExecutionContext, env, SELF, waitOnExecutionContext } from 'cloudflare:test'
|
||||||
import { beforeAll, describe, expect, it } from 'vitest'
|
import { beforeAll, describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import '../../img.app'
|
import app from '../../img.app'
|
||||||
|
|
||||||
import type { Env } from '../../context'
|
import type { Env } from '../../context'
|
||||||
|
|
||||||
@@ -188,6 +188,25 @@ describe('img endpoints', () => {
|
|||||||
expect(res.headers.get('content-signature')).toBeNull()
|
expect(res.headers.get('content-signature')).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('ignores ?sig=p1 when IMG_SIGNING_ENABLED is off', async () => {
|
||||||
|
// The deployed default (see wrangler.jsonc): the param is accepted but does
|
||||||
|
// nothing, so the object streams straight from R2 instead of being buffered,
|
||||||
|
// hashed and RSA-signed. Vitest binds the flag ON, so override it here.
|
||||||
|
const ctx = createExecutionContext()
|
||||||
|
const res = await app.fetch(
|
||||||
|
new Request(`${ORIGIN}/${R2_KEY}?sig=p1`),
|
||||||
|
{ ...env, IMG_SIGNING_ENABLED: false },
|
||||||
|
ctx
|
||||||
|
)
|
||||||
|
await waitOnExecutionContext(ctx)
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(res.headers.get('content-signature')).toBeNull()
|
||||||
|
// Unsigned responses keep the source etag, and the body is untouched.
|
||||||
|
expect(res.headers.get('etag')).toBeTruthy()
|
||||||
|
expect(new Uint8Array(await res.arrayBuffer())).toEqual(IMAGE_BYTES)
|
||||||
|
})
|
||||||
|
|
||||||
it('resizes a static asset to ?width, preserving aspect ratio', async () => {
|
it('resizes a static asset to ?width, preserving aspect ratio', async () => {
|
||||||
const full = new Uint8Array(await (await SELF.fetch(`${ORIGIN}/RecCenter.jpg`)).arrayBuffer())
|
const full = new Uint8Array(await (await SELF.fetch(`${ORIGIN}/RecCenter.jpg`)).arrayBuffer())
|
||||||
const original = jpegSize(full)
|
const original = jpegSize(full)
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ export default defineConfig({
|
|||||||
miniflare: {
|
miniflare: {
|
||||||
bindings: {
|
bindings: {
|
||||||
ENVIRONMENT: 'VITEST',
|
ENVIRONMENT: 'VITEST',
|
||||||
|
// Signing is off in `wrangler.jsonc`; turn it on here so the `?sig=p1`
|
||||||
|
// path stays covered. The flag-off behaviour is tested by calling the
|
||||||
|
// app directly with an overridden env.
|
||||||
|
IMG_SIGNING_ENABLED: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -54,6 +54,12 @@
|
|||||||
"vars": {
|
"vars": {
|
||||||
"ENVIRONMENT": "development", // overridden during deployment
|
"ENVIRONMENT": "development", // overridden during deployment
|
||||||
"SENTRY_RELEASE": "unknown", // overridden during deployment
|
"SENTRY_RELEASE": "unknown", // overridden during deployment
|
||||||
|
// Feature flag for `?sig=p1` response signing. OFF: signing buffers the whole
|
||||||
|
// object into the isolate instead of streaming it from R2, then pays a SHA-1
|
||||||
|
// over the full body plus an RSA-2048 private-key op on every edge-cache miss.
|
||||||
|
// Nothing verifies the `Content-Signature` header today. Flip to true (and set
|
||||||
|
// a real IMG_SIGNING_KEY) if something ever needs to.
|
||||||
|
"IMG_SIGNING_ENABLED": false,
|
||||||
// RSA-2048 private key (PKCS8 DER, base64) used to sign image responses
|
// RSA-2048 private key (PKCS8 DER, base64) used to sign image responses
|
||||||
// requested with ?sig=p1. This is an INSECURE DEV KEY committed for local
|
// requested with ?sig=p1. This is an INSECURE DEV KEY committed for local
|
||||||
// dev / tests — override in production with `wrangler secret put IMG_SIGNING_KEY`.
|
// dev / tests — override in production with `wrangler secret put IMG_SIGNING_KEY`.
|
||||||
|
|||||||
Reference in New Issue
Block a user