processing_options.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. package options
  2. import (
  3. "net/http"
  4. "slices"
  5. "strconv"
  6. "strings"
  7. "github.com/imgproxy/imgproxy/v3/ierrors"
  8. "github.com/imgproxy/imgproxy/v3/imath"
  9. "github.com/imgproxy/imgproxy/v3/options/keys"
  10. )
  11. const maxClientHintDPR = 8
  12. func (p *Parser) applyURLOption(po *Options, name string, args []string, usedPresets ...string) error {
  13. switch name {
  14. case "resize", "rs":
  15. return applyResizeOption(po, args)
  16. case "size", "s":
  17. return applySizeOption(po, args)
  18. case "resizing_type", "rt":
  19. return applyResizingTypeOption(po, args)
  20. case "width", "w":
  21. return applyWidthOption(po, args)
  22. case "height", "h":
  23. return applyHeightOption(po, args)
  24. case "min-width", "mw":
  25. return applyMinWidthOption(po, args)
  26. case "min-height", "mh":
  27. return applyMinHeightOption(po, args)
  28. case "zoom", "z":
  29. return applyZoomOption(po, args)
  30. case "dpr":
  31. return applyDprOption(po, args)
  32. case "enlarge", "el":
  33. return applyEnlargeOption(po, args)
  34. case "extend", "ex":
  35. return applyExtendOption(po, args)
  36. case "extend_aspect_ratio", "extend_ar", "exar":
  37. return applyExtendAspectRatioOption(po, args)
  38. case "gravity", "g":
  39. return applyGravityOption(po, args)
  40. case "crop", "c":
  41. return applyCropOption(po, args)
  42. case "trim", "t":
  43. return applyTrimOption(po, args)
  44. case "padding", "pd":
  45. return applyPaddingOption(po, args)
  46. case "auto_rotate", "ar":
  47. return applyAutoRotateOption(po, args)
  48. case "rotate", "rot":
  49. return applyRotateOption(po, args)
  50. case "background", "bg":
  51. return applyBackgroundOption(po, args)
  52. case "blur", "bl":
  53. return applyBlurOption(po, args)
  54. case "sharpen", "sh":
  55. return applySharpenOption(po, args)
  56. case "pixelate", "pix":
  57. return applyPixelateOption(po, args)
  58. case "watermark", "wm":
  59. return applyWatermarkOption(po, args)
  60. case "strip_metadata", "sm":
  61. return applyStripMetadataOption(po.Main(), args)
  62. case "keep_copyright", "kcr":
  63. return applyKeepCopyrightOption(po.Main(), args)
  64. case "strip_color_profile", "scp":
  65. return applyStripColorProfileOption(po.Main(), args)
  66. case "enforce_thumbnail", "eth":
  67. return applyEnforceThumbnailOption(po.Main(), args)
  68. // Saving options
  69. case "quality", "q":
  70. return applyQualityOption(po.Main(), args)
  71. case "format_quality", "fq":
  72. return applyFormatQualityOption(po.Main(), args)
  73. case "max_bytes", "mb":
  74. return applyMaxBytesOption(po.Main(), args)
  75. case "format", "f", "ext":
  76. return applyFormatOption(po.Main(), args)
  77. // Handling options
  78. case "skip_processing", "skp":
  79. return applySkipProcessingFormatsOption(po.Main(), args)
  80. case "raw":
  81. return applyRawOption(po.Main(), args)
  82. case "cachebuster", "cb":
  83. return applyCacheBusterOption(po.Main(), args)
  84. case "expires", "exp":
  85. return applyExpiresOption(po.Main(), args)
  86. case "filename", "fn":
  87. return applyFilenameOption(po.Main(), args)
  88. case "return_attachment", "att":
  89. return applyReturnAttachmentOption(po.Main(), args)
  90. // Presets
  91. case "preset", "pr":
  92. return applyPresetOption(p, po, args, usedPresets...)
  93. // Security
  94. case "max_src_resolution", "msr":
  95. return applyMaxSrcResolutionOption(p, po.Main(), args)
  96. case "max_src_file_size", "msfs":
  97. return applyMaxSrcFileSizeOption(p, po.Main(), args)
  98. case "max_animation_frames", "maf":
  99. return applyMaxAnimationFramesOption(p, po.Main(), args)
  100. case "max_animation_frame_resolution", "mafr":
  101. return applyMaxAnimationFrameResolutionOption(p, po.Main(), args)
  102. case "max_result_dimension", "mrd":
  103. return applyMaxResultDimensionOption(p, po.Main(), args)
  104. }
  105. return newUnknownOptionError("processing", name)
  106. }
  107. func (p *Parser) applyURLOptions(po *Options, options urlOptions, allowAll bool, usedPresets ...string) error {
  108. allowAll = allowAll || len(p.config.AllowedProcessingOptions) == 0
  109. for _, opt := range options {
  110. if !allowAll && !slices.Contains(p.config.AllowedProcessingOptions, opt.Name) {
  111. return newForbiddenOptionError("processing", opt.Name)
  112. }
  113. if err := p.applyURLOption(po, opt.Name, opt.Args, usedPresets...); err != nil {
  114. return err
  115. }
  116. }
  117. return nil
  118. }
  119. func (p *Parser) defaultProcessingOptions(headers http.Header) (*Options, error) {
  120. po := New()
  121. headerAccept := headers.Get("Accept")
  122. if (p.config.AutoWebp || p.config.EnforceWebp) && strings.Contains(headerAccept, "image/webp") {
  123. po.Set(keys.PreferWebP, true)
  124. if p.config.EnforceWebp {
  125. po.Set(keys.EnforceWebP, true)
  126. }
  127. }
  128. if (p.config.AutoAvif || p.config.EnforceAvif) && strings.Contains(headerAccept, "image/avif") {
  129. po.Set(keys.PreferAvif, true)
  130. if p.config.EnforceAvif {
  131. po.Set(keys.EnforceAvif, true)
  132. }
  133. }
  134. if (p.config.AutoJxl || p.config.EnforceJxl) && strings.Contains(headerAccept, "image/jxl") {
  135. po.Set(keys.PreferJxl, true)
  136. if p.config.EnforceJxl {
  137. po.Set(keys.EnforceJxl, true)
  138. }
  139. }
  140. if p.config.EnableClientHints {
  141. dpr := 1.0
  142. headerDPR := headers.Get("Sec-CH-DPR")
  143. if len(headerDPR) == 0 {
  144. headerDPR = headers.Get("DPR")
  145. }
  146. if len(headerDPR) > 0 {
  147. if d, err := strconv.ParseFloat(headerDPR, 64); err == nil && (d > 0 && d <= maxClientHintDPR) {
  148. dpr = d
  149. po.Set(keys.Dpr, dpr)
  150. }
  151. }
  152. headerWidth := headers.Get("Sec-CH-Width")
  153. if len(headerWidth) == 0 {
  154. headerWidth = headers.Get("Width")
  155. }
  156. if len(headerWidth) > 0 {
  157. if w, err := strconv.Atoi(headerWidth); err == nil {
  158. po.Set(keys.Width, imath.Shrink(w, dpr))
  159. }
  160. }
  161. }
  162. if _, ok := p.presets["default"]; ok {
  163. if err := applyPresetOption(p, po, []string{"default"}); err != nil {
  164. return po, err
  165. }
  166. }
  167. return po, nil
  168. }
  169. // ParsePath parses the given request path and returns the processing options and image URL
  170. func (p *Parser) ParsePath(
  171. path string,
  172. headers http.Header,
  173. ) (po *Options, imageURL string, err error) {
  174. if path == "" || path == "/" {
  175. return nil, "", newInvalidURLError("invalid path: %s", path)
  176. }
  177. parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
  178. if p.config.OnlyPresets {
  179. po, imageURL, err = p.parsePathPresets(parts, headers)
  180. } else {
  181. po, imageURL, err = p.parsePathOptions(parts, headers)
  182. }
  183. if err != nil {
  184. return nil, "", ierrors.Wrap(err, 0)
  185. }
  186. return po, imageURL, nil
  187. }
  188. // parsePathOptions parses processing options from the URL path
  189. func (p *Parser) parsePathOptions(parts []string, headers http.Header) (*Options, string, error) {
  190. if _, ok := resizeTypes[parts[0]]; ok {
  191. return nil, "", newInvalidURLError("It looks like you're using the deprecated basic URL format")
  192. }
  193. po, err := p.defaultProcessingOptions(headers)
  194. if err != nil {
  195. return nil, "", err
  196. }
  197. options, urlParts := p.parseURLOptions(parts)
  198. if err = p.applyURLOptions(po, options, false); err != nil {
  199. return nil, "", err
  200. }
  201. url, extension, err := p.DecodeURL(urlParts)
  202. if err != nil {
  203. return nil, "", err
  204. }
  205. if !Get(po, keys.Raw, false) && len(extension) > 0 {
  206. if err = applyFormatOption(po, []string{extension}); err != nil {
  207. return nil, "", err
  208. }
  209. }
  210. return po, url, nil
  211. }
  212. // parsePathPresets parses presets from the URL path
  213. func (p *Parser) parsePathPresets(parts []string, headers http.Header) (*Options, string, error) {
  214. po, err := p.defaultProcessingOptions(headers)
  215. if err != nil {
  216. return nil, "", err
  217. }
  218. presets := strings.Split(parts[0], p.config.ArgumentsSeparator)
  219. urlParts := parts[1:]
  220. if err = applyPresetOption(p, po, presets); err != nil {
  221. return nil, "", err
  222. }
  223. url, extension, err := p.DecodeURL(urlParts)
  224. if err != nil {
  225. return nil, "", err
  226. }
  227. if !Get(po, keys.Raw, false) && len(extension) > 0 {
  228. if err = applyFormatOption(po, []string{extension}); err != nil {
  229. return nil, "", err
  230. }
  231. }
  232. return po, url, nil
  233. }