diff --git a/README.md b/README.md index c07d20df..44e19f83 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.5-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) +[![Version](https://img.shields.io/badge/version-0.5.1-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) -***Release [v0.5](https://github.com/go-aah/aah/releases/latest) tagged on May 19, 2017*** +***Release [v0.5.1](https://github.com/go-aah/aah/releases/latest) tagged on May 21, 2017*** aah framework - A scalable, performant, rapid development Web framework for Go. diff --git a/aah.go b/aah.go index 03de98fb..da731d2f 100644 --- a/aah.go +++ b/aah.go @@ -21,7 +21,7 @@ import ( ) // Version no. of aah framework -const Version = "0.5" +const Version = "0.5.1" // aah application variables var ( diff --git a/aah_test.go b/aah_test.go index fdeb6b7d..42f3052f 100644 --- a/aah_test.go +++ b/aah_test.go @@ -150,7 +150,7 @@ func TestAahRecover(t *testing.T) { } func TestAahLogDir(t *testing.T) { - logsDir := filepath.Join(getTestdataPath(), "logs") + logsDir := filepath.Join(getTestdataPath(), appLogsDir()) logFile := filepath.Join(logsDir, "test.log") defer ess.DeleteFiles(logsDir) diff --git a/context_test.go b/context_test.go index c288e634..8993d6e7 100644 --- a/context_test.go +++ b/context_test.go @@ -169,12 +169,12 @@ func TestContextNil(t *testing.T) { func TestContextEmbeddedAndController(t *testing.T) { addToCRegistry() - assertEmbeddedIndexes(t, Level1{}, [][]int{{0}}) - assertEmbeddedIndexes(t, Level2{}, [][]int{{0, 0}}) - assertEmbeddedIndexes(t, Level3{}, [][]int{{0, 0, 0}}) - assertEmbeddedIndexes(t, Level4{}, [][]int{{0, 0, 0, 0}}) - assertEmbeddedIndexes(t, Path1{}, [][]int{{1}}) - assertEmbeddedIndexes(t, Path2{}, [][]int{{0, 0}, {1, 1}, {2, 0, 0, 0, 0}}) + testEmbeddedIndexes(t, Level1{}, [][]int{{0}}) + testEmbeddedIndexes(t, Level2{}, [][]int{{0, 0}}) + testEmbeddedIndexes(t, Level3{}, [][]int{{0, 0, 0}}) + testEmbeddedIndexes(t, Level4{}, [][]int{{0, 0, 0, 0}}) + testEmbeddedIndexes(t, Path1{}, [][]int{{1}}) + testEmbeddedIndexes(t, Path2{}, [][]int{{0, 0}, {1, 1}, {2, 0, 0, 0, 0}}) } func TestContextSetURL(t *testing.T) { @@ -234,7 +234,7 @@ func TestContextSetMethod(t *testing.T) { assert.Equal(t, "GET", ctx.Req.Method) } -func assertEmbeddedIndexes(t *testing.T, c interface{}, expected [][]int) { +func testEmbeddedIndexes(t *testing.T, c interface{}, expected [][]int) { actual := findEmbeddedContext(reflect.TypeOf(c)) if !reflect.DeepEqual(expected, actual) { t.Errorf("Indexes do not match. expected %v actual %v", expected, actual) diff --git a/param_test.go b/param_test.go index 1653e9a8..d05699b5 100644 --- a/param_test.go +++ b/param_test.go @@ -12,7 +12,7 @@ import ( "testing" "aahframework.org/ahttp.v0" - ess "aahframework.org/essentials.v0" + "aahframework.org/essentials.v0" "aahframework.org/test.v0/assert" ) @@ -46,8 +46,6 @@ func TestParamTemplateFuncs(t *testing.T) { func TestParamParse(t *testing.T) { defer ess.DeleteFiles("testapp.pid") - testEng.Lock() - defer testEng.Unlock() r1 := httptest.NewRequest("GET", "http://localhost:8080/index.html?lang=en-CA", nil) ctx1 := &Context{ diff --git a/server_test.go b/server_test.go index 13dc31bf..95a2f774 100644 --- a/server_test.go +++ b/server_test.go @@ -31,8 +31,6 @@ func TestServerStart1(t *testing.T) { func TestServerStart2(t *testing.T) { defer ess.DeleteFiles("testapp.pid") - testEng.Lock() - defer testEng.Unlock() // App Config cfgDir := filepath.Join(getTestdataPath(), appConfigDir()) @@ -67,6 +65,6 @@ func TestServerStart2(t *testing.T) { Date: buildTime, Version: "1.0.0", }) - AppConfig().SetString("server.port", "8080") - go Start() + AppConfig().SetString("server.port", "80") + Start() } diff --git a/static.go b/static.go index e821f536..6f2a6688 100644 --- a/static.go +++ b/static.go @@ -19,25 +19,23 @@ import ( "aahframework.org/log.v0" ) +const dirStatic = "static" + // 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 - dir, file := filepath.Split(getFilepath(ctx)) - log.Tracef("Dir: %s, File: %s", dir, file) - ctx.Reply().gzip = checkGzipRequired(file) - e.wrapGzipWriter(ctx) - e.writeHeaders(ctx) + // Determine route is file or directory as per user defined + // static route config (refer to https://docs.aahframework.org/static-files.html#section-static). + // httpDir -> value is from routes config + // filePath -> value is from request + httpDir, filePath := getHTTPDirAndFilePath(ctx) + log.Tracef("Dir: %s, Filepath: %s", httpDir, filePath) res, req := ctx.Res, ctx.Req - fs := ahttp.Dir(dir, ctx.route.ListDir) - f, err := fs.Open(file) + f, err := httpDir.Open(filePath) if err != nil { - if err == ahttp.ErrDirListNotAllowed { - log.Warnf("directory listing not allowed: %s", req.Path) - res.WriteHeader(http.StatusForbidden) - fmt.Fprintf(res, "403 Directory listing not allowed") - } else if os.IsNotExist(err) { + if os.IsNotExist(err) { log.Errorf("file not found: %s", req.Path) return errFileNotFound } else if os.IsPermission(err) { @@ -48,20 +46,36 @@ func (e *engine) serveStatic(ctx *Context) error { res.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(res, "500 Internal Server Error") } - return nil } defer ess.CloseQuietly(f) - fi, err := f.Stat() if err != nil { res.WriteHeader(http.StatusInternalServerError) - _, _ = res.Write([]byte("500 Internal Server Error")) + fmt.Fprintf(res, "500 Internal Server Error") return nil } - if fi.IsDir() { + // Gzip + ctx.Reply().gzip = checkGzipRequired(filePath) + e.wrapGzipWriter(ctx) + e.writeHeaders(ctx) + + // Serve file + if fi.Mode().IsRegular() { + // 'OnPreReply' server extension point + publishOnPreReplyEvent(ctx) + + http.ServeContent(ctx.Res, ctx.Req.Raw, path.Base(filePath), fi.ModTime(), f) + + // 'OnAfterReply' server extension point + publishOnAfterReplyEvent(ctx) + return nil + } + + // Serve directory + if fi.Mode().IsDir() && ctx.route.ListDir { // redirect if the directory name doesn't end in a slash if req.Path[len(req.Path)-1] != '/' { log.Debugf("redirecting to dir: %s", req.Path+"/") @@ -79,13 +93,11 @@ func (e *engine) serveStatic(ctx *Context) error { return nil } - // 'OnPreReply' server extension point - publishOnPreReplyEvent(ctx) - - http.ServeContent(res, req.Raw, file, fi.ModTime(), f) + // Flow reached here it means directory listing is not allowed + log.Warnf("directory listing not allowed: %s", req.Path) + res.WriteHeader(http.StatusForbidden) + fmt.Fprintf(res, "403 Directory listing not allowed") - // 'OnAfterReply' server extension point - publishOnAfterReplyEvent(ctx) return nil } @@ -138,14 +150,13 @@ func checkGzipRequired(file string) bool { } } -func getFilepath(ctx *Context) string { - var file string - if ctx.route.IsDir() { - file = filepath.Join(AppBaseDir(), ctx.route.Dir, ctx.Req.PathValue("filepath")) - } else { - file = filepath.Join(AppBaseDir(), "static", ctx.route.File) +// getHTTPDirAndFilePath method returns the `http.Dir` and requested file path. +// 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 filepath.FromSlash(file) + return http.Dir(filepath.Join(AppBaseDir(), ctx.route.Dir)), ctx.Req.PathValue("filepath") } // Sort interface for Directory list diff --git a/static_test.go b/static_test.go new file mode 100644 index 00000000..e87fa4a4 --- /dev/null +++ b/static_test.go @@ -0,0 +1,67 @@ +// 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 ( + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "aahframework.org/config.v0" + "aahframework.org/router.v0" + "aahframework.org/test.v0/assert" +) + +func TestStaticDirectoryListing(t *testing.T) { + appCfg, _ := config.ParseString("") + e := newEngine(appCfg) + + 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", "", "", "403 Directory listing not allowed", false) + + testStaticServe(t, e, "http://localhost:8080/static", "static", "", "", `Found`, true) + + testStaticServe(t, e, "http://localhost:8080/static/", "static", "", "", `Listing of /static/`, true) + + testStaticServe(t, e, "http://localhost:8080/robots.txt", "", "", "test.txt", "This is file content of test.txt", false) +} + +func TestStaticMisc(t *testing.T) { + // File extension check for gzip + v1 := checkGzipRequired("sample.css") + assert.True(t, v1) + + v2 := checkGzipRequired("font.otf") + assert.True(t, v2) + + // directoryList for read error + r1 := httptest.NewRequest("GET", "http://localhost:8080/assets/css/app.css", nil) + w1 := httptest.NewRecorder() + f, err := os.Open(filepath.Join(getTestdataPath(), "static", "test.txt")) + assert.Nil(t, err) + + directoryList(w1, r1, f) + assert.Equal(t, "Error reading directory", w1.Body.String()) +} + +func testStaticServe(t *testing.T, e *engine, reqURL, dir, filePath, file, result string, listDir bool) { + r := httptest.NewRequest("GET", reqURL, nil) + w := httptest.NewRecorder() + ctx := e.prepareContext(w, r) + ctx.route = &router.Route{IsStatic: true, Dir: dir, ListDir: listDir, File: file} + ctx.Req.Params.Path = map[string]string{ + "filepath": filePath, + } + appBaseDir = getTestdataPath() + err := e.serveStatic(ctx) + appBaseDir = "" + assert.Nil(t, err) + assert.True(t, strings.Contains(w.Body.String(), result)) +} diff --git a/testdata/static/test.txt b/testdata/static/test.txt index 4871fd52..ab92b19d 100644 --- a/testdata/static/test.txt +++ b/testdata/static/test.txt @@ -1 +1 @@ -test.txt +This is file content of test.txt diff --git a/view_test.go b/view_test.go index c2b78306..1a52ffa7 100644 --- a/view_test.go +++ b/view_test.go @@ -9,7 +9,6 @@ import ( "net/http/httptest" "path/filepath" "strings" - "sync" "testing" "aahframework.org/ahttp.v0" @@ -96,12 +95,8 @@ func TestViewStore(t *testing.T) { assert.False(t, found) } -var testEng = sync.Mutex{} - func TestViewResolveView(t *testing.T) { defer ess.DeleteFiles("testapp.pid") - testEng.Lock() - defer testEng.Unlock() appCfg, _ := config.ParseString("") e := newEngine(appCfg) @@ -131,7 +126,7 @@ func TestViewResolveView(t *testing.T) { assert.Equal(t, "http", htmlRdr.ViewArgs["Scheme"]) assert.Equal(t, "localhost:8080", htmlRdr.ViewArgs["Host"]) assert.Equal(t, "/index.html", htmlRdr.ViewArgs["RequestPath"]) - assert.Equal(t, "0.5", htmlRdr.ViewArgs["AahVersion"]) + assert.Equal(t, Version, htmlRdr.ViewArgs["AahVersion"]) assert.Equal(t, "aah framework", htmlRdr.ViewArgs["MyName"]) // cleanup