Skip to content
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

feat: add imgproxy #1337

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft

feat: add imgproxy #1337

wants to merge 3 commits into from

Conversation

casualmatt
Copy link

@casualmatt casualmatt commented Apr 22, 2024

WIP.

To start somewhere, I just imported my custom provider for imgproxy.

I used hash.js but that could probably be switch for ohash.

And I'm open to suggestions on how to secure the imgProxySalt and imgProxyKey.

--> Add support provider "imgproxy"

@@ -33,6 +33,7 @@
"consola": "^3.2.3",
"defu": "^6.1.4",
"h3": "^1.11.1",
"hash.js": "^1.1.7",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as you've suggested, let's switch to ohash 🙏

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not so experienced in hash or ohash, but I think that ohash misses the hmac function to generate the signature.

  const hmac = hash.hmac(hash.sha256, hexDecode(secret));

More in general, I'm open to suggestions on this topic.

Copy link

@everyx everyx Apr 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@casualmatt I have a implement at #963, using uncrypto, but need async getImage support

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ty for the hint, I will work on it tomorrow or later today👍🏻

Copy link
Author

@casualmatt casualmatt Apr 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@everyx
I see, .. so to properly support imgproxy, and to do it safely or imgproxy will allow us to optimize any URL provided; we are waiting for:
#276 --> To securely sign the URLs with uncrypto.
#963 --> To support getImage and not just the NuxtImg component.
I hope to get it right,

For now, as an alternative, @danielroe, we could remove the signing of the URL and add big, pretty big, I would say, disclaimer to use the EnvVar IMGPROXY_ALLOWED_SOURCES to secure the install of imgproxy.

@everyx
Copy link

everyx commented Apr 23, 2024

Some related resource about "secure the imgProxySalt and imgProxyKey"

#276
#963


