From ed6fa3d1fa0e9cb7763651b4f10f2ecb8cf40e76 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Sun, 11 Jun 2017 00:19:15 -0700 Subject: [PATCH 01/38] version bump to v0.7 --- aah.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aah.go b/aah.go index 7afe078d..ccf71fda 100644 --- a/aah.go +++ b/aah.go @@ -21,7 +21,7 @@ import ( ) // Version no. of aah framework -const Version = "0.6" +const Version = "0.7" // aah application variables var ( From f119ae006530adeaab2d9c4696bb591beeda3035 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Sun, 11 Jun 2017 00:20:56 -0700 Subject: [PATCH 02/38] check log level to reduce execution time #61 --- server.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/server.go b/server.go index 5e153354..ec9abec8 100644 --- a/server.go +++ b/server.go @@ -62,12 +62,15 @@ func Start() { log.Infof("App Profile: %v", AppProfile()) log.Infof("App TLS/SSL Enabled: %v", AppIsSSLEnabled()) log.Infof("App Session Mode: %v", sessionMode) - log.Debugf("App i18n Locales: %v", strings.Join(AppI18n().Locales(), ", ")) - log.Debugf("App Route Domains: %v", strings.Join(AppRouter().DomainAddresses(), ", ")) - for event := range AppEventStore().subscribers { - for _, c := range AppEventStore().subscribers[event] { - log.Debugf("Callback: %s, subscribed to event: %s", funcName(c.Callback), event) + if log.IsLevelDebug() { + log.Debugf("App i18n Locales: %v", strings.Join(AppI18n().Locales(), ", ")) + log.Debugf("App Route Domains: %v", strings.Join(AppRouter().DomainAddresses(), ", ")) + + for event := range AppEventStore().subscribers { + for _, c := range AppEventStore().subscribers[event] { + log.Debugf("Callback: %s, subscribed to event: %s", funcName(c.Callback), event) + } } } From c6618bcd9a4c3e1a4d4487372db8e0181cd99183 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Sun, 11 Jun 2017 00:22:21 -0700 Subject: [PATCH 03/38] #4 hot reload support for statis files on dev env profile --- static.go | 8 ++++++-- static_test.go | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/static.go b/static.go index fd17370c..9b6f12c1 100644 --- a/static.go +++ b/static.go @@ -24,8 +24,9 @@ import ( ) const ( - dirStatic = "static" - sniffLen = 512 + dirStatic = "static" + sniffLen = 512 + noCacheHdrValue = "no-cache, no-store, must-revalidate" ) var ( @@ -84,6 +85,9 @@ func (e *engine) serveStatic(ctx *Context) error { // apply cache header if environment profile is `prod` if appIsProfileProd { ctx.Res.Header().Set(ahttp.HeaderCacheControl, cacheHeader(contentType)) + } else { // for hot-reload + ctx.Res.Header().Set(ahttp.HeaderExpires, "0") + ctx.Res.Header().Set(ahttp.HeaderCacheControl, noCacheHdrValue) } } diff --git a/static_test.go b/static_test.go index eb68c24e..056458e5 100644 --- a/static_test.go +++ b/static_test.go @@ -22,18 +22,18 @@ import ( func TestStaticDirectoryListing(t *testing.T) { appCfg, _ := config.ParseString("") e := newEngine(appCfg) - appIsProfileProd = true testStaticServe(t, e, "http://localhost:8080/static/css/aah\x00.css", "static", "css/aah\x00.css", "", "500 Internal Server Error", false) - testStaticServe(t, e, "http://localhost:8080/static/test.txt", "static", "test.txt", "", "This is file content of test.txt", false) + testStaticServe(t, e, "http://localhost:8080/static/", "static", "", "", `Listing of /static/`, true) testStaticServe(t, e, "http://localhost:8080/static", "static", "", "", "403 Directory listing not allowed", false) testStaticServe(t, e, "http://localhost:8080/static", "static", "", "", `Moved Permanently`, true) - testStaticServe(t, e, "http://localhost:8080/static/", "static", "", "", `Listing of /static/`, true) + testStaticServe(t, e, "http://localhost:8080/static/test.txt", "static", "test.txt", "", "This is file content of test.txt", false) + appIsProfileProd = true testStaticServe(t, e, "http://localhost:8080/robots.txt", "", "", "test.txt", "This is file content of test.txt", false) appIsProfileProd = false } From acdc6687f3abc87b04242faa0a8484852206da6c Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Mon, 12 Jun 2017 14:47:38 -0700 Subject: [PATCH 04/38] update log pkg to dev version --- aah.go | 2 +- aah_test.go | 2 +- config.go | 2 +- context.go | 2 +- engine.go | 2 +- engine_test.go | 2 +- event.go | 2 +- middleware.go | 2 +- param.go | 2 +- render.go | 2 +- router.go | 2 +- server.go | 2 +- static.go | 2 +- util.go | 2 +- view.go | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/aah.go b/aah.go index ccf71fda..a082a1a0 100644 --- a/aah.go +++ b/aah.go @@ -17,7 +17,7 @@ import ( "aahframework.org/aruntime.v0" "aahframework.org/config.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0" + "aahframework.org/log.v0-unstable" ) // Version no. of aah framework diff --git a/aah_test.go b/aah_test.go index b02a213b..4dfcc855 100644 --- a/aah_test.go +++ b/aah_test.go @@ -14,7 +14,7 @@ import ( "aahframework.org/config.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0" + "aahframework.org/log.v0-unstable" "aahframework.org/test.v0/assert" ) diff --git a/config.go b/config.go index d150f5c8..6589d3c6 100644 --- a/config.go +++ b/config.go @@ -9,7 +9,7 @@ import ( "path/filepath" "aahframework.org/config.v0" - "aahframework.org/log.v0" + "aahframework.org/log.v0-unstable" ) var appConfig *config.Config diff --git a/context.go b/context.go index 14e6c0bb..a921ec14 100644 --- a/context.go +++ b/context.go @@ -12,7 +12,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0" + "aahframework.org/log.v0-unstable" "aahframework.org/router.v0" "aahframework.org/security.v0/session" ) diff --git a/engine.go b/engine.go index c3a0ce67..a1c1b1bc 100644 --- a/engine.go +++ b/engine.go @@ -16,7 +16,7 @@ import ( "aahframework.org/aruntime.v0" "aahframework.org/config.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0" + "aahframework.org/log.v0-unstable" "aahframework.org/pool.v0" ) diff --git a/engine_test.go b/engine_test.go index aaa76d6f..94ede8b7 100644 --- a/engine_test.go +++ b/engine_test.go @@ -16,7 +16,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/config.v0" - "aahframework.org/log.v0" + "aahframework.org/log.v0-unstable" "aahframework.org/test.v0/assert" ) diff --git a/event.go b/event.go index b88529e2..3477027d 100644 --- a/event.go +++ b/event.go @@ -10,7 +10,7 @@ import ( "sync" "aahframework.org/essentials.v0" - "aahframework.org/log.v0" + "aahframework.org/log.v0-unstable" ) const ( diff --git a/middleware.go b/middleware.go index 9cc3a4fa..a1e7bec0 100644 --- a/middleware.go +++ b/middleware.go @@ -8,7 +8,7 @@ import ( "net/http" "reflect" - "aahframework.org/log.v0" + "aahframework.org/log.v0-unstable" ) var ( diff --git a/param.go b/param.go index 2a7947f3..e2b0406c 100644 --- a/param.go +++ b/param.go @@ -10,7 +10,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0" + "aahframework.org/log.v0-unstable" ) const ( diff --git a/render.go b/render.go index c4335e7d..9d011267 100644 --- a/render.go +++ b/render.go @@ -15,7 +15,7 @@ import ( "path/filepath" "aahframework.org/essentials.v0" - "aahframework.org/log.v0" + "aahframework.org/log.v0-unstable" ) type ( diff --git a/router.go b/router.go index 78145858..2aa21174 100644 --- a/router.go +++ b/router.go @@ -15,7 +15,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/config.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0" + "aahframework.org/log.v0-unstable" "aahframework.org/router.v0" ) diff --git a/server.go b/server.go index ec9abec8..12291b15 100644 --- a/server.go +++ b/server.go @@ -21,7 +21,7 @@ import ( "aahframework.org/config.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0" + "aahframework.org/log.v0-unstable" ) var ( diff --git a/static.go b/static.go index 9b6f12c1..376de5d2 100644 --- a/static.go +++ b/static.go @@ -20,7 +20,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0" + "aahframework.org/log.v0-unstable" ) const ( diff --git a/util.go b/util.go index 457f4df3..505eb51b 100644 --- a/util.go +++ b/util.go @@ -15,7 +15,7 @@ import ( "strings" "aahframework.org/essentials.v0" - "aahframework.org/log.v0" + "aahframework.org/log.v0-unstable" ) var ( diff --git a/view.go b/view.go index a4e37c3a..390e0efd 100644 --- a/view.go +++ b/view.go @@ -13,7 +13,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/config.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0" + "aahframework.org/log.v0-unstable" "aahframework.org/view.v0" ) From 747a91e3dbb94e109beee6afa1481e14ec62d368 Mon Sep 17 00:00:00 2001 From: Lanre Adelowo Date: Sat, 17 Jun 2017 22:43:58 +0100 Subject: [PATCH 05/38] #69 Request access log Implementation (PR #72) Signed-off-by: Lanre Adelowo --- aah.go | 3 + access_log.go | 187 +++++++++++++++++++++++++++++++++++++++ access_log_test.go | 141 +++++++++++++++++++++++++++++ context.go | 2 + engine.go | 35 +++++++- testdata/config/aah.conf | 6 ++ 6 files changed, 371 insertions(+), 3 deletions(-) create mode 100644 access_log.go create mode 100644 access_log_test.go diff --git a/aah.go b/aah.go index a082a1a0..16f3dd93 100644 --- a/aah.go +++ b/aah.go @@ -202,6 +202,9 @@ func Init(importPath string) { logAsFatal(initRoutes(appConfigDir(), AppConfig())) logAsFatal(initSecurity(appConfigDir(), AppConfig())) logAsFatal(initViewEngine(appViewsDir(), AppConfig())) + if AppConfig().BoolDefault("request.access_log.enable", false) { + logAsFatal(initRequestAccessLog(appLogsDir(), AppConfig())) + } } appInitialized = true diff --git a/access_log.go b/access_log.go new file mode 100644 index 00000000..a6559164 --- /dev/null +++ b/access_log.go @@ -0,0 +1,187 @@ +// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) +// go-aah/aah source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package aah + +import ( + "bytes" + "fmt" + "net/http" + "path/filepath" + "sync" + "time" + + "aahframework.org/ahttp.v0" + "aahframework.org/config.v0" + "aahframework.org/essentials.v0-unstable" + "aahframework.org/log.v0" +) + +const ( + fmtFlagClientIP ess.FmtFlag = iota + fmtFlagRequestTime + fmtFlagRequestURL + fmtFlagRequestMethod + fmtFlagRequestID + fmtFlagRequestHeader + fmtFlagQueryString + fmtFlagResponseStatus + fmtFlagResponseSize + fmtFlagResponseHeader + fmtFlagResponseTime +) + +var ( + accessLogFmtFlags = map[string]ess.FmtFlag{ + "clientip": fmtFlagClientIP, + "reqtime": fmtFlagRequestTime, + "requrl": fmtFlagRequestURL, + "reqmethod": fmtFlagRequestMethod, + "reqid": fmtFlagRequestID, + "reqhdr": fmtFlagRequestHeader, + "querystr": fmtFlagQueryString, + "resstatus": fmtFlagResponseStatus, + "ressize": fmtFlagResponseSize, + "reshdr": fmtFlagResponseHeader, + "restime": fmtFlagResponseTime, + } + + defaultRequestAccessLogPattern = "%clientip %reqid %reqtime %restime %resstatus %ressize %reqmethod %requrl" + appStartTimeKey = "appReqStartTime" + appAccessLogBufPool *sync.Pool + appAccessLog *log.Logger + appAccessLogFmtFlags []ess.FmtFlagPart + appAccessLogChan chan *requestAccessLog +) + +type ( + //requestAccessLog contains data about the current request + requestAccessLog struct { + StartTime time.Time + ElapsedDuration time.Duration + RequestID string + Request ahttp.Request + ResStatus int + ResBytes int + ResHdr http.Header + } +) + +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// Unexported methods +//___________________________________ + +func initRequestAccessLog(logsDir string, appCfg *config.Config) error { + // log file configuration + cfg, _ := config.ParseString("") + file := appCfg.StringDefault("request.access_log.file", "") + if ess.IsStrEmpty(file) { + cfg.SetString("log.file", filepath.Join(logsDir, getBinaryFileName()+"-access.log")) + } else if !filepath.IsAbs(file) { + cfg.SetString("log.file", filepath.Join(logsDir, file)) + } else { + cfg.SetString("log.file", file) + } + + cfg.SetString("log.pattern", "%message") + + var err error + + // initialize request access log file + appAccessLog, err = log.New(cfg) + if err != nil { + return err + } + + // parse request access log pattern + pattern := appCfg.StringDefault("request.access_log.pattern", defaultRequestAccessLogPattern) + appAccessLogFmtFlags, err = ess.ParseFmtFlag(pattern, accessLogFmtFlags) + if err != nil { + return err + } + + // initialize request access log channel + appAccessLogChan = make(chan *requestAccessLog, cfg.IntDefault("runtime.pooling.global", defaultGlobalPoolSize)) + appAccessLogBufPool = &sync.Pool{New: func() interface{} { return &bytes.Buffer{} }} + + // start the listener + go listenForAccessLog() + + return nil +} + +func listenForAccessLog() { + for { + info := <-appAccessLogChan + appAccessLog.Print(requestAccessLogFormatter(info)) + } +} + +func requestAccessLogFormatter(ral *requestAccessLog) string { + buf := getBuffer() + defer putBuffer(buf) + + for _, part := range appAccessLogFmtFlags { + switch part.Flag { + case fmtFlagClientIP: + buf.WriteString(ral.Request.ClientIP) + case fmtFlagRequestTime: + //there are two options here for the pattern we have to handle ; + //%reqtime or %reqtime: + if ess.IsStrEmpty(part.Format) || part.Format == "%v" { + buf.WriteString(ral.StartTime.Format(time.RFC3339)) + } else { + buf.WriteString(ral.StartTime.Format(part.Format)) + } + case fmtFlagRequestURL: + buf.WriteString(ral.Request.Path) + case fmtFlagRequestMethod: + buf.WriteString(ral.Request.Method) + case fmtFlagRequestID: + if ess.IsStrEmpty(ral.RequestID) { + buf.WriteByte('-') + } else { + buf.WriteString(ral.RequestID) + } + case fmtFlagRequestHeader: + hdr := ral.Request.Header.Get(http.CanonicalHeaderKey(part.Format)) + if ess.IsStrEmpty(hdr) { + buf.WriteByte('-') + } else { + buf.WriteString(hdr) + } + case fmtFlagQueryString: + queryStr := ral.Request.Raw.URL.Query().Encode() + if ess.IsStrEmpty(queryStr) { + buf.WriteByte('-') + } else { + buf.WriteString(queryStr) + } + case fmtFlagResponseStatus: + buf.WriteString(fmt.Sprintf(part.Format, ral.ResStatus)) + case fmtFlagResponseSize: + buf.WriteString(fmt.Sprintf(part.Format, ral.ResBytes)) + case fmtFlagResponseHeader: + hdr := ral.ResHdr.Get(part.Format) + if ess.IsStrEmpty(hdr) { + buf.WriteByte('-') + } else { + buf.WriteString(hdr) + } + case fmtFlagResponseTime: + buf.WriteString(fmt.Sprintf("%10v", ral.ElapsedDuration)) + } + buf.WriteByte(' ') + } + return buf.String() +} + +func getBuffer() *bytes.Buffer { + return appAccessLogBufPool.Get().(*bytes.Buffer) +} + +func putBuffer(buf *bytes.Buffer) { + buf.Reset() + appAccessLogBufPool.Put(buf) +} diff --git a/access_log_test.go b/access_log_test.go new file mode 100644 index 00000000..22b1935b --- /dev/null +++ b/access_log_test.go @@ -0,0 +1,141 @@ +// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) +// go-aah/aah source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package aah + +import ( + "bytes" + "fmt" + "net/http/httptest" + "path/filepath" + "sync" + "testing" + "time" + + "aahframework.org/ahttp.v0" + "aahframework.org/essentials.v0-unstable" + "aahframework.org/test.v0/assert" +) + +func TestRequestAccessLogFormatter(t *testing.T) { + startTime := time.Now() + req := httptest.NewRequest("GET", "/oops?me=human", nil) + resRec := httptest.NewRecorder() + + ral := &requestAccessLog{ + StartTime: startTime, + ElapsedDuration: time.Now().Add(2 * time.Second).Sub(startTime), + RequestID: "req-id:12345", + Request: ahttp.Request{Raw: req}, + ResStatus: 200, + ResBytes: 63, + ResHdr: resRec.HeaderMap, + } + + //Since we are not bootstrapping the framework's engine, + //We need to manually set this + ral.Request.Path = "/oops" + appAccessLogBufPool = &sync.Pool{New: func() interface{} { return &bytes.Buffer{} }} + + //We test for the default format first + expectedDefaultFormat := fmt.Sprintf(" %s %v %v %d %d %s %s ", + ral.RequestID, ral.StartTime.Format(time.RFC3339), + ral.ElapsedDuration, ral.ResStatus, ral.ResBytes, ral.Request.Method, ral.Request.Path) + + testFormatter(t, ral, defaultRequestAccessLogPattern, expectedDefaultFormat) + + ral.ResHdr.Add("content-type", "application/json") + //Then for something much more diffrent + pattern := "%reqtime:2016-05-16 %reqhdr %querystr %reshdr:content-type" + expected := fmt.Sprintf("%s %s %s %s ", + ral.StartTime.Format("2016-05-16"), + "-", "me=human", ral.ResHdr.Get("content-type"), + ) + + testFormatter(t, ral, pattern, expected) + + //Test for equest access log format + ral.Request.Header = ral.Request.Raw.Header + ral.Request.Header.Add(ahttp.HeaderAccept, "text/html") + ral.Request.ClientIP = "127.0.0.1" + allAvailablePatterns := "%clientip %reqid %reqtime %restime %resstatus %ressize %reqmethod %requrl %reqhdr:accept %querystr %reshdr" + expectedForAllAvailablePatterns := fmt.Sprintf("%s %s %s %v %d %d %s %s %s %s %s", + ral.Request.ClientIP, ral.RequestID, + ral.StartTime.Format(time.RFC3339), ral.ElapsedDuration, + ral.ResStatus, ral.ResBytes, ral.Request.Method, + ral.Request.Path, "text/html", "me=human", "- ") + + testFormatter(t, ral, allAvailablePatterns, expectedForAllAvailablePatterns) +} + +func TestRequestAccessLogFormatterInvalidPattern(t *testing.T) { + _, err := ess.ParseFmtFlag("%oops", accessLogFmtFlags) + + assert.NotNil(t, err) +} + +func testFormatter(t *testing.T, ral *requestAccessLog, pattern, expected string) { + var err error + appAccessLogFmtFlags, err = ess.ParseFmtFlag(pattern, accessLogFmtFlags) + + assert.Nil(t, err) + assert.Equal(t, expected, requestAccessLogFormatter(ral)) +} + +func TestInitRequestAccessLog(t *testing.T) { + cfgDir := filepath.Join(getTestdataPath(), appConfigDir()) + _ = initConfig(cfgDir) + + err := initRequestAccessLog(appLogsDir(), AppConfig()) + + assert.Nil(t, err) + assert.NotNil(t, appAccessLog) +} + +func TestEngineRequestAccessLog(t *testing.T) { + // App Config + cfgDir := filepath.Join(getTestdataPath(), appConfigDir()) + err := initConfig(cfgDir) + assert.Nil(t, err) + assert.NotNil(t, AppConfig()) + + AppConfig().SetString("server.port", "8080") + + // Router + err = initRoutes(cfgDir, AppConfig()) + assert.Nil(t, err) + assert.NotNil(t, AppRouter()) + + // Security + err = initSecurity(cfgDir, AppConfig()) + assert.Nil(t, err) + assert.True(t, AppSessionManager().IsStateful()) + + // Controllers + cRegistry = controllerRegistry{} + + AddController((*Site)(nil), []*MethodInfo{ + { + Name: "GetInvolved", + Parameters: []*ParameterInfo{}, + }, + { + Name: "ContributeCode", + Parameters: []*ParameterInfo{}, + }, + { + Name: "Credits", + Parameters: []*ParameterInfo{}, + }, + }) + + AppConfig().SetBool("request.access_log.enable", true) + + e := newEngine(AppConfig()) + req := httptest.NewRequest("GET", "localhost:8080/get-involved.html", nil) + res := httptest.NewRecorder() + e.ServeHTTP(res, req) + + assert.True(t, e.isAccessLogEnabled) +} diff --git a/context.go b/context.go index a921ec14..f29ff456 100644 --- a/context.go +++ b/context.go @@ -47,6 +47,7 @@ type ( session *session.Session reply *Reply viewArgs map[string]interface{} + values map[string]interface{} abort bool decorated bool } @@ -203,6 +204,7 @@ func (ctx *Context) Reset() { ctx.session = nil ctx.reply = nil ctx.viewArgs = nil + ctx.values = nil ctx.abort = false ctx.decorated = false } diff --git a/engine.go b/engine.go index a1c1b1bc..969b5a67 100644 --- a/engine.go +++ b/engine.go @@ -11,6 +11,7 @@ import ( "io" "net/http" "os" + "time" "aahframework.org/ahttp.v0" "aahframework.org/aruntime.v0" @@ -53,6 +54,7 @@ type ( isRequestIDEnabled bool requestIDHeader string isGzipEnabled bool + isAccessLogEnabled bool ctxPool *pool.Pool reqPool *pool.Pool replyPool *pool.Pool @@ -68,7 +70,12 @@ type ( // ServeHTTP method implementation of http.Handler interface. func (e *engine) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Capture the startTime earlier. + // This value is as accurate as could be. + startTime := time.Now() + ctx := e.prepareContext(w, r) + ctx.values[appStartTimeKey] = startTime defer e.putContext(ctx) // Recovery handling, capture every possible panic(s) @@ -82,6 +89,7 @@ func (e *engine) ServeHTTP(w http.ResponseWriter, r *http.Request) { publishOnRequestEvent(ctx) // Handling route + if e.handleRoute(ctx) == flowStop { return } @@ -133,9 +141,7 @@ func (e *engine) handleRecovery(ctx *Context) { // It won't set new request id header already present. func (e *engine) setRequestID(ctx *Context) { if ess.IsStrEmpty(ctx.Req.Header.Get(e.requestIDHeader)) { - guid := ess.NewGUID() - log.Debugf("Request ID: %v", guid) - ctx.Req.Header.Set(e.requestIDHeader, guid) + ctx.Req.Header.Set(e.requestIDHeader, ess.NewGUID()) } else { log.Debugf("Request already has ID: %v", ctx.Req.Header.Get(e.requestIDHeader)) } @@ -150,6 +156,7 @@ func (e *engine) prepareContext(w http.ResponseWriter, req *http.Request) *Conte ctx.Res = ahttp.GetResponseWriter(w) ctx.reply = e.getReply() ctx.viewArgs = make(map[string]interface{}) + ctx.values = make(map[string]interface{}) return ctx } @@ -298,6 +305,26 @@ func (e *engine) writeReply(ctx *Context) { // 'OnAfterReply' server extension point publishOnAfterReplyEvent(ctx) + + // Send data to access log channel + if e.isAccessLogEnabled { + startTime := ctx.values[appStartTimeKey].(time.Time) + + // All the bytes have been written on the wire + // so calculate elapsed time + elapsedDuration := time.Since(startTime) + ral := &requestAccessLog{ + StartTime: startTime, + ElapsedDuration: elapsedDuration, + RequestID: ctx.Req.Header.Get(e.requestIDHeader), + Request: *ctx.Req, + ResStatus: ctx.Res.Status(), + ResBytes: ctx.Res.BytesWritten(), + ResHdr: ctx.Res.Header(), + } + + appAccessLogChan <- ral + } } // negotiateContentType method tries to identify if reply.ContType is empty. @@ -422,6 +449,7 @@ func (e *engine) putBuffer(b *bytes.Buffer) { func newEngine(cfg *config.Config) *engine { ahttp.GzipLevel = cfg.IntDefault("render.gzip.level", 5) + if !(ahttp.GzipLevel >= 1 && ahttp.GzipLevel <= 9) { logAsFatal(fmt.Errorf("'render.gzip.level' is not a valid level value: %v", ahttp.GzipLevel)) } @@ -430,6 +458,7 @@ func newEngine(cfg *config.Config) *engine { isRequestIDEnabled: cfg.BoolDefault("request.id.enable", true), requestIDHeader: cfg.StringDefault("request.id.header", ahttp.HeaderXRequestID), isGzipEnabled: cfg.BoolDefault("render.gzip.enable", true), + isAccessLogEnabled: cfg.BoolDefault("request.access_log.enable", false), ctxPool: pool.NewPool( cfg.IntDefault("runtime.pooling.global", defaultGlobalPoolSize), func() interface{} { diff --git a/testdata/config/aah.conf b/testdata/config/aah.conf index 3dc962f2..84951a9f 100644 --- a/testdata/config/aah.conf +++ b/testdata/config/aah.conf @@ -79,6 +79,12 @@ request { header = "X-Request-Id" } + access_log { + enable = false + + file = "test-access.log" + pattern = "%clientip %reqid %reqtime %restime %resstatus %ressize %reqmethod %requrl" + } # Default value is 32mb, choose your value based on your use case multipart_size = "32mb" } From fa52a656c7ea7fd2475ebfc150aee4b087728d23 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Sun, 18 Jun 2017 20:36:08 -0700 Subject: [PATCH 06/38] #69 improvements and test case --- aah.go | 3 +- aah_test.go | 30 ++++++++++ access_log.go | 115 ++++++++++++++++++++------------------- access_log_test.go | 102 ++++++++++++++++++---------------- engine.go | 57 +++++++++---------- engine_test.go | 5 +- render.go | 2 +- testdata/config/aah.conf | 21 +++++-- 8 files changed, 193 insertions(+), 142 deletions(-) diff --git a/aah.go b/aah.go index 16f3dd93..c243bd8d 100644 --- a/aah.go +++ b/aah.go @@ -49,6 +49,7 @@ var ( appDefaultHTTPPort = "8080" appDefaultDateFormat = "2006-01-02" appDefaultDateTimeFormat = "2006-01-02 15:04:05" + appLogFatal = log.Fatal goPath string goSrcDir string @@ -231,7 +232,7 @@ func appLogsDir() string { func logAsFatal(err error) { if err != nil { - log.Fatal(err) + appLogFatal(err) } } diff --git a/aah_test.go b/aah_test.go index 4dfcc855..42081588 100644 --- a/aah_test.go +++ b/aah_test.go @@ -5,6 +5,7 @@ package aah import ( + "errors" "fmt" "io/ioutil" "path/filepath" @@ -172,6 +173,35 @@ func TestAahLogDir(t *testing.T) { cfg, _ := config.ParseString("") logger, _ := log.New(cfg) log.SetDefaultLogger(logger) + + // relative path filename + cfgRelativeFile, _ := config.ParseString(` + log { + receiver = "file" + file = "my-test-file.log" + } + `) + err = initLogs(logsDir, cfgRelativeFile) + assert.Nil(t, err) + + // no filename mentioned + cfgNoFile, _ := config.ParseString(` + log { + receiver = "file" + } + `) + SetAppBuildInfo(&BuildInfo{ + BinaryName: "testapp", + Date: time.Now().Format(time.RFC3339), + Version: "1.0.0", + }) + err = initLogs(logsDir, cfgNoFile) + assert.Nil(t, err) + appBuildInfo = nil + + appLogFatal = func(v ...interface{}) { fmt.Println(v) } + logAsFatal(errors.New("test msg")) + } func TestWritePID(t *testing.T) { diff --git a/access_log.go b/access_log.go index a6559164..52202e88 100644 --- a/access_log.go +++ b/access_log.go @@ -5,11 +5,10 @@ package aah import ( - "bytes" "fmt" "net/http" "path/filepath" - "sync" + "strings" "time" "aahframework.org/ahttp.v0" @@ -47,12 +46,12 @@ var ( "restime": fmtFlagResponseTime, } - defaultRequestAccessLogPattern = "%clientip %reqid %reqtime %restime %resstatus %ressize %reqmethod %requrl" - appStartTimeKey = "appReqStartTime" - appAccessLogBufPool *sync.Pool - appAccessLog *log.Logger - appAccessLogFmtFlags []ess.FmtFlagPart - appAccessLogChan chan *requestAccessLog + appDefaultAccessLogPattern = "%clientip %reqid %reqtime %restime %resstatus %ressize %reqmethod %requrl" + appReqStartTimeKey = "appReqStartTimeKey" + appReqIDHdrKey = ahttp.HeaderXRequestID + appAccessLog *log.Logger + appAccessLogFmtFlags []ess.FmtFlagPart + appAccessLogChan chan *requestAccessLog ) type ( @@ -60,7 +59,6 @@ type ( requestAccessLog struct { StartTime time.Time ElapsedDuration time.Duration - RequestID string Request ahttp.Request ResStatus int ResBytes int @@ -68,6 +66,43 @@ type ( } ) +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// requestAccessLog methods +//___________________________________ + +// FmtRequestTime method returns the formatted request time. There are three +// possibilities to handle, `%reqtime`, `%reqtime:` and `%reqtime:`. +func (al *requestAccessLog) FmtRequestTime(format string) string { + if format == "%v" || ess.IsStrEmpty(format) { + return al.StartTime.Format(time.RFC3339) + } + return al.StartTime.Format(format) +} + +func (al *requestAccessLog) GetRequestHdr(hdrKey string) string { + hdrValues := al.Request.Header[http.CanonicalHeaderKey(hdrKey)] + if len(hdrValues) == 0 { + return "-" + } + return strings.Join(hdrValues, ", ") +} + +func (al *requestAccessLog) GetResponseHdr(hdrKey string) string { + hdrValues := al.ResHdr[http.CanonicalHeaderKey(hdrKey)] + if len(hdrValues) == 0 { + return "-" + } + return strings.Join(hdrValues, ", ") +} + +func (al *requestAccessLog) GetQueryString() string { + queryStr := al.Request.Raw.URL.Query().Encode() + if ess.IsStrEmpty(queryStr) { + return "-" + } + return queryStr +} + //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ // Unexported methods //___________________________________ @@ -95,26 +130,26 @@ func initRequestAccessLog(logsDir string, appCfg *config.Config) error { } // parse request access log pattern - pattern := appCfg.StringDefault("request.access_log.pattern", defaultRequestAccessLogPattern) + pattern := appCfg.StringDefault("request.access_log.pattern", appDefaultAccessLogPattern) appAccessLogFmtFlags, err = ess.ParseFmtFlag(pattern, accessLogFmtFlags) if err != nil { return err } // initialize request access log channel - appAccessLogChan = make(chan *requestAccessLog, cfg.IntDefault("runtime.pooling.global", defaultGlobalPoolSize)) - appAccessLogBufPool = &sync.Pool{New: func() interface{} { return &bytes.Buffer{} }} + if appAccessLogChan == nil { + appAccessLogChan = make(chan *requestAccessLog, cfg.IntDefault("runtime.pooling.global", defaultGlobalPoolSize)) + go listenForAccessLog() + } - // start the listener - go listenForAccessLog() + appReqIDHdrKey = cfg.StringDefault("request.id.header", ahttp.HeaderXRequestID) return nil } func listenForAccessLog() { for { - info := <-appAccessLogChan - appAccessLog.Print(requestAccessLogFormatter(info)) + appAccessLog.Print(requestAccessLogFormatter(<-appAccessLogChan)) } } @@ -127,61 +162,27 @@ func requestAccessLogFormatter(ral *requestAccessLog) string { case fmtFlagClientIP: buf.WriteString(ral.Request.ClientIP) case fmtFlagRequestTime: - //there are two options here for the pattern we have to handle ; - //%reqtime or %reqtime: - if ess.IsStrEmpty(part.Format) || part.Format == "%v" { - buf.WriteString(ral.StartTime.Format(time.RFC3339)) - } else { - buf.WriteString(ral.StartTime.Format(part.Format)) - } + buf.WriteString(ral.FmtRequestTime(part.Format)) case fmtFlagRequestURL: buf.WriteString(ral.Request.Path) case fmtFlagRequestMethod: buf.WriteString(ral.Request.Method) case fmtFlagRequestID: - if ess.IsStrEmpty(ral.RequestID) { - buf.WriteByte('-') - } else { - buf.WriteString(ral.RequestID) - } + buf.WriteString(ral.GetRequestHdr(appReqIDHdrKey)) case fmtFlagRequestHeader: - hdr := ral.Request.Header.Get(http.CanonicalHeaderKey(part.Format)) - if ess.IsStrEmpty(hdr) { - buf.WriteByte('-') - } else { - buf.WriteString(hdr) - } + buf.WriteString(ral.GetRequestHdr(part.Format)) case fmtFlagQueryString: - queryStr := ral.Request.Raw.URL.Query().Encode() - if ess.IsStrEmpty(queryStr) { - buf.WriteByte('-') - } else { - buf.WriteString(queryStr) - } + buf.WriteString(ral.GetQueryString()) case fmtFlagResponseStatus: buf.WriteString(fmt.Sprintf(part.Format, ral.ResStatus)) case fmtFlagResponseSize: buf.WriteString(fmt.Sprintf(part.Format, ral.ResBytes)) case fmtFlagResponseHeader: - hdr := ral.ResHdr.Get(part.Format) - if ess.IsStrEmpty(hdr) { - buf.WriteByte('-') - } else { - buf.WriteString(hdr) - } + buf.WriteString(ral.GetResponseHdr(part.Format)) case fmtFlagResponseTime: - buf.WriteString(fmt.Sprintf("%10v", ral.ElapsedDuration)) + buf.WriteString(fmt.Sprintf(part.Format, ral.ElapsedDuration.Nanoseconds())) } buf.WriteByte(' ') } - return buf.String() -} - -func getBuffer() *bytes.Buffer { - return appAccessLogBufPool.Get().(*bytes.Buffer) -} - -func putBuffer(buf *bytes.Buffer) { - buf.Reset() - appAccessLogBufPool.Put(buf) + return strings.TrimSpace(buf.String()) } diff --git a/access_log_test.go b/access_log_test.go index 22b1935b..c6b32baf 100644 --- a/access_log_test.go +++ b/access_log_test.go @@ -7,64 +7,65 @@ package aah import ( "bytes" "fmt" + "net/http" "net/http/httptest" "path/filepath" - "sync" "testing" "time" "aahframework.org/ahttp.v0" + "aahframework.org/config.v0" "aahframework.org/essentials.v0-unstable" + "aahframework.org/pool.v0" "aahframework.org/test.v0/assert" ) func TestRequestAccessLogFormatter(t *testing.T) { startTime := time.Now() req := httptest.NewRequest("GET", "/oops?me=human", nil) - resRec := httptest.NewRecorder() + req.Header = make(http.Header) + + w := httptest.NewRecorder() ral := &requestAccessLog{ StartTime: startTime, ElapsedDuration: time.Now().Add(2 * time.Second).Sub(startTime), - RequestID: "req-id:12345", - Request: ahttp.Request{Raw: req}, + Request: ahttp.Request{Raw: req, Header: req.Header, ClientIP: "[::1]"}, ResStatus: 200, ResBytes: 63, - ResHdr: resRec.HeaderMap, + ResHdr: w.HeaderMap, } - //Since we are not bootstrapping the framework's engine, - //We need to manually set this + // Since we are not bootstrapping the framework's engine, + // We need to manually set this ral.Request.Path = "/oops" - appAccessLogBufPool = &sync.Pool{New: func() interface{} { return &bytes.Buffer{} }} + ral.Request.Header.Set(ahttp.HeaderXRequestID, "5946ed129bf23409520736de") + bufPool = pool.NewPool(2, func() interface{} { return &bytes.Buffer{} }) - //We test for the default format first - expectedDefaultFormat := fmt.Sprintf(" %s %v %v %d %d %s %s ", - ral.RequestID, ral.StartTime.Format(time.RFC3339), - ral.ElapsedDuration, ral.ResStatus, ral.ResBytes, ral.Request.Method, ral.Request.Path) + // Testing for the default access log pattern first + expectedDefaultFormat := fmt.Sprintf("%s %s %v %v %d %d %s %s", + "[::1]", "5946ed129bf23409520736de", ral.StartTime.Format(time.RFC3339), + ral.ElapsedDuration.Nanoseconds(), ral.ResStatus, ral.ResBytes, ral.Request.Method, ral.Request.Path) - testFormatter(t, ral, defaultRequestAccessLogPattern, expectedDefaultFormat) + testFormatter(t, ral, appDefaultAccessLogPattern, expectedDefaultFormat) + // Testing custom access log pattern ral.ResHdr.Add("content-type", "application/json") - //Then for something much more diffrent pattern := "%reqtime:2016-05-16 %reqhdr %querystr %reshdr:content-type" - expected := fmt.Sprintf("%s %s %s %s ", - ral.StartTime.Format("2016-05-16"), - "-", "me=human", ral.ResHdr.Get("content-type"), - ) + expected := fmt.Sprintf("%s %s %s %s", ral.StartTime.Format("2016-05-16"), "-", "me=human", ral.ResHdr.Get("Content-Type")) testFormatter(t, ral, pattern, expected) - //Test for equest access log format + // Testing all available access log pattern ral.Request.Header = ral.Request.Raw.Header ral.Request.Header.Add(ahttp.HeaderAccept, "text/html") ral.Request.ClientIP = "127.0.0.1" allAvailablePatterns := "%clientip %reqid %reqtime %restime %resstatus %ressize %reqmethod %requrl %reqhdr:accept %querystr %reshdr" expectedForAllAvailablePatterns := fmt.Sprintf("%s %s %s %v %d %d %s %s %s %s %s", - ral.Request.ClientIP, ral.RequestID, - ral.StartTime.Format(time.RFC3339), ral.ElapsedDuration, + ral.Request.ClientIP, ral.Request.Header.Get(ahttp.HeaderXRequestID), + ral.StartTime.Format(time.RFC3339), ral.ElapsedDuration.Nanoseconds(), ral.ResStatus, ral.ResBytes, ral.Request.Method, - ral.Request.Path, "text/html", "me=human", "- ") + ral.Request.Path, "text/html", "me=human", "-") testFormatter(t, ral, allAvailablePatterns, expectedForAllAvailablePatterns) } @@ -75,22 +76,15 @@ func TestRequestAccessLogFormatterInvalidPattern(t *testing.T) { assert.NotNil(t, err) } -func testFormatter(t *testing.T, ral *requestAccessLog, pattern, expected string) { - var err error - appAccessLogFmtFlags, err = ess.ParseFmtFlag(pattern, accessLogFmtFlags) - - assert.Nil(t, err) - assert.Equal(t, expected, requestAccessLogFormatter(ral)) -} - -func TestInitRequestAccessLog(t *testing.T) { - cfgDir := filepath.Join(getTestdataPath(), appConfigDir()) - _ = initConfig(cfgDir) - - err := initRequestAccessLog(appLogsDir(), AppConfig()) - - assert.Nil(t, err) - assert.NotNil(t, appAccessLog) +func TestRequestAccessLogInitDefault(t *testing.T) { + testAccessInit(t, ` + request { + access_log { + # Default value is false + enable = true + } + } + `) } func TestEngineRequestAccessLog(t *testing.T) { @@ -120,14 +114,6 @@ func TestEngineRequestAccessLog(t *testing.T) { Name: "GetInvolved", Parameters: []*ParameterInfo{}, }, - { - Name: "ContributeCode", - Parameters: []*ParameterInfo{}, - }, - { - Name: "Credits", - Parameters: []*ParameterInfo{}, - }, }) AppConfig().SetBool("request.access_log.enable", true) @@ -139,3 +125,27 @@ func TestEngineRequestAccessLog(t *testing.T) { assert.True(t, e.isAccessLogEnabled) } + +func testFormatter(t *testing.T, ral *requestAccessLog, pattern, expected string) { + var err error + appAccessLogFmtFlags, err = ess.ParseFmtFlag(pattern, accessLogFmtFlags) + + assert.Nil(t, err) + assert.Equal(t, expected, requestAccessLogFormatter(ral)) +} + +func testAccessInit(t *testing.T, cfgStr string) { + buildTime := time.Now().Format(time.RFC3339) + SetAppBuildInfo(&BuildInfo{ + BinaryName: "testapp", + Date: buildTime, + Version: "1.0.0", + }) + + cfg, _ := config.ParseString(cfgStr) + logsDir := filepath.Join(getTestdataPath(), appLogsDir()) + err := initRequestAccessLog(logsDir, cfg) + + assert.Nil(t, err) + assert.NotNil(t, appAccessLog) +} diff --git a/engine.go b/engine.go index 969b5a67..45b3acd5 100644 --- a/engine.go +++ b/engine.go @@ -36,6 +36,7 @@ const ( var ( minifier MinifierFunc + bufPool *pool.Pool errFileNotFound = errors.New("file not found") noGzipStatusCodes = []int{http.StatusNotModified, http.StatusNoContent} ) @@ -58,7 +59,6 @@ type ( ctxPool *pool.Pool reqPool *pool.Pool replyPool *pool.Pool - bufPool *pool.Pool } byName []os.FileInfo @@ -75,7 +75,7 @@ func (e *engine) ServeHTTP(w http.ResponseWriter, r *http.Request) { startTime := time.Now() ctx := e.prepareContext(w, r) - ctx.values[appStartTimeKey] = startTime + ctx.values[appReqStartTimeKey] = startTime defer e.putContext(ctx) // Recovery handling, capture every possible panic(s) @@ -117,8 +117,8 @@ func (e *engine) handleRecovery(ctx *Context) { log.Errorf("Internal Server Error on %s", ctx.Req.Path) st := aruntime.NewStacktrace(r, AppConfig()) - buf := e.getBuffer() - defer e.putBuffer(buf) + buf := getBuffer() + defer putBuffer(buf) st.Print(buf) log.Error(buf.String()) @@ -308,7 +308,7 @@ func (e *engine) writeReply(ctx *Context) { // Send data to access log channel if e.isAccessLogEnabled { - startTime := ctx.values[appStartTimeKey].(time.Time) + startTime := ctx.values[appReqStartTimeKey].(time.Time) // All the bytes have been written on the wire // so calculate elapsed time @@ -316,7 +316,6 @@ func (e *engine) writeReply(ctx *Context) { ral := &requestAccessLog{ StartTime: startTime, ElapsedDuration: elapsedDuration, - RequestID: ctx.Req.Header.Get(e.requestIDHeader), Request: *ctx.Req, ResStatus: ctx.Res.Status(), ResBytes: ctx.Res.BytesWritten(), @@ -419,7 +418,7 @@ func (e *engine) putContext(ctx *Context) { // clear and put `Reply` into pool if ctx.reply != nil { - e.putBuffer(ctx.reply.body) + putBuffer(ctx.reply.body) ctx.reply.Reset() e.replyPool.Put(ctx.reply) } @@ -429,31 +428,25 @@ func (e *engine) putContext(ctx *Context) { e.ctxPool.Put(ctx) } -// getBuffer method gets buffer from pool -func (e *engine) getBuffer() *bytes.Buffer { - return e.bufPool.Get().(*bytes.Buffer) -} - -// putBPool puts buffer into pool -func (e *engine) putBuffer(b *bytes.Buffer) { - if b == nil { - return - } - b.Reset() - e.bufPool.Put(b) -} - //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ // Unexported methods //___________________________________ func newEngine(cfg *config.Config) *engine { ahttp.GzipLevel = cfg.IntDefault("render.gzip.level", 5) - if !(ahttp.GzipLevel >= 1 && ahttp.GzipLevel <= 9) { logAsFatal(fmt.Errorf("'render.gzip.level' is not a valid level value: %v", ahttp.GzipLevel)) } + if bufPool == nil { + bufPool = pool.NewPool( + cfg.IntDefault("runtime.pooling.buffer", defaultBufPoolSize), + func() interface{} { + return &bytes.Buffer{} + }, + ) + } + return &engine{ isRequestIDEnabled: cfg.BoolDefault("request.id.enable", true), requestIDHeader: cfg.StringDefault("request.id.header", ahttp.HeaderXRequestID), @@ -477,11 +470,19 @@ func newEngine(cfg *config.Config) *engine { return NewReply() }, ), - bufPool: pool.NewPool( - cfg.IntDefault("runtime.pooling.buffer", defaultBufPoolSize), - func() interface{} { - return &bytes.Buffer{} - }, - ), } } + +// getBuffer method gets buffer from pool +func getBuffer() *bytes.Buffer { + return bufPool.Get().(*bytes.Buffer) +} + +// putBPool puts buffer into pool +func putBuffer(b *bytes.Buffer) { + if b == nil { + return + } + b.Reset() + bufPool.Put(b) +} diff --git a/engine_test.go b/engine_test.go index 94ede8b7..c91ac09a 100644 --- a/engine_test.go +++ b/engine_test.go @@ -78,7 +78,6 @@ func TestEngineNew(t *testing.T) { assert.True(t, e.isRequestIDEnabled) assert.True(t, e.isGzipEnabled) assert.NotNil(t, e.ctxPool) - assert.NotNil(t, e.bufPool) assert.NotNil(t, e.reqPool) req := e.getRequest() @@ -89,9 +88,9 @@ func TestEngineNew(t *testing.T) { assert.NotNil(t, ctx.Req) e.putContext(ctx) - buf := e.getBuffer() + buf := getBuffer() assert.NotNil(t, buf) - e.putBuffer(buf) + putBuffer(buf) } func TestEngineServeHTTP(t *testing.T) { diff --git a/render.go b/render.go index 9d011267..8715909f 100644 --- a/render.go +++ b/render.go @@ -200,7 +200,7 @@ func (h *HTML) Render(w io.Writer) error { func (e *engine) doRender(ctx *Context) { if ctx.Reply().Rdr != nil { reply := ctx.Reply() - reply.body = e.getBuffer() + reply.body = getBuffer() if jsonp, ok := reply.Rdr.(*JSON); ok && jsonp.IsJSONP { if ess.IsStrEmpty(jsonp.Callback) { jsonp.Callback = ctx.Req.QueryValue("callback") diff --git a/testdata/config/aah.conf b/testdata/config/aah.conf index 84951a9f..c90256a0 100644 --- a/testdata/config/aah.conf +++ b/testdata/config/aah.conf @@ -79,12 +79,21 @@ request { header = "X-Request-Id" } - access_log { - enable = false - - file = "test-access.log" - pattern = "%clientip %reqid %reqtime %restime %resstatus %ressize %reqmethod %requrl" - } # Default value is 32mb, choose your value based on your use case multipart_size = "32mb" + + # To manage server effectively it is necessary to know details about the + # request, response, processing time, client IP address, etc. aah framework + # provides the flexible and configurable access log capabilities. + access_log { + # Default value is `false` + enable = true + + # Absolute path of access log file + # Default location is application logs directory + #file = "{{ .AppName }}-access.log" + + # Default request access log pattern as follows + #pattern = "%clientip %reqid %reqtime %restime %resstatus %ressize %reqmethod %requrl" + } } From 725c87ee4b3d65e105a9278b3f8952fb3694df86 Mon Sep 17 00:00:00 2001 From: Lanre Adelowo Date: Mon, 19 Jun 2017 04:43:43 +0100 Subject: [PATCH 07/38] #71 Allow i18n URL query param to be configurable (#73) --- param.go | 6 ++++-- param_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/param.go b/param.go index e2b0406c..3b127ace 100644 --- a/param.go +++ b/param.go @@ -68,8 +68,10 @@ func (e *engine) parseRequestParams(ctx *Context) { }(req) } - // i18n option override by Query parameter `lang` - if lang := ctx.Req.QueryValue(keyOverrideLocale); !ess.IsStrEmpty(lang) { + // i18n option via the config value "i18n.url_param_name". + // If that value is missing, we default to the `lang` query parameter + queryParam := AppConfig().StringDefault("i18n.param_name.query", keyOverrideLocale) + if lang := ctx.Req.QueryValue(queryParam); !ess.IsStrEmpty(lang) { ctx.Req.Locale = ahttp.NewLocale(lang) } diff --git a/param_test.go b/param_test.go index 018ae77c..a4234fd8 100644 --- a/param_test.go +++ b/param_test.go @@ -12,6 +12,7 @@ import ( "testing" "aahframework.org/ahttp.v0" + "aahframework.org/config.v0" "aahframework.org/essentials.v0" "aahframework.org/test.v0/assert" ) @@ -79,3 +80,33 @@ func TestParamParse(t *testing.T) { e.parseRequestParams(ctx2) assert.NotNil(t, ctx2.Req.Params.Form) } + +func TestParamParseLocaleFromAppConfiguration(t *testing.T) { + defer ess.DeleteFiles("testapp.pid") + + cfg, err := config.ParseString(` + i18n { + param_name { + query = "language" + } + } + `) + appConfig = cfg + + assert.Nil(t, err) + + r := httptest.NewRequest("GET", "http://localhost:8080/index.html?language=en-CA", nil) + ctx1 := &Context{ + Req: ahttp.ParseRequest(r, &ahttp.Request{}), + viewArgs: make(map[string]interface{}), + } + + e := &engine{} + + assert.Nil(t, ctx1.Req.Locale) + e.parseRequestParams(ctx1) + assert.NotNil(t, ctx1.Req.Locale) + assert.Equal(t, "en", ctx1.Req.Locale.Language) + assert.Equal(t, "CA", ctx1.Req.Locale.Region) + assert.Equal(t, "en-CA", ctx1.Req.Locale.String()) +} From 37561e82e3709609083d148825a2ef14843b479b Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Sun, 18 Jun 2017 21:07:58 -0700 Subject: [PATCH 08/38] #71 path variable locale override support added --- param.go | 33 +++++++++++++++++++++++++-------- param_test.go | 5 +++-- util.go | 11 +++++++++++ 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/param.go b/param.go index 3b127ace..61481caa 100644 --- a/param.go +++ b/param.go @@ -14,12 +14,17 @@ import ( ) const ( - keyRequestParams = "RequestParams" - keyOverrideLocale = "lang" + keyRequestParams = "_RequestParams" + keyOverrideI18nName = "lang" +) + +var ( + keyQueryParamName = keyOverrideI18nName + keyPathParamName = keyOverrideI18nName ) //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Params method +// Params Unexported method //___________________________________ // parseRequestParams method parses the incoming HTTP request to collects request @@ -68,11 +73,14 @@ func (e *engine) parseRequestParams(ctx *Context) { }(req) } - // i18n option via the config value "i18n.url_param_name". - // If that value is missing, we default to the `lang` query parameter - queryParam := AppConfig().StringDefault("i18n.param_name.query", keyOverrideLocale) - if lang := ctx.Req.QueryValue(queryParam); !ess.IsStrEmpty(lang) { - ctx.Req.Locale = ahttp.NewLocale(lang) + // i18n locale HTTP header `Accept-Language` value override via + // Path Variable and URL Query Param (config i18n { param_name { ... } }). + // Note: Query parameter takes precedence of all. + // Default parameter name is `lang` + pathValue := ctx.Req.PathValue(keyPathParamName) + queryValue := ctx.Req.QueryValue(keyQueryParamName) + if locale := firstNonEmpty(queryValue, pathValue); !ess.IsStrEmpty(locale) { + ctx.Req.Locale = ahttp.NewLocale(locale) } // All the request parameters made available to templates via funcs. @@ -100,3 +108,12 @@ func tmplQueryParam(viewArgs map[string]interface{}, key string) interface{} { params := viewArgs[keyRequestParams].(*ahttp.Params) return sanatizeValue(params.QueryValue(key)) } + +func paramInitialize(e *Event) { + keyPathParamName = AppConfig().StringDefault("i18n.param_name.path", keyOverrideI18nName) + keyQueryParamName = AppConfig().StringDefault("i18n.param_name.query", keyOverrideI18nName) +} + +func init() { + OnStart(paramInitialize) +} diff --git a/param_test.go b/param_test.go index a4234fd8..514000d7 100644 --- a/param_test.go +++ b/param_test.go @@ -85,13 +85,14 @@ func TestParamParseLocaleFromAppConfiguration(t *testing.T) { defer ess.DeleteFiles("testapp.pid") cfg, err := config.ParseString(` - i18n { - param_name { + i18n { + param_name { query = "language" } } `) appConfig = cfg + paramInitialize(&Event{}) assert.Nil(t, err) diff --git a/util.go b/util.go index 505eb51b..627af462 100644 --- a/util.go +++ b/util.go @@ -95,3 +95,14 @@ func resolveControllerName(ctx *Context) string { func isCharsetExists(value string) bool { return strings.Contains(value, "charset") } + +// this method is candidate for essentials library +// move it when you get a time +func firstNonEmpty(values ...string) string { + for _, v := range values { + if !ess.IsStrEmpty(v) { + return v + } + } + return "" +} From 7744562d267f882bd64d67344c9fd38d0ddb46d6 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Sat, 1 Jul 2017 14:16:28 -0700 Subject: [PATCH 09/38] #69 log elpased time in milliseconds --- access_log.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/access_log.go b/access_log.go index 52202e88..5b3ea668 100644 --- a/access_log.go +++ b/access_log.go @@ -180,7 +180,7 @@ func requestAccessLogFormatter(ral *requestAccessLog) string { case fmtFlagResponseHeader: buf.WriteString(ral.GetResponseHdr(part.Format)) case fmtFlagResponseTime: - buf.WriteString(fmt.Sprintf(part.Format, ral.ElapsedDuration.Nanoseconds())) + buf.WriteString(fmt.Sprintf("%.4f", ral.ElapsedDuration.Seconds()*1e3)) } buf.WriteByte(' ') } From b82423e7b3c60f7378a9e9c229705cdf79d1ebba Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Sat, 1 Jul 2017 14:17:39 -0700 Subject: [PATCH 10/38] make req params key unique --- param.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/param.go b/param.go index 61481caa..d9be6bff 100644 --- a/param.go +++ b/param.go @@ -14,7 +14,7 @@ import ( ) const ( - keyRequestParams = "_RequestParams" + keyRequestParams = "_aahRequestParams" keyOverrideI18nName = "lang" ) From f469f3ff48deb71469c5ed2f24f3ef539cab41c2 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Mon, 3 Jul 2017 15:56:13 -0700 Subject: [PATCH 11/38] #69 test update for milliseconds --- access_log_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/access_log_test.go b/access_log_test.go index c6b32baf..079f99b0 100644 --- a/access_log_test.go +++ b/access_log_test.go @@ -45,7 +45,8 @@ func TestRequestAccessLogFormatter(t *testing.T) { // Testing for the default access log pattern first expectedDefaultFormat := fmt.Sprintf("%s %s %v %v %d %d %s %s", "[::1]", "5946ed129bf23409520736de", ral.StartTime.Format(time.RFC3339), - ral.ElapsedDuration.Nanoseconds(), ral.ResStatus, ral.ResBytes, ral.Request.Method, ral.Request.Path) + fmt.Sprintf("%.4f", ral.ElapsedDuration.Seconds()*1e3), ral.ResStatus, + ral.ResBytes, ral.Request.Method, ral.Request.Path) testFormatter(t, ral, appDefaultAccessLogPattern, expectedDefaultFormat) @@ -63,7 +64,7 @@ func TestRequestAccessLogFormatter(t *testing.T) { allAvailablePatterns := "%clientip %reqid %reqtime %restime %resstatus %ressize %reqmethod %requrl %reqhdr:accept %querystr %reshdr" expectedForAllAvailablePatterns := fmt.Sprintf("%s %s %s %v %d %d %s %s %s %s %s", ral.Request.ClientIP, ral.Request.Header.Get(ahttp.HeaderXRequestID), - ral.StartTime.Format(time.RFC3339), ral.ElapsedDuration.Nanoseconds(), + ral.StartTime.Format(time.RFC3339), fmt.Sprintf("%.4f", ral.ElapsedDuration.Seconds()*1e3), ral.ResStatus, ral.ResBytes, ral.Request.Method, ral.Request.Path, "text/html", "me=human", "-") @@ -102,7 +103,7 @@ func TestEngineRequestAccessLog(t *testing.T) { assert.NotNil(t, AppRouter()) // Security - err = initSecurity(cfgDir, AppConfig()) + err = initSecurity(AppConfig()) assert.Nil(t, err) assert.True(t, AppSessionManager().IsStateful()) From 6de33714f6e6c35ff89b0aa83067d1fbb08d97fd Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Mon, 3 Jul 2017 15:57:19 -0700 Subject: [PATCH 12/38] #37 auth module intergration progress --- security.go | 132 ++++++++++++++++++++++++++++++++++----- security_test.go | 113 ++++++++++++++++++++++++++++++++- testdata/config/aah.conf | 6 ++ 3 files changed, 236 insertions(+), 15 deletions(-) diff --git a/security.go b/security.go index 3b018ecd..eed6c656 100644 --- a/security.go +++ b/security.go @@ -6,31 +6,38 @@ package aah import ( "fmt" - "path/filepath" + ahttp "aahframework.org/ahttp.v0" "aahframework.org/config.v0" - "aahframework.org/security.v0" - "aahframework.org/security.v0/session" + "aahframework.org/essentials.v0" + "aahframework.org/log.v0" + "aahframework.org/security.v0-unstable" + "aahframework.org/security.v0-unstable/authc" + "aahframework.org/security.v0-unstable/scheme" + "aahframework.org/security.v0-unstable/session" ) -const keySessionValues = "SessionValues" +const ( + keySessionValues = "_aahSessionValues" + keyAuthcInfo = "_aahAuthcInfo" +) -var appSecurity *security.Security +var appSecurityManager = security.New() //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ // Global methods //___________________________________ -// AppSecurity method returns the application security instance, +// AppSecurityManager method returns the application security instance, // which manages the Session, CORS, CSRF, Security Headers, etc. -func AppSecurity() *security.Security { - return appSecurity +func AppSecurityManager() *security.Manager { + return appSecurityManager } // AppSessionManager method returns the application session manager. // By default session is stateless. func AppSessionManager() *session.Manager { - return AppSecurity().SessionManager + return AppSecurityManager().SessionManager } // AddSessionStore method allows you to add custom session store which @@ -40,15 +47,104 @@ func AddSessionStore(name string, store session.Storer) error { return session.AddStore(name, store) } +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// Authentication and Authorization methods +//__________________________________________ + +func (e engine) handleAuthcAndAuthz(ctx *Context) flowResult { + // If route auth is `anonymous` then continue the request flow + // No authentication or authorization is required for that route. + if ctx.route.Auth == "anonymous" { + log.Debugf("Route auth is anonymous: %v", ctx.Req.Path) + return flowCont + } + + authScheme := AppSecurityManager().GetAuthScheme(ctx.route.Auth) + if authScheme == nil { + // If auth scheme is nil then treat it as `anonymous`. + log.Infof("Route auth scheme is nil, treating as anonymous: %v", ctx.Req.Path) + return flowCont + } + + log.Debugf("Route auth scheme: %s", authScheme.Scheme()) + switch authScheme.Scheme() { + case "form": + return e.doFormAuthcAndAuthz(authScheme, ctx) + } + + return flowCont +} + +// doFormAuthcAndAuthz method does Form Authentication and Authorization. +func (e *engine) doFormAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowResult { + formAuth := ascheme.(*scheme.FormAuth) + + // In Form authentication check session is already authentication if yes + // then continue the request flow immediately. + if ctx.Subject().IsAuthenticated() { + if ctx.Session().IsKeyExists(keyAuthcInfo) { + ctx.Subject().AuthenticationInfo = ctx.Session().Get(keyAuthcInfo).(*authc.AuthenticationInfo) + + // TODO cache for AuthorizationInfo + ctx.Subject().AuthorizationInfo = formAuth.DoAuthorizationInfo(ctx.Subject().AuthenticationInfo) + } else { + log.Warn("It seems there is an issue with session data of AuthenticationInfo") + } + + return flowCont + } + + // Check route is login submit URL otherwise send it login URL. + // Since session is not authenticated. + if formAuth.LoginSubmitURL != ctx.route.Path && ctx.Req.Method != ahttp.MethodPost { + loginURL := formAuth.LoginURL + if formAuth.LoginURL != ctx.Req.Path { + loginURL = fmt.Sprintf("%s?_rt=%s", loginURL, ctx.Req.Raw.RequestURI) + } + ctx.Reply().Redirect(loginURL) + e.writeReply(ctx) + return flowStop + } + + // Do Authentication + // TODO publish pre auth server event + authcInfo, err := formAuth.DoAuthenticate(formAuth.ExtractAuthenticationToken(ctx.Req)) + if err == authc.ErrAuthenticationFailed { + log.Infof("Authentication is failed, sending to login failure URL") + ctx.Reply().Redirect(formAuth.LoginFailureURL + "&_rt=" + ctx.Req.Raw.FormValue("_rt")) + e.writeReply(ctx) + return flowStop + } + + ctx.Subject().AuthenticationInfo = authcInfo + ctx.Subject().AuthorizationInfo = formAuth.DoAuthorizationInfo(authcInfo) + ctx.Session().IsAuthenticated = true + + // Remove the credential + ctx.Subject().AuthenticationInfo.Credential = nil + ctx.Session().Set(keyAuthcInfo, ctx.Subject().AuthenticationInfo) + + // TODO publish post auth server event + + rt := ctx.Req.Raw.FormValue("_rt") + if formAuth.IsAlwaysToDefaultTarget || ess.IsStrEmpty(rt) { + ctx.Reply().Redirect(formAuth.DefaultTargetURL) + } else { + log.Debugf("Redirect to URL found: %v", rt) + ctx.Reply().Redirect(rt) + } + + e.writeReply(ctx) + return flowStop +} + //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ // Unexported methods //___________________________________ -func initSecurity(cfgDir string, appCfg *config.Config) error { - var err error - configPath := filepath.Join(cfgDir, "security.conf") - if appSecurity, err = security.New(configPath, appCfg); err != nil { - return fmt.Errorf("security init: %s", err) +func initSecurity(appCfg *config.Config) error { + if err := appSecurityManager.Init(appCfg); err != nil { + return err } // Based on aah server SSL configuration `http.Cookie.Secure` value is set, even @@ -60,6 +156,14 @@ func initSecurity(cfgDir string, appCfg *config.Config) error { return nil } +func isFormAuthLoginRoute(ctx *Context) bool { + authScheme := AppSecurityManager().GetAuthScheme(ctx.route.Auth) + if authScheme != nil && authScheme.Scheme() == "form" { + return authScheme.(*scheme.FormAuth).LoginSubmitURL == ctx.route.Path + } + return false +} + //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ // Template methods //___________________________________ diff --git a/security_test.go b/security_test.go index 73629e6a..f2e85cd7 100644 --- a/security_test.go +++ b/security_test.go @@ -5,9 +5,18 @@ package aah import ( + "net/http/httptest" + "strings" "testing" - "aahframework.org/security.v0/session" + "aahframework.org/ahttp.v0" + "aahframework.org/config.v0" + "aahframework.org/router.v0" + "aahframework.org/security.v0-unstable" + "aahframework.org/security.v0-unstable/authc" + "aahframework.org/security.v0-unstable/authz" + "aahframework.org/security.v0-unstable/scheme" + "aahframework.org/security.v0-unstable/session" "aahframework.org/test.v0/assert" ) @@ -47,4 +56,106 @@ func TestSecuritySessionTemplateFuns(t *testing.T) { v3 := tmplIsAuthenticated(viewArgs) assert.False(t, v3) + + delete(viewArgs, keySessionValues) + v4 := tmplIsAuthenticated(viewArgs) + assert.False(t, v4) +} + +type testFormAuthentication struct { +} + +func (tfa *testFormAuthentication) Init(cfg *config.Config) error { + return nil +} + +func (tfa *testFormAuthentication) GetAuthenticationInfo(authcToken *authc.AuthenticationToken) *authc.AuthenticationInfo { + return testGetAuthenticationInfo() +} + +func (tfa *testFormAuthentication) GetAuthorizationInfo(authcInfo *authc.AuthenticationInfo) *authz.AuthorizationInfo { + return nil +} + +func TestSecurityHandleAuthcAndAuthz(t *testing.T) { + e := engine{} + + // anonymous + r1 := httptest.NewRequest("GET", "http://localhost:8080/doc/v0.3/mydoc.html", nil) + ctx1 := &Context{ + Req: ahttp.ParseRequest(r1, &ahttp.Request{}), + route: &router.Route{Auth: "anonymous"}, + } + result1 := e.handleAuthcAndAuthz(ctx1) + assert.True(t, result1 == flowCont) + + // form auth scheme + cfg, _ := config.ParseString(` + security { + auth_schemes { + # HTTP Form Auth Scheme + form_auth { + scheme = "form" + + # Authenticator is used to validate the subject (aka User) + authenticator = "security/Authentication" + + # Authorizer is used to get Subject authorization information, + # such as Roles and Permissions + authorizer = "security/Authorization" + } + } + } + `) + err := initSecurity(cfg) + assert.Nil(t, err) + r2 := httptest.NewRequest("GET", "http://localhost:8080/doc/v0.3/mydoc.html", nil) + w2 := httptest.NewRecorder() + ctx2 := &Context{ + Req: ahttp.ParseRequest(r2, &ahttp.Request{}), + Res: ahttp.GetResponseWriter(w2), + route: &router.Route{Auth: "form_auth"}, + subject: &security.Subject{}, + reply: NewReply(), + } + result2 := e.handleAuthcAndAuthz(ctx2) + assert.True(t, result2 == flowStop) + + // session is authenticated + ctx2.Session().IsAuthenticated = true + result3 := e.handleAuthcAndAuthz(ctx2) + assert.True(t, result3 == flowCont) + + // form auth + testFormAuth := &testFormAuthentication{} + formAuth := AppSecurityManager().GetAuthScheme("form_auth").(*scheme.FormAuth) + err = formAuth.SetAuthenticator(testFormAuth) + assert.Nil(t, err) + err = formAuth.SetAuthorizer(testFormAuth) + assert.Nil(t, err) + r3 := httptest.NewRequest("POST", "http://localhost:8080/login", nil) + ctx2.Req = ahttp.ParseRequest(r3, &ahttp.Request{}) + ctx2.Session().Set(keyAuthcInfo, testGetAuthenticationInfo()) + result4 := e.handleAuthcAndAuthz(ctx2) + assert.True(t, result4 == flowCont) + + // form auth not authenticated and no credentials + ctx2.Session().IsAuthenticated = false + delete(ctx2.Session().Values, keyAuthcInfo) + result5 := e.handleAuthcAndAuthz(ctx2) + assert.True(t, result5 == flowStop) + + // form auth not authenticated and with credentials + r4 := httptest.NewRequest("POST", "http://localhost:8080/login", strings.NewReader("username=jeeva&password=welcome123")) + r4.Header.Set(ahttp.HeaderContentType, "application/x-www-form-urlencoded") + ctx2.Req = ahttp.ParseRequest(r4, &ahttp.Request{}) + result6 := e.handleAuthcAndAuthz(ctx2) + assert.True(t, result6 == flowStop) +} + +func testGetAuthenticationInfo() *authc.AuthenticationInfo { + authcInfo := authc.NewAuthenticationInfo() + authcInfo.Principals = append(authcInfo.Principals, &authc.Principal{Realm: "database", Value: "jeeva", IsPrimary: true}) + authcInfo.Credential = []byte("$2y$10$2A4GsJ6SmLAMvDe8XmTam.MSkKojdobBVJfIU7GiyoM.lWt.XV3H6") // welcome123 + return authcInfo } diff --git a/testdata/config/aah.conf b/testdata/config/aah.conf index c90256a0..54c7a622 100644 --- a/testdata/config/aah.conf +++ b/testdata/config/aah.conf @@ -97,3 +97,9 @@ request { #pattern = "%clientip %reqid %reqtime %restime %resstatus %ressize %reqmethod %requrl" } } + +# -------------------------------------------------------------- +# Application Security +# Doc: https://docs.aahframework.org/security-config.html +# -------------------------------------------------------------- +include "./security.conf" From a56306a63f5b7fd5172d6f668c74db26bf5eb067 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Mon, 3 Jul 2017 15:57:54 -0700 Subject: [PATCH 13/38] #37 security module progress --- aah.go | 2 +- context.go | 18 ++++++++++----- context_test.go | 5 +++-- engine.go | 60 ++++++++++++++++++++++++++++++++----------------- engine_test.go | 2 +- server_test.go | 2 +- 6 files changed, 58 insertions(+), 31 deletions(-) diff --git a/aah.go b/aah.go index c243bd8d..e9795cb6 100644 --- a/aah.go +++ b/aah.go @@ -201,7 +201,7 @@ func Init(importPath string) { logAsFatal(initLogs(appLogsDir(), AppConfig())) logAsFatal(initI18n(appI18nDir())) logAsFatal(initRoutes(appConfigDir(), AppConfig())) - logAsFatal(initSecurity(appConfigDir(), AppConfig())) + logAsFatal(initSecurity(AppConfig())) logAsFatal(initViewEngine(appViewsDir(), AppConfig())) if AppConfig().BoolDefault("request.access_log.enable", false) { logAsFatal(initRequestAccessLog(appLogsDir(), AppConfig())) diff --git a/context.go b/context.go index f29ff456..1faba625 100644 --- a/context.go +++ b/context.go @@ -14,7 +14,8 @@ import ( "aahframework.org/essentials.v0" "aahframework.org/log.v0-unstable" "aahframework.org/router.v0" - "aahframework.org/security.v0/session" + "aahframework.org/security.v0-unstable" + "aahframework.org/security.v0-unstable/session" ) var ( @@ -44,7 +45,7 @@ type ( target interface{} domain *router.Domain route *router.Route - session *session.Session + subject *security.Subject reply *Reply viewArgs map[string]interface{} values map[string]interface{} @@ -110,14 +111,19 @@ func (ctx *Context) Subdomain() string { return "" } +// Subject method the subject (aka application user) of current request. +func (ctx *Context) Subject() *security.Subject { + return ctx.subject +} + // Session method always returns `session.Session` object. Use `Session.IsNew` // to identify whether sesison is newly created or restored from the request // which was already created. func (ctx *Context) Session() *session.Session { - if ctx.session == nil { - ctx.session = AppSessionManager().NewSession() + if ctx.subject.Session == nil { + ctx.subject.Session = AppSessionManager().NewSession() } - return ctx.session + return ctx.subject.Session } // Abort method sets the abort to true. It means framework will not proceed with @@ -201,7 +207,7 @@ func (ctx *Context) Reset() { ctx.target = nil ctx.domain = nil ctx.route = nil - ctx.session = nil + ctx.subject = nil ctx.reply = nil ctx.viewArgs = nil ctx.values = nil diff --git a/context_test.go b/context_test.go index 6bc9e160..e4c13391 100644 --- a/context_test.go +++ b/context_test.go @@ -14,6 +14,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/config.v0" "aahframework.org/router.v0" + "aahframework.org/security.v0-unstable" "aahframework.org/test.v0/assert" ) @@ -131,10 +132,10 @@ func TestContextSession(t *testing.T) { err := initConfig(cfgDir) assert.Nil(t, err) - err = initSecurity(cfgDir, AppConfig()) + err = initSecurity(AppConfig()) assert.Nil(t, err) - ctx := &Context{viewArgs: make(map[string]interface{})} + ctx := &Context{viewArgs: make(map[string]interface{}), subject: &security.Subject{}} s1 := ctx.Session() assert.NotNil(t, s1) assert.True(t, s1.IsNew) diff --git a/engine.go b/engine.go index 45b3acd5..ed5f984d 100644 --- a/engine.go +++ b/engine.go @@ -19,6 +19,7 @@ import ( "aahframework.org/essentials.v0" "aahframework.org/log.v0-unstable" "aahframework.org/pool.v0" + "aahframework.org/security.v0-unstable" ) const ( @@ -59,6 +60,7 @@ type ( ctxPool *pool.Pool reqPool *pool.Pool replyPool *pool.Pool + subPool *pool.Pool } byName []os.FileInfo @@ -97,6 +99,11 @@ func (e *engine) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Load session e.loadSession(ctx) + // Authentication and Authorization + if e.handleAuthcAndAuthz(ctx) == flowStop { + return + } + // Parsing request params e.parseRequestParams(ctx) @@ -155,6 +162,7 @@ func (e *engine) prepareContext(w http.ResponseWriter, req *http.Request) *Conte ctx.Req = ahttp.ParseRequest(req, r) ctx.Res = ahttp.GetResponseWriter(w) ctx.reply = e.getReply() + ctx.subject = e.getSubject() ctx.viewArgs = make(map[string]interface{}) ctx.values = make(map[string]interface{}) @@ -199,6 +207,11 @@ func (e *engine) handleRoute(ctx *Context) flowResult { ctx.route = route ctx.domain = domain + // security form auth case + if isFormAuthLoginRoute(ctx) { + return flowCont + } + // Path parameters if pathParams.Len() > 0 { ctx.Req.Params.Path = make(map[string]string, pathParams.Len()) @@ -229,7 +242,7 @@ func (e *engine) handleRoute(ctx *Context) flowResult { // loadSession method loads session from request for `stateful` session. func (e *engine) loadSession(ctx *Context) { if AppSessionManager().IsStateful() { - ctx.session = AppSessionManager().GetSession(ctx.Req.Raw) + ctx.subject.Session = AppSessionManager().GetSession(ctx.Req.Raw) } } @@ -374,11 +387,11 @@ func (e *engine) setCookies(ctx *Context) { http.SetCookie(ctx.Res, c) } - if AppSessionManager().IsStateful() && ctx.session != nil { + if AppSessionManager().IsStateful() && ctx.subject.Session != nil { // Pass it to view args before saving cookie - session := *ctx.session + session := *ctx.subject.Session ctx.AddViewArg(keySessionValues, &session) - if err := AppSessionManager().SaveSession(ctx.Res, ctx.session); err != nil { + if err := AppSessionManager().SaveSession(ctx.Res, ctx.subject.Session); err != nil { log.Error(err) } } @@ -389,16 +402,21 @@ func (e *engine) getContext() *Context { return e.ctxPool.Get().(*Context) } -// getRequest method gets request instance from the pool +// getRequest method gets request instance from the pool. func (e *engine) getRequest() *ahttp.Request { return e.reqPool.Get().(*ahttp.Request) } -// getReply method gets reply instance from the pool +// getReply method gets reply instance from the pool. func (e *engine) getReply() *Reply { return e.replyPool.Get().(*Reply) } +// getSubject method gets subject instance from the pool. +func (e *engine) getSubject() *security.Subject { + return e.subPool.Get().(*security.Subject) +} + // putContext method puts context back to pool func (e *engine) putContext(ctx *Context) { // Close the writer and Put back to pool @@ -410,7 +428,7 @@ func (e *engine) putContext(ctx *Context) { } } - // clear and put `ahttp.Request` into pool + // clear and put `ahttp.Request` back to pool if ctx.Req != nil { ctx.Req.Reset() e.reqPool.Put(ctx.Req) @@ -423,7 +441,13 @@ func (e *engine) putContext(ctx *Context) { e.replyPool.Put(ctx.reply) } - // clear and put `aah.Context` into pool + // clear and put `Subject` back to pool + if ctx.subject != nil { + ctx.subject.Reset() + e.subPool.Put(ctx.subject) + } + + // clear and put `aah.Context` back to pool ctx.Reset() e.ctxPool.Put(ctx) } @@ -441,9 +465,7 @@ func newEngine(cfg *config.Config) *engine { if bufPool == nil { bufPool = pool.NewPool( cfg.IntDefault("runtime.pooling.buffer", defaultBufPoolSize), - func() interface{} { - return &bytes.Buffer{} - }, + func() interface{} { return &bytes.Buffer{} }, ) } @@ -454,21 +476,19 @@ func newEngine(cfg *config.Config) *engine { isAccessLogEnabled: cfg.BoolDefault("request.access_log.enable", false), ctxPool: pool.NewPool( cfg.IntDefault("runtime.pooling.global", defaultGlobalPoolSize), - func() interface{} { - return &Context{} - }, + func() interface{} { return &Context{} }, ), reqPool: pool.NewPool( cfg.IntDefault("runtime.pooling.global", defaultGlobalPoolSize), - func() interface{} { - return &ahttp.Request{} - }, + func() interface{} { return &ahttp.Request{} }, ), replyPool: pool.NewPool( cfg.IntDefault("runtime.pooling.global", defaultGlobalPoolSize), - func() interface{} { - return NewReply() - }, + func() interface{} { return NewReply() }, + ), + subPool: pool.NewPool( + cfg.IntDefault("runtime.pooling.global", defaultGlobalPoolSize), + func() interface{} { return &security.Subject{} }, ), } } diff --git a/engine_test.go b/engine_test.go index c91ac09a..9cbf3bf4 100644 --- a/engine_test.go +++ b/engine_test.go @@ -108,7 +108,7 @@ func TestEngineServeHTTP(t *testing.T) { assert.NotNil(t, AppRouter()) // Security - err = initSecurity(cfgDir, AppConfig()) + err = initSecurity(AppConfig()) assert.Nil(t, err) assert.True(t, AppSessionManager().IsStateful()) diff --git a/server_test.go b/server_test.go index 95a2f774..26c820a6 100644 --- a/server_test.go +++ b/server_test.go @@ -50,7 +50,7 @@ func TestServerStart2(t *testing.T) { assert.NotNil(t, AppRouter()) // Security - err = initSecurity(cfgDir, AppConfig()) + err = initSecurity(AppConfig()) assert.Nil(t, err) // i18n From dbaac4dcc1bcb892abcfe43ec52325d7b1bd3e09 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Mon, 3 Jul 2017 16:05:44 -0700 Subject: [PATCH 14/38] router refernce update to dev and test case fix --- context.go | 2 +- context_test.go | 2 +- controller.go | 2 +- controller_test.go | 2 +- router.go | 2 +- security.go | 2 +- security_test.go | 2 +- static_test.go | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/context.go b/context.go index 1faba625..baa83ff0 100644 --- a/context.go +++ b/context.go @@ -13,7 +13,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/essentials.v0" "aahframework.org/log.v0-unstable" - "aahframework.org/router.v0" + "aahframework.org/router.v0-unstable" "aahframework.org/security.v0-unstable" "aahframework.org/security.v0-unstable/session" ) diff --git a/context_test.go b/context_test.go index e4c13391..e25013d3 100644 --- a/context_test.go +++ b/context_test.go @@ -13,7 +13,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/config.v0" - "aahframework.org/router.v0" + "aahframework.org/router.v0-unstable" "aahframework.org/security.v0-unstable" "aahframework.org/test.v0/assert" ) diff --git a/controller.go b/controller.go index bf82dc66..187208f1 100644 --- a/controller.go +++ b/controller.go @@ -10,7 +10,7 @@ import ( "strings" "aahframework.org/essentials.v0" - "aahframework.org/router.v0" + "aahframework.org/router.v0-unstable" ) const ( diff --git a/controller_test.go b/controller_test.go index d84c33eb..451a030c 100644 --- a/controller_test.go +++ b/controller_test.go @@ -7,7 +7,7 @@ package aah import ( "testing" - "aahframework.org/router.v0" + "aahframework.org/router.v0-unstable" "aahframework.org/test.v0/assert" ) diff --git a/router.go b/router.go index 2aa21174..80eabad4 100644 --- a/router.go +++ b/router.go @@ -16,7 +16,7 @@ import ( "aahframework.org/config.v0" "aahframework.org/essentials.v0" "aahframework.org/log.v0-unstable" - "aahframework.org/router.v0" + "aahframework.org/router.v0-unstable" ) var appRouter *router.Router diff --git a/security.go b/security.go index eed6c656..2653f6fb 100644 --- a/security.go +++ b/security.go @@ -7,7 +7,7 @@ package aah import ( "fmt" - ahttp "aahframework.org/ahttp.v0" + "aahframework.org/ahttp.v0" "aahframework.org/config.v0" "aahframework.org/essentials.v0" "aahframework.org/log.v0" diff --git a/security_test.go b/security_test.go index f2e85cd7..c67dc494 100644 --- a/security_test.go +++ b/security_test.go @@ -11,7 +11,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/config.v0" - "aahframework.org/router.v0" + "aahframework.org/router.v0-unstable" "aahframework.org/security.v0-unstable" "aahframework.org/security.v0-unstable/authc" "aahframework.org/security.v0-unstable/authz" diff --git a/static_test.go b/static_test.go index 056458e5..1784d3b0 100644 --- a/static_test.go +++ b/static_test.go @@ -15,7 +15,7 @@ import ( "testing" "aahframework.org/config.v0" - "aahframework.org/router.v0" + "aahframework.org/router.v0-unstable" "aahframework.org/test.v0/assert" ) From 2c346758737243fa0aeae27a3ba68b569673e6a6 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Sat, 8 Jul 2017 11:50:44 -0700 Subject: [PATCH 15/38] #69 added object pool, access log refactered --- aah.go | 2 +- access_log.go | 79 ++++++++++++++++++++------------ access_log_test.go | 112 ++++++++++++++++++++++++++++----------------- 3 files changed, 122 insertions(+), 71 deletions(-) diff --git a/aah.go b/aah.go index e9795cb6..9549ab72 100644 --- a/aah.go +++ b/aah.go @@ -204,7 +204,7 @@ func Init(importPath string) { logAsFatal(initSecurity(AppConfig())) logAsFatal(initViewEngine(appViewsDir(), AppConfig())) if AppConfig().BoolDefault("request.access_log.enable", false) { - logAsFatal(initRequestAccessLog(appLogsDir(), AppConfig())) + logAsFatal(initAccessLog(appLogsDir(), AppConfig())) } } diff --git a/access_log.go b/access_log.go index 5b3ea668..9912679e 100644 --- a/access_log.go +++ b/access_log.go @@ -9,6 +9,7 @@ import ( "net/http" "path/filepath" "strings" + "sync" "time" "aahframework.org/ahttp.v0" @@ -46,20 +47,21 @@ var ( "restime": fmtFlagResponseTime, } - appDefaultAccessLogPattern = "%clientip %reqid %reqtime %restime %resstatus %ressize %reqmethod %requrl" - appReqStartTimeKey = "appReqStartTimeKey" + appDefaultAccessLogPattern = "%clientip %reqtime %restime %resstatus %ressize %reqmethod %requrl" + appReqStartTimeKey = "_appReqStartTimeKey" appReqIDHdrKey = ahttp.HeaderXRequestID appAccessLog *log.Logger appAccessLogFmtFlags []ess.FmtFlagPart - appAccessLogChan chan *requestAccessLog + appAccessLogChan chan *accessLog + accessLogPool = &sync.Pool{New: func() interface{} { return &accessLog{} }} ) type ( - //requestAccessLog contains data about the current request - requestAccessLog struct { + //accessLog contains data about the current request + accessLog struct { StartTime time.Time ElapsedDuration time.Duration - Request ahttp.Request + Request *ahttp.Request ResStatus int ResBytes int ResHdr http.Header @@ -67,19 +69,19 @@ type ( ) //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// requestAccessLog methods +// accessLog methods //___________________________________ // FmtRequestTime method returns the formatted request time. There are three // possibilities to handle, `%reqtime`, `%reqtime:` and `%reqtime:`. -func (al *requestAccessLog) FmtRequestTime(format string) string { +func (al *accessLog) FmtRequestTime(format string) string { if format == "%v" || ess.IsStrEmpty(format) { return al.StartTime.Format(time.RFC3339) } return al.StartTime.Format(format) } -func (al *requestAccessLog) GetRequestHdr(hdrKey string) string { +func (al *accessLog) GetRequestHdr(hdrKey string) string { hdrValues := al.Request.Header[http.CanonicalHeaderKey(hdrKey)] if len(hdrValues) == 0 { return "-" @@ -87,7 +89,7 @@ func (al *requestAccessLog) GetRequestHdr(hdrKey string) string { return strings.Join(hdrValues, ", ") } -func (al *requestAccessLog) GetResponseHdr(hdrKey string) string { +func (al *accessLog) GetResponseHdr(hdrKey string) string { hdrValues := al.ResHdr[http.CanonicalHeaderKey(hdrKey)] if len(hdrValues) == 0 { return "-" @@ -95,7 +97,7 @@ func (al *requestAccessLog) GetResponseHdr(hdrKey string) string { return strings.Join(hdrValues, ", ") } -func (al *requestAccessLog) GetQueryString() string { +func (al *accessLog) GetQueryString() string { queryStr := al.Request.Raw.URL.Query().Encode() if ess.IsStrEmpty(queryStr) { return "-" @@ -103,11 +105,20 @@ func (al *requestAccessLog) GetQueryString() string { return queryStr } +func (al *accessLog) Reset() { + al.StartTime = time.Time{} + al.ElapsedDuration = 0 + al.Request = nil + al.ResStatus = 0 + al.ResBytes = 0 + al.ResHdr = nil +} + //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ // Unexported methods //___________________________________ -func initRequestAccessLog(logsDir string, appCfg *config.Config) error { +func initAccessLog(logsDir string, appCfg *config.Config) error { // log file configuration cfg, _ := config.ParseString("") file := appCfg.StringDefault("request.access_log.file", "") @@ -138,7 +149,7 @@ func initRequestAccessLog(logsDir string, appCfg *config.Config) error { // initialize request access log channel if appAccessLogChan == nil { - appAccessLogChan = make(chan *requestAccessLog, cfg.IntDefault("runtime.pooling.global", defaultGlobalPoolSize)) + appAccessLogChan = make(chan *accessLog, cfg.IntDefault("request.access_log.channel_buffer_size", 500)) go listenForAccessLog() } @@ -149,40 +160,52 @@ func initRequestAccessLog(logsDir string, appCfg *config.Config) error { func listenForAccessLog() { for { - appAccessLog.Print(requestAccessLogFormatter(<-appAccessLogChan)) + appAccessLog.Print(accessLogFormatter(<-appAccessLogChan)) } } -func requestAccessLogFormatter(ral *requestAccessLog) string { - buf := getBuffer() - defer putBuffer(buf) +func accessLogFormatter(al *accessLog) string { + defer releaseAccessLog(al) + buf := acquireBuffer() + defer releaseBuffer(buf) for _, part := range appAccessLogFmtFlags { switch part.Flag { case fmtFlagClientIP: - buf.WriteString(ral.Request.ClientIP) + buf.WriteString(al.Request.ClientIP) case fmtFlagRequestTime: - buf.WriteString(ral.FmtRequestTime(part.Format)) + buf.WriteString(al.FmtRequestTime(part.Format)) case fmtFlagRequestURL: - buf.WriteString(ral.Request.Path) + buf.WriteString(al.Request.Path) case fmtFlagRequestMethod: - buf.WriteString(ral.Request.Method) + buf.WriteString(al.Request.Method) case fmtFlagRequestID: - buf.WriteString(ral.GetRequestHdr(appReqIDHdrKey)) + buf.WriteString(al.GetRequestHdr(appReqIDHdrKey)) case fmtFlagRequestHeader: - buf.WriteString(ral.GetRequestHdr(part.Format)) + buf.WriteString(al.GetRequestHdr(part.Format)) case fmtFlagQueryString: - buf.WriteString(ral.GetQueryString()) + buf.WriteString(al.GetQueryString()) case fmtFlagResponseStatus: - buf.WriteString(fmt.Sprintf(part.Format, ral.ResStatus)) + buf.WriteString(fmt.Sprintf(part.Format, al.ResStatus)) case fmtFlagResponseSize: - buf.WriteString(fmt.Sprintf(part.Format, ral.ResBytes)) + buf.WriteString(fmt.Sprintf(part.Format, al.ResBytes)) case fmtFlagResponseHeader: - buf.WriteString(ral.GetResponseHdr(part.Format)) + buf.WriteString(al.GetResponseHdr(part.Format)) case fmtFlagResponseTime: - buf.WriteString(fmt.Sprintf("%.4f", ral.ElapsedDuration.Seconds()*1e3)) + buf.WriteString(fmt.Sprintf("%.4f", al.ElapsedDuration.Seconds()*1e3)) } buf.WriteByte(' ') } return strings.TrimSpace(buf.String()) } + +func acquireAccessLog() *accessLog { + return accessLogPool.Get().(*accessLog) +} + +func releaseAccessLog(al *accessLog) { + if al != nil { + al.Reset() + accessLogPool.Put(al) + } +} diff --git a/access_log_test.go b/access_log_test.go index 079f99b0..0daed25a 100644 --- a/access_log_test.go +++ b/access_log_test.go @@ -5,7 +5,6 @@ package aah import ( - "bytes" "fmt" "net/http" "net/http/httptest" @@ -16,68 +15,57 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/config.v0" "aahframework.org/essentials.v0-unstable" - "aahframework.org/pool.v0" "aahframework.org/test.v0/assert" ) -func TestRequestAccessLogFormatter(t *testing.T) { - startTime := time.Now() - req := httptest.NewRequest("GET", "/oops?me=human", nil) - req.Header = make(http.Header) - - w := httptest.NewRecorder() - - ral := &requestAccessLog{ - StartTime: startTime, - ElapsedDuration: time.Now().Add(2 * time.Second).Sub(startTime), - Request: ahttp.Request{Raw: req, Header: req.Header, ClientIP: "[::1]"}, - ResStatus: 200, - ResBytes: 63, - ResHdr: w.HeaderMap, - } +func TestAccessLogFormatter(t *testing.T) { + al := createTestAccessLog() // Since we are not bootstrapping the framework's engine, // We need to manually set this - ral.Request.Path = "/oops" - ral.Request.Header.Set(ahttp.HeaderXRequestID, "5946ed129bf23409520736de") - bufPool = pool.NewPool(2, func() interface{} { return &bytes.Buffer{} }) + al.Request.Path = "/oops" + al.Request.Header.Set(ahttp.HeaderXRequestID, "5946ed129bf23409520736de") // Testing for the default access log pattern first - expectedDefaultFormat := fmt.Sprintf("%s %s %v %v %d %d %s %s", - "[::1]", "5946ed129bf23409520736de", ral.StartTime.Format(time.RFC3339), - fmt.Sprintf("%.4f", ral.ElapsedDuration.Seconds()*1e3), ral.ResStatus, - ral.ResBytes, ral.Request.Method, ral.Request.Path) + expectedDefaultFormat := fmt.Sprintf("%s %v %v %d %d %s %s", + "[::1]", al.StartTime.Format(time.RFC3339), + fmt.Sprintf("%.4f", al.ElapsedDuration.Seconds()*1e3), al.ResStatus, + al.ResBytes, al.Request.Method, al.Request.Path) - testFormatter(t, ral, appDefaultAccessLogPattern, expectedDefaultFormat) + testFormatter(t, al, appDefaultAccessLogPattern, expectedDefaultFormat) // Testing custom access log pattern - ral.ResHdr.Add("content-type", "application/json") + al = createTestAccessLog() + al.ResHdr.Add("content-type", "application/json") pattern := "%reqtime:2016-05-16 %reqhdr %querystr %reshdr:content-type" - expected := fmt.Sprintf("%s %s %s %s", ral.StartTime.Format("2016-05-16"), "-", "me=human", ral.ResHdr.Get("Content-Type")) + expected := fmt.Sprintf("%s %s %s %s", al.StartTime.Format("2016-05-16"), "-", "me=human", al.ResHdr.Get("Content-Type")) - testFormatter(t, ral, pattern, expected) + testFormatter(t, al, pattern, expected) // Testing all available access log pattern - ral.Request.Header = ral.Request.Raw.Header - ral.Request.Header.Add(ahttp.HeaderAccept, "text/html") - ral.Request.ClientIP = "127.0.0.1" + al = createTestAccessLog() + al.Request.Header = al.Request.Raw.Header + al.Request.Header.Add(ahttp.HeaderAccept, "text/html") + al.Request.Header.Set(ahttp.HeaderXRequestID, "5946ed129bf23409520736de") + al.Request.ClientIP = "127.0.0.1" + al.ResHdr.Add("content-type", "application/json") allAvailablePatterns := "%clientip %reqid %reqtime %restime %resstatus %ressize %reqmethod %requrl %reqhdr:accept %querystr %reshdr" expectedForAllAvailablePatterns := fmt.Sprintf("%s %s %s %v %d %d %s %s %s %s %s", - ral.Request.ClientIP, ral.Request.Header.Get(ahttp.HeaderXRequestID), - ral.StartTime.Format(time.RFC3339), fmt.Sprintf("%.4f", ral.ElapsedDuration.Seconds()*1e3), - ral.ResStatus, ral.ResBytes, ral.Request.Method, - ral.Request.Path, "text/html", "me=human", "-") + al.Request.ClientIP, al.Request.Header.Get(ahttp.HeaderXRequestID), + al.StartTime.Format(time.RFC3339), fmt.Sprintf("%.4f", al.ElapsedDuration.Seconds()*1e3), + al.ResStatus, al.ResBytes, al.Request.Method, + al.Request.Path, "text/html", "me=human", "-") - testFormatter(t, ral, allAvailablePatterns, expectedForAllAvailablePatterns) + testFormatter(t, al, allAvailablePatterns, expectedForAllAvailablePatterns) } -func TestRequestAccessLogFormatterInvalidPattern(t *testing.T) { +func TestAccessLogFormatterInvalidPattern(t *testing.T) { _, err := ess.ParseFmtFlag("%oops", accessLogFmtFlags) assert.NotNil(t, err) } -func TestRequestAccessLogInitDefault(t *testing.T) { +func TestAccessLogInitDefault(t *testing.T) { testAccessInit(t, ` request { access_log { @@ -86,9 +74,31 @@ func TestRequestAccessLogInitDefault(t *testing.T) { } } `) + + testAccessInit(t, ` + request { + access_log { + # Default value is false + enable = true + + file = "testdata/test-access.log" + } + } + `) + + testAccessInit(t, ` + request { + access_log { + # Default value is false + enable = true + + file = "/tmp/test-access.log" + } + } + `) } -func TestEngineRequestAccessLog(t *testing.T) { +func TestEngineAccessLog(t *testing.T) { // App Config cfgDir := filepath.Join(getTestdataPath(), appConfigDir()) err := initConfig(cfgDir) @@ -127,12 +137,12 @@ func TestEngineRequestAccessLog(t *testing.T) { assert.True(t, e.isAccessLogEnabled) } -func testFormatter(t *testing.T, ral *requestAccessLog, pattern, expected string) { +func testFormatter(t *testing.T, al *accessLog, pattern, expected string) { var err error appAccessLogFmtFlags, err = ess.ParseFmtFlag(pattern, accessLogFmtFlags) assert.Nil(t, err) - assert.Equal(t, expected, requestAccessLogFormatter(ral)) + assert.Equal(t, expected, accessLogFormatter(al)) } func testAccessInit(t *testing.T, cfgStr string) { @@ -145,8 +155,26 @@ func testAccessInit(t *testing.T, cfgStr string) { cfg, _ := config.ParseString(cfgStr) logsDir := filepath.Join(getTestdataPath(), appLogsDir()) - err := initRequestAccessLog(logsDir, cfg) + err := initAccessLog(logsDir, cfg) assert.Nil(t, err) assert.NotNil(t, appAccessLog) } + +func createTestAccessLog() *accessLog { + startTime := time.Now() + req := httptest.NewRequest("GET", "/oops?me=human", nil) + req.Header = http.Header{} + + w := httptest.NewRecorder() + + al := acquireAccessLog() + al.StartTime = startTime + al.ElapsedDuration = time.Now().Add(2 * time.Second).Sub(startTime) + al.Request = &ahttp.Request{Raw: req, Header: req.Header, ClientIP: "[::1]"} + al.ResStatus = 200 + al.ResBytes = 63 + al.ResHdr = w.HeaderMap + + return al +} From 9e65d3cba4bc2c43a13af9d81af0aa64d92ddf39 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Sat, 8 Jul 2017 11:52:25 -0700 Subject: [PATCH 16/38] #37 OnPreAuth and OnPostAuth server event added --- event.go | 46 ++++++++++++++++++++++++++++++++++++++++++++-- event_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/event.go b/event.go index 3477027d..8586f09c 100644 --- a/event.go +++ b/event.go @@ -20,10 +20,10 @@ const ( // EventOnStart event is fired before HTTP/Unix listener starts EventOnStart = "OnStart" - // EventOnShutdown event is fired when server recevies interrupt or kill command. + // EventOnShutdown event is fired when server recevies an interrupt or kill command. EventOnShutdown = "OnShutdown" - // EventOnRequest event is fired when server recevies incoming request. + // EventOnRequest event is fired when server recevies an incoming request. EventOnRequest = "OnRequest" // EventOnPreReply event is fired when before server writes the reply on the wire. @@ -39,6 +39,12 @@ const ( // 2) `Reply().Redirect(...)` is called. // Refer `aah.Reply.Done()` godoc for more info. EventOnAfterReply = "OnAfterReply" + + // EventOnPreAuth event is fired before server Authenticates & Authorizes an incoming request. + EventOnPreAuth = "OnPreAuth" + + // EventOnPostAuth event is fired after server Authenticates & Authorizes an incoming request. + EventOnPostAuth = "OnPostAuth" ) var ( @@ -46,6 +52,8 @@ var ( onRequestFunc EventCallbackFunc onPreReplyFunc EventCallbackFunc onAfterReplyFunc EventCallbackFunc + onPreAuthFunc EventCallbackFunc + onPostAuthFunc EventCallbackFunc ) type ( @@ -199,6 +207,28 @@ func OnAfterReply(sef EventCallbackFunc) { log.Warn("'OnAfterReply' aah server extension point is already subscribed.") } +// OnPreAuth method is to subscribe to aah application `OnPreAuth` event. +// `OnPreAuth` event pubished right before the aah server is authenticates & +// authorizes an incoming request. +func OnPreAuth(sef EventCallbackFunc) { + if onPreAuthFunc == nil { + onPreAuthFunc = sef + return + } + log.Warn("'OnPreAuth' aah server extension point is already subscribed.") +} + +// OnPostAuth method is to subscribe to aah application `OnPreAuth` event. +// `OnPostAuth` event pubished right after the aah server is authenticates & +// authorizes an incoming request. +func OnPostAuth(sef EventCallbackFunc) { + if onPostAuthFunc == nil { + onPostAuthFunc = sef + return + } + log.Warn("'OnPostAuth' aah server extension point is already subscribed.") +} + //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ // EventStore methods //___________________________________ @@ -341,6 +371,18 @@ func publishOnAfterReplyEvent(ctx *Context) { } } +func publishOnPreAuthEvent(ctx *Context) { + if onPreAuthFunc != nil { + onPreAuthFunc(&Event{Name: EventOnPreAuth, Data: ctx}) + } +} + +func publishOnPostAuthEvent(ctx *Context) { + if onPostAuthFunc != nil { + onPostAuthFunc(&Event{Name: EventOnPostAuth, Data: ctx}) + } +} + // funcEqual method to compare to function callback interface data. In effect // comparing the pointers of the indirect layer. Read more about the // representation of functions here: http://golang.org/s/go11func diff --git a/event_test.go b/event_test.go index f5b70fe7..f37e755b 100644 --- a/event_test.go +++ b/event_test.go @@ -213,6 +213,34 @@ func TestServerExtensionEvent(t *testing.T) { OnAfterReply(func(e *Event) { t.Log("OnAfterReply event func called 2") }) + + // OnPreAuth + assert.Nil(t, onPreAuthFunc) + publishOnPreAuthEvent(&Context{}) + OnPreAuth(func(e *Event) { + t.Log("OnPreAuth event func called") + }) + assert.NotNil(t, onPreAuthFunc) + + onPreAuthFunc(&Event{Name: EventOnPreAuth, Data: "Context Data OnPreAuth"}) + publishOnPreAuthEvent(&Context{}) + OnPreAuth(func(e *Event) { + t.Log("OnPreAuth event func called 2") + }) + + // OnPostAuth + assert.Nil(t, onPostAuthFunc) + publishOnPostAuthEvent(&Context{}) + OnPostAuth(func(e *Event) { + t.Log("OnPostAuth event func called") + }) + assert.NotNil(t, onPostAuthFunc) + + onPostAuthFunc(&Event{Name: EventOnPostAuth, Data: "Context Data OnPostAuth"}) + publishOnPostAuthEvent(&Context{}) + OnPostAuth(func(e *Event) { + t.Log("OnPostAuth event func called 2") + }) } func TestSubscribeAndUnsubscribeAndPublish(t *testing.T) { From a76a2fcce24e0df84d16e76102857ec1e1421a11 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Sat, 8 Jul 2017 11:54:25 -0700 Subject: [PATCH 17/38] #37 basic, api auth scheme and server events integrated --- security.go | 75 ++++++++++++++++++++++++++++++++++++++---------- security_test.go | 8 +++--- 2 files changed, 64 insertions(+), 19 deletions(-) diff --git a/security.go b/security.go index 2653f6fb..642d4c69 100644 --- a/security.go +++ b/security.go @@ -18,8 +18,8 @@ import ( ) const ( - keySessionValues = "_aahSessionValues" - keyAuthcInfo = "_aahAuthcInfo" + keyAuthcInfo = "_aahAuthcInfo" + keySubjectValue = "_aahSubject" ) var appSecurityManager = security.New() @@ -62,7 +62,7 @@ func (e engine) handleAuthcAndAuthz(ctx *Context) flowResult { authScheme := AppSecurityManager().GetAuthScheme(ctx.route.Auth) if authScheme == nil { // If auth scheme is nil then treat it as `anonymous`. - log.Infof("Route auth scheme is nil, treating as anonymous: %v", ctx.Req.Path) + log.Tracef("Route auth scheme is nil, treat it as anonymous: %v", ctx.Req.Path) return flowCont } @@ -70,9 +70,9 @@ func (e engine) handleAuthcAndAuthz(ctx *Context) flowResult { switch authScheme.Scheme() { case "form": return e.doFormAuthcAndAuthz(authScheme, ctx) + default: + return e.doAuthcAndAuthz(authScheme, ctx) } - - return flowCont } // doFormAuthcAndAuthz method does Form Authentication and Authorization. @@ -106,8 +106,9 @@ func (e *engine) doFormAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowR return flowStop } + publishOnPreAuthEvent(ctx) + // Do Authentication - // TODO publish pre auth server event authcInfo, err := formAuth.DoAuthenticate(formAuth.ExtractAuthenticationToken(ctx.Req)) if err == authc.ErrAuthenticationFailed { log.Infof("Authentication is failed, sending to login failure URL") @@ -124,7 +125,7 @@ func (e *engine) doFormAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowR ctx.Subject().AuthenticationInfo.Credential = nil ctx.Session().Set(keyAuthcInfo, ctx.Subject().AuthenticationInfo) - // TODO publish post auth server event + publishOnPostAuthEvent(ctx) rt := ctx.Req.Raw.FormValue("_rt") if formAuth.IsAlwaysToDefaultTarget || ess.IsStrEmpty(rt) { @@ -138,6 +139,37 @@ func (e *engine) doFormAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowR return flowStop } +// doAuthcAndAuthz method does Authentication and Authorization. +func (e *engine) doAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowResult { + publishOnPreAuthEvent(ctx) + + // Do Authentication + authcInfo, err := ascheme.DoAuthenticate(ascheme.ExtractAuthenticationToken(ctx.Req)) + if err == authc.ErrAuthenticationFailed { + log.Infof("Authentication is failed") + ctx.Reply().Unauthorized() + + if ascheme.Scheme() == "basic" { + basicAuth := ascheme.(*scheme.BasicAuth) + ctx.Reply().Header(ahttp.HeaderWWWAuthenticate, `Basic realm="`+basicAuth.RealmName+`"`) + } + // TODO write response based on Content type + e.writeReply(ctx) + return flowStop + } + + ctx.Subject().AuthenticationInfo = authcInfo + ctx.Subject().AuthorizationInfo = ascheme.DoAuthorizationInfo(authcInfo) + ctx.Session().IsAuthenticated = true + + // Remove the credential + ctx.Subject().AuthenticationInfo.Credential = nil + + publishOnPostAuthEvent(ctx) + + return flowCont +} + //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ // Unexported methods //___________________________________ @@ -171,9 +203,11 @@ func isFormAuthLoginRoute(ctx *Context) bool { // tmplSessionValue method returns session value for the given key. If session // object unavailable this method returns nil. func tmplSessionValue(viewArgs map[string]interface{}, key string) interface{} { - if sv, found := viewArgs[keySessionValues]; found { - value := sv.(*session.Session).Get(key) - return sanatizeValue(value) + if sub := getSubjectFromViewArgs(viewArgs); sub != nil { + if sub.Session != nil { + value := sub.Session.Get(key) + return sanatizeValue(value) + } } return nil } @@ -181,17 +215,28 @@ func tmplSessionValue(viewArgs map[string]interface{}, key string) interface{} { // tmplFlashValue method returns session value for the given key. If session // object unavailable this method returns nil. func tmplFlashValue(viewArgs map[string]interface{}, key string) interface{} { - if sv, found := viewArgs[keySessionValues]; found { - value := sv.(*session.Session).GetFlash(key) - return sanatizeValue(value) + if sub := getSubjectFromViewArgs(viewArgs); sub != nil { + if sub.Session != nil { + value := sub.Session.GetFlash(key) + return sanatizeValue(value) + } } return nil } // tmplIsAuthenticated method returns the value of `Session.IsAuthenticated`. func tmplIsAuthenticated(viewArgs map[string]interface{}) interface{} { - if sv, found := viewArgs[keySessionValues]; found { - return sv.(*session.Session).IsAuthenticated + if sub := getSubjectFromViewArgs(viewArgs); sub != nil { + if sub.Session != nil { + return sub.Session.IsAuthenticated + } } return false } + +func getSubjectFromViewArgs(viewArgs map[string]interface{}) *security.Subject { + if sv, found := viewArgs[keySubjectValue]; found { + return sv.(*security.Subject) + } + return nil +} diff --git a/security_test.go b/security_test.go index c67dc494..96346bdb 100644 --- a/security_test.go +++ b/security_test.go @@ -33,7 +33,7 @@ func TestSecuritySessionStore(t *testing.T) { func TestSecuritySessionTemplateFuns(t *testing.T) { viewArgs := make(map[string]interface{}) - assert.Nil(t, viewArgs[keySessionValues]) + assert.Nil(t, viewArgs[keySubjectValue]) bv1 := tmplSessionValue(viewArgs, "my-testvalue") assert.Nil(t, bv1) @@ -45,8 +45,8 @@ func TestSecuritySessionTemplateFuns(t *testing.T) { session.Set("my-testvalue", 38458473684763) session.SetFlash("my-flashvalue", "user not found") - viewArgs[keySessionValues] = session - assert.NotNil(t, viewArgs[keySessionValues]) + viewArgs[keySubjectValue] = &security.Subject{Session: session} + assert.NotNil(t, viewArgs[keySubjectValue]) v1 := tmplSessionValue(viewArgs, "my-testvalue") assert.Equal(t, 38458473684763, v1) @@ -57,7 +57,7 @@ func TestSecuritySessionTemplateFuns(t *testing.T) { v3 := tmplIsAuthenticated(viewArgs) assert.False(t, v3) - delete(viewArgs, keySessionValues) + delete(viewArgs, keySubjectValue) v4 := tmplIsAuthenticated(viewArgs) assert.False(t, v4) } From fce7cc29d0b5db12502c5a49133b8df6b04546da Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Sat, 8 Jul 2017 11:55:43 -0700 Subject: [PATCH 18/38] code improvements around memory pooling --- context.go | 4 +- engine.go | 177 ++++++++++++++++--------------------------------- engine_test.go | 12 ++-- render.go | 2 +- reply.go | 35 +++++++++- static.go | 2 + view.go | 3 +- view_test.go | 2 +- 8 files changed, 103 insertions(+), 134 deletions(-) diff --git a/context.go b/context.go index baa83ff0..95aaef9f 100644 --- a/context.go +++ b/context.go @@ -209,8 +209,8 @@ func (ctx *Context) Reset() { ctx.route = nil ctx.subject = nil ctx.reply = nil - ctx.viewArgs = nil - ctx.values = nil + ctx.viewArgs = make(map[string]interface{}) + ctx.values = make(map[string]interface{}) ctx.abort = false ctx.decorated = false } diff --git a/engine.go b/engine.go index ed5f984d..67b085db 100644 --- a/engine.go +++ b/engine.go @@ -5,12 +5,11 @@ package aah import ( - "bytes" "errors" "fmt" "io" "net/http" - "os" + "sync" "time" "aahframework.org/ahttp.v0" @@ -18,7 +17,6 @@ import ( "aahframework.org/config.v0" "aahframework.org/essentials.v0" "aahframework.org/log.v0-unstable" - "aahframework.org/pool.v0" "aahframework.org/security.v0-unstable" ) @@ -28,18 +26,18 @@ const ( ) const ( - aahServerName = "aah-go-server" - gzipContentEncoding = "gzip" - hstsHeaderValue = "max-age=31536000; includeSubDomains" - defaultGlobalPoolSize = 500 - defaultBufPoolSize = 200 + aahServerName = "aah-go-server" + gzipContentEncoding = "gzip" + hstsHeaderValue = "max-age=31536000; includeSubDomains" ) var ( + errFileNotFound = errors.New("file not found") + minifier MinifierFunc - bufPool *pool.Pool - errFileNotFound = errors.New("file not found") noGzipStatusCodes = []int{http.StatusNotModified, http.StatusNoContent} + ctxPool *sync.Pool + reqPool *sync.Pool ) type ( @@ -57,13 +55,7 @@ type ( requestIDHeader string isGzipEnabled bool isAccessLogEnabled bool - ctxPool *pool.Pool - reqPool *pool.Pool - replyPool *pool.Pool - subPool *pool.Pool } - - byName []os.FileInfo ) //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ @@ -78,7 +70,7 @@ func (e *engine) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := e.prepareContext(w, r) ctx.values[appReqStartTimeKey] = startTime - defer e.putContext(ctx) + defer releaseContext(ctx) // Recovery handling, capture every possible panic(s) defer e.handleRecovery(ctx) @@ -124,8 +116,8 @@ func (e *engine) handleRecovery(ctx *Context) { log.Errorf("Internal Server Error on %s", ctx.Req.Path) st := aruntime.NewStacktrace(r, AppConfig()) - buf := getBuffer() - defer putBuffer(buf) + buf := acquireBuffer() + defer releaseBuffer(buf) st.Print(buf) log.Error(buf.String()) @@ -158,14 +150,11 @@ func (e *engine) setRequestID(ctx *Context) { // prepareContext method gets controller, request from pool, set the targeted // controller, parses the request and returns the controller. func (e *engine) prepareContext(w http.ResponseWriter, req *http.Request) *Context { - ctx, r := e.getContext(), e.getRequest() + ctx, r := acquireContext(), acquireRequest() ctx.Req = ahttp.ParseRequest(req, r) ctx.Res = ahttp.GetResponseWriter(w) - ctx.reply = e.getReply() - ctx.subject = e.getSubject() - ctx.viewArgs = make(map[string]interface{}) - ctx.values = make(map[string]interface{}) - + ctx.reply = acquireReply() + ctx.subject = security.AcquireSubject() return ctx } @@ -321,21 +310,20 @@ func (e *engine) writeReply(ctx *Context) { // Send data to access log channel if e.isAccessLogEnabled { - startTime := ctx.values[appReqStartTimeKey].(time.Time) + al := acquireAccessLog() + al.StartTime = ctx.values[appReqStartTimeKey].(time.Time) // All the bytes have been written on the wire // so calculate elapsed time - elapsedDuration := time.Since(startTime) - ral := &requestAccessLog{ - StartTime: startTime, - ElapsedDuration: elapsedDuration, - Request: *ctx.Req, - ResStatus: ctx.Res.Status(), - ResBytes: ctx.Res.BytesWritten(), - ResHdr: ctx.Res.Header(), - } + al.ElapsedDuration = time.Since(al.StartTime) + + req := *ctx.Req + al.Request = &req + al.ResStatus = ctx.Res.Status() + al.ResBytes = ctx.Res.BytesWritten() + al.ResHdr = ctx.Res.Header() - appAccessLogChan <- ral + appAccessLogChan <- al } } @@ -388,37 +376,39 @@ func (e *engine) setCookies(ctx *Context) { } if AppSessionManager().IsStateful() && ctx.subject.Session != nil { - // Pass it to view args before saving cookie - session := *ctx.subject.Session - ctx.AddViewArg(keySessionValues, &session) if err := AppSessionManager().SaveSession(ctx.Res, ctx.subject.Session); err != nil { log.Error(err) } } } -// getContext method gets context instance from the pool -func (e *engine) getContext() *Context { - return e.ctxPool.Get().(*Context) -} +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// Unexported methods +//___________________________________ + +func newEngine(cfg *config.Config) *engine { + ahttp.GzipLevel = cfg.IntDefault("render.gzip.level", 5) + if !(ahttp.GzipLevel >= 1 && ahttp.GzipLevel <= 9) { + logAsFatal(fmt.Errorf("'render.gzip.level' is not a valid level value: %v", ahttp.GzipLevel)) + } -// getRequest method gets request instance from the pool. -func (e *engine) getRequest() *ahttp.Request { - return e.reqPool.Get().(*ahttp.Request) + return &engine{ + isRequestIDEnabled: cfg.BoolDefault("request.id.enable", true), + requestIDHeader: cfg.StringDefault("request.id.header", ahttp.HeaderXRequestID), + isGzipEnabled: cfg.BoolDefault("render.gzip.enable", true), + isAccessLogEnabled: cfg.BoolDefault("request.access_log.enable", false), + } } -// getReply method gets reply instance from the pool. -func (e *engine) getReply() *Reply { - return e.replyPool.Get().(*Reply) +func acquireContext() *Context { + return ctxPool.Get().(*Context) } -// getSubject method gets subject instance from the pool. -func (e *engine) getSubject() *security.Subject { - return e.subPool.Get().(*security.Subject) +func acquireRequest() *ahttp.Request { + return reqPool.Get().(*ahttp.Request) } -// putContext method puts context back to pool -func (e *engine) putContext(ctx *Context) { +func releaseContext(ctx *Context) { // Close the writer and Put back to pool if ctx.Res != nil { if _, ok := ctx.Res.(*ahttp.GzipResponse); ok { @@ -431,78 +421,23 @@ func (e *engine) putContext(ctx *Context) { // clear and put `ahttp.Request` back to pool if ctx.Req != nil { ctx.Req.Reset() - e.reqPool.Put(ctx.Req) + reqPool.Put(ctx.Req) } - // clear and put `Reply` into pool - if ctx.reply != nil { - putBuffer(ctx.reply.body) - ctx.reply.Reset() - e.replyPool.Put(ctx.reply) - } - - // clear and put `Subject` back to pool - if ctx.subject != nil { - ctx.subject.Reset() - e.subPool.Put(ctx.subject) - } + releaseReply(ctx.reply) + security.ReleaseSubject(ctx.subject) - // clear and put `aah.Context` back to pool ctx.Reset() - e.ctxPool.Put(ctx) + ctxPool.Put(ctx) } -//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Unexported methods -//___________________________________ - -func newEngine(cfg *config.Config) *engine { - ahttp.GzipLevel = cfg.IntDefault("render.gzip.level", 5) - if !(ahttp.GzipLevel >= 1 && ahttp.GzipLevel <= 9) { - logAsFatal(fmt.Errorf("'render.gzip.level' is not a valid level value: %v", ahttp.GzipLevel)) - } - - if bufPool == nil { - bufPool = pool.NewPool( - cfg.IntDefault("runtime.pooling.buffer", defaultBufPoolSize), - func() interface{} { return &bytes.Buffer{} }, - ) - } - - return &engine{ - isRequestIDEnabled: cfg.BoolDefault("request.id.enable", true), - requestIDHeader: cfg.StringDefault("request.id.header", ahttp.HeaderXRequestID), - isGzipEnabled: cfg.BoolDefault("render.gzip.enable", true), - isAccessLogEnabled: cfg.BoolDefault("request.access_log.enable", false), - ctxPool: pool.NewPool( - cfg.IntDefault("runtime.pooling.global", defaultGlobalPoolSize), - func() interface{} { return &Context{} }, - ), - reqPool: pool.NewPool( - cfg.IntDefault("runtime.pooling.global", defaultGlobalPoolSize), - func() interface{} { return &ahttp.Request{} }, - ), - replyPool: pool.NewPool( - cfg.IntDefault("runtime.pooling.global", defaultGlobalPoolSize), - func() interface{} { return NewReply() }, - ), - subPool: pool.NewPool( - cfg.IntDefault("runtime.pooling.global", defaultGlobalPoolSize), - func() interface{} { return &security.Subject{} }, - ), - } -} - -// getBuffer method gets buffer from pool -func getBuffer() *bytes.Buffer { - return bufPool.Get().(*bytes.Buffer) -} +func init() { + ctxPool = &sync.Pool{New: func() interface{} { + return &Context{ + viewArgs: make(map[string]interface{}), + values: make(map[string]interface{}), + } + }} -// putBPool puts buffer into pool -func putBuffer(b *bytes.Buffer) { - if b == nil { - return - } - b.Reset() - bufPool.Put(b) + reqPool = &sync.Pool{New: func() interface{} { return &ahttp.Request{} }} } diff --git a/engine_test.go b/engine_test.go index 9cbf3bf4..f85f0130 100644 --- a/engine_test.go +++ b/engine_test.go @@ -77,20 +77,18 @@ func TestEngineNew(t *testing.T) { assert.Equal(t, "X-Test-Request-Id", e.requestIDHeader) assert.True(t, e.isRequestIDEnabled) assert.True(t, e.isGzipEnabled) - assert.NotNil(t, e.ctxPool) - assert.NotNil(t, e.reqPool) - req := e.getRequest() - ctx := e.getContext() + req := acquireRequest() + ctx := acquireContext() ctx.Req = req assert.NotNil(t, ctx) assert.NotNil(t, req) assert.NotNil(t, ctx.Req) - e.putContext(ctx) + releaseContext(ctx) - buf := getBuffer() + buf := acquireBuffer() assert.NotNil(t, buf) - putBuffer(buf) + releaseBuffer(buf) } func TestEngineServeHTTP(t *testing.T) { diff --git a/render.go b/render.go index 8715909f..d8c1fdfb 100644 --- a/render.go +++ b/render.go @@ -200,7 +200,7 @@ func (h *HTML) Render(w io.Writer) error { func (e *engine) doRender(ctx *Context) { if ctx.Reply().Rdr != nil { reply := ctx.Reply() - reply.body = getBuffer() + reply.body = acquireBuffer() if jsonp, ok := reply.Rdr.(*JSON); ok && jsonp.IsJSONP { if ess.IsStrEmpty(jsonp.Callback) { jsonp.Callback = ctx.Req.QueryValue("callback") diff --git a/reply.go b/reply.go index 5e5a8572..c87d521c 100644 --- a/reply.go +++ b/reply.go @@ -9,11 +9,17 @@ import ( "io" "net/http" "strings" + "sync" "aahframework.org/ahttp.v0" "aahframework.org/essentials.v0" ) +var ( + bufPool = &sync.Pool{New: func() interface{} { return &bytes.Buffer{} }} + replyPool = &sync.Pool{New: func() interface{} { return NewReply() }} +) + // Reply gives you control and convenient way to write a response effectively. type Reply struct { Code int @@ -361,7 +367,7 @@ func (r *Reply) Body() *bytes.Buffer { return r.body } -// Reset method resets the values into initialized state. +// Reset method resets the instance values for repurpose. func (r *Reply) Reset() { r.Code = http.StatusOK r.ContType = "" @@ -374,3 +380,30 @@ func (r *Reply) Reset() { r.done = false r.gzip = true } + +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// Unexported methods +//___________________________________ + +func acquireReply() *Reply { + return replyPool.Get().(*Reply) +} + +func releaseReply(r *Reply) { + if r != nil { + releaseBuffer(r.body) + r.Reset() + replyPool.Put(r) + } +} + +func acquireBuffer() *bytes.Buffer { + return bufPool.Get().(*bytes.Buffer) +} + +func releaseBuffer(b *bytes.Buffer) { + if b != nil { + b.Reset() + bufPool.Put(b) + } +} diff --git a/static.go b/static.go index 376de5d2..8531894a 100644 --- a/static.go +++ b/static.go @@ -35,6 +35,8 @@ var ( errSeeker = errors.New("static: seeker can't seek") ) +type byName []os.FileInfo + // serveStatic method static file/directory delivery. func (e *engine) serveStatic(ctx *Context) error { // TODO static assets Dynamic minify for JS and CSS for non-dev profile diff --git a/view.go b/view.go index 390e0efd..a737020d 100644 --- a/view.go +++ b/view.go @@ -14,7 +14,7 @@ import ( "aahframework.org/config.v0" "aahframework.org/essentials.v0" "aahframework.org/log.v0-unstable" - "aahframework.org/view.v0" + "aahframework.org/view.v0-unstable" ) var ( @@ -136,6 +136,7 @@ func (e *engine) resolveView(ctx *Context) { htmlRdr.ViewArgs["AahVersion"] = Version htmlRdr.ViewArgs["EnvProfile"] = AppProfile() htmlRdr.ViewArgs["AppBuildInfo"] = AppBuildInfo() + htmlRdr.ViewArgs[keySubjectValue] = ctx.Subject() // find view template by convention if not provided findViewTemplate(ctx) diff --git a/view_test.go b/view_test.go index 6967f4ad..f1da4b0c 100644 --- a/view_test.go +++ b/view_test.go @@ -18,7 +18,7 @@ import ( "aahframework.org/config.v0" "aahframework.org/essentials.v0" "aahframework.org/test.v0/assert" - "aahframework.org/view.v0" + "aahframework.org/view.v0-unstable" ) func TestViewInit(t *testing.T) { From 41bea4906da51c56baca007c45da6f2693436f74 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Mon, 10 Jul 2017 00:21:33 -0700 Subject: [PATCH 19/38] godoc update and comments --- aah.go | 2 +- config.go | 2 +- controller.go | 2 +- event.go | 4 ++-- i18n.go | 2 +- middleware.go | 2 +- reply.go | 2 +- router.go | 2 +- security.go | 5 ++--- server.go | 2 +- view.go | 2 +- 11 files changed, 13 insertions(+), 14 deletions(-) diff --git a/aah.go b/aah.go index 9549ab72..1fe75d81 100644 --- a/aah.go +++ b/aah.go @@ -64,7 +64,7 @@ type BuildInfo struct { } //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Global methods +// Package methods //___________________________________ // AppName method returns aah application name from app config otherwise app name diff --git a/config.go b/config.go index 6589d3c6..6990b443 100644 --- a/config.go +++ b/config.go @@ -15,7 +15,7 @@ import ( var appConfig *config.Config //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Global methods +// Package methods //___________________________________ // AppConfig method returns aah application configuration instance. diff --git a/controller.go b/controller.go index 187208f1..cfa1638e 100644 --- a/controller.go +++ b/controller.go @@ -55,7 +55,7 @@ type ( ) //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Global methods +// Package methods //___________________________________ // AddController method adds given controller into controller registory. diff --git a/event.go b/event.go index 8586f09c..da43ac7e 100644 --- a/event.go +++ b/event.go @@ -85,7 +85,7 @@ type ( ) //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Global methods +// Package methods //___________________________________ // AppEventStore method returns aah application event store. @@ -127,7 +127,7 @@ func UnsubscribeEventf(eventName string, ecf EventCallbackFunc) { } //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Global methods - Server events +// Package methods - Server events //___________________________________ // OnInit method is to subscribe to aah application `OnInit` event. `OnInit` event diff --git a/i18n.go b/i18n.go index 7a2e404f..c4a5488c 100644 --- a/i18n.go +++ b/i18n.go @@ -17,7 +17,7 @@ const keyLocale = "Locale" var appI18n *i18n.I18n //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Global methods +// Package methods //___________________________________ // AppDefaultI18nLang method returns aah application i18n default language if diff --git a/middleware.go b/middleware.go index a1e7bec0..6102ac28 100644 --- a/middleware.go +++ b/middleware.go @@ -28,7 +28,7 @@ type ( ) //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Global methods +// Package methods //___________________________________ // Middlewares method adds given middleware into middleware stack diff --git a/reply.go b/reply.go index c87d521c..084efe26 100644 --- a/reply.go +++ b/reply.go @@ -35,7 +35,7 @@ type Reply struct { } //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Global methods +// Package methods //___________________________________ // NewReply method returns the new instance on reply builder. diff --git a/router.go b/router.go index 80eabad4..5d0ee6f5 100644 --- a/router.go +++ b/router.go @@ -22,7 +22,7 @@ import ( var appRouter *router.Router //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Global methods +// Package methods //___________________________________ // AppRouter method returns aah application router instance. diff --git a/security.go b/security.go index 642d4c69..ea992f3d 100644 --- a/security.go +++ b/security.go @@ -25,7 +25,7 @@ const ( var appSecurityManager = security.New() //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Global methods +// Package methods //___________________________________ // AppSecurityManager method returns the application security instance, @@ -217,8 +217,7 @@ func tmplSessionValue(viewArgs map[string]interface{}, key string) interface{} { func tmplFlashValue(viewArgs map[string]interface{}, key string) interface{} { if sub := getSubjectFromViewArgs(viewArgs); sub != nil { if sub.Session != nil { - value := sub.Session.GetFlash(key) - return sanatizeValue(value) + return sanatizeValue(sub.Session.GetFlash(key)) } } return nil diff --git a/server.go b/server.go index 12291b15..337f3f2d 100644 --- a/server.go +++ b/server.go @@ -31,7 +31,7 @@ var ( ) //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Global methods +// Package methods //___________________________________ // AddServerTLSConfig method can be used for custom TLS config for aah server. diff --git a/view.go b/view.go index a737020d..561b395c 100644 --- a/view.go +++ b/view.go @@ -30,7 +30,7 @@ var ( ) //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Global methods +// Package methods //___________________________________ // AppViewEngine method returns aah application view Engine instance. From 3fa7dc8d512a9d07a949fb79f3a0bcebae95ff3c Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Tue, 11 Jul 2017 13:02:26 -0700 Subject: [PATCH 20/38] #69 static files delivery access log --- access_log.go | 17 +++++++++++++++++ engine.go | 15 +-------------- static.go | 10 ++++++++++ 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/access_log.go b/access_log.go index 9912679e..20213d91 100644 --- a/access_log.go +++ b/access_log.go @@ -164,6 +164,23 @@ func listenForAccessLog() { } } +func sendToAccessLog(ctx *Context) { + al := acquireAccessLog() + al.StartTime = ctx.values[appReqStartTimeKey].(time.Time) + + // All the bytes have been written on the wire + // so calculate elapsed time + al.ElapsedDuration = time.Since(al.StartTime) + + req := *ctx.Req + al.Request = &req + al.ResStatus = ctx.Res.Status() + al.ResBytes = ctx.Res.BytesWritten() + al.ResHdr = ctx.Res.Header() + + appAccessLogChan <- al +} + func accessLogFormatter(al *accessLog) string { defer releaseAccessLog(al) buf := acquireBuffer() diff --git a/engine.go b/engine.go index 67b085db..39ccafa4 100644 --- a/engine.go +++ b/engine.go @@ -310,20 +310,7 @@ func (e *engine) writeReply(ctx *Context) { // Send data to access log channel if e.isAccessLogEnabled { - al := acquireAccessLog() - al.StartTime = ctx.values[appReqStartTimeKey].(time.Time) - - // All the bytes have been written on the wire - // so calculate elapsed time - al.ElapsedDuration = time.Since(al.StartTime) - - req := *ctx.Req - al.Request = &req - al.ResStatus = ctx.Res.Status() - al.ResBytes = ctx.Res.BytesWritten() - al.ResHdr = ctx.Res.Header() - - appAccessLogChan <- al + sendToAccessLog(ctx) } } diff --git a/static.go b/static.go index 8531894a..f005799f 100644 --- a/static.go +++ b/static.go @@ -100,6 +100,11 @@ func (e *engine) serveStatic(ctx *Context) error { // 'OnAfterReply' server extension point publishOnAfterReplyEvent(ctx) + + // Send data to access log channel + if e.isAccessLogEnabled { + sendToAccessLog(ctx) + } return nil } @@ -119,6 +124,11 @@ func (e *engine) serveStatic(ctx *Context) error { // 'OnAfterReply' server extension point publishOnAfterReplyEvent(ctx) + + // Send data to access log channel + if e.isAccessLogEnabled { + sendToAccessLog(ctx) + } return nil } From 6af24085fe7df651ae43fd5bd42541fa092174f9 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Wed, 12 Jul 2017 11:03:57 -0700 Subject: [PATCH 21/38] #37 security template funcs added --- security.go | 57 ++++++++++++++++++++++++++++++++++++++++++------ security_test.go | 28 +++++++++++++++++++----- view.go | 9 ++++++-- 3 files changed, 79 insertions(+), 15 deletions(-) diff --git a/security.go b/security.go index ea992f3d..ea238ba4 100644 --- a/security.go +++ b/security.go @@ -18,8 +18,11 @@ import ( ) const ( - keyAuthcInfo = "_aahAuthcInfo" - keySubjectValue = "_aahSubject" + // KeyViewArgAuthcInfo key name is used to store `AuthenticationInfo` instance into `ViewArgs`. + KeyViewArgAuthcInfo = "_aahAuthcInfo" + + // KeyViewArgSubject key name is used to store `Subject` instance into `ViewArgs`. + KeyViewArgSubject = "_aahSubject" ) var appSecurityManager = security.New() @@ -82,8 +85,8 @@ func (e *engine) doFormAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowR // In Form authentication check session is already authentication if yes // then continue the request flow immediately. if ctx.Subject().IsAuthenticated() { - if ctx.Session().IsKeyExists(keyAuthcInfo) { - ctx.Subject().AuthenticationInfo = ctx.Session().Get(keyAuthcInfo).(*authc.AuthenticationInfo) + if ctx.Session().IsKeyExists(KeyViewArgAuthcInfo) { + ctx.Subject().AuthenticationInfo = ctx.Session().Get(KeyViewArgAuthcInfo).(*authc.AuthenticationInfo) // TODO cache for AuthorizationInfo ctx.Subject().AuthorizationInfo = formAuth.DoAuthorizationInfo(ctx.Subject().AuthenticationInfo) @@ -123,7 +126,7 @@ func (e *engine) doFormAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowR // Remove the credential ctx.Subject().AuthenticationInfo.Credential = nil - ctx.Session().Set(keyAuthcInfo, ctx.Subject().AuthenticationInfo) + ctx.Session().Set(KeyViewArgAuthcInfo, ctx.Subject().AuthenticationInfo) publishOnPostAuthEvent(ctx) @@ -224,7 +227,7 @@ func tmplFlashValue(viewArgs map[string]interface{}, key string) interface{} { } // tmplIsAuthenticated method returns the value of `Session.IsAuthenticated`. -func tmplIsAuthenticated(viewArgs map[string]interface{}) interface{} { +func tmplIsAuthenticated(viewArgs map[string]interface{}) bool { if sub := getSubjectFromViewArgs(viewArgs); sub != nil { if sub.Session != nil { return sub.Session.IsAuthenticated @@ -233,8 +236,48 @@ func tmplIsAuthenticated(viewArgs map[string]interface{}) interface{} { return false } +// tmplHasRole method returns the value of `Subject.HasRole`. +func tmplHasRole(viewArgs map[string]interface{}, role string) bool { + if sub := getSubjectFromViewArgs(viewArgs); sub != nil { + return sub.HasRole(role) + } + return false +} + +// tmplHasAllRoles method returns the value of `Subject.HasAllRoles`. +func tmplHasAllRoles(viewArgs map[string]interface{}, roles ...string) bool { + if sub := getSubjectFromViewArgs(viewArgs); sub != nil { + return sub.HasAllRoles(roles...) + } + return false +} + +// tmplHasAnyRole method returns the value of `Subject.HasAnyRole`. +func tmplHasAnyRole(viewArgs map[string]interface{}, roles ...string) bool { + if sub := getSubjectFromViewArgs(viewArgs); sub != nil { + return sub.HasAnyRole(roles...) + } + return false +} + +// tmplIsPermitted method returns the value of `Subject.IsPermitted`. +func tmplIsPermitted(viewArgs map[string]interface{}, permission string) bool { + if sub := getSubjectFromViewArgs(viewArgs); sub != nil { + return sub.IsPermitted(permission) + } + return false +} + +// tmplIsPermittedAll method returns the value of `Subject.IsPermittedAll`. +func tmplIsPermittedAll(viewArgs map[string]interface{}, permissions ...string) bool { + if sub := getSubjectFromViewArgs(viewArgs); sub != nil { + return sub.IsPermittedAll(permissions...) + } + return false +} + func getSubjectFromViewArgs(viewArgs map[string]interface{}) *security.Subject { - if sv, found := viewArgs[keySubjectValue]; found { + if sv, found := viewArgs[KeyViewArgSubject]; found { return sv.(*security.Subject) } return nil diff --git a/security_test.go b/security_test.go index 96346bdb..1df5e36a 100644 --- a/security_test.go +++ b/security_test.go @@ -33,7 +33,7 @@ func TestSecuritySessionStore(t *testing.T) { func TestSecuritySessionTemplateFuns(t *testing.T) { viewArgs := make(map[string]interface{}) - assert.Nil(t, viewArgs[keySubjectValue]) + assert.Nil(t, viewArgs[KeyViewArgSubject]) bv1 := tmplSessionValue(viewArgs, "my-testvalue") assert.Nil(t, bv1) @@ -45,8 +45,18 @@ func TestSecuritySessionTemplateFuns(t *testing.T) { session.Set("my-testvalue", 38458473684763) session.SetFlash("my-flashvalue", "user not found") - viewArgs[keySubjectValue] = &security.Subject{Session: session} - assert.NotNil(t, viewArgs[keySubjectValue]) + assert.False(t, tmplHasRole(viewArgs, "role1")) + assert.False(t, tmplHasAllRoles(viewArgs, "role1", "role2", "role3")) + assert.False(t, tmplHasAnyRole(viewArgs, "role1", "role2", "role3")) + assert.False(t, tmplIsPermitted(viewArgs, "*")) + assert.False(t, tmplIsPermittedAll(viewArgs, "news:read,write", "manage:*")) + + viewArgs[KeyViewArgSubject] = &security.Subject{ + Session: session, + AuthenticationInfo: authc.NewAuthenticationInfo(), + AuthorizationInfo: authz.NewAuthorizationInfo(), + } + assert.NotNil(t, viewArgs[KeyViewArgSubject]) v1 := tmplSessionValue(viewArgs, "my-testvalue") assert.Equal(t, 38458473684763, v1) @@ -57,7 +67,13 @@ func TestSecuritySessionTemplateFuns(t *testing.T) { v3 := tmplIsAuthenticated(viewArgs) assert.False(t, v3) - delete(viewArgs, keySubjectValue) + assert.False(t, tmplHasRole(viewArgs, "role1")) + assert.False(t, tmplHasAllRoles(viewArgs, "role1", "role2", "role3")) + assert.False(t, tmplHasAnyRole(viewArgs, "role1", "role2", "role3")) + assert.False(t, tmplIsPermitted(viewArgs, "*")) + assert.False(t, tmplIsPermittedAll(viewArgs, "news:read,write", "manage:*")) + + delete(viewArgs, KeyViewArgSubject) v4 := tmplIsAuthenticated(viewArgs) assert.False(t, v4) } @@ -135,13 +151,13 @@ func TestSecurityHandleAuthcAndAuthz(t *testing.T) { assert.Nil(t, err) r3 := httptest.NewRequest("POST", "http://localhost:8080/login", nil) ctx2.Req = ahttp.ParseRequest(r3, &ahttp.Request{}) - ctx2.Session().Set(keyAuthcInfo, testGetAuthenticationInfo()) + ctx2.Session().Set(KeyViewArgAuthcInfo, testGetAuthenticationInfo()) result4 := e.handleAuthcAndAuthz(ctx2) assert.True(t, result4 == flowCont) // form auth not authenticated and no credentials ctx2.Session().IsAuthenticated = false - delete(ctx2.Session().Values, keyAuthcInfo) + delete(ctx2.Session().Values, KeyViewArgAuthcInfo) result5 := e.handleAuthcAndAuthz(ctx2) assert.True(t, result5 == flowStop) diff --git a/view.go b/view.go index 561b395c..a4deccff 100644 --- a/view.go +++ b/view.go @@ -136,7 +136,7 @@ func (e *engine) resolveView(ctx *Context) { htmlRdr.ViewArgs["AahVersion"] = Version htmlRdr.ViewArgs["EnvProfile"] = AppProfile() htmlRdr.ViewArgs["AppBuildInfo"] = AppBuildInfo() - htmlRdr.ViewArgs[keySubjectValue] = ctx.Subject() + htmlRdr.ViewArgs[KeyViewArgSubject] = ctx.Subject() // find view template by convention if not provided findViewTemplate(ctx) @@ -235,7 +235,12 @@ func init() { "fparam": tmplFormParam, "qparam": tmplQueryParam, "session": tmplSessionValue, - "isauthenticated": tmplIsAuthenticated, "flash": tmplFlashValue, + "isauthenticated": tmplIsAuthenticated, + "hasrole": tmplHasRole, + "hasallroles": tmplHasAllRoles, + "hasanyrole": tmplHasAnyRole, + "ispermitted": tmplIsPermitted, + "ispermittedall": tmplIsPermittedAll, }) } From 19e685f6c430a50d6d643a6560868f67386014e2 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Wed, 12 Jul 2017 11:04:45 -0700 Subject: [PATCH 22/38] expose viewargs key for request params --- param.go | 13 ++++++++----- param_test.go | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/param.go b/param.go index d9be6bff..886d0137 100644 --- a/param.go +++ b/param.go @@ -14,7 +14,10 @@ import ( ) const ( - keyRequestParams = "_aahRequestParams" + // KeyViewArgRequestParams key name is used to store HTTP Request Params instance + // into `ViewArgs`. + KeyViewArgRequestParams = "_aahRequestParams" + keyOverrideI18nName = "lang" ) @@ -84,7 +87,7 @@ func (e *engine) parseRequestParams(ctx *Context) { } // All the request parameters made available to templates via funcs. - ctx.AddViewArg(keyRequestParams, ctx.Req.Params) + ctx.AddViewArg(KeyViewArgRequestParams, ctx.Req.Params) } //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ @@ -93,19 +96,19 @@ func (e *engine) parseRequestParams(ctx *Context) { // tmplPathParam method returns Request Path Param value for the given key. func tmplPathParam(viewArgs map[string]interface{}, key string) interface{} { - params := viewArgs[keyRequestParams].(*ahttp.Params) + params := viewArgs[KeyViewArgRequestParams].(*ahttp.Params) return sanatizeValue(params.PathValue(key)) } // tmplFormParam method returns Request Form value for the given key. func tmplFormParam(viewArgs map[string]interface{}, key string) interface{} { - params := viewArgs[keyRequestParams].(*ahttp.Params) + params := viewArgs[KeyViewArgRequestParams].(*ahttp.Params) return sanatizeValue(params.FormValue(key)) } // tmplQueryParam method returns Request Query String value for the given key. func tmplQueryParam(viewArgs map[string]interface{}, key string) interface{} { - params := viewArgs[keyRequestParams].(*ahttp.Params) + params := viewArgs[KeyViewArgRequestParams].(*ahttp.Params) return sanatizeValue(params.QueryValue(key)) } diff --git a/param_test.go b/param_test.go index 514000d7..9e40e1ee 100644 --- a/param_test.go +++ b/param_test.go @@ -33,7 +33,7 @@ func TestParamTemplateFuncs(t *testing.T) { aahReq1.Params.Path["userId"] = "100001" viewArgs := map[string]interface{}{} - viewArgs[keyRequestParams] = aahReq1.Params + viewArgs[KeyViewArgRequestParams] = aahReq1.Params v1 := tmplQueryParam(viewArgs, "_ref") assert.Equal(t, "true", v1) From 1e3c249f8c6ac9a41e462680123f25b241bfd308 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Sat, 15 Jul 2017 17:25:34 -0700 Subject: [PATCH 23/38] added auth scheme configured check --- security.go | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/security.go b/security.go index ea238ba4..b7f9eb84 100644 --- a/security.go +++ b/security.go @@ -64,8 +64,16 @@ func (e engine) handleAuthcAndAuthz(ctx *Context) flowResult { authScheme := AppSecurityManager().GetAuthScheme(ctx.route.Auth) if authScheme == nil { - // If auth scheme is nil then treat it as `anonymous`. - log.Tracef("Route auth scheme is nil, treat it as anonymous: %v", ctx.Req.Path) + // If one or more auth schemes are defined in `security.auth_schemes { ... }` + // and routes `auth` attribute is not defined then framework treats that route as `403 Forbidden`. + if AppSecurityManager().IsAuthSchemesConfigured() { + log.Warnf("Auth schemes is configured in security.conf, however not in defined routes `auth` config, so treat it as 403 forbidden: %v", ctx.Req.Path) + ctx.Reply().Forbidden().Text("403 Forbidden") + return flowStop + } + + // If auth scheme is not configured in security.conf then treat it as `anonymous`. + log.Tracef("Route auth scheme is not configured, so treat it as anonymous: %v", ctx.Req.Path) return flowCont } @@ -113,13 +121,15 @@ func (e *engine) doFormAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowR // Do Authentication authcInfo, err := formAuth.DoAuthenticate(formAuth.ExtractAuthenticationToken(ctx.Req)) - if err == authc.ErrAuthenticationFailed { - log.Infof("Authentication is failed, sending to login failure URL") + if err == authc.ErrAuthenticationFailed || err != nil || authcInfo == nil { + log.Info("Authentication is failed, sending to login failure URL") ctx.Reply().Redirect(formAuth.LoginFailureURL + "&_rt=" + ctx.Req.Raw.FormValue("_rt")) e.writeReply(ctx) return flowStop } + log.Info("Authentication successful") + ctx.Subject().AuthenticationInfo = authcInfo ctx.Subject().AuthorizationInfo = formAuth.DoAuthorizationInfo(authcInfo) ctx.Session().IsAuthenticated = true @@ -148,8 +158,8 @@ func (e *engine) doAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowResul // Do Authentication authcInfo, err := ascheme.DoAuthenticate(ascheme.ExtractAuthenticationToken(ctx.Req)) - if err == authc.ErrAuthenticationFailed { - log.Infof("Authentication is failed") + if err == authc.ErrAuthenticationFailed || err != nil || authcInfo == nil { + log.Info("Authentication is failed") ctx.Reply().Unauthorized() if ascheme.Scheme() == "basic" { @@ -157,10 +167,13 @@ func (e *engine) doAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowResul ctx.Reply().Header(ahttp.HeaderWWWAuthenticate, `Basic realm="`+basicAuth.RealmName+`"`) } // TODO write response based on Content type + ctx.Reply().Text("401 Unauthorized") e.writeReply(ctx) return flowStop } + log.Info("Authentication successful") + ctx.Subject().AuthenticationInfo = authcInfo ctx.Subject().AuthorizationInfo = ascheme.DoAuthorizationInfo(authcInfo) ctx.Session().IsAuthenticated = true From 649ea0fbf6afd540e00741a167077a639d805228 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Mon, 17 Jul 2017 00:12:04 -0700 Subject: [PATCH 24/38] #68 added simple cache burst option, best class defer for asset pipeline --- security_test.go | 69 +++++++++++++++++++++++++++++++++++++++++++----- static.go | 15 +++++++++-- static_test.go | 2 +- 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/security_test.go b/security_test.go index 1df5e36a..c107bc3c 100644 --- a/security_test.go +++ b/security_test.go @@ -93,7 +93,14 @@ func (tfa *testFormAuthentication) GetAuthorizationInfo(authcInfo *authc.Authent return nil } -func TestSecurityHandleAuthcAndAuthz(t *testing.T) { +func testGetAuthenticationInfo() *authc.AuthenticationInfo { + authcInfo := authc.NewAuthenticationInfo() + authcInfo.Principals = append(authcInfo.Principals, &authc.Principal{Realm: "database", Value: "jeeva", IsPrimary: true}) + authcInfo.Credential = []byte("$2y$10$2A4GsJ6SmLAMvDe8XmTam.MSkKojdobBVJfIU7GiyoM.lWt.XV3H6") // welcome123 + return authcInfo +} + +func TestSecurityHandleFormAuthcAndAuthz(t *testing.T) { e := engine{} // anonymous @@ -169,9 +176,59 @@ func TestSecurityHandleAuthcAndAuthz(t *testing.T) { assert.True(t, result6 == flowStop) } -func testGetAuthenticationInfo() *authc.AuthenticationInfo { - authcInfo := authc.NewAuthenticationInfo() - authcInfo.Principals = append(authcInfo.Principals, &authc.Principal{Realm: "database", Value: "jeeva", IsPrimary: true}) - authcInfo.Credential = []byte("$2y$10$2A4GsJ6SmLAMvDe8XmTam.MSkKojdobBVJfIU7GiyoM.lWt.XV3H6") // welcome123 - return authcInfo +type testBasicAuthentication struct { +} + +func (tba *testBasicAuthentication) Init(cfg *config.Config) error { + return nil +} + +func (tba *testBasicAuthentication) GetAuthenticationInfo(authcToken *authc.AuthenticationToken) *authc.AuthenticationInfo { + return testGetAuthenticationInfo() +} + +func (tba *testBasicAuthentication) GetAuthorizationInfo(authcInfo *authc.AuthenticationInfo) *authz.AuthorizationInfo { + return nil +} + +func TestSecurityHandleBasicAuthcAndAuthz(t *testing.T) { + e := engine{} + + // basic auth scheme + cfg, _ := config.ParseString(` + security { + auth_schemes { + # HTTP Basic Auth Scheme + basic_auth { + scheme = "basic" + authenticator = "security/Authentication" + authorizer = "security/Authorization" + } + } + } + `) + err := initSecurity(cfg) + assert.Nil(t, err) + r1 := httptest.NewRequest("GET", "http://localhost:8080/doc/v0.3/mydoc.html", nil) + w1 := httptest.NewRecorder() + ctx1 := &Context{ + Req: ahttp.ParseRequest(r1, &ahttp.Request{}), + Res: ahttp.GetResponseWriter(w1), + route: &router.Route{Auth: "basic_auth"}, + subject: &security.Subject{}, + reply: NewReply(), + } + result1 := e.handleAuthcAndAuthz(ctx1) + assert.True(t, result1 == flowStop) + + testBasicAuth := &testBasicAuthentication{} + basicAuth := AppSecurityManager().GetAuthScheme("basic_auth").(*scheme.BasicAuth) + err = basicAuth.SetAuthenticator(testBasicAuth) + assert.Nil(t, err) + err = basicAuth.SetAuthorizer(testBasicAuth) + assert.Nil(t, err) + r2 := httptest.NewRequest("GET", "http://localhost:8080/doc/v0.3/mydoc.html", nil) + ctx1.Req = ahttp.ParseRequest(r2, &ahttp.Request{}) + result2 := e.handleAuthcAndAuthz(ctx1) + assert.True(t, result2 == flowStop) } diff --git a/static.go b/static.go index f005799f..1dd58e5d 100644 --- a/static.go +++ b/static.go @@ -193,9 +193,11 @@ func checkGzipRequired(file string) bool { // Note: `ctx.route.*` values come from application routes configuration. func getHTTPDirAndFilePath(ctx *Context) (http.Dir, string) { if ctx.route.IsFile() { // this is configured value from routes.conf - return http.Dir(filepath.Join(AppBaseDir(), dirStatic)), ctx.route.File + return http.Dir(filepath.Join(AppBaseDir(), dirStatic)), + parseCacheBustPart(ctx.route.File, AppBuildInfo().Version) } - return http.Dir(filepath.Join(AppBaseDir(), ctx.route.Dir)), ctx.Req.PathValue("filepath") + return http.Dir(filepath.Join(AppBaseDir(), ctx.route.Dir)), + parseCacheBustPart(ctx.Req.PathValue("filepath"), AppBuildInfo().Version) } // detectFileContentType method to identify the static file content-type. @@ -250,6 +252,15 @@ func parseStaticMimeCacheMap(e *Event) { } } +func parseCacheBustPart(name, part string) string { + if strings.Contains(name, part) { + name = strings.Replace(name, "-"+part, "", 1) + name = strings.Replace(name, part+"-", "", 1) + return name + } + return name +} + func init() { OnStart(parseStaticMimeCacheMap) } diff --git a/static_test.go b/static_test.go index 1784d3b0..c2838e10 100644 --- a/static_test.go +++ b/static_test.go @@ -19,7 +19,7 @@ import ( "aahframework.org/test.v0/assert" ) -func TestStaticDirectoryListing(t *testing.T) { +func TestStaticFileAndDirectoryListing(t *testing.T) { appCfg, _ := config.ParseString("") e := newEngine(appCfg) From 1b6e050e31ab16a28d4a14a36a60aee89f5fe7c9 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Tue, 18 Jul 2017 11:01:26 -0700 Subject: [PATCH 25/38] memory pooling improvement --- render.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ reply.go | 25 +++++++++++++++++-------- reply_test.go | 37 +++++++++++++++++++------------------ 3 files changed, 87 insertions(+), 26 deletions(-) diff --git a/render.go b/render.go index d8c1fdfb..3415067e 100644 --- a/render.go +++ b/render.go @@ -13,11 +13,18 @@ import ( "io" "os" "path/filepath" + "sync" "aahframework.org/essentials.v0" "aahframework.org/log.v0-unstable" ) +var ( + rdrHTMLPool = &sync.Pool{New: func() interface{} { return &HTML{} }} + rdrJSONPool = &sync.Pool{New: func() interface{} { return &JSON{} }} + rdrXMLPool = &sync.Pool{New: func() interface{} { return &XML{} }} +) + type ( // Data type used for convenient data type of map[string]interface{} Data map[string]interface{} @@ -115,6 +122,12 @@ func (j *JSON) Render(w io.Writer) error { return nil } +func (j *JSON) reset() { + j.Callback = "" + j.IsJSONP = false + j.Data = nil +} + //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ // XML Render methods //___________________________________ @@ -143,6 +156,10 @@ func (x *XML) Render(w io.Writer) error { return nil } +func (x *XML) reset() { + x.Data = nil +} + //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ // File and Reader Render methods //___________________________________ @@ -195,6 +212,13 @@ func (h *HTML) Render(w io.Writer) error { return h.Template.ExecuteTemplate(w, h.Layout, h.ViewArgs) } +func (h *HTML) reset() { + h.Template = nil + h.Filename = "" + h.Layout = "" + h.ViewArgs = make(Data) +} + // doRender method renders and detects the errors earlier. Writes the // error info if any. func (e *engine) doRender(ctx *Context) { @@ -215,3 +239,30 @@ func (e *engine) doRender(ctx *Context) { } } } + +func acquireHTML() *HTML { + return rdrHTMLPool.Get().(*HTML) +} + +func acquireJSON() *JSON { + return rdrJSONPool.Get().(*JSON) +} + +func acquireXML() *XML { + return rdrXMLPool.Get().(*XML) +} + +func releaseRender(r Render) { + if r != nil { + if t, ok := r.(*JSON); ok { + t.reset() + rdrJSONPool.Put(t) + } else if t, ok := r.(*HTML); ok { + t.reset() + rdrHTMLPool.Put(t) + } else if t, ok := r.(*XML); ok { + t.reset() + rdrXMLPool.Put(t) + } + } +} diff --git a/reply.go b/reply.go index 084efe26..064a45c9 100644 --- a/reply.go +++ b/reply.go @@ -161,7 +161,9 @@ func (r *Reply) ContentType(contentType string) *Reply { // Also it sets HTTP 'Content-Type' as 'application/json; charset=utf-8'. // Response rendered pretty if 'render.pretty' is true. func (r *Reply) JSON(data interface{}) *Reply { - r.Rdr = &JSON{Data: data} + j := acquireJSON() + j.Data = data + r.Rdr = j r.ContentType(ahttp.ContentTypeJSON.Raw()) return r } @@ -172,7 +174,11 @@ func (r *Reply) JSON(data interface{}) *Reply { // Note: If `callback` param is empty and `callback` query param is exists then // query param value will be used. func (r *Reply) JSONP(data interface{}, callback string) *Reply { - r.Rdr = &JSON{Data: data, IsJSONP: true, Callback: callback} + j := acquireJSON() + j.Data = data + j.IsJSONP = true + j.Callback = callback + r.Rdr = j r.ContentType(ahttp.ContentTypeJSON.Raw()) return r } @@ -181,7 +187,9 @@ func (r *Reply) JSONP(data interface{}, callback string) *Reply { // HTTP Content-Type as 'application/xml; charset=utf-8'. // Response rendered pretty if 'render.pretty' is true. func (r *Reply) XML(data interface{}) *Reply { - r.Rdr = &XML{Data: data} + x := acquireXML() + x.Data = data + r.Rdr = x r.ContentType(ahttp.ContentTypeXML.Raw()) return r } @@ -268,11 +276,11 @@ func (r *Reply) HTMLf(filename string, data Data) *Reply { // HTMLlf method renders based on given layout, filename and data. Refer `Reply.HTML(...)` // method. func (r *Reply) HTMLlf(layout, filename string, data Data) *Reply { - r.Rdr = &HTML{ - Layout: layout, - Filename: filename, - ViewArgs: data, - } + html := acquireHTML() + html.Layout = layout + html.Filename = filename + html.ViewArgs = data + r.Rdr = html r.ContentType(ahttp.ContentTypeHTML.String()) return r } @@ -392,6 +400,7 @@ func acquireReply() *Reply { func releaseReply(r *Reply) { if r != nil { releaseBuffer(r.body) + releaseRender(r.Rdr) r.Reset() replyPool.Put(r) } diff --git a/reply_test.go b/reply_test.go index 4bf2e6a0..02da33f7 100644 --- a/reply_test.go +++ b/reply_test.go @@ -5,7 +5,6 @@ package aah import ( - "bytes" "html/template" "net/http" "os" @@ -71,7 +70,7 @@ func TestReplyStatusCodes(t *testing.T) { } func TestReplyText(t *testing.T) { - buf, re1 := getBufferAndReply() + buf, re1 := acquireBuffer(), acquireReply() re1.Text("welcome to %s %s", "aah", "framework") assert.True(t, re1.IsContentTypeSet()) @@ -91,7 +90,7 @@ func TestReplyText(t *testing.T) { } func TestReplyJSON(t *testing.T) { - buf, re1 := getBufferAndReply() + buf, re1 := acquireBuffer(), acquireReply() appConfig = getReplyRenderCfg() data := struct { @@ -132,7 +131,7 @@ func TestReplyJSON(t *testing.T) { } func TestReplyJSONP(t *testing.T) { - buf, re1 := getBufferAndReply() + buf, re1 := acquireBuffer(), acquireReply() re1.body = buf appConfig = getReplyRenderCfg() @@ -172,7 +171,7 @@ func TestReplyJSONP(t *testing.T) { } func TestReplyXML(t *testing.T) { - buf, re1 := getBufferAndReply() + buf, re1 := acquireBuffer(), acquireReply() appConfig = getReplyRenderCfg() type Sample struct { @@ -209,7 +208,7 @@ func TestReplyXML(t *testing.T) { } func TestReplyReadfrom(t *testing.T) { - buf, re1 := getBufferAndReply() + buf, re1 := acquireBuffer(), acquireReply() re1.ContentType(ahttp.ContentTypeOctetStream.Raw()). Binary([]byte(`John28
this is my street
`)) @@ -225,7 +224,7 @@ func TestReplyReadfrom(t *testing.T) { } func TestReplyFileDownload(t *testing.T) { - buf, re1 := getBufferAndReply() + buf, re1 := acquireBuffer(), acquireReply() re1.FileDownload(getReplyFilepath("file1.txt"), "sample.txt") assert.Equal(t, http.StatusOK, re1.Code) @@ -253,7 +252,7 @@ func TestReplyHTML(t *testing.T) { {{ define "body" }}

This is test body

{{ end }} ` - buf, re1 := getBufferAndReply() + buf, re1 := acquireBuffer(), acquireReply() tmpl := template.Must(template.New("test").Parse(tmplStr)) assert.NotNil(t, tmpl) @@ -285,42 +284,47 @@ func TestReplyHTML(t *testing.T) { err = re1.Rdr.Render(buf) assert.NotNil(t, err) assert.Equal(t, "template is nil", err.Error()) + releaseReply(re1) // HTMLlf - relf := NewReply() + relf := acquireReply() relf.HTMLlf("docs.html", "Filename.html", nil) assert.Equal(t, "text/html; charset=utf-8", relf.ContType) htmllf := relf.Rdr.(*HTML) assert.Equal(t, "docs.html", htmllf.Layout) assert.Equal(t, "Filename.html", htmllf.Filename) + releaseRender(htmllf) // HTMLf - ref := NewReply() + ref := acquireReply() ref.HTMLf("Filename1.html", nil) assert.Equal(t, "text/html; charset=utf-8", ref.ContType) htmlf := ref.Rdr.(*HTML) assert.True(t, ess.IsStrEmpty(htmlf.Layout)) assert.Equal(t, "Filename1.html", htmlf.Filename) + releaseRender(htmlf) } func TestReplyRedirect(t *testing.T) { - redirect1 := NewReply() + redirect1 := acquireReply() redirect1.Redirect("/go-to-see.page") assert.Equal(t, http.StatusFound, redirect1.Code) assert.True(t, redirect1.redirect) assert.Equal(t, "/go-to-see.page", redirect1.path) + releaseReply(redirect1) - redirect2 := NewReply() + redirect2 := acquireReply() redirect2.RedirectSts("/go-to-see-gone-premanent.page", http.StatusMovedPermanently) assert.Equal(t, http.StatusMovedPermanently, redirect2.Code) assert.True(t, redirect2.redirect) assert.Equal(t, "/go-to-see-gone-premanent.page", redirect2.path) + releaseReply(redirect2) } func TestReplyDone(t *testing.T) { - re1 := NewReply() + re1 := acquireReply() assert.False(t, re1.done) re1.Done() @@ -328,7 +332,7 @@ func TestReplyDone(t *testing.T) { } func TestReplyCookie(t *testing.T) { - re1 := NewReply() + re1 := acquireReply() assert.Nil(t, re1.cookies) re1.Cookie(&http.Cookie{ @@ -343,6 +347,7 @@ func TestReplyCookie(t *testing.T) { cookie := re1.cookies[0] assert.Equal(t, "aah-test-cookie", cookie.Name) + releaseReply(re1) } func getReplyRenderCfg() *config.Config { @@ -354,10 +359,6 @@ func getReplyRenderCfg() *config.Config { return cfg } -func getBufferAndReply() (*bytes.Buffer, *Reply) { - return &bytes.Buffer{}, NewReply() -} - func getReplyFilepath(name string) string { wd, _ := os.Getwd() return filepath.Join(wd, "testdata", "reply", name) From 260c6716e973146772225b22488989750d813b97 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Fri, 21 Jul 2017 23:27:31 -0700 Subject: [PATCH 26/38] #69 access log added request proto and custom flag added. default pattern update --- access_log.go | 16 ++++++++++++---- access_log_test.go | 10 +++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/access_log.go b/access_log.go index 20213d91..f903d50a 100644 --- a/access_log.go +++ b/access_log.go @@ -23,6 +23,7 @@ const ( fmtFlagRequestTime fmtFlagRequestURL fmtFlagRequestMethod + fmtFlagRequestProto fmtFlagRequestID fmtFlagRequestHeader fmtFlagQueryString @@ -30,6 +31,7 @@ const ( fmtFlagResponseSize fmtFlagResponseHeader fmtFlagResponseTime + fmtFlagCustom ) var ( @@ -38,6 +40,7 @@ var ( "reqtime": fmtFlagRequestTime, "requrl": fmtFlagRequestURL, "reqmethod": fmtFlagRequestMethod, + "reqproto": fmtFlagRequestProto, "reqid": fmtFlagRequestID, "reqhdr": fmtFlagRequestHeader, "querystr": fmtFlagQueryString, @@ -45,9 +48,10 @@ var ( "ressize": fmtFlagResponseSize, "reshdr": fmtFlagResponseHeader, "restime": fmtFlagResponseTime, + "custom": fmtFlagCustom, } - appDefaultAccessLogPattern = "%clientip %reqtime %restime %resstatus %ressize %reqmethod %requrl" + appDefaultAccessLogPattern = "%clientip %custom:- %reqtime %reqmethod %requrl %reqproto %resstatus %ressize %restime %reqhdr:referer" appReqStartTimeKey = "_appReqStartTimeKey" appReqIDHdrKey = ahttp.HeaderXRequestID appAccessLog *log.Logger @@ -86,7 +90,7 @@ func (al *accessLog) GetRequestHdr(hdrKey string) string { if len(hdrValues) == 0 { return "-" } - return strings.Join(hdrValues, ", ") + return `"` + strings.Join(hdrValues, ", ") + `"` } func (al *accessLog) GetResponseHdr(hdrKey string) string { @@ -94,7 +98,7 @@ func (al *accessLog) GetResponseHdr(hdrKey string) string { if len(hdrValues) == 0 { return "-" } - return strings.Join(hdrValues, ", ") + return `"` + strings.Join(hdrValues, ", ") + `"` } func (al *accessLog) GetQueryString() string { @@ -102,7 +106,7 @@ func (al *accessLog) GetQueryString() string { if ess.IsStrEmpty(queryStr) { return "-" } - return queryStr + return `"` + queryStr + `"` } func (al *accessLog) Reset() { @@ -196,6 +200,8 @@ func accessLogFormatter(al *accessLog) string { buf.WriteString(al.Request.Path) case fmtFlagRequestMethod: buf.WriteString(al.Request.Method) + case fmtFlagRequestProto: + buf.WriteString(al.Request.Raw.Proto) case fmtFlagRequestID: buf.WriteString(al.GetRequestHdr(appReqIDHdrKey)) case fmtFlagRequestHeader: @@ -210,6 +216,8 @@ func accessLogFormatter(al *accessLog) string { buf.WriteString(al.GetResponseHdr(part.Format)) case fmtFlagResponseTime: buf.WriteString(fmt.Sprintf("%.4f", al.ElapsedDuration.Seconds()*1e3)) + case fmtFlagCustom: + buf.WriteString(part.Format) } buf.WriteByte(' ') } diff --git a/access_log_test.go b/access_log_test.go index 0daed25a..85b3d259 100644 --- a/access_log_test.go +++ b/access_log_test.go @@ -27,10 +27,10 @@ func TestAccessLogFormatter(t *testing.T) { al.Request.Header.Set(ahttp.HeaderXRequestID, "5946ed129bf23409520736de") // Testing for the default access log pattern first - expectedDefaultFormat := fmt.Sprintf("%s %v %v %d %d %s %s", + expectedDefaultFormat := fmt.Sprintf("%v - %v %v %v %v %v %v %v %v", "[::1]", al.StartTime.Format(time.RFC3339), - fmt.Sprintf("%.4f", al.ElapsedDuration.Seconds()*1e3), al.ResStatus, - al.ResBytes, al.Request.Method, al.Request.Path) + al.Request.Method, al.Request.Path, al.Request.Raw.Proto, al.ResStatus, + al.ResBytes, fmt.Sprintf("%.4f", al.ElapsedDuration.Seconds()*1e3), "-") testFormatter(t, al, appDefaultAccessLogPattern, expectedDefaultFormat) @@ -38,7 +38,7 @@ func TestAccessLogFormatter(t *testing.T) { al = createTestAccessLog() al.ResHdr.Add("content-type", "application/json") pattern := "%reqtime:2016-05-16 %reqhdr %querystr %reshdr:content-type" - expected := fmt.Sprintf("%s %s %s %s", al.StartTime.Format("2016-05-16"), "-", "me=human", al.ResHdr.Get("Content-Type")) + expected := fmt.Sprintf(`%s %s "%s" "%s"`, al.StartTime.Format("2016-05-16"), "-", "me=human", al.ResHdr.Get("Content-Type")) testFormatter(t, al, pattern, expected) @@ -50,7 +50,7 @@ func TestAccessLogFormatter(t *testing.T) { al.Request.ClientIP = "127.0.0.1" al.ResHdr.Add("content-type", "application/json") allAvailablePatterns := "%clientip %reqid %reqtime %restime %resstatus %ressize %reqmethod %requrl %reqhdr:accept %querystr %reshdr" - expectedForAllAvailablePatterns := fmt.Sprintf("%s %s %s %v %d %d %s %s %s %s %s", + expectedForAllAvailablePatterns := fmt.Sprintf(`%s "%s" %s %v %d %d %s %s "%s" "%s" %s`, al.Request.ClientIP, al.Request.Header.Get(ahttp.HeaderXRequestID), al.StartTime.Format(time.RFC3339), fmt.Sprintf("%.4f", al.ElapsedDuration.Seconds()*1e3), al.ResStatus, al.ResBytes, al.Request.Method, From 26b82aca93c3908b100978ec8f151e290de05cc5 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Fri, 21 Jul 2017 23:28:54 -0700 Subject: [PATCH 27/38] godoc and small test update --- static.go | 2 +- static_test.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/static.go b/static.go index 1dd58e5d..2e2930e0 100644 --- a/static.go +++ b/static.go @@ -87,7 +87,7 @@ func (e *engine) serveStatic(ctx *Context) error { // apply cache header if environment profile is `prod` if appIsProfileProd { ctx.Res.Header().Set(ahttp.HeaderCacheControl, cacheHeader(contentType)) - } else { // for hot-reload + } else { // for static files hot-reload ctx.Res.Header().Set(ahttp.HeaderExpires, "0") ctx.Res.Header().Set(ahttp.HeaderCacheControl, noCacheHdrValue) } diff --git a/static_test.go b/static_test.go index c2838e10..35a972d4 100644 --- a/static_test.go +++ b/static_test.go @@ -54,6 +54,10 @@ func TestStaticMisc(t *testing.T) { directoryList(w1, r1, f) assert.Equal(t, "Error reading directory", w1.Body.String()) + + // cache bust filename parse + filename := parseCacheBustPart("aah-813e524.css", "813e524") + assert.Equal(t, "aah.css", filename) } func TestParseStaticCacheMap(t *testing.T) { From 2ea70f0d78f11f0a098ceaa8d24f4350947b35ae Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Fri, 21 Jul 2017 23:31:12 -0700 Subject: [PATCH 28/38] #37 error info based on content-type, code improvements, test update --- engine.go | 63 +++++++++--------------------------------------- engine_test.go | 4 +-- error.go | 40 ++++++++++++++++++++++++++++++ error_test.go | 35 +++++++++++++++++++++++++++ render.go | 36 ++++++++++++++++++++++++--- render_test.go | 6 +++-- reply_test.go | 23 ++++++++++++++++-- router.go | 33 +++++++++++++------------ security.go | 26 ++++++++++++-------- security_test.go | 18 +++++++++++--- util.go | 12 +++++++++ 11 files changed, 205 insertions(+), 91 deletions(-) create mode 100644 error.go create mode 100644 error_test.go diff --git a/engine.go b/engine.go index 39ccafa4..ed27321a 100644 --- a/engine.go +++ b/engine.go @@ -37,7 +37,6 @@ var ( minifier MinifierFunc noGzipStatusCodes = []int{http.StatusNotModified, http.StatusNoContent} ctxPool *sync.Pool - reqPool *sync.Pool ) type ( @@ -122,16 +121,7 @@ func (e *engine) handleRecovery(ctx *Context) { st.Print(buf) log.Error(buf.String()) - ctx.Reply().InternalServerError() - e.negotiateContentType(ctx) - if ahttp.ContentTypeJSON.IsEqual(ctx.Reply().ContType) { - ctx.Reply().JSON(Data{"code": "500", "message": "Internal Server Error"}) - } else if ahttp.ContentTypeXML.IsEqual(ctx.Reply().ContType) { - ctx.Reply().XML(Data{"code": "500", "message": "Internal Server Error"}) - } else { - ctx.Reply().Text("500 Internal Server Error") - } - + writeErrorInfo(ctx, http.StatusInternalServerError, "Internal Server Error") e.writeReply(ctx) } } @@ -149,10 +139,10 @@ func (e *engine) setRequestID(ctx *Context) { // prepareContext method gets controller, request from pool, set the targeted // controller, parses the request and returns the controller. -func (e *engine) prepareContext(w http.ResponseWriter, req *http.Request) *Context { - ctx, r := acquireContext(), acquireRequest() - ctx.Req = ahttp.ParseRequest(req, r) - ctx.Res = ahttp.GetResponseWriter(w) +func (e *engine) prepareContext(w http.ResponseWriter, r *http.Request) *Context { + ctx := acquireContext() + ctx.Req = ahttp.AcquireRequest(r) + ctx.Res = ahttp.AcquireResponseWriter(w) ctx.reply = acquireReply() ctx.subject = security.AcquireSubject() return ctx @@ -175,7 +165,7 @@ func (e *engine) prepareContext(w http.ResponseWriter, req *http.Request) *Conte func (e *engine) handleRoute(ctx *Context) flowResult { domain := AppRouter().FindDomain(ctx.Req) if domain == nil { - ctx.Reply().NotFound().Text("404 Not Found") + writeErrorInfo(ctx, http.StatusNotFound, "Not Found") e.writeReply(ctx) return flowStop } @@ -271,7 +261,11 @@ func (e *engine) writeReply(ctx *Context) { } // ContentType - e.negotiateContentType(ctx) + if !ctx.Reply().IsContentTypeSet() { + if ct := identifyContentType(ctx); ct != nil { + ctx.Reply().ContentType(ct.String()) + } + } // resolving view template e.resolveView(ctx) @@ -314,19 +308,6 @@ func (e *engine) writeReply(ctx *Context) { } } -// negotiateContentType method tries to identify if reply.ContType is empty. -// Not necessarily it will set one. -func (e *engine) negotiateContentType(ctx *Context) { - if !ctx.Reply().IsContentTypeSet() { - if !ess.IsStrEmpty(ctx.Req.AcceptContentType.Mime) && - ctx.Req.AcceptContentType.Mime != "*/*" { // based on 'Accept' Header - ctx.Reply().ContentType(ctx.Req.AcceptContentType.String()) - } else if ct := defaultContentType(); ct != nil { // as per 'render.default' in aah.conf - ctx.Reply().ContentType(ct.String()) - } - } -} - // wrapGzipWriter method writes respective header for gzip and wraps write into // gzip writer. func (e *engine) wrapGzipWriter(ctx *Context) { @@ -391,26 +372,8 @@ func acquireContext() *Context { return ctxPool.Get().(*Context) } -func acquireRequest() *ahttp.Request { - return reqPool.Get().(*ahttp.Request) -} - func releaseContext(ctx *Context) { - // Close the writer and Put back to pool - if ctx.Res != nil { - if _, ok := ctx.Res.(*ahttp.GzipResponse); ok { - ahttp.PutGzipResponseWiriter(ctx.Res) - } else { - ahttp.PutResponseWriter(ctx.Res) - } - } - - // clear and put `ahttp.Request` back to pool - if ctx.Req != nil { - ctx.Req.Reset() - reqPool.Put(ctx.Req) - } - + ahttp.ReleaseResponseWriter(ctx.Res) releaseReply(ctx.reply) security.ReleaseSubject(ctx.subject) @@ -425,6 +388,4 @@ func init() { values: make(map[string]interface{}), } }} - - reqPool = &sync.Pool{New: func() interface{} { return &ahttp.Request{} }} } diff --git a/engine_test.go b/engine_test.go index f85f0130..bbc0acf5 100644 --- a/engine_test.go +++ b/engine_test.go @@ -78,11 +78,9 @@ func TestEngineNew(t *testing.T) { assert.True(t, e.isRequestIDEnabled) assert.True(t, e.isGzipEnabled) - req := acquireRequest() ctx := acquireContext() - ctx.Req = req + ctx.Req = &ahttp.Request{} assert.NotNil(t, ctx) - assert.NotNil(t, req) assert.NotNil(t, ctx.Req) releaseContext(ctx) diff --git a/error.go b/error.go new file mode 100644 index 00000000..1de8ce34 --- /dev/null +++ b/error.go @@ -0,0 +1,40 @@ +// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) +// go-aah/aah source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package aah + +import ( + "strings" + + "aahframework.org/ahttp.v0" + "aahframework.org/essentials.v0" +) + +// writeError method writes the server error response based content type. +// It's handy internal method. +func writeErrorInfo(ctx *Context, code int, msg string) { + ct := ctx.Reply().ContType + if ess.IsStrEmpty(ct) { + if ict := identifyContentType(ctx); ict != nil { + ct = ict.Mime + } + } else if idx := strings.IndexByte(ct, ';'); idx > 0 { + ct = ct[:idx] + } + + switch ct { + case ahttp.ContentTypeJSON.Mime, ahttp.ContentTypeJSONText.Mime: + ctx.Reply().Status(code).JSON(Data{ + "code": code, + "message": msg, + }) + case ahttp.ContentTypeXML.Mime, ahttp.ContentTypeXMLText.Mime: + ctx.Reply().Status(code).XML(Data{ + "code": code, + "message": msg, + }) + default: + ctx.Reply().Status(code).Text("%d %s", code, msg) + } +} diff --git a/error_test.go b/error_test.go new file mode 100644 index 00000000..21db6a65 --- /dev/null +++ b/error_test.go @@ -0,0 +1,35 @@ +// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) +// go-aah/aah source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package aah + +import ( + "testing" + + "aahframework.org/test.v0/assert" +) + +func TestErrorWriteInfo(t *testing.T) { + ctx1 := &Context{reply: acquireReply()} + ctx1.Reply().ContentType("application/json") + writeErrorInfo(ctx1, 400, "Bad Request") + + assert.NotNil(t, ctx1.Reply().Rdr) + jsonr := ctx1.Reply().Rdr.(*JSON) + assert.NotNil(t, jsonr) + assert.NotNil(t, jsonr.Data) + assert.Equal(t, 400, jsonr.Data.(Data)["code"]) + assert.Equal(t, "Bad Request", jsonr.Data.(Data)["message"]) + + ctx2 := &Context{reply: acquireReply()} + ctx2.Reply().ContentType("application/xml") + writeErrorInfo(ctx2, 500, "Internal Server Error") + + assert.NotNil(t, ctx2.Reply().Rdr) + xmlr := ctx2.Reply().Rdr.(*XML) + assert.NotNil(t, xmlr) + assert.NotNil(t, xmlr.Data) + assert.Equal(t, 500, xmlr.Data.(Data)["code"]) + assert.Equal(t, "Internal Server Error", xmlr.Data.(Data)["message"]) +} diff --git a/render.go b/render.go index 3415067e..8e668362 100644 --- a/render.go +++ b/render.go @@ -13,6 +13,7 @@ import ( "io" "os" "path/filepath" + "strings" "sync" "aahframework.org/essentials.v0" @@ -20,9 +21,10 @@ import ( ) var ( - rdrHTMLPool = &sync.Pool{New: func() interface{} { return &HTML{} }} - rdrJSONPool = &sync.Pool{New: func() interface{} { return &JSON{} }} - rdrXMLPool = &sync.Pool{New: func() interface{} { return &XML{} }} + xmlHeaderBytes = []byte(xml.Header) + rdrHTMLPool = &sync.Pool{New: func() interface{} { return &HTML{} }} + rdrJSONPool = &sync.Pool{New: func() interface{} { return &JSON{} }} + rdrXMLPool = &sync.Pool{New: func() interface{} { return &XML{} }} ) type ( @@ -149,6 +151,10 @@ func (x *XML) Render(w io.Writer) error { return err } + if _, err = w.Write(xmlHeaderBytes); err != nil { + return err + } + if _, err = w.Write(bytes); err != nil { return err } @@ -160,6 +166,29 @@ func (x *XML) reset() { x.Data = nil } +// MarshalXML method is to marshal `aah.Data` into XML. +func (d Data) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + tokens := []xml.Token{start} + for k, v := range d { + token := xml.StartElement{Name: xml.Name{Local: strings.Title(k)}} + tokens = append(tokens, token, + xml.CharData(fmt.Sprintf("%v", v)), + xml.EndElement{Name: token.Name}) + } + + tokens = append(tokens, xml.EndElement{Name: start.Name}) + + var err error + for _, t := range tokens { + if err = e.EncodeToken(t); err != nil { + return err + } + } + + // flush to ensure tokens are written + return e.Flush() +} + //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ // File and Reader Render methods //___________________________________ @@ -233,6 +262,7 @@ func (e *engine) doRender(ctx *Context) { if err := reply.Rdr.Render(reply.body); err != nil { log.Error("Render response body error: ", err) + // TODO handle response based on content type reply.InternalServerError() reply.body.Reset() reply.body.WriteString("500 Internal Server Error\n") diff --git a/render_test.go b/render_test.go index 9c600018..21420e86 100644 --- a/render_test.go +++ b/render_test.go @@ -119,7 +119,8 @@ func TestRenderXML(t *testing.T) { xml1 := XML{Data: data} err := xml1.Render(buf) assert.FailOnError(t, err, "") - assert.Equal(t, ` + assert.Equal(t, ` + John 28
this is my street
@@ -131,7 +132,8 @@ func TestRenderXML(t *testing.T) { err = xml1.Render(buf) assert.FailOnError(t, err, "") - assert.Equal(t, `John28
this is my street
`, + assert.Equal(t, ` +John28
this is my street
`, buf.String()) } diff --git a/reply_test.go b/reply_test.go index 02da33f7..fd479c97 100644 --- a/reply_test.go +++ b/reply_test.go @@ -191,7 +191,8 @@ func TestReplyXML(t *testing.T) { err := re1.Rdr.Render(buf) assert.FailOnError(t, err, "") - assert.Equal(t, ` + assert.Equal(t, ` + John 28
this is my street
@@ -203,8 +204,26 @@ func TestReplyXML(t *testing.T) { err = re1.Rdr.Render(buf) assert.FailOnError(t, err, "") - assert.Equal(t, `John28
this is my street
`, + assert.Equal(t, ` +John28
this is my street
`, buf.String()) + + buf.Reset() + + data2 := Data{ + "Name": "John", + "Age": 28, + "Address": "this is my street", + } + + re1.Rdr.(*XML).reset() + re1.XML(data2) + assert.True(t, re1.IsContentTypeSet()) + + err = re1.Rdr.Render(buf) + assert.FailOnError(t, err, "") + assert.True(t, strings.HasPrefix(buf.String(), ``)) + re1.Rdr.(*XML).reset() } func TestReplyReadfrom(t *testing.T) { diff --git a/router.go b/router.go index 5d0ee6f5..6843a27b 100644 --- a/router.go +++ b/router.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "html/template" + "net/http" "path/filepath" "reflect" "strings" @@ -147,43 +148,43 @@ func handleRtsOptionsMna(ctx *Context, domain *router.Domain, rts bool) error { // HTTP: OPTIONS if reqMethod == ahttp.MethodOptions { if domain.AutoOptions { - if allowed := domain.Allowed(reqMethod, reqPath); !ess.IsStrEmpty(allowed) { - allowed += ", " + ahttp.MethodOptions - log.Debugf("Auto 'OPTIONS' allowed HTTP Methods: %s", allowed) - reply.Header(ahttp.HeaderAllow, allowed) - return nil - } + processAllowedMethods(reply, domain.Allowed(reqMethod, reqPath), "Auto 'OPTIONS', ") + writeErrorInfo(ctx, http.StatusOK, "'OPTIONS' allowed HTTP Methods") + return nil } } // 405 Method Not Allowed if domain.MethodNotAllowed { - if allowed := domain.Allowed(reqMethod, reqPath); !ess.IsStrEmpty(allowed) { - allowed += ", " + ahttp.MethodOptions - log.Debugf("Allowed HTTP Methods for 405 response: %s", allowed) - reply.MethodNotAllowed(). - Header(ahttp.HeaderAllow, allowed). - Text("405 Method Not Allowed") - return nil - } + processAllowedMethods(reply, domain.Allowed(reqMethod, reqPath), "405 response, ") + writeErrorInfo(ctx, http.StatusMethodNotAllowed, "Method Not Allowed") + return nil } return errors.New("route not found") } +func processAllowedMethods(reply *Reply, allowed, prefix string) { + if !ess.IsStrEmpty(allowed) { + allowed += ", " + ahttp.MethodOptions + reply.Header(ahttp.HeaderAllow, allowed) + log.Debugf("%sAllowed HTTP Methods: %s", prefix, allowed) + } +} + // handleRouteNotFound method is used for 1. route action not found, 2. route is // not found and 3. static file/directory. func handleRouteNotFound(ctx *Context, domain *router.Domain, route *router.Route) { // handle effectively to reduce heap allocation if domain.NotFoundRoute == nil { log.Warnf("Route not found: %s, isStaticRoute: false", ctx.Req.Path) - ctx.Reply().NotFound().Text("404 Not Found") + writeErrorInfo(ctx, http.StatusNotFound, "Not Found") return } log.Warnf("Route not found: %s, isStaticRoute: %v", ctx.Req.Path, route.IsStatic) if err := ctx.setTarget(route); err == errTargetNotFound { - ctx.Reply().NotFound().Text("404 Not Found") + writeErrorInfo(ctx, http.StatusNotFound, "Not Found") return } diff --git a/security.go b/security.go index b7f9eb84..39787e28 100644 --- a/security.go +++ b/security.go @@ -6,6 +6,7 @@ package aah import ( "fmt" + "net/http" "aahframework.org/ahttp.v0" "aahframework.org/config.v0" @@ -67,8 +68,9 @@ func (e engine) handleAuthcAndAuthz(ctx *Context) flowResult { // If one or more auth schemes are defined in `security.auth_schemes { ... }` // and routes `auth` attribute is not defined then framework treats that route as `403 Forbidden`. if AppSecurityManager().IsAuthSchemesConfigured() { - log.Warnf("Auth schemes is configured in security.conf, however not in defined routes `auth` config, so treat it as 403 forbidden: %v", ctx.Req.Path) - ctx.Reply().Forbidden().Text("403 Forbidden") + log.Warnf("Auth schemes are configured in security.conf, however attribute 'auth' or 'default_auth' is not defined in routes.conf, so treat it as 403 forbidden: %v", ctx.Req.Path) + writeErrorInfo(ctx, http.StatusForbidden, "Forbidden") + e.writeReply(ctx) return flowStop } @@ -95,8 +97,6 @@ func (e *engine) doFormAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowR if ctx.Subject().IsAuthenticated() { if ctx.Session().IsKeyExists(KeyViewArgAuthcInfo) { ctx.Subject().AuthenticationInfo = ctx.Session().Get(KeyViewArgAuthcInfo).(*authc.AuthenticationInfo) - - // TODO cache for AuthorizationInfo ctx.Subject().AuthorizationInfo = formAuth.DoAuthorizationInfo(ctx.Subject().AuthenticationInfo) } else { log.Warn("It seems there is an issue with session data of AuthenticationInfo") @@ -121,9 +121,16 @@ func (e *engine) doFormAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowR // Do Authentication authcInfo, err := formAuth.DoAuthenticate(formAuth.ExtractAuthenticationToken(ctx.Req)) - if err == authc.ErrAuthenticationFailed || err != nil || authcInfo == nil { + if err != nil || authcInfo == nil { log.Info("Authentication is failed, sending to login failure URL") - ctx.Reply().Redirect(formAuth.LoginFailureURL + "&_rt=" + ctx.Req.Raw.FormValue("_rt")) + + redirectURL := formAuth.LoginFailureURL + redirectTarget := ctx.Req.Raw.FormValue("_rt") + if !ess.IsStrEmpty(redirectTarget) { + redirectURL = redirectURL + "&_rt=" + redirectTarget + } + + ctx.Reply().Redirect(redirectURL) e.writeReply(ctx) return flowStop } @@ -158,16 +165,15 @@ func (e *engine) doAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowResul // Do Authentication authcInfo, err := ascheme.DoAuthenticate(ascheme.ExtractAuthenticationToken(ctx.Req)) - if err == authc.ErrAuthenticationFailed || err != nil || authcInfo == nil { + if err != nil || authcInfo == nil { log.Info("Authentication is failed") - ctx.Reply().Unauthorized() if ascheme.Scheme() == "basic" { basicAuth := ascheme.(*scheme.BasicAuth) ctx.Reply().Header(ahttp.HeaderWWWAuthenticate, `Basic realm="`+basicAuth.RealmName+`"`) } - // TODO write response based on Content type - ctx.Reply().Text("401 Unauthorized") + + writeErrorInfo(ctx, http.StatusUnauthorized, "Unauthorized") e.writeReply(ctx) return flowStop } diff --git a/security_test.go b/security_test.go index c107bc3c..daa82c77 100644 --- a/security_test.go +++ b/security_test.go @@ -81,12 +81,17 @@ func TestSecuritySessionTemplateFuns(t *testing.T) { type testFormAuthentication struct { } +var ( + _ authc.Authenticator = (*testFormAuthentication)(nil) + _ authz.Authorizer = (*testFormAuthentication)(nil) +) + func (tfa *testFormAuthentication) Init(cfg *config.Config) error { return nil } -func (tfa *testFormAuthentication) GetAuthenticationInfo(authcToken *authc.AuthenticationToken) *authc.AuthenticationInfo { - return testGetAuthenticationInfo() +func (tfa *testFormAuthentication) GetAuthenticationInfo(authcToken *authc.AuthenticationToken) (*authc.AuthenticationInfo, error) { + return testGetAuthenticationInfo(), nil } func (tfa *testFormAuthentication) GetAuthorizationInfo(authcInfo *authc.AuthenticationInfo) *authz.AuthorizationInfo { @@ -179,12 +184,17 @@ func TestSecurityHandleFormAuthcAndAuthz(t *testing.T) { type testBasicAuthentication struct { } +var ( + _ authc.Authenticator = (*testBasicAuthentication)(nil) + _ authz.Authorizer = (*testBasicAuthentication)(nil) +) + func (tba *testBasicAuthentication) Init(cfg *config.Config) error { return nil } -func (tba *testBasicAuthentication) GetAuthenticationInfo(authcToken *authc.AuthenticationToken) *authc.AuthenticationInfo { - return testGetAuthenticationInfo() +func (tba *testBasicAuthentication) GetAuthenticationInfo(authcToken *authc.AuthenticationToken) (*authc.AuthenticationInfo, error) { + return testGetAuthenticationInfo(), nil } func (tba *testBasicAuthentication) GetAuthorizationInfo(authcInfo *authc.AuthenticationInfo) *authz.AuthorizationInfo { diff --git a/util.go b/util.go index 627af462..e02caacf 100644 --- a/util.go +++ b/util.go @@ -14,6 +14,7 @@ import ( "strconv" "strings" + ahttp "aahframework.org/ahttp.v0" "aahframework.org/essentials.v0" "aahframework.org/log.v0-unstable" ) @@ -106,3 +107,14 @@ func firstNonEmpty(values ...string) string { } return "" } + +func identifyContentType(ctx *Context) *ahttp.ContentType { + // based on 'Accept' Header + if !ess.IsStrEmpty(ctx.Req.AcceptContentType.Mime) && + ctx.Req.AcceptContentType.Mime != "*/*" { + return ctx.Req.AcceptContentType + } + + // as per 'render.default' in aah.conf or nil + return defaultContentType() +} From 348c3569b965578394daa70aa047cbabf5e280de Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Mon, 24 Jul 2017 00:17:44 -0700 Subject: [PATCH 29/38] use unwrap method of request instaed of raw --- context.go | 4 ++-- engine.go | 7 ++++--- middleware.go | 2 +- param.go | 2 +- router.go | 6 +++--- security.go | 6 +++--- static.go | 2 +- 7 files changed, 15 insertions(+), 14 deletions(-) diff --git a/context.go b/context.go index 95aaef9f..88ba85a5 100644 --- a/context.go +++ b/context.go @@ -163,7 +163,7 @@ func (ctx *Context) SetURL(pathURL string) { return } - rawReq := ctx.Req.Raw + rawReq := ctx.Req.Unwrap() if !ess.IsStrEmpty(u.Host) { log.Debugf("Host have been updated from '%s' to '%s'", ctx.Req.Host, u.Host) rawReq.Host = u.Host @@ -194,7 +194,7 @@ func (ctx *Context) SetMethod(method string) { } log.Debugf("Request method have been updated from '%s' to '%s'", ctx.Req.Method, method) - ctx.Req.Raw.Method = method + ctx.Req.Unwrap().Method = method ctx.Req.Method = method } diff --git a/engine.go b/engine.go index ed27321a..70a1fc15 100644 --- a/engine.go +++ b/engine.go @@ -221,7 +221,7 @@ func (e *engine) handleRoute(ctx *Context) flowResult { // loadSession method loads session from request for `stateful` session. func (e *engine) loadSession(ctx *Context) { if AppSessionManager().IsStateful() { - ctx.subject.Session = AppSessionManager().GetSession(ctx.Req.Raw) + ctx.subject.Session = AppSessionManager().GetSession(ctx.Req.Unwrap()) } } @@ -256,7 +256,7 @@ func (e *engine) writeReply(ctx *Context) { if reply.redirect { // handle redirects log.Debugf("Redirecting to '%s' with status '%d'", reply.path, reply.Code) - http.Redirect(ctx.Res, ctx.Req.Raw, reply.path, reply.Code) + http.Redirect(ctx.Res, ctx.Req.Unwrap(), reply.path, reply.Code) return } @@ -373,9 +373,10 @@ func acquireContext() *Context { } func releaseContext(ctx *Context) { + ahttp.ReleaseRequest(ctx.Req) ahttp.ReleaseResponseWriter(ctx.Res) - releaseReply(ctx.reply) security.ReleaseSubject(ctx.subject) + releaseReply(ctx.reply) ctx.Reset() ctxPool.Put(ctx) diff --git a/middleware.go b/middleware.go index 6102ac28..a17a7726 100644 --- a/middleware.go +++ b/middleware.go @@ -64,7 +64,7 @@ func ToMiddleware(handler interface{}) MiddlewareFunc { case http.Handler: h := handler.(http.Handler) return func(ctx *Context, m *Middleware) { - h.ServeHTTP(ctx.Res, ctx.Req.Raw) + h.ServeHTTP(ctx.Res, ctx.Req.Unwrap()) m.Next(ctx) } case func(http.ResponseWriter, *http.Request): diff --git a/param.go b/param.go index 886d0137..81dda709 100644 --- a/param.go +++ b/param.go @@ -34,7 +34,7 @@ var ( // parameters (Payload, Form, Query, Multi-part) stores into context. Request // params are made available in View via template functions. func (e *engine) parseRequestParams(ctx *Context) { - req := ctx.Req.Raw + req := ctx.Req.Unwrap() if ctx.Req.Method != ahttp.MethodGet { contentType := ctx.Req.ContentType.Mime diff --git a/router.go b/router.go index 6843a27b..46d5e836 100644 --- a/router.go +++ b/router.go @@ -134,12 +134,12 @@ func handleRtsOptionsMna(ctx *Context, domain *router.Domain, rts bool) error { } if len(reqPath) > 1 && reqPath[len(reqPath)-1] == '/' { - ctx.Req.Raw.URL.Path = reqPath[:len(reqPath)-1] + ctx.Req.Unwrap().URL.Path = reqPath[:len(reqPath)-1] } else { - ctx.Req.Raw.URL.Path = reqPath + "/" + ctx.Req.Unwrap().URL.Path = reqPath + "/" } - reply.Redirect(ctx.Req.Raw.URL.String()) + reply.Redirect(ctx.Req.Unwrap().URL.String()) log.Debugf("RedirectTrailingSlash: %d, %s ==> %s", reply.Code, reqPath, reply.path) return nil } diff --git a/security.go b/security.go index 39787e28..18b0ceb3 100644 --- a/security.go +++ b/security.go @@ -110,7 +110,7 @@ func (e *engine) doFormAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowR if formAuth.LoginSubmitURL != ctx.route.Path && ctx.Req.Method != ahttp.MethodPost { loginURL := formAuth.LoginURL if formAuth.LoginURL != ctx.Req.Path { - loginURL = fmt.Sprintf("%s?_rt=%s", loginURL, ctx.Req.Raw.RequestURI) + loginURL = fmt.Sprintf("%s?_rt=%s", loginURL, ctx.Req.Unwrap().RequestURI) } ctx.Reply().Redirect(loginURL) e.writeReply(ctx) @@ -125,7 +125,7 @@ func (e *engine) doFormAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowR log.Info("Authentication is failed, sending to login failure URL") redirectURL := formAuth.LoginFailureURL - redirectTarget := ctx.Req.Raw.FormValue("_rt") + redirectTarget := ctx.Req.Unwrap().FormValue("_rt") if !ess.IsStrEmpty(redirectTarget) { redirectURL = redirectURL + "&_rt=" + redirectTarget } @@ -147,7 +147,7 @@ func (e *engine) doFormAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowR publishOnPostAuthEvent(ctx) - rt := ctx.Req.Raw.FormValue("_rt") + rt := ctx.Req.Unwrap().FormValue("_rt") if formAuth.IsAlwaysToDefaultTarget || ess.IsStrEmpty(rt) { ctx.Reply().Redirect(formAuth.DefaultTargetURL) } else { diff --git a/static.go b/static.go index 2e2930e0..a6a099c6 100644 --- a/static.go +++ b/static.go @@ -96,7 +96,7 @@ func (e *engine) serveStatic(ctx *Context) error { // 'OnPreReply' server extension point publishOnPreReplyEvent(ctx) - http.ServeContent(ctx.Res, ctx.Req.Raw, path.Base(filePath), fi.ModTime(), f) + http.ServeContent(ctx.Res, ctx.Req.Unwrap(), path.Base(filePath), fi.ModTime(), f) // 'OnAfterReply' server extension point publishOnAfterReplyEvent(ctx) From 258632eea4cb95905ec8d6a134b686c03ea75e04 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Mon, 24 Jul 2017 00:20:24 -0700 Subject: [PATCH 30/38] update lib dependency to release version --- aah.go | 2 +- aah_test.go | 2 +- access_log.go | 2 +- access_log_test.go | 2 +- config.go | 2 +- context.go | 4 ++-- context_test.go | 2 +- controller.go | 2 +- controller_test.go | 2 +- event.go | 2 +- middleware.go | 2 +- render.go | 2 +- reply_test.go | 1 - router.go | 4 ++-- security.go | 4 ---- security_test.go | 2 +- server.go | 2 +- static.go | 5 ++++- static_test.go | 2 +- util.go | 2 +- view.go | 4 ++-- view_test.go | 2 +- 22 files changed, 26 insertions(+), 28 deletions(-) diff --git a/aah.go b/aah.go index 1fe75d81..97477235 100644 --- a/aah.go +++ b/aah.go @@ -17,7 +17,7 @@ import ( "aahframework.org/aruntime.v0" "aahframework.org/config.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0-unstable" + "aahframework.org/log.v0" ) // Version no. of aah framework diff --git a/aah_test.go b/aah_test.go index 42081588..996a07b5 100644 --- a/aah_test.go +++ b/aah_test.go @@ -15,7 +15,7 @@ import ( "aahframework.org/config.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0-unstable" + "aahframework.org/log.v0" "aahframework.org/test.v0/assert" ) diff --git a/access_log.go b/access_log.go index f903d50a..a1c84790 100644 --- a/access_log.go +++ b/access_log.go @@ -14,7 +14,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/config.v0" - "aahframework.org/essentials.v0-unstable" + "aahframework.org/essentials.v0" "aahframework.org/log.v0" ) diff --git a/access_log_test.go b/access_log_test.go index 85b3d259..f02f0d0f 100644 --- a/access_log_test.go +++ b/access_log_test.go @@ -14,7 +14,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/config.v0" - "aahframework.org/essentials.v0-unstable" + "aahframework.org/essentials.v0" "aahframework.org/test.v0/assert" ) diff --git a/config.go b/config.go index 6990b443..355727e0 100644 --- a/config.go +++ b/config.go @@ -9,7 +9,7 @@ import ( "path/filepath" "aahframework.org/config.v0" - "aahframework.org/log.v0-unstable" + "aahframework.org/log.v0" ) var appConfig *config.Config diff --git a/context.go b/context.go index 88ba85a5..a59b8192 100644 --- a/context.go +++ b/context.go @@ -12,8 +12,8 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0-unstable" - "aahframework.org/router.v0-unstable" + "aahframework.org/log.v0" + "aahframework.org/router.v0" "aahframework.org/security.v0-unstable" "aahframework.org/security.v0-unstable/session" ) diff --git a/context_test.go b/context_test.go index e25013d3..e4c13391 100644 --- a/context_test.go +++ b/context_test.go @@ -13,7 +13,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/config.v0" - "aahframework.org/router.v0-unstable" + "aahframework.org/router.v0" "aahframework.org/security.v0-unstable" "aahframework.org/test.v0/assert" ) diff --git a/controller.go b/controller.go index cfa1638e..eccb5737 100644 --- a/controller.go +++ b/controller.go @@ -10,7 +10,7 @@ import ( "strings" "aahframework.org/essentials.v0" - "aahframework.org/router.v0-unstable" + "aahframework.org/router.v0" ) const ( diff --git a/controller_test.go b/controller_test.go index 451a030c..d84c33eb 100644 --- a/controller_test.go +++ b/controller_test.go @@ -7,7 +7,7 @@ package aah import ( "testing" - "aahframework.org/router.v0-unstable" + "aahframework.org/router.v0" "aahframework.org/test.v0/assert" ) diff --git a/event.go b/event.go index da43ac7e..0da5a0c9 100644 --- a/event.go +++ b/event.go @@ -10,7 +10,7 @@ import ( "sync" "aahframework.org/essentials.v0" - "aahframework.org/log.v0-unstable" + "aahframework.org/log.v0" ) const ( diff --git a/middleware.go b/middleware.go index a17a7726..3ebc7e2c 100644 --- a/middleware.go +++ b/middleware.go @@ -8,7 +8,7 @@ import ( "net/http" "reflect" - "aahframework.org/log.v0-unstable" + "aahframework.org/log.v0" ) var ( diff --git a/render.go b/render.go index 8e668362..ffde4e7d 100644 --- a/render.go +++ b/render.go @@ -17,7 +17,7 @@ import ( "sync" "aahframework.org/essentials.v0" - "aahframework.org/log.v0-unstable" + "aahframework.org/log.v0" ) var ( diff --git a/reply_test.go b/reply_test.go index fd479c97..4c10c159 100644 --- a/reply_test.go +++ b/reply_test.go @@ -353,7 +353,6 @@ func TestReplyDone(t *testing.T) { func TestReplyCookie(t *testing.T) { re1 := acquireReply() - assert.Nil(t, re1.cookies) re1.Cookie(&http.Cookie{ Name: "aah-test-cookie", Value: "This is reply cookie interface test value", diff --git a/router.go b/router.go index 46d5e836..1d9c9d70 100644 --- a/router.go +++ b/router.go @@ -16,8 +16,8 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/config.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0-unstable" - "aahframework.org/router.v0-unstable" + "aahframework.org/log.v0" + "aahframework.org/router.v0" ) var appRouter *router.Router diff --git a/security.go b/security.go index 18b0ceb3..c0fc13df 100644 --- a/security.go +++ b/security.go @@ -70,7 +70,6 @@ func (e engine) handleAuthcAndAuthz(ctx *Context) flowResult { if AppSecurityManager().IsAuthSchemesConfigured() { log.Warnf("Auth schemes are configured in security.conf, however attribute 'auth' or 'default_auth' is not defined in routes.conf, so treat it as 403 forbidden: %v", ctx.Req.Path) writeErrorInfo(ctx, http.StatusForbidden, "Forbidden") - e.writeReply(ctx) return flowStop } @@ -113,7 +112,6 @@ func (e *engine) doFormAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowR loginURL = fmt.Sprintf("%s?_rt=%s", loginURL, ctx.Req.Unwrap().RequestURI) } ctx.Reply().Redirect(loginURL) - e.writeReply(ctx) return flowStop } @@ -131,7 +129,6 @@ func (e *engine) doFormAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowR } ctx.Reply().Redirect(redirectURL) - e.writeReply(ctx) return flowStop } @@ -155,7 +152,6 @@ func (e *engine) doFormAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowR ctx.Reply().Redirect(rt) } - e.writeReply(ctx) return flowStop } diff --git a/security_test.go b/security_test.go index daa82c77..05d0335f 100644 --- a/security_test.go +++ b/security_test.go @@ -11,7 +11,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/config.v0" - "aahframework.org/router.v0-unstable" + "aahframework.org/router.v0" "aahframework.org/security.v0-unstable" "aahframework.org/security.v0-unstable/authc" "aahframework.org/security.v0-unstable/authz" diff --git a/server.go b/server.go index 337f3f2d..35b12f30 100644 --- a/server.go +++ b/server.go @@ -21,7 +21,7 @@ import ( "aahframework.org/config.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0-unstable" + "aahframework.org/log.v0" ) var ( diff --git a/static.go b/static.go index a6a099c6..9e246ee3 100644 --- a/static.go +++ b/static.go @@ -20,7 +20,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0-unstable" + "aahframework.org/log.v0" ) const ( @@ -39,6 +39,9 @@ type byName []os.FileInfo // serveStatic method static file/directory delivery. func (e *engine) serveStatic(ctx *Context) error { + // Taking control over for static file delivery + ctx.Reply().Done() + // TODO static assets Dynamic minify for JS and CSS for non-dev profile // Determine route is file or directory as per user defined diff --git a/static_test.go b/static_test.go index 35a972d4..84efb10c 100644 --- a/static_test.go +++ b/static_test.go @@ -15,7 +15,7 @@ import ( "testing" "aahframework.org/config.v0" - "aahframework.org/router.v0-unstable" + "aahframework.org/router.v0" "aahframework.org/test.v0/assert" ) diff --git a/util.go b/util.go index e02caacf..f7df4d67 100644 --- a/util.go +++ b/util.go @@ -16,7 +16,7 @@ import ( ahttp "aahframework.org/ahttp.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0-unstable" + "aahframework.org/log.v0" ) var ( diff --git a/view.go b/view.go index a4deccff..9adfc3c8 100644 --- a/view.go +++ b/view.go @@ -13,8 +13,8 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/config.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0-unstable" - "aahframework.org/view.v0-unstable" + "aahframework.org/log.v0" + "aahframework.org/view.v0" ) var ( diff --git a/view_test.go b/view_test.go index f1da4b0c..6967f4ad 100644 --- a/view_test.go +++ b/view_test.go @@ -18,7 +18,7 @@ import ( "aahframework.org/config.v0" "aahframework.org/essentials.v0" "aahframework.org/test.v0/assert" - "aahframework.org/view.v0-unstable" + "aahframework.org/view.v0" ) func TestViewInit(t *testing.T) { From 024665dd7836bb8476b391d27b70fff42188c5b9 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Mon, 24 Jul 2017 00:21:05 -0700 Subject: [PATCH 31/38] code improvement and test update --- engine.go | 32 +++++++++++++++----------------- engine_test.go | 14 +++++++++++++- param.go | 10 +++++++--- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/engine.go b/engine.go index 70a1fc15..75c42200 100644 --- a/engine.go +++ b/engine.go @@ -16,7 +16,7 @@ import ( "aahframework.org/aruntime.v0" "aahframework.org/config.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0-unstable" + "aahframework.org/log.v0" "aahframework.org/security.v0-unstable" ) @@ -84,7 +84,7 @@ func (e *engine) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Handling route if e.handleRoute(ctx) == flowStop { - return + goto wReply } // Load session @@ -92,11 +92,13 @@ func (e *engine) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Authentication and Authorization if e.handleAuthcAndAuthz(ctx) == flowStop { - return + goto wReply } // Parsing request params - e.parseRequestParams(ctx) + if e.parseRequestParams(ctx) == flowStop { + goto wReply + } // Set defaults when actual value not found e.setDefaults(ctx) @@ -104,6 +106,7 @@ func (e *engine) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Middlewares, interceptors, targeted controller e.executeMiddlewares(ctx) +wReply: // Write Reply on the wire e.writeReply(ctx) } @@ -166,20 +169,17 @@ func (e *engine) handleRoute(ctx *Context) flowResult { domain := AppRouter().FindDomain(ctx.Req) if domain == nil { writeErrorInfo(ctx, http.StatusNotFound, "Not Found") - e.writeReply(ctx) return flowStop } route, pathParams, rts := domain.Lookup(ctx.Req) if route == nil { // route not found if err := handleRtsOptionsMna(ctx, domain, rts); err == nil { - e.writeReply(ctx) return flowStop } ctx.route = domain.NotFoundRoute handleRouteNotFound(ctx, domain, domain.NotFoundRoute) - e.writeReply(ctx) return flowStop } @@ -203,7 +203,7 @@ func (e *engine) handleRoute(ctx *Context) flowResult { if route.IsStatic { if err := e.serveStatic(ctx); err == errFileNotFound { handleRouteNotFound(ctx, domain, route) - e.writeReply(ctx) + ctx.Reply().done = false // override } return flowStop } @@ -211,7 +211,6 @@ func (e *engine) handleRoute(ctx *Context) flowResult { // No controller or action found for the route if err := ctx.setTarget(route); err == errTargetNotFound { handleRouteNotFound(ctx, domain, route) - e.writeReply(ctx) return flowStop } @@ -240,11 +239,9 @@ func (e *engine) executeMiddlewares(ctx *Context) { // writeReply method writes the response on the wire based on `Reply` instance. func (e *engine) writeReply(ctx *Context) { - reply := ctx.Reply() - // Response already written on the wire, don't go forward. - // refer `ctx.Abort()` method. - if reply.done { + // refer to `Reply().Done()` method. + if ctx.Reply().done { return } @@ -254,6 +251,7 @@ func (e *engine) writeReply(ctx *Context) { // Set Cookies e.setCookies(ctx) + reply := ctx.Reply() if reply.redirect { // handle redirects log.Debugf("Redirecting to '%s' with status '%d'", reply.path, reply.Code) http.Redirect(ctx.Res, ctx.Req.Unwrap(), reply.path, reply.Code) @@ -261,9 +259,9 @@ func (e *engine) writeReply(ctx *Context) { } // ContentType - if !ctx.Reply().IsContentTypeSet() { + if !reply.IsContentTypeSet() { if ct := identifyContentType(ctx); ct != nil { - ctx.Reply().ContentType(ct.String()) + reply.ContentType(ct.String()) } } @@ -280,7 +278,7 @@ func (e *engine) writeReply(ctx *Context) { } // ContentType, if it's not set then auto detect later in the writer - if ctx.Reply().IsContentTypeSet() { + if reply.IsContentTypeSet() { ctx.Res.Header().Set(ahttp.HeaderContentType, reply.ContType) } @@ -373,8 +371,8 @@ func acquireContext() *Context { } func releaseContext(ctx *Context) { - ahttp.ReleaseRequest(ctx.Req) ahttp.ReleaseResponseWriter(ctx.Res) + ahttp.ReleaseRequest(ctx.Req) security.ReleaseSubject(ctx.subject) releaseReply(ctx.reply) diff --git a/engine_test.go b/engine_test.go index bbc0acf5..2b9b47db 100644 --- a/engine_test.go +++ b/engine_test.go @@ -16,7 +16,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/config.v0" - "aahframework.org/log.v0-unstable" + "aahframework.org/log.v0" "aahframework.org/test.v0/assert" ) @@ -225,6 +225,18 @@ func TestEngineServeHTTP(t *testing.T) { assert.Equal(t, "405 Method Not Allowed", string(body8)) assert.Equal(t, "GET, OPTIONS", resp8.Header.Get("Allow")) + // Auto Options + r9 := httptest.NewRequest("OPTIONS", "http://localhost:8080/credits", nil) + r9.Header.Add(ahttp.HeaderAcceptEncoding, "gzip, deflate, sdch, br") + w9 := httptest.NewRecorder() + e.ServeHTTP(w9, r9) + + resp9 := w9.Result() + reader9, _ := gzip.NewReader(resp9.Body) + body9, _ := ioutil.ReadAll(reader9) + assert.Equal(t, "200 'OPTIONS' allowed HTTP Methods", string(body9)) + assert.Equal(t, "GET, OPTIONS", resp9.Header.Get("Allow")) + appBaseDir = "" } diff --git a/param.go b/param.go index 81dda709..135cb91d 100644 --- a/param.go +++ b/param.go @@ -10,7 +10,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/essentials.v0" - "aahframework.org/log.v0-unstable" + "aahframework.org/log.v0" ) const ( @@ -33,7 +33,7 @@ var ( // parseRequestParams method parses the incoming HTTP request to collects request // parameters (Payload, Form, Query, Multi-part) stores into context. Request // params are made available in View via template functions. -func (e *engine) parseRequestParams(ctx *Context) { +func (e *engine) parseRequestParams(ctx *Context) flowResult { req := ctx.Req.Unwrap() if ctx.Req.Method != ahttp.MethodGet { @@ -44,11 +44,14 @@ func (e *engine) parseRequestParams(ctx *Context) { // TODO HTML sanitizer for Form and Multipart Form switch contentType { - case ahttp.ContentTypeJSON.Mime, ahttp.ContentTypeXML.Mime, ahttp.ContentTypeXMLText.Mime: + case ahttp.ContentTypeJSON.Mime, ahttp.ContentTypeJSONText.Mime, + ahttp.ContentTypeXML.Mime, ahttp.ContentTypeXMLText.Mime: if payloadBytes, err := ioutil.ReadAll(req.Body); err == nil { ctx.Req.Payload = payloadBytes } else { log.Errorf("unable to read request body for '%s': %s", contentType, err) + writeErrorInfo(ctx, http.StatusBadRequest, "unable to read request body") + return flowStop } case ahttp.ContentTypeForm.Mime: if err := req.ParseForm(); err == nil { @@ -88,6 +91,7 @@ func (e *engine) parseRequestParams(ctx *Context) { // All the request parameters made available to templates via funcs. ctx.AddViewArg(KeyViewArgRequestParams, ctx.Req.Params) + return flowCont } //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ From 40171af86c4c9b6bc86d0b0909b50f3a1d21cea8 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Mon, 24 Jul 2017 14:56:30 -0700 Subject: [PATCH 32/38] use wrapgzipwriter method and fix response write 401 --- engine.go | 2 +- security.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/engine.go b/engine.go index 75c42200..eade622a 100644 --- a/engine.go +++ b/engine.go @@ -313,7 +313,7 @@ func (e *engine) wrapGzipWriter(ctx *Context) { ctx.Res.Header().Add(ahttp.HeaderVary, ahttp.HeaderAcceptEncoding) ctx.Res.Header().Add(ahttp.HeaderContentEncoding, gzipContentEncoding) ctx.Res.Header().Del(ahttp.HeaderContentLength) - ctx.Res = ahttp.GetGzipResponseWriter(ctx.Res) + ctx.Res = ahttp.WrapGzipWriter(ctx.Res) } } diff --git a/security.go b/security.go index c0fc13df..e602888d 100644 --- a/security.go +++ b/security.go @@ -170,7 +170,6 @@ func (e *engine) doAuthcAndAuthz(ascheme scheme.Schemer, ctx *Context) flowResul } writeErrorInfo(ctx, http.StatusUnauthorized, "Unauthorized") - e.writeReply(ctx) return flowStop } From e24b7de0196e3e18a7fc7ec49175bd127cc89464 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Tue, 25 Jul 2017 10:14:24 -0700 Subject: [PATCH 33/38] improvements * apply gzip for 1kb above bytes size * improved check for body allowed by status * finally interceptor runs always if defined * less allocations --- access_log.go | 2 +- context_test.go | 3 +++ engine.go | 29 ++++++++++++++++------------- engine_test.go | 45 +++++++++++++++++++++++++++------------------ middleware.go | 6 +----- router.go | 8 ++------ static.go | 8 +++++--- util.go | 16 +++++++++------- view.go | 3 ++- 9 files changed, 66 insertions(+), 54 deletions(-) diff --git a/access_log.go b/access_log.go index a1c84790..56a52260 100644 --- a/access_log.go +++ b/access_log.go @@ -201,7 +201,7 @@ func accessLogFormatter(al *accessLog) string { case fmtFlagRequestMethod: buf.WriteString(al.Request.Method) case fmtFlagRequestProto: - buf.WriteString(al.Request.Raw.Proto) + buf.WriteString(al.Request.Unwrap().Proto) case fmtFlagRequestID: buf.WriteString(al.GetRequestHdr(appReqIDHdrKey)) case fmtFlagRequestHeader: diff --git a/context_test.go b/context_test.go index e4c13391..67ebad0a 100644 --- a/context_test.go +++ b/context_test.go @@ -120,6 +120,9 @@ func TestContextSetTarget(t *testing.T) { assert.NotNil(t, ctx.action.Parameters) assert.Equal(t, "userId", ctx.action.Parameters[0].Name) + ctx.controller.Namespace = "" + assert.Equal(t, "Level3", resolveControllerName(ctx)) + err2 := ctx.setTarget(&router.Route{Controller: "NoController"}) assert.Equal(t, errTargetNotFound, err2) diff --git a/engine.go b/engine.go index eade622a..9de655bd 100644 --- a/engine.go +++ b/engine.go @@ -33,10 +33,10 @@ const ( var ( errFileNotFound = errors.New("file not found") + ctHTML = ahttp.ContentTypeHTML - minifier MinifierFunc - noGzipStatusCodes = []int{http.StatusNotModified, http.StatusNoContent} - ctxPool *sync.Pool + minifier MinifierFunc + ctxPool *sync.Pool ) type ( @@ -82,7 +82,6 @@ func (e *engine) ServeHTTP(w http.ResponseWriter, r *http.Request) { publishOnRequestEvent(ctx) // Handling route - if e.handleRoute(ctx) == flowStop { goto wReply } @@ -272,8 +271,9 @@ func (e *engine) writeReply(ctx *Context) { // error info without messing with response on the wire. e.doRender(ctx) - // Gzip - if !isNoGzipStatusCode(reply.Code) && reply.body.Len() != 0 { + isBodyAllowed := isResponseBodyAllowed(reply.Code) + // Gzip, 1kb above TODO make it configurable for bytes size value + if isBodyAllowed && reply.body.Len() > 1024 { e.wrapGzipWriter(ctx) } @@ -288,13 +288,16 @@ func (e *engine) writeReply(ctx *Context) { // HTTP status ctx.Res.WriteHeader(reply.Code) - // Write response buffer on the wire - if minifier == nil || !appIsProfileProd || - isNoGzipStatusCode(reply.Code) || - !ahttp.ContentTypeHTML.IsEqual(reply.ContType) { - _, _ = reply.body.WriteTo(ctx.Res) - } else if err := minifier(reply.ContType, ctx.Res, reply.body); err != nil { - log.Errorf("Minifier error: %v", err) + if isBodyAllowed { + // Write response on the wire + var err error + if minifier == nil || !appIsProfileProd || !ctHTML.IsEqual(reply.ContType) { + if _, err = reply.body.WriteTo(ctx.Res); err != nil { + log.Error(err) + } + } else if err = minifier(reply.ContType, ctx.Res, reply.body); err != nil { + log.Errorf("Minifier error: %s", err.Error()) + } } // 'OnAfterReply' server extension point diff --git a/engine_test.go b/engine_test.go index 2b9b47db..f6530952 100644 --- a/engine_test.go +++ b/engine_test.go @@ -16,6 +16,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/config.v0" + "aahframework.org/essentials.v0" "aahframework.org/log.v0" "aahframework.org/test.v0/assert" ) @@ -149,11 +150,10 @@ func TestEngineServeHTTP(t *testing.T) { e.ServeHTTP(w2, r2) resp2 := w2.Result() - gr2, _ := gzip.NewReader(resp2.Body) - body2, _ := ioutil.ReadAll(gr2) + body2 := getResponseBody(resp2) assert.Equal(t, 200, resp2.StatusCode) assert.True(t, strings.Contains(resp2.Status, "OK")) - assert.Equal(t, "GetInvolved action", string(body2)) + assert.Equal(t, "GetInvolved action", body2) assert.Equal(t, "test engine middleware", resp2.Header.Get("X-Custom-Name")) // Request 3 @@ -162,10 +162,10 @@ func TestEngineServeHTTP(t *testing.T) { e.ServeHTTP(w3, r3) resp3 := w3.Result() - body3, _ := ioutil.ReadAll(resp3.Body) + body3 := getResponseBody(resp3) assert.Equal(t, 500, resp3.StatusCode) assert.True(t, strings.Contains(resp3.Status, "Internal Server Error")) - assert.True(t, strings.Contains(string(body3), "Internal Server Error")) + assert.True(t, strings.Contains(body3, "Internal Server Error")) // Request 4 static r4 := httptest.NewRequest("GET", "http://localhost:8080/assets/logo.png", nil) @@ -196,10 +196,9 @@ func TestEngineServeHTTP(t *testing.T) { e.ServeHTTP(w6, r6) resp6 := w6.Result() - body6, _ := ioutil.ReadAll(resp6.Body) - body6Str := string(body6) - assert.True(t, strings.Contains(body6Str, "Listing of /testdata/")) - assert.True(t, strings.Contains(body6Str, "config/")) + body6 := getResponseBody(resp6) + assert.True(t, strings.Contains(body6, "Listing of /testdata/")) + assert.True(t, strings.Contains(body6, "config/")) // Custom Headers r7 := httptest.NewRequest("GET", "http://localhost:8080/credits", nil) @@ -208,9 +207,8 @@ func TestEngineServeHTTP(t *testing.T) { e.ServeHTTP(w7, r7) resp7 := w7.Result() - body7, _ := ioutil.ReadAll(resp7.Body) - body7Str := string(body7) - assert.Equal(t, `{"code":1000001,"message":"This is credits page"}`, body7Str) + body7 := getResponseBody(resp7) + assert.Equal(t, `{"code":1000001,"message":"This is credits page"}`, body7) assert.Equal(t, "custom value", resp7.Header.Get("X-Custom-Header")) r8 := httptest.NewRequest("POST", "http://localhost:8080/credits", nil) @@ -220,9 +218,8 @@ func TestEngineServeHTTP(t *testing.T) { // Method Not Allowed 405 response resp8 := w8.Result() - reader8, _ := gzip.NewReader(resp8.Body) - body8, _ := ioutil.ReadAll(reader8) - assert.Equal(t, "405 Method Not Allowed", string(body8)) + body8 := getResponseBody(resp8) + assert.Equal(t, "405 Method Not Allowed", body8) assert.Equal(t, "GET, OPTIONS", resp8.Header.Get("Allow")) // Auto Options @@ -232,9 +229,8 @@ func TestEngineServeHTTP(t *testing.T) { e.ServeHTTP(w9, r9) resp9 := w9.Result() - reader9, _ := gzip.NewReader(resp9.Body) - body9, _ := ioutil.ReadAll(reader9) - assert.Equal(t, "200 'OPTIONS' allowed HTTP Methods", string(body9)) + body9 := getResponseBody(resp9) + assert.Equal(t, "200 'OPTIONS' allowed HTTP Methods", body9) assert.Equal(t, "GET, OPTIONS", resp9.Header.Get("Allow")) appBaseDir = "" @@ -252,4 +248,17 @@ func TestEngineGzipHeaders(t *testing.T) { assert.True(t, ctx.Req.IsGzipAccepted) assert.Equal(t, "gzip", ctx.Res.Header().Get(ahttp.HeaderContentEncoding)) assert.Equal(t, "Accept-Encoding", ctx.Res.Header().Get(ahttp.HeaderVary)) + assert.False(t, isResponseBodyAllowed(199)) + assert.False(t, isResponseBodyAllowed(304)) + assert.False(t, isResponseBodyAllowed(100)) +} + +func getResponseBody(res *http.Response) string { + r := res.Body + defer ess.CloseQuietly(r) + if strings.Contains(res.Header.Get("Content-Encoding"), "gzip") { + r, _ = gzip.NewReader(r) + } + body, _ := ioutil.ReadAll(r) + return string(body) } diff --git a/middleware.go b/middleware.go index 3ebc7e2c..8d3aae21 100644 --- a/middleware.go +++ b/middleware.go @@ -102,12 +102,8 @@ func interceptorMiddleware(ctx *Context, m *Middleware) { target := reflect.ValueOf(ctx.target) controller := resolveControllerName(ctx) - // Finally action and method + // Finally action and method. Always executed if present defer func() { - if ctx.abort { - return - } - if finallyActionMethod := target.MethodByName(incpFinallyActionName + ctx.action.Name); finallyActionMethod.IsValid() { log.Debugf("Calling interceptor: %s.%s", controller, incpFinallyActionName+ctx.action.Name) finallyActionMethod.Call(emptyArg) diff --git a/router.go b/router.go index 1d9c9d70..280ed188 100644 --- a/router.go +++ b/router.go @@ -206,15 +206,11 @@ func tmplURL(viewArgs map[string]interface{}, args ...interface{}) template.URL log.Errorf("router: template 'rurl' - route name is empty: %v", args) return template.URL("#") } - - host := viewArgs["Host"].(string) - routeName := args[0].(string) - return template.URL(createReverseURL(host, routeName, nil, args[1:]...)) + return template.URL(createReverseURL(viewArgs["Host"].(string), args[0].(string), nil, args[1:]...)) } // tmplURLm method returns reverse URL by given route name and // map[string]interface{}. Mapped to Go template func. func tmplURLm(viewArgs map[string]interface{}, routeName string, args map[string]interface{}) template.URL { - host := viewArgs["Host"].(string) - return template.URL(createReverseURL(host, routeName, args)) + return template.URL(createReverseURL(viewArgs["Host"].(string), routeName, args)) } diff --git a/static.go b/static.go index 9e246ee3..3c73c87e 100644 --- a/static.go +++ b/static.go @@ -76,9 +76,11 @@ func (e *engine) serveStatic(ctx *Context) error { return nil } - // Gzip - ctx.Reply().gzip = checkGzipRequired(filePath) - e.wrapGzipWriter(ctx) + // Gzip, 1kb above + if fi.Size() > 1024 { + ctx.Reply().gzip = checkGzipRequired(filePath) + e.wrapGzipWriter(ctx) + } e.writeHeaders(ctx) // Serve file diff --git a/util.go b/util.go index f7df4d67..bf2aef53 100644 --- a/util.go +++ b/util.go @@ -8,13 +8,14 @@ import ( "errors" "fmt" "io/ioutil" + "net/http" "os" "path" "path/filepath" "strconv" "strings" - ahttp "aahframework.org/ahttp.v0" + "aahframework.org/ahttp.v0" "aahframework.org/essentials.v0" "aahframework.org/log.v0" ) @@ -77,13 +78,14 @@ func getBinaryFileName() string { return ess.StripExt(AppBuildInfo().BinaryName) } -func isNoGzipStatusCode(code int) bool { - for _, c := range noGzipStatusCodes { - if c == code { - return true - } +// This method is similar to +// https://golang.org/src/net/http/transfer.go#bodyAllowedForStatus +func isResponseBodyAllowed(code int) bool { + if (code >= http.StatusContinue && code < http.StatusOK) || + code == http.StatusNoContent || code == http.StatusNotModified { + return false } - return false + return true } func resolveControllerName(ctx *Context) string { diff --git a/view.go b/view.go index 9adfc3c8..eca8453e 100644 --- a/view.go +++ b/view.go @@ -49,6 +49,7 @@ func AddViewEngine(name string, engine view.Enginer) error { } // SetMinifier method sets the given minifier func into aah framework. +// Note: currently minifier is called only for HTML contentType. func SetMinifier(fn MinifierFunc) { if minifier == nil { minifier = fn @@ -101,7 +102,7 @@ func (e *engine) resolveView(ctx *Context) { reply := ctx.Reply() // HTML response - if ahttp.ContentTypeHTML.IsEqual(reply.ContType) && appViewEngine != nil { + if ctHTML.IsEqual(reply.ContType) && appViewEngine != nil { if reply.Rdr == nil { reply.Rdr = &HTML{} } From 9be4e7bd3b74b74b10b9c615599197bd4b80bbd7 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Thu, 27 Jul 2017 15:52:25 -0700 Subject: [PATCH 34/38] hot-reload proxy port, signal handling improvement --- aah.go | 13 +++---------- engine.go | 6 +++--- server.go | 37 ++++++++++++++++++++++--------------- util.go | 12 ++++++++++++ 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/aah.go b/aah.go index 97477235..45219486 100644 --- a/aah.go +++ b/aah.go @@ -106,16 +106,9 @@ func AppHTTPAddress() string { // AppHTTPPort method returns aah application HTTP port number based on `server.port` // value. Possible outcomes are user-defined port, `80`, `443` and `8080`. func AppHTTPPort() string { - port := AppConfig().StringDefault("server.port", appDefaultHTTPPort) - if !ess.IsStrEmpty(port) { - return port - } - - if AppIsSSLEnabled() { - return "443" - } - - return "80" + port := firstNonEmpty(AppConfig().StringDefault("server.proxyport", ""), + AppConfig().StringDefault("server.port", appDefaultHTTPPort)) + return parsePort(port) } // AppDateFormat method returns aah application date format diff --git a/engine.go b/engine.go index 9de655bd..0cb0b92d 100644 --- a/engine.go +++ b/engine.go @@ -244,6 +244,9 @@ func (e *engine) writeReply(ctx *Context) { return } + // 'OnPreReply' server extension point + publishOnPreReplyEvent(ctx) + // HTTP headers e.writeHeaders(ctx) @@ -282,9 +285,6 @@ func (e *engine) writeReply(ctx *Context) { ctx.Res.Header().Set(ahttp.HeaderContentType, reply.ContType) } - // 'OnPreReply' server extension point - publishOnPreReplyEvent(ctx) - // HTTP status ctx.Res.WriteHeader(reply.Code) diff --git a/server.go b/server.go index 35b12f30..fc18ab21 100644 --- a/server.go +++ b/server.go @@ -124,6 +124,9 @@ func Shutdown() { graceTimeout, _ := time.ParseDuration(graceTime) ctx, cancel := context.WithTimeout(context.Background(), graceTimeout) + defer cancel() + + log.Trace("aah go server shutdown with timeout: ", graceTime) if err := aahServer.Shutdown(ctx); err != nil && err != http.ErrServerClosed { log.Error(err) } @@ -131,10 +134,9 @@ func Shutdown() { // Publish `OnShutdown` event AppEventStore().sortAndPublishSync(&Event{Name: EventOnShutdown}) - // Exit normally - cancel() log.Infof("'%v' application stopped", AppName()) - os.Exit(0) + + // bye bye } //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ @@ -185,14 +187,14 @@ func startHTTPS() { aahServer.TLSConfig.NextProtos = append(aahServer.TLSConfig.NextProtos, "h2") } - log.Infof("aah go server running on %v", aahServer.Addr) + printStartupNote() if err := aahServer.ListenAndServeTLS(appSSLCert, appSSLKey); err != nil && err != http.ErrServerClosed { log.Error(err) } } func startHTTP() { - log.Infof("aah go server running on %v", aahServer.Addr) + printStartupNote() if err := aahServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Error(err) } @@ -200,17 +202,17 @@ func startHTTP() { // listenSignals method listens to OS signals for aah server Shutdown. func listenSignals() { - sc := make(chan os.Signal, 2) + sc := make(chan os.Signal, 1) signal.Notify(sc, os.Interrupt, syscall.SIGTERM) - go func() { - switch <-sc { - case os.Interrupt: - log.Warn("Interrupt signal received") - case syscall.SIGTERM: - log.Warn("Termination signal received") - } - Shutdown() - }() + + sig := <-sc + switch sig { + case os.Interrupt: + log.Warn("Interrupt signal received") + case syscall.SIGTERM: + log.Warn("Termination signal received") + } + Shutdown() } func initAutoCertManager(cfg *config.Config) error { @@ -240,3 +242,8 @@ func initAutoCertManager(cfg *config.Config) error { return nil } + +func printStartupNote() { + port := firstNonEmpty(AppConfig().StringDefault("server.port", appDefaultHTTPPort), AppConfig().StringDefault("server.proxyport", "")) + log.Infof("aah go server running on %s:%s", AppHTTPAddress(), parsePort(port)) +} diff --git a/util.go b/util.go index bf2aef53..b8f8d1c2 100644 --- a/util.go +++ b/util.go @@ -120,3 +120,15 @@ func identifyContentType(ctx *Context) *ahttp.ContentType { // as per 'render.default' in aah.conf or nil return defaultContentType() } + +func parsePort(port string) string { + if !ess.IsStrEmpty(port) { + return port + } + + if AppIsSSLEnabled() { + return "443" + } + + return "80" +} From 29f244644ed45edf868411388fb718c364fb2d51 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Thu, 27 Jul 2017 15:52:42 -0700 Subject: [PATCH 35/38] build config update --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 16bfc8eb..e4c63799 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,10 @@ go: go_import_path: aahframework.org/aah.v0 +install: + - git config --global http.https://aahframework.org.followRedirects true + - go get -t -v ./... + script: - bash <(curl -s https://aahframework.org/go-test) From d92fa3ae3de06634ec1c342f5eee10c794b2a04a Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Mon, 31 Jul 2017 18:43:54 -0700 Subject: [PATCH 36/38] server access log config update and default file creation --- aah.go | 2 +- access_log.go | 16 ++++++++++------ access_log_test.go | 8 ++++---- engine.go | 18 ++++++++++-------- engine_test.go | 6 ++++++ static.go | 4 ++-- testdata/config/aah.conf | 41 ++++++++++++++++++++++++---------------- 7 files changed, 58 insertions(+), 37 deletions(-) diff --git a/aah.go b/aah.go index 45219486..a8148167 100644 --- a/aah.go +++ b/aah.go @@ -196,7 +196,7 @@ func Init(importPath string) { logAsFatal(initRoutes(appConfigDir(), AppConfig())) logAsFatal(initSecurity(AppConfig())) logAsFatal(initViewEngine(appViewsDir(), AppConfig())) - if AppConfig().BoolDefault("request.access_log.enable", false) { + if AppConfig().BoolDefault("server.access_log.enable", false) { logAsFatal(initAccessLog(appLogsDir(), AppConfig())) } } diff --git a/access_log.go b/access_log.go index 56a52260..383a3a10 100644 --- a/access_log.go +++ b/access_log.go @@ -125,13 +125,17 @@ func (al *accessLog) Reset() { func initAccessLog(logsDir string, appCfg *config.Config) error { // log file configuration cfg, _ := config.ParseString("") - file := appCfg.StringDefault("request.access_log.file", "") + file := appCfg.StringDefault("server.access_log.file", "") + + cfg.SetString("log.receiver", "file") if ess.IsStrEmpty(file) { cfg.SetString("log.file", filepath.Join(logsDir, getBinaryFileName()+"-access.log")) - } else if !filepath.IsAbs(file) { - cfg.SetString("log.file", filepath.Join(logsDir, file)) } else { - cfg.SetString("log.file", file) + abspath, err := filepath.Abs(file) + if err != nil { + return err + } + cfg.SetString("log.file", abspath) } cfg.SetString("log.pattern", "%message") @@ -145,7 +149,7 @@ func initAccessLog(logsDir string, appCfg *config.Config) error { } // parse request access log pattern - pattern := appCfg.StringDefault("request.access_log.pattern", appDefaultAccessLogPattern) + pattern := appCfg.StringDefault("server.access_log.pattern", appDefaultAccessLogPattern) appAccessLogFmtFlags, err = ess.ParseFmtFlag(pattern, accessLogFmtFlags) if err != nil { return err @@ -153,7 +157,7 @@ func initAccessLog(logsDir string, appCfg *config.Config) error { // initialize request access log channel if appAccessLogChan == nil { - appAccessLogChan = make(chan *accessLog, cfg.IntDefault("request.access_log.channel_buffer_size", 500)) + appAccessLogChan = make(chan *accessLog, cfg.IntDefault("server.access_log.channel_buffer_size", 500)) go listenForAccessLog() } diff --git a/access_log_test.go b/access_log_test.go index f02f0d0f..96cb1fad 100644 --- a/access_log_test.go +++ b/access_log_test.go @@ -67,7 +67,7 @@ func TestAccessLogFormatterInvalidPattern(t *testing.T) { func TestAccessLogInitDefault(t *testing.T) { testAccessInit(t, ` - request { + server { access_log { # Default value is false enable = true @@ -76,7 +76,7 @@ func TestAccessLogInitDefault(t *testing.T) { `) testAccessInit(t, ` - request { + server { access_log { # Default value is false enable = true @@ -87,7 +87,7 @@ func TestAccessLogInitDefault(t *testing.T) { `) testAccessInit(t, ` - request { + server { access_log { # Default value is false enable = true @@ -127,7 +127,7 @@ func TestEngineAccessLog(t *testing.T) { }, }) - AppConfig().SetBool("request.access_log.enable", true) + AppConfig().SetBool("server.access_log.enable", true) e := newEngine(AppConfig()) req := httptest.NewRequest("GET", "localhost:8080/get-involved.html", nil) diff --git a/engine.go b/engine.go index 0cb0b92d..c248b298 100644 --- a/engine.go +++ b/engine.go @@ -50,10 +50,11 @@ type ( // Engine is the aah framework application server handler for request and response. // Implements `http.Handler` interface. engine struct { - isRequestIDEnabled bool - requestIDHeader string - isGzipEnabled bool - isAccessLogEnabled bool + isRequestIDEnabled bool + requestIDHeader string + isGzipEnabled bool + isAccessLogEnabled bool + isStaticAccessLogEnabled bool } ) @@ -362,10 +363,11 @@ func newEngine(cfg *config.Config) *engine { } return &engine{ - isRequestIDEnabled: cfg.BoolDefault("request.id.enable", true), - requestIDHeader: cfg.StringDefault("request.id.header", ahttp.HeaderXRequestID), - isGzipEnabled: cfg.BoolDefault("render.gzip.enable", true), - isAccessLogEnabled: cfg.BoolDefault("request.access_log.enable", false), + isRequestIDEnabled: cfg.BoolDefault("request.id.enable", true), + requestIDHeader: cfg.StringDefault("request.id.header", ahttp.HeaderXRequestID), + isGzipEnabled: cfg.BoolDefault("render.gzip.enable", true), + isAccessLogEnabled: cfg.BoolDefault("server.access_log.enable", false), + isStaticAccessLogEnabled: cfg.BoolDefault("server.access_log.static_file", true), } } diff --git a/engine_test.go b/engine_test.go index f6530952..67ddeac7 100644 --- a/engine_test.go +++ b/engine_test.go @@ -6,6 +6,7 @@ package aah import ( "compress/gzip" + "fmt" "io/ioutil" "net/http" "net/http/httptest" @@ -88,6 +89,11 @@ func TestEngineNew(t *testing.T) { buf := acquireBuffer() assert.NotNil(t, buf) releaseBuffer(buf) + + appLogFatal = func(v ...interface{}) { fmt.Println(v) } + AppConfig().SetInt("render.gzip.level", 10) + e = newEngine(AppConfig()) + fmt.Println(e) } func TestEngineServeHTTP(t *testing.T) { diff --git a/static.go b/static.go index 3c73c87e..e8efe823 100644 --- a/static.go +++ b/static.go @@ -107,7 +107,7 @@ func (e *engine) serveStatic(ctx *Context) error { publishOnAfterReplyEvent(ctx) // Send data to access log channel - if e.isAccessLogEnabled { + if e.isAccessLogEnabled && e.isStaticAccessLogEnabled { sendToAccessLog(ctx) } return nil @@ -131,7 +131,7 @@ func (e *engine) serveStatic(ctx *Context) error { publishOnAfterReplyEvent(ctx) // Send data to access log channel - if e.isAccessLogEnabled { + if e.isAccessLogEnabled && e.isStaticAccessLogEnabled { sendToAccessLog(ctx) } return nil diff --git a/testdata/config/aah.conf b/testdata/config/aah.conf index 54c7a622..0b65ac9f 100644 --- a/testdata/config/aah.conf +++ b/testdata/config/aah.conf @@ -61,6 +61,30 @@ server { host_policy = ["sample.com"] } } + + # To manage aah server effectively it is necessary to know details about the + # request, response, processing time, client IP address, etc. aah framework + # provides the flexible and configurable access log capabilities. + access_log { + # Enabling server access log + # Default value is `false`. + enable = true + + # Absolute path to access log file or relative path. + # Default location is application logs directory + #file = "{{ .AppName }}-access.log" + + # Default server access log pattern + #pattern = "%clientip %custom:- %reqtime %reqmethod %requrl %reqproto %resstatus %ressize %restime %reqhdr:referer" + + # Access Log channel buffer size + # Default value is `500`. + #channel_buffer_size = 500 + + # Include static files access log too. + # Default value is `true`. + #static_file = false + } } # --------------------- @@ -81,25 +105,10 @@ request { # Default value is 32mb, choose your value based on your use case multipart_size = "32mb" - - # To manage server effectively it is necessary to know details about the - # request, response, processing time, client IP address, etc. aah framework - # provides the flexible and configurable access log capabilities. - access_log { - # Default value is `false` - enable = true - - # Absolute path of access log file - # Default location is application logs directory - #file = "{{ .AppName }}-access.log" - - # Default request access log pattern as follows - #pattern = "%clientip %reqid %reqtime %restime %resstatus %ressize %reqmethod %requrl" - } } # -------------------------------------------------------------- # Application Security # Doc: https://docs.aahframework.org/security-config.html # -------------------------------------------------------------- -include "./security.conf" +include "./security.conf" From af2b67a50cab9c5a9235995621a96a1aad2dd4b9 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Tue, 1 Aug 2017 10:11:34 -0700 Subject: [PATCH 37/38] preparing for v0.7 release --- context.go | 4 ++-- context_test.go | 2 +- engine.go | 2 +- security.go | 8 ++++---- security_test.go | 10 +++++----- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/context.go b/context.go index a59b8192..48de7deb 100644 --- a/context.go +++ b/context.go @@ -14,8 +14,8 @@ import ( "aahframework.org/essentials.v0" "aahframework.org/log.v0" "aahframework.org/router.v0" - "aahframework.org/security.v0-unstable" - "aahframework.org/security.v0-unstable/session" + "aahframework.org/security.v0" + "aahframework.org/security.v0/session" ) var ( diff --git a/context_test.go b/context_test.go index 67ebad0a..405999e6 100644 --- a/context_test.go +++ b/context_test.go @@ -14,7 +14,7 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/config.v0" "aahframework.org/router.v0" - "aahframework.org/security.v0-unstable" + "aahframework.org/security.v0" "aahframework.org/test.v0/assert" ) diff --git a/engine.go b/engine.go index c248b298..a4db4d3c 100644 --- a/engine.go +++ b/engine.go @@ -17,7 +17,7 @@ import ( "aahframework.org/config.v0" "aahframework.org/essentials.v0" "aahframework.org/log.v0" - "aahframework.org/security.v0-unstable" + "aahframework.org/security.v0" ) const ( diff --git a/security.go b/security.go index e602888d..4491dd70 100644 --- a/security.go +++ b/security.go @@ -12,10 +12,10 @@ import ( "aahframework.org/config.v0" "aahframework.org/essentials.v0" "aahframework.org/log.v0" - "aahframework.org/security.v0-unstable" - "aahframework.org/security.v0-unstable/authc" - "aahframework.org/security.v0-unstable/scheme" - "aahframework.org/security.v0-unstable/session" + "aahframework.org/security.v0" + "aahframework.org/security.v0/authc" + "aahframework.org/security.v0/scheme" + "aahframework.org/security.v0/session" ) const ( diff --git a/security_test.go b/security_test.go index 05d0335f..f659025c 100644 --- a/security_test.go +++ b/security_test.go @@ -12,11 +12,11 @@ import ( "aahframework.org/ahttp.v0" "aahframework.org/config.v0" "aahframework.org/router.v0" - "aahframework.org/security.v0-unstable" - "aahframework.org/security.v0-unstable/authc" - "aahframework.org/security.v0-unstable/authz" - "aahframework.org/security.v0-unstable/scheme" - "aahframework.org/security.v0-unstable/session" + "aahframework.org/security.v0" + "aahframework.org/security.v0/authc" + "aahframework.org/security.v0/authz" + "aahframework.org/security.v0/scheme" + "aahframework.org/security.v0/session" "aahframework.org/test.v0/assert" ) From 971e0672c9568d1ca9d4b6e640825624866917b9 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Tue, 1 Aug 2017 10:12:51 -0700 Subject: [PATCH 38/38] readme update for v0.7 release [ci skip] --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ae8948b0..32dcc642 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # aah web framework for Go [![Build Status](https://travis-ci.org/go-aah/aah.svg?branch=master)](https://travis-ci.org/go-aah/aah) [![codecov](https://codecov.io/gh/go-aah/aah/branch/master/graph/badge.svg)](https://codecov.io/gh/go-aah/aah/branch/master) [![Go Report Card](https://goreportcard.com/badge/aahframework.org/aah.v0)](https://goreportcard.com/report/aahframework.org/aah.v0) [![Powered by Go](https://img.shields.io/badge/powered_by-go-blue.svg)](https://golang.org) -[![Version](https://img.shields.io/badge/version-0.6-blue.svg)](https://github.com/go-aah/aah/releases/latest) [![GoDoc](https://godoc.org/aahframework.org/aah.v0?status.svg)](https://godoc.org/aahframework.org/aah.v0) -[![License](https://img.shields.io/github/license/go-aah/aah.svg)](LICENSE) +[![Version](https://img.shields.io/badge/version-0.7-blue.svg)](https://github.com/go-aah/aah/releases/latest) [![GoDoc](https://godoc.org/aahframework.org/aah.v0?status.svg)](https://godoc.org/aahframework.org/aah.v0) +[![License](https://img.shields.io/github/license/go-aah/aah.svg)](LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@aahframework-55acee.svg)](https://twitter.com/aahframework) -***Release [v0.6](https://github.com/go-aah/aah/releases/latest) tagged on Jun 07, 2017*** +***Release [v0.7](https://github.com/go-aah/aah/releases/latest) tagged on Aug 01, 2017*** aah framework - A scalable, performant, rapid development Web framework for Go.