diff --git a/.github/composite/deploy-cloudflare/action.yaml b/.github/composite/deploy-cloudflare/action.yaml index 136ad52b1..f8b7181a5 100644 --- a/.github/composite/deploy-cloudflare/action.yaml +++ b/.github/composite/deploy-cloudflare/action.yaml @@ -57,7 +57,8 @@ runs: GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST GITBOOK_INTEGRATIONS_CONTENT_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_CONTENT_HOST GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY - GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL + GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL_2 + GITBOOK_IMAGE_RESIZE_SALT: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SALT GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL diff --git a/.github/composite/deploy-vercel/action.yaml b/.github/composite/deploy-vercel/action.yaml index 8c62577c7..b796c5a59 100644 --- a/.github/composite/deploy-vercel/action.yaml +++ b/.github/composite/deploy-vercel/action.yaml @@ -59,7 +59,8 @@ runs: GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST GITBOOK_INTEGRATIONS_CONTENT_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_CONTENT_HOST GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY - GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL + GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL_2 + GITBOOK_IMAGE_RESIZE_SALT: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SALT GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL diff --git a/packages/gitbook/next.config.mjs b/packages/gitbook/next.config.mjs index 309e7d0e5..992281a11 100644 --- a/packages/gitbook/next.config.mjs +++ b/packages/gitbook/next.config.mjs @@ -50,6 +50,7 @@ const nextConfig = { GITBOOK_INTEGRATIONS_HOST: process.env.GITBOOK_INTEGRATIONS_HOST, GITBOOK_INTEGRATIONS_CONTENT_HOST: process.env.GITBOOK_INTEGRATIONS_CONTENT_HOST, GITBOOK_IMAGE_RESIZE_URL: process.env.GITBOOK_IMAGE_RESIZE_URL, + GITBOOK_IMAGE_RESIZE_SALT: process.env.GITBOOK_IMAGE_RESIZE_SALT, GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL, GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN, GITBOOK_URL: process.env.GITBOOK_URL, diff --git a/packages/gitbook/src/lib/env/globals.ts b/packages/gitbook/src/lib/env/globals.ts index 85b069cd9..92970e753 100644 --- a/packages/gitbook/src/lib/env/globals.ts +++ b/packages/gitbook/src/lib/env/globals.ts @@ -105,14 +105,15 @@ export const GITBOOK_FONTS_URL = process.env.GITBOOK_FONTS_URL || 'https://fonts export const GITBOOK_IMAGE_RESIZE_URL = process.env.GITBOOK_IMAGE_RESIZE_URL ?? null; export const GITBOOK_IMAGE_RESIZE_SIGNING_KEY = process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY ?? null; +export const GITBOOK_IMAGE_RESIZE_SALT = process.env.GITBOOK_IMAGE_RESIZE_SALT ?? null; /** * Mode used for resizing images. */ export const GITBOOK_IMAGE_RESIZE_MODE = enforceEnum( 'GITBOOK_IMAGE_RESIZE_MODE', - process.env.GITBOOK_IMAGE_RESIZE_MODE || 'cdn-cgi', - ['cdn-cgi', 'cf-fetch'] + process.env.GITBOOK_IMAGE_RESIZE_MODE || 'cf-fetch', + ['cf-fetch', 'gitbook-service', 'cdn-cgi'] ); /** diff --git a/packages/gitbook/src/lib/images/resizer/cdn-cgi.ts b/packages/gitbook/src/lib/images/resizer/cdn-cgi.ts deleted file mode 100644 index 1151c59b0..000000000 --- a/packages/gitbook/src/lib/images/resizer/cdn-cgi.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { GITBOOK_IMAGE_RESIZE_URL } from '@/lib/env'; -import { getLogger } from '@/lib/logger'; -import type { CloudflareImageOptions } from './types'; -import { copyImageResponse } from './utils'; - -/** - * Resize an image by doing a request to a /cdn/cgi/ endpoint. - * https://developers.cloudflare.com/images/transform-images/transform-via-url/ - */ -export async function resizeImageWithCDNCgi( - input: string, - options: CloudflareImageOptions & { - signal?: AbortSignal; - } -): Promise { - const { signal, ...resizeOptions } = options; - - // Since Cloudflare Images options on fetch are not supported on Cloudflare Pages, - // we need to use the Cloudflare Image Resize API directly. - if (!GITBOOK_IMAGE_RESIZE_URL) { - throw new Error('GITBOOK_IMAGE_RESIZE_URL is not set for cdn-cgi image resize mode'); - } - - const resizeURL = `${GITBOOK_IMAGE_RESIZE_URL}${stringifyOptions( - resizeOptions - )}/${encodeURIComponent(input)}`; - - const logger = getLogger().subLogger('imageResizing'); - logger.log(`resize image using cdn-cgi: ${resizeURL}`); - - return copyImageResponse( - await fetch(resizeURL, { - headers: { - // Pass the `Accept` header, as Cloudflare uses this to validate the format. - Accept: - resizeOptions.format === 'json' - ? 'application/json' - : `image/${resizeOptions.format || 'jpeg'}`, - }, - signal, - }) - ); -} - -function stringifyOptions(options: CloudflareImageOptions): string { - return Object.entries({ ...options }).reduce((rest, [key, value]) => { - return `${rest}${rest ? ',' : ''}${key}=${value}`; - }, ''); -} diff --git a/packages/gitbook/src/lib/images/resizer/gitbook-service.ts b/packages/gitbook/src/lib/images/resizer/gitbook-service.ts new file mode 100644 index 000000000..f1e890afd --- /dev/null +++ b/packages/gitbook/src/lib/images/resizer/gitbook-service.ts @@ -0,0 +1,65 @@ +import { GITBOOK_IMAGE_RESIZE_SALT, GITBOOK_IMAGE_RESIZE_URL } from '@/lib/env'; +import { getLogger } from '@/lib/logger'; +import type { CloudflareImageOptions } from './types'; +import { copyImageResponse } from './utils'; + +function sdbmHash(str: string): number { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + (hash << 6) + (hash << 16) - hash; + hash |= 0; // ensure 32-bit int + } + return hash; +} + +/** + * Resize an image using the GitBook image service. + * https://images.gitbook.com/__img/options/encoded-url + */ +export async function resizeImageWithGitbookServices( + input: string, + options: CloudflareImageOptions & { + signal?: AbortSignal; + } +): Promise { + const { signal, ...resizeOptions } = options; + + if (!GITBOOK_IMAGE_RESIZE_SALT) { + throw new Error( + 'GITBOOK_IMAGE_RESIZE_SALT is not set for gitbook-service image resize mode' + ); + } + + if (!GITBOOK_IMAGE_RESIZE_URL) { + throw new Error( + 'GITBOOK_IMAGE_RESIZE_URL is not set for gitbook-service image resize mode' + ); + } + + const signature = sdbmHash(`${input}:${GITBOOK_IMAGE_RESIZE_SALT}`).toString(); + const resizeURL = `${GITBOOK_IMAGE_RESIZE_URL}${stringifyOptions({ + ...resizeOptions, + signature, + })}/${encodeURIComponent(input)}`; + + const logger = getLogger().subLogger('imageResizing'); + logger.log(`resize image using gitbook-service: ${resizeURL}, format: ${resizeOptions.format}`); + + return copyImageResponse( + await fetch(resizeURL, { + headers: { + Accept: + resizeOptions.format === 'json' + ? 'application/json' + : `image/${resizeOptions.format || 'jpeg'}`, + }, + signal, + }) + ); +} + +function stringifyOptions(options: CloudflareImageOptions & { signature: string }): string { + return Object.entries({ ...options }).reduce((rest, [key, value]) => { + return `${rest}${rest ? ',' : ''}${key}=${value}`; + }, ''); +} diff --git a/packages/gitbook/src/lib/images/resizer/resizeImage.ts b/packages/gitbook/src/lib/images/resizer/resizeImage.ts index 1e65ffc1e..f2dccbd44 100644 --- a/packages/gitbook/src/lib/images/resizer/resizeImage.ts +++ b/packages/gitbook/src/lib/images/resizer/resizeImage.ts @@ -3,8 +3,8 @@ import { getLogger } from '@/lib/logger'; import assertNever from 'assert-never'; import { GITBOOK_IMAGE_RESIZE_MODE } from '../../env'; import { SizableImageAction, checkIsSizableImageURL } from '../checkIsSizableImageURL'; -import { resizeImageWithCDNCgi } from './cdn-cgi'; import { resizeImageWithCFFetch } from './cf-fetch'; +import { resizeImageWithGitbookServices } from './gitbook-service'; import type { CloudflareImageJsonFormat, CloudflareImageOptions } from './types'; /** @@ -71,10 +71,11 @@ export async function resizeImage( } switch (GITBOOK_IMAGE_RESIZE_MODE) { - case 'cdn-cgi': - return resizeImageWithCDNCgi(input, options); case 'cf-fetch': return resizeImageWithCFFetch(input, options); + case 'gitbook-service': + case 'cdn-cgi': + return resizeImageWithGitbookServices(input, options); default: assertNever(GITBOOK_IMAGE_RESIZE_MODE); }