export const getImage: ProviderGetImage = (src, options) => {
const { modifiers, url, salt, key } = options;
const mergeModifiers = { ...defaultModifiers, ...modifiers };
Copy link

@tombonez tombonez Jun 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per the Cloudinary provider, defu should be used here to ensure the defaults are correctly merged.

<NuxtImg src="..." width="400" height="400" /> results in the following mergeModifiers values with and without defu, if modifiers isn't set in nuxt.config.ts.

Without (this results in srcset not having the defaults set on the <img />):

{ fit: undefined, width: 400, height: 400, gravity: 'no', enlarge: 1, format: undefined, quality: undefined, background: undefined }
{ fit: undefined, width: 800, height: 800, gravity: 'no', enlarge: 1, format: undefined, quality: undefined, background: undefined }
{ fit: 'fill', width: 400, height: 400, gravity: 'no', enlarge: 1, format: 'webp' }

With:

{ fit: 'fill', width: 400, height: 400, gravity: 'no', enlarge: 1, format: 'webp' }
{ fit: 'fill', width: 800, height: 800, gravity: 'no', enlarge: 1, format: 'webp' }
{ fit: 'fill', width: 400, height: 400, gravity: 'no', enlarge: 1, format: 'webp' }

@everyx
Copy link

everyx commented Jun 1, 2024

@casualmatt I have implemented a version myself, you can use it as a reference

import type { ImageModifiers } from '@nuxt/image'

import { joinURL } from 'ufo'
import { defu } from 'defu'

import { urlSafeBase64 } from '../utils'
import { createOperationsGenerator } from '#image'

export interface ImgproxyModifiers extends ImageModifiers {
  quality: string
  background: string
  rotate: 'auto_right' | 'auto_left' | 'ignore' | 'vflip' | 'hflip' | number
  roundCorner: string
  gravity: 'sm' | string
  effect: string
  color: string
  flags: string
  dpr: string
  opacity: number
  overlay: string
  underlay: string
  transformation: string
  zoom: number
  colorSpace: string
  customFunc: string
  density: number
  aspectRatio: string
}

export interface ImgproxyOptions {
  baseURL?: string
  modifiers?: Partial<ImgproxyOptions>
  key?: string
  salt?: string
  signatureSize?: number
  srcPrefix?: string
  [key: string]: any
}

const operationsGenerator = createOperationsGenerator({
  keyMap: {
    // standard
    width: 'w',
    height: 'h',
    // format will act as a extension
    // format: 'f',
    quality: 'q',
    fit: 'rs',

    // imgporxy
    formatQuality: 'fq', // fq:%format1:%quality1:%format2:%quality2:...:%formatN:%qualityN
    resize: 'rs', // rs:%resizing_type:%width:%height:%enlarge:%extend
    size: 's', // s:%width:%height:%enlarge:%extend
    resizingType: 'rt', // rt:%resizing_type
    enlarge: 'el', // el:%enlarge
    extend: 'ex', // ex:%extend:%gravity
    minWidth: 'mw', // mw:%width
    minHeight: 'mh', // min-height
    zoom: 'z', // z:%zoom_x_y | z:%zoom_x:%zoom_y
    dpr: 'dpr', // dpr:%dpr
    extendAspectRatio: 'exar', // exar:%extend:%gravity
    gravity: 'g', // g:%type:%x_offset:%y_offset
    crop: 'c', // c:%width:%height:%gravity
    trim: 't', // t:%threshold:%color:%equal_hor:%equal_ver
    padding: 'pd', // pd:%top:%right:%bottom:%left
    autoRotate: 'ar', // ar:%auto_rotate
    rotate: 'rot', // rot:%angle
    background: 'bg', // bg:%R:%G:%B | bg:%hex_color
    blur: 'bl', // bl:%sigma
    sharpen: 'sh', // sh:%sigma
    pixelate: 'pix', // pix:%size
    watermark: 'wm', // wm:%opacity:%position:%x_offset:%y_offset:%scale
    stripMetadata: 'sm', // sm:%strip_metadata
    keepCopyright: 'kcr', // kcr:%keep_copyright
    stripColorProfile: 'scp', // scp:%strip_color_profile
    enforceThumbnail: 'eth', // eth:%enforce_thumbnail
    max_bytes: 'mb', // mb:%bytes
    skipProcessing: 'skp', // skp:%extension1:%extension2:...:%extensionN
    raw: 'raw', // raw:%raw
    cachebuster: 'cb', // cb:%string
    expires: 'exp', // exp:%timestamp
    filename: 'fn', // fn:%string

    // pro features
    resizingAlgorithm: 'ra', // * ra:%algorithm
    unsharpening: 'ush', // * ush:%mode:%weight:%dividor
    blurDetections: 'bd', // * bd:%sigma:%class_name1:%class_name2:...:%class_nameN
    drawDetections: 'dd', // * dd:%draw:%class_name1:%class_name2:...:%class_nameN
    gradient: 'gr', // * gr:%opacity:%color:%direction:%start%stop
    watermarkURL: 'wmu', // * wmu:%url
    watermarkText: 'wmt', // * wmt:%text
    watermarkSize: 'wms', // * wms:%width:%height
    watermarkShadow: 'wmsh', // * wmsh:%sigma
    style: 'st', // * st:%style
    backgroundAlpha: 'bga', // * bga:%alpha
    adjust: 'a', // * a:%brightness:%contrast:%saturation
    brightness: 'br', // * br:%brightness
    contrast: 'co', // * co:%contrast
    saturation: 'sa', // * sa:%saturation
    autoquality: 'aq', // * aq:%method:%target:%min_quality:%max_quality:%allowed_error
    jpegOptions: 'jpgo', // * jpgo:%progressive:%no_subsample:%trellis_quant:%overshoot_deringing:%optimize_scans:%quant_table
    pngOptions: 'pngo', // * pngo:%interlaced:%quantize:%quantization_colors
    webpOptions: 'pngo', // * webpo:%compression
    page: 'pg', // * pg:%page
    disableAnimation: 'da', // * da:%disable
    videoThumbnailSecond: 'vts', // * vts:%second
    fallbackImageUrl: 'fiu', // * fiu:%url
  },
  valueMap: {
    fit: {
      cover: 'fill:::1:0',
      contain: 'fit:::0:1',
      fill: 'force:::1:0',
      inside: 'fit:::0:0', // inside use min dimensions
      outside: 'fit:::0:0', // outside use max dimensions
    },
  },
  joinWith: '/',
  formatter: (key: string, val: string) => `${key}:${val}`,
})

/**
 * 让修饰符兼容 nuxt image 默认选项值
 */
function makeModifiersCompatible(modifiers: Partial<ImgproxyModifiers> = {}): Partial<ImgproxyModifiers> {
  const _modifiers: Partial<ImgproxyModifiers> = { ...modifiers }

  if (_modifiers.fit === 'outside' && _modifiers.width && _modifiers.height) {
    if (_modifiers.width > _modifiers.height)
      delete _modifiers.height
    else
      delete _modifiers.width
  }

  // 这里采用 URL 后缀方式来设置 format,不使用 format 参数
  if (_modifiers.format)
    delete _modifiers.format

  return _modifiers
}

const defaultModifiers = {
  fit: 'cover',
}

export function getImage(
  src: string,
  { modifiers = {}, baseURL = '/', srcPrefix = '' }: ImgproxyOptions = {}, // signatureSize = 32, key = '', salt = '',
) {
  const mergedModifiers = defu(modifiers, defaultModifiers)
  const compModifiers = makeModifiersCompatible(mergedModifiers)
  const processingOptions = operationsGenerator(compModifiers)

  const finalSrc = srcPrefix.length > 0 ? src.replace(new RegExp(srcPrefix), '') : src
  const encodedURL = urlSafeBase64(finalSrc)

  // const signature = await sign(salt, `/${processingOptions}/${encodedURL}`, key, signatureSize);
  const signature = '_'
  const extension = (typeof modifiers.format === 'string' && modifiers.format.length > 0)
    ? modifiers.format
    : undefined

  // https://docs.imgproxy.net/generating_the_url?id=example
  return {
    url: joinURL(
      baseURL,
      signature,
      processingOptions,
      extension ? `${encodedURL}.${extension}` : encodedURL,
    ),
  }
}

export default getImage

@casualmatt
Copy link
Author

@everyx Without support for server-side signatures, I wouldn't recommend publishing this. The risk is that users might expose their Imgproxy instance to the public, which could lead to security vulnerabilities or abuses.

@productdevbook
Copy link
Member

productdevbook commented Dec 23, 2024

you don't have to use salt and salt, by the way. can remove these 2 situations and assign them to normal requests. Salt protection is an additional feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants