-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhandler.go
414 lines (363 loc) · 11.1 KB
/
handler.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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
package ae
import (
"crypto/md5"
"encoding/base64"
"encoding/json"
"fmt"
"html/template"
"io"
"net/http"
"net/url"
"path/filepath"
"strings"
"time"
"github.com/chrisolsen/ae/flash"
"golang.org/x/net/context"
"google.golang.org/appengine"
"google.golang.org/appengine/log"
)
type handlerError struct {
AppVersion string `json:"appVersion"`
URL *url.URL `json:"url"`
Method string `json:"method"`
StatusCode int `json:"statusCode"`
InstanceID string `json:"instanceId"`
VersionID string `json:"versionId"`
RequestID string `json:"requestId"`
ModuleName string `json:"moduleName"`
Err string `json:"message"`
}
func (e *handlerError) Error() string {
b, err := json.MarshalIndent(e, "", " ")
if err != nil {
return err.Error()
}
return string(b)
}
// Handler struct designed to be extended by more specific url handlers
type Handler struct {
Ctx context.Context
Req *http.Request
Res http.ResponseWriter
config HandlerConfig
templates map[string]*template.Template
templateHelpers map[string]interface{}
}
// HandlerConfig contains the custom handler configuration settings
type HandlerConfig struct {
// File name of the layout
LayoutFileName string
// Path, relative to the app root, of layouts
LayoutPath string
// Path, relative to the app root, of layouts
ViewPath string
// ???
ParentLayoutName string
}
var defaultHandlerConfig = HandlerConfig{
LayoutFileName: "application.html",
LayoutPath: "layouts",
ViewPath: "views",
ParentLayoutName: "layout",
}
// NewHandler allows one to override the default configuration settings.
// func NewRootHandler() rootHandler {
// return rootHandler{Handler: handler.New(&handler.Config{
// LayoutPath: "layouts",
// })}
// }
func NewHandler(c *HandlerConfig) Handler {
if c.LayoutFileName == "" {
c.LayoutFileName = defaultHandlerConfig.LayoutFileName
}
if c.LayoutPath == "" {
c.LayoutPath = defaultHandlerConfig.LayoutPath
}
if c.ParentLayoutName == "" {
c.ParentLayoutName = defaultHandlerConfig.ParentLayoutName
}
if c.ViewPath == "" {
c.ViewPath = defaultHandlerConfig.ViewPath
}
b := Handler{config: *c} // copy the passed in pointer
b.templates = make(map[string]*template.Template)
return b
}
// DefaultHandler uses the default config settings
// func NewRootHandler() rootHandler {
// return rootHandler{Handler: handler.Default()}
// }
func DefaultHandler() Handler {
return NewHandler(&defaultHandlerConfig)
}
// AddHelpers sets the html.template functions for the handler. This method should be
// called once to intialize the handler with a set of common template helpers used
// throughout the app.
func (h *Handler) AddHelpers(helpers map[string]interface{}) {
dup := make(map[string]interface{})
for k, v := range helpers {
dup[k] = v
}
h.templateHelpers = dup
}
// AddHelper allows one to add additional helpers to a handler. Use this when a handler
// needs a less common helper.
func (h *Handler) AddHelper(name string, fn interface{}) {
if h.templateHelpers == nil {
h.templateHelpers = make(map[string]interface{})
}
h.templateHelpers[name] = fn
}
// Auth is a helper for the handler operation method that will only call on the operation if allowed
func (h *Handler) Auth(allowed bool, op func()) {
if !allowed {
// FIXME: this path needs to be settable
url := fmt.Sprintf("/sessions/new?returnUrl=%s", h.Req.RequestURI)
h.SetFlash("Sign in is required")
http.Redirect(h.Res, h.Req, url, 301)
return
}
op()
}
// OriginMiddleware returns a middleware function that validates the origin
// header within the request matches the allowed values
func OriginMiddleware(allowed []string) func(context.Context, http.ResponseWriter, *http.Request) context.Context {
return func(c context.Context, w http.ResponseWriter, r *http.Request) context.Context {
origin := r.Header.Get("Origin")
if len(origin) == 0 {
return c
}
ok := validateOrigin(origin, allowed)
if !ok {
c2, cancel := context.WithCancel(c)
cancel()
return c2
}
w.Header().Add("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
w.Header().Add("Access-Control-Allow-Origin", origin)
return c
}
}
// ValidateOrigin is a helper method called within the ServeHTTP method on
// OPTION requests to validate the allowed origins
func (h *Handler) ValidateOrigin(allowed []string) {
origin := h.Req.Header.Get("Origin")
ok := validateOrigin(origin, allowed)
if !ok {
_, cancel := context.WithCancel(h.Ctx)
cancel()
}
}
func validateOrigin(origin string, allowed []string) bool {
if allowed == nil || len(allowed) == 0 {
return true
}
if len(origin) == 0 {
return false
}
for _, allowedOrigin := range allowed {
if origin == allowedOrigin {
return true
}
}
return false
}
// ToJSON encodes an interface into the response writer with a default http
// status code of 200
func (h *Handler) ToJSON(data interface{}) {
h.Res.Header().Add("Content-Type", "application/json")
err := json.NewEncoder(h.Res).Encode(data)
if err != nil {
h.Abort(http.StatusInternalServerError, fmt.Errorf("Decoding JSON: %v", err))
}
}
// ToJSONWithStatus json encodes an interface into the response writer with a
// custom http status code
func (h *Handler) ToJSONWithStatus(data interface{}, status int) {
h.Res.Header().Add("Content-Type", "application/json")
h.Res.WriteHeader(status)
h.ToJSON(data)
}
// SendStatus writes the passed in status to the response without any data
func (h *Handler) SendStatus(status int) {
h.Res.WriteHeader(status)
}
// Bind must be called at the beginning of every request to set the required references
func (h *Handler) Bind(c context.Context, w http.ResponseWriter, r *http.Request) {
h.Ctx, h.Res, h.Req = c, w, r
}
// Header gets the request header value
func (h *Handler) Header(name string) string {
return h.Req.Header.Get(name)
}
// SetHeader sets a response header value
func (h *Handler) SetHeader(name, value string) {
h.Res.Header().Set(name, value)
}
// Abort is called when pre-maturally exiting from a handler function due to an
// error. A detailed error is delivered to the client and logged to provide the
// details required to identify the issue.
func (h *Handler) Abort(statusCode int, err error) {
c, cancel := context.WithCancel(h.Ctx)
defer cancel()
// testapp is the name given to all apps when being tested
var isTest = appengine.AppID(c) == "testapp"
hErr := &handlerError{
URL: h.Req.URL,
Method: h.Req.Method,
StatusCode: statusCode,
AppVersion: appengine.AppID(c),
RequestID: appengine.RequestID(c),
}
if err != nil {
hErr.Err = err.Error()
}
if !isTest {
hErr.InstanceID = appengine.InstanceID()
hErr.VersionID = appengine.VersionID(c)
hErr.ModuleName = appengine.ModuleName(c)
}
// log method to appengine log
log.Errorf(c, hErr.Error())
h.Res.WriteHeader(statusCode)
if strings.Index(h.Req.Header.Get("Accept"), "application/json") >= 0 {
json.NewEncoder(h.Res).Encode(hErr)
}
}
// Redirect is a simple wrapper around the core http method
func (h *Handler) Redirect(str string, args ...interface{}) {
http.Redirect(h.Res, h.Req, fmt.Sprintf(str, args...), 303)
}
// Render pre-caches and renders template.
func (h *Handler) Render(path string, data interface{}) {
h.RenderTemplate(path, data, RenderOptions{
Name: h.config.ParentLayoutName,
FuncMap: h.templateHelpers,
Parents: []string{filepath.Join(h.config.LayoutPath, h.config.LayoutFileName)},
})
}
// RenderOptions contain the optional data items for rendering
type RenderOptions struct {
// http status to return in the response
Status int
// template functions
FuncMap template.FuncMap
// parent layout paths to render the defined view within
Parents []string
// the defined *name* to render
// {{define "layout"}}...{{end}}
Name string
}
// RenderTemplate renders the template without any layout
func (h *Handler) RenderTemplate(tmplPath string, data interface{}, opts RenderOptions) {
name := strings.TrimPrefix(tmplPath, "/")
tmpl := h.templates[name]
if tmpl == nil {
t := template.New(name)
if opts.FuncMap != nil {
t.Funcs(opts.FuncMap)
}
var views []string
if opts.Parents != nil {
for _, p := range opts.Parents {
views = append(views, h.fileNameWithExt(p))
}
} else {
views = make([]string, 0)
}
views = append(views, filepath.Join(h.config.ViewPath, h.fileNameWithExt(name)))
tmpl = template.Must(t.ParseFiles(views...))
h.templates[name] = tmpl
}
if opts.Status != 0 {
h.Res.WriteHeader(opts.Status)
} else {
h.Res.WriteHeader(http.StatusOK)
}
var renderErr error
if opts.Name != "" {
renderErr = tmpl.ExecuteTemplate(h.Res, opts.Name, data)
} else {
renderErr = tmpl.Execute(h.Res, data)
}
if renderErr != nil {
panic(renderErr)
}
}
// SetLastModified sets the Last-Modified header in the RFC1123 time format
func (h *Handler) SetLastModified(t time.Time) {
h.Res.Header().Set("Last-Modified", t.Format(time.RFC1123))
}
// SetETag sets the etag with the md5 value
func (h *Handler) SetETag(val interface{}) {
var str string
switch val.(type) {
case string:
str = val.(string)
case time.Time:
str = val.(time.Time).Format(time.RFC1123)
case fmt.Stringer:
str = val.(fmt.Stringer).String()
default:
str = fmt.Sprintf("%v", val)
}
hash := md5.New()
io.WriteString(hash, str)
etag := base64.StdEncoding.EncodeToString(hash.Sum(nil))
h.Res.Header().Set("ETag", etag)
}
// SetExpires sets the Expires response header with a properly formatted time value
func (h *Handler) SetExpires(t time.Time) {
h.Res.Header().Set("Expires", t.Format(time.RFC1123))
}
// SetExpiresIn is a helper to simplify the calling of SetExpires
func (h *Handler) SetExpiresIn(d time.Duration) {
h.Res.Header().Set("Expires", time.Now().Add(d).Format(time.RFC1123))
}
func (h *Handler) fileNameWithExt(name string) string {
var ext string
if strings.Index(name, ".") > 0 {
ext = ""
} else {
ext = ".html"
}
return fmt.Sprintf("%s%s", name, ext)
}
// SetFlash sets a temporary message into a response cookie, that after
// being viewed will be removed, to prevent it from being viewed again.
func (h *Handler) SetFlash(msg string, args ...interface{}) {
if len(args) > 0 {
msg = fmt.Sprintf(msg, args...)
}
flash.Set(h.Res, msg)
}
// SetCookie is a simple wrapper around the http.SetCookie methoe
func (h *Handler) SetCookie(c *http.Cookie) {
http.SetCookie(h.Res, c)
}
// Flash gets the flash value
func (h *Handler) Flash() string {
return flash.Get(h.Res, h.Req)
}
type ServerError struct {
Status int
Message, Details, StackTrace string
}
var defaultServerErrMsgs = map[int]string{
500: "Something bad happened!",
404: "Oops! Page not found",
401: "Authorized access only. Identify yourself!",
403: "Security access fail!",
}
// RenderError will render a the file that corresponds to the status code ex. http.InternalServerError will render 500.html
func (h *Handler) RenderError(status int, err *ServerError) {
if err == nil {
err = &ServerError{}
}
err.Status = status
if err.Message == "" {
err.Message = defaultServerErrMsgs[status]
}
h.RenderTemplate("error.html", err, RenderOptions{Status: status})
}