mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-22 10:33:22 +00:00
Passthrough SVG images in image resizing (#3224)
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"gitbook-v2": minor
|
||||||
|
"gitbook": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Pass SVG images through image resizing without resizing them to serve them from optimal host.
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
import { checkIsHttpURL } from '@/lib/urls';
|
export enum SizableImageAction {
|
||||||
|
Resize = 'resize',
|
||||||
|
Skip = 'skip',
|
||||||
|
Passthrough = 'passthrough',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an image URL is resizable.
|
* Check if an image URL is resizable.
|
||||||
@@ -6,22 +10,24 @@ import { checkIsHttpURL } from '@/lib/urls';
|
|||||||
* Skip it for SVGs.
|
* Skip it for SVGs.
|
||||||
* Skip it for GitBook images (to avoid recursion).
|
* Skip it for GitBook images (to avoid recursion).
|
||||||
*/
|
*/
|
||||||
export function checkIsSizableImageURL(input: string): boolean {
|
export function checkIsSizableImageURL(input: string): SizableImageAction {
|
||||||
if (!URL.canParse(input)) {
|
if (!URL.canParse(input)) {
|
||||||
return false;
|
return SizableImageAction.Skip;
|
||||||
}
|
|
||||||
|
|
||||||
if (input.includes('/~gitbook/image')) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = new URL(input);
|
const parsed = new URL(input);
|
||||||
if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) {
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||||
return false;
|
return SizableImageAction.Skip;
|
||||||
}
|
}
|
||||||
if (!checkIsHttpURL(parsed)) {
|
if (parsed.hostname === 'localhost') {
|
||||||
return false;
|
return SizableImageAction.Skip;
|
||||||
|
}
|
||||||
|
if (parsed.pathname.includes('/~gitbook/image')) {
|
||||||
|
return SizableImageAction.Skip;
|
||||||
|
}
|
||||||
|
if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) {
|
||||||
|
return SizableImageAction.Passthrough;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return SizableImageAction.Resize;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'server-only';
|
import 'server-only';
|
||||||
import { GITBOOK_IMAGE_RESIZE_SIGNING_KEY, GITBOOK_IMAGE_RESIZE_URL } from '../env';
|
import { GITBOOK_IMAGE_RESIZE_SIGNING_KEY, GITBOOK_IMAGE_RESIZE_URL } from '../env';
|
||||||
import type { GitBookLinker } from '../links';
|
import type { GitBookLinker } from '../links';
|
||||||
import { checkIsSizableImageURL } from './checkIsSizableImageURL';
|
import { SizableImageAction, checkIsSizableImageURL } from './checkIsSizableImageURL';
|
||||||
import { getImageSize } from './resizer';
|
import { getImageSize } from './resizer';
|
||||||
import { type SignatureVersion, generateImageSignature } from './signatures';
|
import { type SignatureVersion, generateImageSignature } from './signatures';
|
||||||
import type { ImageResizer } from './types';
|
import type { ImageResizer } from './types';
|
||||||
@@ -24,7 +24,7 @@ export function createImageResizer({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
getResizedImageURL: (urlInput) => {
|
getResizedImageURL: (urlInput) => {
|
||||||
if (!checkIsSizableImageURL(urlInput)) {
|
if (checkIsSizableImageURL(urlInput) === SizableImageAction.Skip) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ export function createImageResizer({
|
|||||||
},
|
},
|
||||||
|
|
||||||
getImageSize: async (input, options) => {
|
getImageSize: async (input, options) => {
|
||||||
if (!checkIsSizableImageURL(input)) {
|
if (checkIsSizableImageURL(input) !== SizableImageAction.Resize) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'server-only';
|
import 'server-only';
|
||||||
import assertNever from 'assert-never';
|
import assertNever from 'assert-never';
|
||||||
import { GITBOOK_IMAGE_RESIZE_MODE } from '../../env';
|
import { GITBOOK_IMAGE_RESIZE_MODE } from '../../env';
|
||||||
import { checkIsSizableImageURL } from '../checkIsSizableImageURL';
|
import { SizableImageAction, checkIsSizableImageURL } from '../checkIsSizableImageURL';
|
||||||
import { resizeImageWithCDNCgi } from './cdn-cgi';
|
import { resizeImageWithCDNCgi } from './cdn-cgi';
|
||||||
import { resizeImageWithCFFetch } from './cf-fetch';
|
import { resizeImageWithCFFetch } from './cf-fetch';
|
||||||
import type { CloudflareImageJsonFormat, CloudflareImageOptions } from './types';
|
import type { CloudflareImageJsonFormat, CloudflareImageOptions } from './types';
|
||||||
@@ -13,7 +13,7 @@ export async function getImageSize(
|
|||||||
input: string,
|
input: string,
|
||||||
defaultSize: Partial<CloudflareImageOptions> = {}
|
defaultSize: Partial<CloudflareImageOptions> = {}
|
||||||
): Promise<{ width: number; height: number } | null> {
|
): Promise<{ width: number; height: number } | null> {
|
||||||
if (!checkIsSizableImageURL(input)) {
|
if (checkIsSizableImageURL(input) !== SizableImageAction.Resize) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,13 +48,17 @@ export async function resizeImage(
|
|||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
}
|
}
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const parsed = new URL(input);
|
const action = checkIsSizableImageURL(input);
|
||||||
if (parsed.protocol === 'data:') {
|
if (action === SizableImageAction.Skip) {
|
||||||
throw new Error('Cannot resize data: URLs');
|
throw new Error(
|
||||||
|
'Cannot resize this image, this function should have never been called on this url'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.hostname === 'localhost') {
|
if (action === SizableImageAction.Passthrough) {
|
||||||
throw new Error('Cannot resize localhost URLs');
|
return fetch(input, {
|
||||||
|
signal: options.signal,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (GITBOOK_IMAGE_RESIZE_MODE) {
|
switch (GITBOOK_IMAGE_RESIZE_MODE) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
CURRENT_SIGNATURE_VERSION,
|
CURRENT_SIGNATURE_VERSION,
|
||||||
type CloudflareImageOptions,
|
type CloudflareImageOptions,
|
||||||
type SignatureVersion,
|
type SignatureVersion,
|
||||||
|
SizableImageAction,
|
||||||
checkIsSizableImageURL,
|
checkIsSizableImageURL,
|
||||||
isSignatureVersion,
|
isSignatureVersion,
|
||||||
parseImageAPIURL,
|
parseImageAPIURL,
|
||||||
@@ -40,7 +41,7 @@ export async function serveResizedImage(
|
|||||||
// Check again if the image can be sized, even though we checked when rendering the Image component
|
// Check again if the image can be sized, even though we checked when rendering the Image component
|
||||||
// Otherwise, it's possible to pass just any link to this endpoint and trigger HTML injection on the domain
|
// Otherwise, it's possible to pass just any link to this endpoint and trigger HTML injection on the domain
|
||||||
// Also prevent infinite redirects.
|
// Also prevent infinite redirects.
|
||||||
if (!checkIsSizableImageURL(url)) {
|
if (checkIsSizableImageURL(url) === SizableImageAction.Skip) {
|
||||||
return new Response('Invalid url parameter', { status: 400 });
|
return new Response('Invalid url parameter', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user