repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

pico / shared / storage
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}