-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgofre.go
364 lines (324 loc) · 14.1 KB
/
gofre.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
package gofre
import (
"context"
"expvar"
"fmt"
"github.com/ixtendio/gofre/auth"
"github.com/ixtendio/gofre/auth/oauth"
"github.com/ixtendio/gofre/errors"
"github.com/ixtendio/gofre/handler"
"github.com/ixtendio/gofre/middleware"
"github.com/ixtendio/gofre/router/path"
"github.com/ixtendio/gofre/response"
"github.com/ixtendio/gofre/router"
"html/template"
"log"
"math/rand"
"net/http"
"net/http/pprof"
"unsafe"
)
var defaultTemplateFunc = func(templatesPathPattern string) (*template.Template, error) {
return template.New("").Funcs(template.FuncMap{
"safe": func(s string) template.HTML { return template.HTML(s) }, //https://stackoverflow.com/questions/34348072/go-html-comments-are-not-rendered
}).ParseGlob(templatesPathPattern)
}
// ResourcesConfig contains the settings for static resources and templating.
// If no custom values are provided for the struct fields then, the default one are used
type ResourcesConfig struct {
//the dir path pattern that matches the Go templates. Default: "resources/templates/*.html"
TemplatesPathPattern string
//the dir path to the static resources (HTML pages, JS pages, Images, etc). Default: "./resources/assets"
AssetsDirPath string
//the web path to server the static resources. Default: "assets"
AssetsMappingPath string
//the Go templates. Default: template.HTML
Template response.ExecutableTemplate
}
func (c *ResourcesConfig) setDefaults() error {
if c.TemplatesPathPattern == "" {
c.TemplatesPathPattern = "resources/templates/*.html"
}
if c.AssetsDirPath == "" {
c.AssetsDirPath = "./resources/assets"
}
if c.AssetsMappingPath == "" {
c.AssetsMappingPath = "assets"
}
if c.Template == nil {
tmpl, err := defaultTemplateFunc(c.TemplatesPathPattern)
if err != nil {
return fmt.Errorf("failed parsing the templates, err: %w", err)
}
c.Template = tmpl
}
return nil
}
// A Config is a type used to pass the configuration to the MuxHandler
type Config struct {
//if the path match should be case-sensitive or not. Default false
CaseInsensitivePathMatch bool
//the application context path. Default: "/"
ContextPath string
//the ResourcesConfig if the application supports static resources and templates. Default: nil
ResourcesConfig *ResourcesConfig
//a log function for critical errors. Default: defaultErrLogFunc
ErrLogFunc func(err error)
}
func (c *Config) setDefaults() error {
if c.ContextPath == "" {
c.ContextPath = "/"
}
if c.ErrLogFunc == nil {
c.ErrLogFunc = func(err error) {
log.Printf("An error occured while handling the request, err: %v\n", err)
}
}
if c.ResourcesConfig != nil {
if err := c.ResourcesConfig.setDefaults(); err != nil {
return err
}
}
return nil
}
// MuxHandler implements http.Handler that serves the HTTP requests
type MuxHandler struct {
pathPrefix string
router *router.Router
commonMiddlewares []middleware.Middleware
webConfig *Config
}
// NewMuxHandlerWithDefaultConfig returns a new MuxHandler using a default configuration without templating support
func NewMuxHandlerWithDefaultConfig() (*MuxHandler, error) {
return NewMuxHandler(&Config{})
}
// NewMuxHandlerWithDefaultConfigAndTemplateSupport returns a new MuxHandler using a default configuration with static resources and HTML templating support
func NewMuxHandlerWithDefaultConfigAndTemplateSupport() (*MuxHandler, error) {
return NewMuxHandler(&Config{ResourcesConfig: NewDefaultResourcesConfig()})
}
// NewDefaultResourcesConfig returns a new ResourcesConfig with default values
func NewDefaultResourcesConfig() *ResourcesConfig {
rc := &ResourcesConfig{}
if err := rc.setDefaults(); err != nil {
log.Fatalf("failed to set the default values for ResourcesConfig, err: %v", err)
}
return rc
}
// NewMuxHandler creates a new MuxHandler instance
func NewMuxHandler(config *Config) (*MuxHandler, error) {
if err := config.setDefaults(); err != nil {
return nil, err
}
r := router.NewRouter(config.CaseInsensitivePathMatch, config.ErrLogFunc)
if config.ResourcesConfig != nil {
contextPath := config.ContextPath
if contextPath == "/" {
contextPath = ""
}
assetsPath := config.ResourcesConfig.AssetsMappingPath
assetsDirPath := config.ResourcesConfig.AssetsDirPath
r.Handle(http.MethodGet, contextPath+"/"+assetsPath+"/**", handler.Handler2Handler(http.StripPrefix(contextPath+"/"+assetsPath+"/", http.FileServer(http.Dir(assetsDirPath)))))
}
return &MuxHandler{
router: r,
webConfig: config,
}, nil
}
// Config returns a Config copy
func (m *MuxHandler) Config() Config {
return *m.webConfig
}
// ExecutableTemplate returns the response.ExecutableTemplate from ResourcesConfig or nil
func (m *MuxHandler) ExecutableTemplate() response.ExecutableTemplate {
if m.webConfig.ResourcesConfig == nil {
return nil
}
return m.webConfig.ResourcesConfig.Template
}
// Clone creates a new MuxHandler that will inherit all the settings from the parent.
// One important aspect to the new MuxHandler is that, the new added common middlewares will not be shared with the parent.
func (m *MuxHandler) Clone() *MuxHandler {
return &MuxHandler{
pathPrefix: m.pathPrefix,
router: m.router,
commonMiddlewares: append([]middleware.Middleware(nil), m.commonMiddlewares...),
webConfig: m.webConfig,
}
}
// RouteUsingPathPrefix creates a new MuxHandler that will inherit all the settings from the parent, excepting the path prefix which will be concatenated to the parent path prefix.
// One important aspect to the new MuxHandler is that, the new added common middlewares will not be shared with the parent.
func (m *MuxHandler) RouteUsingPathPrefix(pathPrefix string) *MuxHandler {
if len(pathPrefix) == 0 || pathPrefix == m.pathPrefix {
return m
}
return &MuxHandler{
pathPrefix: m.resolvePath(pathPrefix),
router: m.router,
commonMiddlewares: append([]middleware.Middleware(nil), m.commonMiddlewares...),
webConfig: m.webConfig,
}
}
// CommonMiddlewares registers middlewares that will be applied for all handlers
func (m *MuxHandler) CommonMiddlewares(middlewares ...middleware.Middleware) {
if len(middlewares) > 0 {
m.commonMiddlewares = append(m.commonMiddlewares, middlewares...)
}
}
// HandleOAUTH2 registers the necessary handlers to initiate and complete the OAUTH2 flow
//
// this method registers two endpoints:
// 1. GET: /oauth/initiate - initiate the OAUTH2 flow using a provider. If multiple providers are passed in the oauth.Config, then the parameter `provider` should be specified in the query string (example: /oauth/initiate?provider=github)
// 2. GET: /oauth/authorize/{provider} - (the redirect URI) exchange the authorization code for a JWT. The provider value is the name of the OAUTH2 provider (example: /oauth/authorize/github )
//
// If the OAUTH2 flow successfully completes, then the oauth.AccessToken will be passed to context.Context
// to extract it, you have to use the method oauth.GetAccessTokenFromContext(context.Context)
func (m *MuxHandler) HandleOAUTH2(oauthConfig oauth.Config, handler handler.Handler, initiateMiddlewares []middleware.Middleware, authorizeMiddlewares []middleware.Middleware) {
m.HandleOAUTH2WithCustomPaths("/oauth/initiate", "/oauth/authorize", oauthConfig, handler, initiateMiddlewares, authorizeMiddlewares)
}
// HandleOAUTH2WithCustomPaths registers the necessary handlers to initiate and complete the OAUTH2 flow using custom paths
func (m *MuxHandler) HandleOAUTH2WithCustomPaths(initiatePath string,
authorizeBasePath string,
oauthConfig oauth.Config,
handler handler.Handler,
initiateMiddlewares []middleware.Middleware,
authorizeMiddlewares []middleware.Middleware) {
cache := oauthConfig.CacheConfig.Cache
// initiate OAUTH flow handler
authorizationFlowBasePath := authorizeBasePath
m.HandleGet(initiatePath, func(ctx context.Context, mc path.MatchingContext) (response.HttpResponse, error) {
var provider oauth.Provider
if len(oauthConfig.Providers) == 1 {
provider = oauthConfig.Providers[0]
} else {
providerName := mc.R.FormValue("provider")
if providerName == "" {
return nil, errors.NewBadRequestWithMessage("oauth provider not specified")
}
provider = oauthConfig.GetProviderByName(providerName)
}
if provider == nil {
return nil, errors.NewBadRequestWithMessage("oauth provider not supported")
}
redirectUrl := oauthConfig.WebsiteUrl + m.resolvePath(authorizationFlowBasePath) + "/" + provider.Name()
state := uniqueIdFunc(12)
if cache != nil {
if err := cache.Add(state, oauthConfig.CacheConfig.KeyExpirationTime); err != nil {
return nil, fmt.Errorf("failed to save the OAUTH2 state random value, err: %w", err)
}
}
return response.RedirectHttpResponse(provider.InitiateUrl(redirectUrl, state, oauthConfig.FetchUserDetails)), nil
}, initiateMiddlewares...)
// authorize OAUTH flow handler
m.HandleGet(authorizationFlowBasePath+"/{providerName}", func(ctx context.Context, mc path.MatchingContext) (response.HttpResponse, error) {
providerName := mc.PathVar("providerName")
provider := oauthConfig.GetProviderByName(providerName)
if provider == nil {
return nil, errors.NewBadRequestWithMessage("oauth provider not supported")
}
redirectUrl := oauthConfig.WebsiteUrl + m.resolvePath(authorizationFlowBasePath) + "/" + provider.Name()
errCode := mc.R.FormValue("error")
if errCode != "" {
return nil, errors.ErrUnauthorizedRequest
}
state := mc.R.FormValue("state")
if cache != nil && !cache.Contains(state) {
return nil, errors.ErrUnauthorizedRequest
}
code := mc.R.FormValue("code")
accessToken, err := provider.FetchAccessToken(ctx, redirectUrl, code)
if err != nil {
return nil, err
}
ctx = context.WithValue(ctx, oauth.AccessTokenCtxKey, accessToken)
if oauthConfig.FetchUserDetails {
user, err := provider.FetchAuthenticatedUser(ctx, accessToken)
if err != nil {
return nil, err
}
ctx = context.WithValue(ctx, auth.SecurityPrincipalCtxKey, &user)
}
return handler(ctx, mc)
}, authorizeMiddlewares...)
}
// HandleGet registers a handler with custom middlewares for GET requests
func (m *MuxHandler) HandleGet(path string, handler handler.Handler, middlewares ...middleware.Middleware) {
m.HandleRequest(http.MethodGet, path, handler, middlewares...)
}
// HandlePost registers a handler with custom middlewares for POST requests
func (m *MuxHandler) HandlePost(path string, handler handler.Handler, middlewares ...middleware.Middleware) {
m.HandleRequest(http.MethodPost, path, handler, middlewares...)
}
// HandlePut registers a handler with custom middlewares for PUT requests
func (m *MuxHandler) HandlePut(path string, handler handler.Handler, middlewares ...middleware.Middleware) {
m.HandleRequest(http.MethodPut, path, handler, middlewares...)
}
// HandlePatch registers a handler with custom middlewares for PATCH requests
func (m *MuxHandler) HandlePatch(path string, handler handler.Handler, middlewares ...middleware.Middleware) {
m.HandleRequest(http.MethodPatch, path, handler, middlewares...)
}
// HandleDelete registers a handler with custom middlewares for DELETE requests
func (m *MuxHandler) HandleDelete(path string, handler handler.Handler, middlewares ...middleware.Middleware) {
m.HandleRequest(http.MethodDelete, path, handler, middlewares...)
}
// HandleRequest registers a handler with custom middlewares for the specified HTTP method
func (m *MuxHandler) HandleRequest(httpMethod string, path string, h handler.Handler, middlewares ...middleware.Middleware) {
h = wrapMiddleware(wrapMiddleware(h, middlewares...), m.commonMiddlewares...)
m.router.Handle(httpMethod, m.resolvePath(path), h)
}
func (m *MuxHandler) resolvePath(path string) string {
pathPrefix := m.pathPrefix
if len(pathPrefix) == 0 {
return path
}
if len(path) == 0 {
return pathPrefix
}
if pathPrefix[len(pathPrefix)-1] == '/' && path[0] == '/' {
return pathPrefix + path[1:]
} else if pathPrefix[len(pathPrefix)-1] != '/' && path[0] != '/' {
return pathPrefix + "/" + path
} else {
return pathPrefix + path
}
}
// EnableDebugEndpoints enable debug endpoints
func (m MuxHandler) EnableDebugEndpoints() {
// Register all the standard library debug endpoints.
m.router.Handle(http.MethodGet, m.resolvePath("/debug/pprof/"), handler.HandlerFunc2Handler(pprof.Index))
m.router.Handle(http.MethodGet, m.resolvePath("/debug/pprof/allocs"), handler.HandlerFunc2Handler(pprof.Index))
m.router.Handle(http.MethodGet, m.resolvePath("/debug/pprof/block"), handler.HandlerFunc2Handler(pprof.Index))
m.router.Handle(http.MethodGet, m.resolvePath("/debug/pprof/goroutine"), handler.HandlerFunc2Handler(pprof.Index))
m.router.Handle(http.MethodGet, m.resolvePath("/debug/pprof/heap"), handler.HandlerFunc2Handler(pprof.Index))
m.router.Handle(http.MethodGet, m.resolvePath("/debug/pprof/mutex"), handler.HandlerFunc2Handler(pprof.Index))
m.router.Handle(http.MethodGet, m.resolvePath("/debug/pprof/threadcreate"), handler.HandlerFunc2Handler(pprof.Index))
m.router.Handle(http.MethodGet, m.resolvePath("/debug/pprof/cmdline"), handler.HandlerFunc2Handler(pprof.Cmdline))
m.router.Handle(http.MethodGet, m.resolvePath("/debug/pprof/profile"), handler.HandlerFunc2Handler(pprof.Profile))
m.router.Handle(http.MethodGet, m.resolvePath("/debug/pprof/symbol"), handler.HandlerFunc2Handler(pprof.Symbol))
m.router.Handle(http.MethodGet, m.resolvePath("/debug/pprof/trace"), handler.HandlerFunc2Handler(pprof.Trace))
m.router.Handle(http.MethodGet, m.resolvePath("/debug/vars"), handler.Handler2Handler(expvar.Handler()))
}
func (m *MuxHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
m.router.ServeHTTP(w, req)
}
func wrapMiddleware(handler handler.Handler, middlewares ...middleware.Middleware) handler.Handler {
if len(middlewares) == 0 {
return handler
}
wrappedHandlers := handler
for i := len(middlewares) - 1; i >= 0; i-- {
mid := middlewares[i]
if mid != nil {
wrappedHandlers = mid(wrappedHandlers)
}
}
return wrappedHandlers
}
const randLetters = "abcdefghijklmnopqrstuvwxyz1234567890"
var uniqueIdFunc = generateUniqueId
func generateUniqueId(length int) string {
b := make([]byte, length)
for i := range b {
b[i] = randLetters[rand.Int63()%int64(len(randLetters))]
}
return *(*string)(unsafe.Pointer(&b))
}