Eric Bower
·
2026-04-20
proxy.go
1package storage
2
3import (
4 "crypto/hmac"
5 "crypto/sha256"
6 "encoding/base64"
7 "encoding/hex"
8 "fmt"
9 "net/http"
10 "net/http/httputil"
11 "net/url"
12 "os"
13 "strconv"
14 "strings"
15
16 "github.com/picosh/pico/pkg/shared/mime"
17)
18
19type ImgProxy struct {
20 url string
21 salt string
22 key string
23 filepath string
24 opts *ImgProcessOpts
25}
26
27func NewImgProxy(fp string, opts *ImgProcessOpts) *ImgProxy {
28 return &ImgProxy{
29 url: os.Getenv("IMGPROXY_URL"),
30 salt: os.Getenv("IMGPROXY_SALT"),
31 key: os.Getenv("IMGPROXY_KEY"),
32 filepath: fp,
33 opts: opts,
34 }
35}
36
37func (img *ImgProxy) CanServe() error {
38 if img.url == "" {
39 return fmt.Errorf("no imgproxy url provided")
40 }
41 if img.opts == nil {
42 return fmt.Errorf("no image options provided")
43 }
44 mimeType := mime.GetMimeType(img.filepath)
45 if !strings.HasPrefix(mimeType, "image/") {
46 return fmt.Errorf("file mimetype not an image")
47 }
48 return nil
49}
50
51func (img *ImgProxy) GetSig(ppath []byte) string {
52 signature := "_"
53 imgProxySalt := img.salt
54 imgProxyKey := img.key
55 if imgProxySalt == "" || imgProxyKey == "" {
56 return signature
57 }
58
59 keyBin, err := hex.DecodeString(imgProxyKey)
60 if err != nil {
61 return signature
62 }
63
64 saltBin, err := hex.DecodeString(imgProxySalt)
65 if err != nil {
66 return signature
67 }
68
69 mac := hmac.New(sha256.New, keyBin)
70 mac.Write(saltBin)
71 mac.Write(ppath)
72 return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
73}
74
75func (img *ImgProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
76 dataURL := fmt.Sprintf("local:///%s", img.filepath)
77 imgProxyURL := img.url
78 processOpts := img.opts.String()
79 processPath := fmt.Sprintf(
80 "%s/%s",
81 processOpts,
82 base64.StdEncoding.EncodeToString([]byte(dataURL)),
83 )
84 sig := img.GetSig([]byte(processPath))
85
86 rurl := fmt.Sprintf("%s/%s%s", imgProxyURL, sig, processPath)
87 destUrl, err := url.Parse(rurl)
88 if err != nil {
89 msg := fmt.Sprintf("could not parse url: %s", rurl)
90 http.Error(w, msg, http.StatusInternalServerError)
91 return
92 }
93 proxy := httputil.NewSingleHostReverseProxy(destUrl)
94 proxy.ServeHTTP(w, r)
95}
96
97func UriToImgProcessOpts(uri string) (*ImgProcessOpts, error) {
98 opts := &ImgProcessOpts{}
99 parts := strings.Split(uri, "/")
100
101 for _, part := range parts {
102 ratio, err := GetRatio(part)
103 if err != nil {
104 return opts, err
105 }
106
107 if ratio != nil {
108 opts.Ratio = ratio
109 }
110
111 if strings.HasPrefix(part, "s:") {
112 segs := strings.SplitN(part, ":", 4)
113 r := &Ratio{}
114 for idx, sg := range segs {
115 if sg == "" {
116 continue
117 }
118 switch idx {
119 case 1:
120 r.Width, err = strconv.Atoi(sg)
121 if err != nil {
122 return opts, err
123 }
124 case 2:
125 r.Height, err = strconv.Atoi(sg)
126 if err != nil {
127 return opts, err
128 }
129 }
130 }
131 opts.Ratio = r
132 }
133
134 if strings.HasPrefix(part, "q:") {
135 quality := strings.Replace(part, "q:", "", 1)
136 opts.Quality, err = strconv.Atoi(quality)
137 if err != nil {
138 return opts, err
139 }
140 }
141
142 if strings.HasPrefix(part, "rt:") {
143 angle := strings.Replace(part, "rt:", "", 1)
144 opts.Rotate, err = strconv.Atoi(angle)
145 if err != nil {
146 return opts, err
147 }
148 }
149
150 if strings.HasPrefix(part, "ext:") {
151 ext := strings.Replace(part, "ext:", "", 1)
152 opts.Ext = ext
153 if err != nil {
154 return opts, err
155 }
156 }
157 }
158
159 return opts, nil
160}
161
162type ImgProcessOpts struct {
163 Quality int
164 Ratio *Ratio
165 Rotate int
166 Ext string
167 NoRaw bool
168}
169
170func (img *ImgProcessOpts) String() string {
171 processOpts := ""
172
173 // https://docs.imgproxy.net/usage/processing#quality
174 if img.Quality != 0 {
175 processOpts = fmt.Sprintf("%s/q:%d", processOpts, img.Quality)
176 }
177
178 // https://docs.imgproxy.net/usage/processing#size
179 if img.Ratio != nil {
180 processOpts = fmt.Sprintf(
181 "%s/size:%d:%d",
182 processOpts,
183 img.Ratio.Width,
184 img.Ratio.Height,
185 )
186 }
187
188 // https://docs.imgproxy.net/usage/processing#rotate
189 // Only 0, 90, 180, 270, etc., degree angles are supported.
190 if img.Rotate != 0 {
191 rot := img.Rotate
192 if rot == 90 || rot == 180 || rot == 280 {
193 processOpts = fmt.Sprintf(
194 "%s/rotate:%d",
195 processOpts,
196 rot,
197 )
198 }
199 }
200
201 // https://docs.imgproxy.net/usage/processing#format
202 if img.Ext != "" {
203 processOpts = fmt.Sprintf("%s/ext:%s", processOpts, img.Ext)
204 }
205
206 if processOpts == "" && !img.NoRaw {
207 processOpts = fmt.Sprintf("%s/raw:true", processOpts)
208 }
209
210 return processOpts
211}