/
get.go
164 lines (138 loc) · 4.01 KB
/
get.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
package handler
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"mime"
"net/http"
"net/http/httputil"
"strconv"
imagesconfig "github.com/authgear/authgear-server/pkg/images/config"
"github.com/authgear/authgear-server/pkg/util/httproute"
utilhttputil "github.com/authgear/authgear-server/pkg/util/httputil"
"github.com/authgear/authgear-server/pkg/util/log"
"github.com/authgear/authgear-server/pkg/util/vipsutil"
)
//go:generate mockgen -source=get.go -destination=get_mock_test.go -package handler
func ConfigureGetRoute(route httproute.Route) httproute.Route {
return route.
WithMethods("GET", "OPTIONS").
WithPathPattern("/_images/:appid/:objectid/:options")
}
func ExtractKey(r *http.Request) string {
return fmt.Sprintf(
"%s/%s",
httproute.GetParam(r, "appid"),
httproute.GetParam(r, "objectid"),
)
}
type GetHandlerLogger struct{ *log.Logger }
func NewGetHandlerLogger(lf *log.Factory) GetHandlerLogger {
return GetHandlerLogger{lf.New("get-handler")}
}
type VipsDaemon interface {
Process(i vipsutil.Input) (*vipsutil.Output, error)
}
type ImageVariant string
const (
ImageVariantOriginal ImageVariant = "original"
ImageVariantProfile ImageVariant = "profile"
)
func ParseImageVariant(s string) (ImageVariant, bool) {
switch s {
case string(ImageVariantOriginal):
return ImageVariantOriginal, true
case string(ImageVariantProfile):
return ImageVariantProfile, true
default:
return "", false
}
}
type DirectorMaker interface {
MakeDirector(extractKey func(*http.Request) string) func(*http.Request)
}
type GetHandler struct {
DirectorMaker DirectorMaker
Logger GetHandlerLogger
ImagesCDNHost imagesconfig.ImagesCDNHost
HTTPHost utilhttputil.HTTPHost
HTTPProto utilhttputil.HTTPProto
VipsDaemon VipsDaemon
}
func (h *GetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h.ImagesCDNHost != "" {
if string(h.ImagesCDNHost) != string(h.HTTPHost) {
u := *r.URL
u.Scheme = string(h.HTTPProto)
u.Host = string(h.ImagesCDNHost)
http.Redirect(w, r, u.String(), http.StatusFound)
return
}
}
imageVariant, ok := ParseImageVariant(httproute.GetParam(r, "options"))
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}
director := h.DirectorMaker.MakeDirector(ExtractKey)
reverseProxy := httputil.ReverseProxy{
Director: director,
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
h.Logger.WithError(err).Errorf("reverse proxy error")
w.WriteHeader(http.StatusBadGateway)
},
ModifyResponse: func(resp *http.Response) error {
// Reset the header so that we will not accidentally return any headers we do not support,
// such as Accept-Ranges, X-Amz-Request-Id, etc.
resp.Header = make(http.Header)
// Do not modify response with unknown status code.
if resp.StatusCode != 200 {
return nil
}
switch imageVariant {
case ImageVariantOriginal:
return nil
case ImageVariantProfile:
return h.modifyResponse(resp)
default:
return nil
}
},
}
reverseProxy.ServeHTTP(w, r)
}
func (h *GetHandler) modifyResponse(resp *http.Response) error {
originalBody := resp.Body
originalBytes, err := ioutil.ReadAll(originalBody)
if err != nil {
return err
}
defer originalBody.Close()
input := vipsutil.Input{
Reader: bytes.NewReader(originalBytes),
Options: vipsutil.Options{
ResizingModeType: vipsutil.ResizingModeTypeCover,
Width: 240,
Height: 240,
},
}
output, err := h.VipsDaemon.Process(input)
if err != nil {
return err
}
// Set Content-Length
resp.ContentLength = int64(len(output.Data))
resp.Header.Set("Content-Length", strconv.Itoa(len(output.Data)))
// Set Content-Type
mediaType := mime.TypeByExtension(output.FileExtension)
if mediaType != "" {
resp.Header.Set("Content-Type", mediaType)
} else {
resp.Header.Set("Content-Type", "application/octet-stream")
}
// Cache the response for 15 minutes.
resp.Header.Set("Cache-Control", "public, immutable, max-age=900")
resp.Body = io.NopCloser(bytes.NewReader(output.Data))
return nil
}