Eric Bower
·
02 Apr 24
proxy.go
1package storage
2
3import (
4 "crypto/hmac"
5 "crypto/sha256"
6 "encoding/base64"
7 "encoding/hex"
8 "fmt"
9 "io"
10 "net/http"
11 "os"
12 "path/filepath"
13 "strconv"
14 "strings"
15)
16
17func GetMimeType(fpath string) string {
18 ext := filepath.Ext(fpath)
19 if ext == ".svg" {
20 return "image/svg+xml"
21 } else if ext == ".css" {
22 return "text/css"
23 } else if ext == ".js" {
24 return "text/javascript"
25 } else if ext == ".ico" {
26 return "image/x-icon"
27 } else if ext == ".pdf" {
28 return "application/pdf"
29 } else if ext == ".html" || ext == ".htm" {
30 return "text/html"
31 } else if ext == ".jpg" || ext == ".jpeg" {
32 return "image/jpeg"
33 } else if ext == ".png" {
34 return "image/png"
35 } else if ext == ".gif" {
36 return "image/gif"
37 } else if ext == ".webp" {
38 return "image/webp"
39 } else if ext == ".otf" {
40 return "font/otf"
41 } else if ext == ".woff" {
42 return "font/woff"
43 } else if ext == ".woff2" {
44 return "font/woff2"
45 } else if ext == ".ttf" {
46 return "font/ttf"
47 } else if ext == ".md" {
48 return "text/markdown; charset=UTF-8"
49 } else if ext == ".json" || ext == ".map" {
50 return "application/json"
51 } else if ext == ".rss" {
52 return "application/rss+xml"
53 } else if ext == ".atom" {
54 return "application/atom+xml"
55 } else if ext == ".webmanifest" {
56 return "application/manifest+json"
57 } else if ext == ".xml" {
58 return "application/xml"
59 } else if ext == ".xsl" {
60 return "application/xml"
61 } else if ext == ".avif" {
62 return "image/avif"
63 } else if ext == ".heif" {
64 return "image/heif"
65 } else if ext == ".heic" {
66 return "image/heif"
67 } else if ext == ".opus" {
68 return "audio/opus"
69 } else if ext == ".wav" {
70 return "audio/wav"
71 } else if ext == ".mp3" {
72 return "audio/mpeg"
73 } else if ext == ".mp4" {
74 return "video/mp4"
75 } else if ext == ".mpeg" {
76 return "video/mpeg"
77 } else if ext == ".wasm" {
78 return "application/wasm"
79 } else if ext == ".opml" {
80 return "text/x-opml"
81 } else if ext == ".eot" {
82 return "application/vnd.ms-fontobject"
83 } else if ext == ".yml" || ext == ".yaml" {
84 return "text/x-yaml"
85 } else if ext == ".txt" {
86 return "text/plain"
87 }
88
89 return "text/plain"
90}
91
92func UriToImgProcessOpts(uri string) (*ImgProcessOpts, error) {
93 opts := &ImgProcessOpts{}
94 parts := strings.Split(uri, "/")
95
96 for _, part := range parts {
97 ratio, err := GetRatio(part)
98 if err != nil {
99 return opts, err
100 }
101
102 if ratio != nil {
103 opts.Ratio = ratio
104 }
105
106 if strings.HasPrefix(part, "s:") {
107 segs := strings.SplitN(part, ":", 4)
108 r := &Ratio{}
109 for idx, sg := range segs {
110 if sg == "" {
111 continue
112 }
113 if idx == 1 {
114 r.Width, err = strconv.Atoi(sg)
115 if err != nil {
116 return opts, err
117 }
118 } else if idx == 2 {
119 r.Height, err = strconv.Atoi(sg)
120 if err != nil {
121 return opts, err
122 }
123 }
124 }
125 opts.Ratio = r
126 }
127
128 if strings.HasPrefix(part, "q:") {
129 quality := strings.Replace(part, "q:", "", 1)
130 opts.Quality, err = strconv.Atoi(quality)
131 if err != nil {
132 return opts, err
133 }
134 }
135
136 if strings.HasPrefix(part, "rt:") {
137 angle := strings.Replace(part, "rt:", "", 1)
138 opts.Rotate, err = strconv.Atoi(angle)
139 if err != nil {
140 return opts, err
141 }
142 }
143
144 if strings.HasPrefix(part, "ext:") {
145 ext := strings.Replace(part, "ext:", "", 1)
146 opts.Ext = ext
147 if err != nil {
148 return opts, err
149 }
150 }
151 }
152
153 return opts, nil
154}
155
156type ImgProcessOpts struct {
157 Quality int
158 Ratio *Ratio
159 Rotate int
160 Ext string
161}
162
163func (img *ImgProcessOpts) String() string {
164 processOpts := ""
165
166 // https://docs.imgproxy.net/usage/processing#quality
167 if img.Quality != 0 {
168 processOpts = fmt.Sprintf("%s/q:%d", processOpts, img.Quality)
169 }
170
171 // https://docs.imgproxy.net/usage/processing#size
172 if img.Ratio != nil {
173 processOpts = fmt.Sprintf(
174 "%s/size:%d:%d",
175 processOpts,
176 img.Ratio.Width,
177 img.Ratio.Height,
178 )
179 }
180
181 // https://docs.imgproxy.net/usage/processing#rotate
182 // Only 0, 90, 180, 270, etc., degree angles are supported.
183 if img.Rotate != 0 {
184 rot := img.Rotate
185 if rot == 90 || rot == 180 || rot == 280 {
186 processOpts = fmt.Sprintf(
187 "%s/rotate:%d",
188 processOpts,
189 rot,
190 )
191 }
192 }
193
194 // https://docs.imgproxy.net/usage/processing#format
195 if img.Ext != "" {
196 processOpts = fmt.Sprintf("%s/ext:%s", processOpts, img.Ext)
197 }
198
199 return processOpts
200}
201
202func HandleProxy(dataURL string, opts *ImgProcessOpts) (io.ReadCloser, string, error) {
203 imgProxyURL := os.Getenv("IMGPROXY_URL")
204 imgProxySalt := os.Getenv("IMGPROXY_SALT")
205 imgProxyKey := os.Getenv("IMGPROXY_KEY")
206
207 signature := "_"
208
209 processOpts := opts.String()
210
211 processPath := fmt.Sprintf("%s/%s", processOpts, base64.StdEncoding.EncodeToString([]byte(dataURL)))
212
213 if imgProxySalt != "" && imgProxyKey != "" {
214 keyBin, err := hex.DecodeString(imgProxyKey)
215 if err != nil {
216 return nil, "", err
217 }
218
219 saltBin, err := hex.DecodeString(imgProxySalt)
220 if err != nil {
221 return nil, "", err
222 }
223
224 mac := hmac.New(sha256.New, keyBin)
225 mac.Write(saltBin)
226 mac.Write([]byte(processPath))
227 signature = base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
228 }
229
230 proxyAddress := fmt.Sprintf("%s/%s%s", imgProxyURL, signature, processPath)
231
232 res, err := http.Get(proxyAddress)
233 if err != nil {
234 return nil, "", err
235 }
236
237 if res.StatusCode < 200 || res.StatusCode >= 300 {
238 return nil, "", fmt.Errorf("%s", res.Status)
239 }
240
241 return res.Body, res.Header.Get("Content-Type"), nil
242}