diff --git a/HISTORY.md b/HISTORY.md index 1956ac4d..31431775 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,7 +2,8 @@ **How to upgrade**: remove your `$GOPATH/src/github.com/kataras` folder, open your command-line and execute this command: `go get -u github.com/kataras/iris/iris`. -## 5.0.1 -> 5.1.0 + +## v3 -> v4 (fasthttp-based) long term support - **NEW FEATURE**: `CacheService` simple, cache service for your app's static body content(can work as external service if you are doing horizontal scaling, the `Cache` is just a `Handler` :) ) @@ -25,15 +26,13 @@ Cache any content, templates, static files, even the error handlers, anything. // use the app.Cache instead of iris.Cache Cache(bodyHandler HandlerFunc, expiration time.Duration) HandlerFunc -// InvalidateCache clears the cache body for a specific key(request uri, can be retrieved by GetCacheKey(ctx)) +// InvalidateCache clears the cache body for a specific context's url path(cache unique key) // // Note that it depends on a station instance's cache service. // Do not try to call it from default' station if you use the form of app := iris.New(), -// use the app.Cache instead of iris.Cache -InvalidateCache(key string) +// use the app.InvalidateCache instead of iris.InvalidateCache +InvalidateCache(ctx *Context) -// GetCacheKey returns the cache key(string) from a Context -GetCacheKey(ctx *Context) string ``` @@ -50,9 +49,6 @@ iris.Get("/hi", iris.Cache(func(c *iris.Context) { - - - ```go package main @@ -131,8 +127,6 @@ func main() { ``` -## v4 -> 5.0.1 - - **IMPROVE**: [Iris command line tool](https://github.com/kataras/iris/tree/master/iris) introduces a **new** `get` command (replacement for the old `create`) @@ -153,9 +147,6 @@ Downloads the [basic](https://github.com/iris-contrib/examples/tree/master/AIO_ - **CHANGE**: The `Path parameters` are now **immutable**. Now you don't have to copy a `path parameter` before passing to another function which maybe modifies it, this has a side-affect of `context.GetString("key") = context.Param("key")` so you have to be careful to not override a path parameter via other custom (per-context) user value. - -## v3 -> v4 long term support - - **NEW**: `iris.StaticEmbedded`/`app := iris.New(); app.StaticEmbedded` - Embed static assets into your executable with [go-bindata](https://github.com/jteeuwen/go-bindata) and serve them. > Note: This was already buitl'n feature for templates using `iris.UseTemplate(html.New()).Directory("./templates",".html").Binary(Asset,AssetNames)`, after v4.6.1 you can do that for other static files too, with the `StaticEmbedded` function @@ -882,8 +873,6 @@ Zero front-end changes. No real improvements, developers can ignore this update. - Replace the main and underline websocket implementation with [go-websocket](https://github.com/kataras/go-websocket). Note that we still need the [ris-contrib/websocket](https://github.com/iris-contrib/websocket) package. - Replace the use of iris-contrib/errors with [go-errors](https://github.com/kataras/go-errors), which has more features - -- **NEW FEATURE**: Basic remote control through SSH, example [here](https://github.com/iris-contrib/examples/blob/master/ssh/main.go) - **NEW FEATURE**: Optionally `OnError` foreach Party (by prefix, use it with your own risk), example [here](https://github.com/iris-contrib/examples/blob/master/httperrors/main.go#L37) - **NEW**: `iris.Config.Sessions.CookieLength`, You're able to customize the length of each sessionid's cookie's value. Default (and previous' implementation) is 32. - **FIX**: Websocket panic on non-websocket connection[*](https://github.com/kataras/iris/issues/367) diff --git a/README.md b/README.md index cd63e1e2..4e4cb67c 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@
-Releases +Releases Examples @@ -71,6 +71,7 @@ Ideally suited for both experienced and novice Developers. - Limit request body - Localization i18N - Serve static files +- Cache - Log requests - Define your format and output for the logger - Define custom HTTP errors handlers @@ -882,7 +883,7 @@ I recommend writing your API tests using this new library, [httpexpect](https:// Versioning ------------ -Current: **5.1.0** +Current: **v4 LTS** Todo ------------ @@ -921,7 +922,7 @@ under the Apache Version 2 license found in the [LICENSE file](LICENSE). [Travis]: http://travis-ci.org/kataras/iris [License Widget]: https://img.shields.io/badge/license-Apache%20Version%202-E91E63.svg?style=flat-square [License]: https://github.com/kataras/iris/blob/master/LICENSE -[Release Widget]: https://img.shields.io/badge/release-V5.1.0%20-blue.svg?style=flat-square +[Release Widget]: https://img.shields.io/badge/release-V4%20LTS%20-blue.svg?style=flat-square [Release]: https://github.com/kataras/iris/releases [Chat Widget]: https://img.shields.io/badge/community-chat%20-00BCD4.svg?style=flat-square [Chat]: https://kataras.rocket.chat/channel/iris diff --git a/cache.go b/cache.go deleted file mode 100644 index c78a5477..00000000 --- a/cache.go +++ /dev/null @@ -1,384 +0,0 @@ -package iris - -import ( - "github.com/valyala/fasthttp" - "net/url" - "strconv" - "sync" - "time" -) - -type ( - // CacheServiceAPI describes the cache service which caches the whole response body - CacheServiceAPI interface { - // Cache accepts a route's handler which will cache its response and a time.Duration(int64) which is the expiration duration - Cache(HandlerFunc, time.Duration) HandlerFunc - // ServeRemoteCache creates & returns a new handler which saves cache by POST method and serves a cache entry by GET method to clients - // usually set it with iris.Any, - // but developer is able to set different paths for save or get cache entries: using the iris.Post/.Get(...,iris.ServeRemote()) - // CacheRemote IS not ready for production yet and that's why it is not in docs or history yet. - // propably this method will go to another package which will be ready at the next 24 hours, - // because it can work both on iris and raw net/http, lets no limit it:) - ServeRemoteCache(time.Duration) HandlerFunc - // Invalidate accepts a cache key (which can be retrieved by 'GetCacheKey') and remove its cache response body - InvalidateCache(string) - } - - cacheService struct { - cache map[string]*cacheEntry - mu sync.RWMutex - // keep track of the minimum cache duration of all cache entries, this will be used when gcDuration inside .start() is <=time.Second - gcDuration time.Duration - } - - cacheEntry struct { - statusCode int - contentType string - body []byte - // we could have a new Timer foreach cache entry in order to be persise on the expiration but this will cost us a lot of performance, - // (the ticker should be stopped if delete or key ovveride and so on...) - // but I chosen to just have a generic timer with its tick on the lowest 'expires' of all cache entries that cache keeps - expires time.Time - } -) - -func (e *cacheEntry) serve(ctx *Context) { - ctx.SetContentType(e.contentType) - ctx.SetStatusCode(e.statusCode) - ctx.RequestCtx.Write(e.body) -} - -var _ CacheServiceAPI = &cacheService{} - -func newCacheService() *cacheService { - cs := &cacheService{ - cache: make(map[string]*cacheEntry), - mu: sync.RWMutex{}, - gcDuration: -1, // will set as the lowest of the cache entries, if not set then the cache doesn't starts its garbage collector - } - - return cs -} - -// start called last (after the lowest cache gc duration has been setted by the Cache funcs) -func (cs *cacheService) start() { - if cs.gcDuration > 0 { - // start the timer to check for expirated cache entries - tick := time.Tick(cs.gcDuration) - go func() { - for range tick { - cs.mu.Lock() - now := time.Now() - for k, v := range cs.cache { - if now.After(v.expires) { - delete(cs.cache, k) - } - } - cs.mu.Unlock() - } - }() - } - -} - -func (cs *cacheService) get(key string) *cacheEntry { - cs.mu.RLock() - if v, ok := cs.cache[key]; ok && time.Now().Before(v.expires) { // we check for expiration, the gc clears the cache but gc maybe late - cs.mu.RUnlock() - return v - } - cs.mu.RUnlock() - return nil -} - -var minimumAllowedCacheDuration = 2 * time.Second - -func validateCacheDuration(expiration time.Duration) time.Duration { - if expiration <= minimumAllowedCacheDuration { - expiration = minimumAllowedCacheDuration * 2 - } - return expiration -} - -func validateStatusCode(statusCode int) int { - if statusCode <= 0 { - statusCode = StatusOK - } - return statusCode -} - -func validateContentType(cType string) string { - if cType == "" { - cType = contentText - } - return cType -} - -func (cs *cacheService) set(key string, statusCode int, cType string, body []byte, expiration time.Duration) { - entry := &cacheEntry{ - statusCode: validateStatusCode(statusCode), - contentType: validateContentType(cType), - expires: time.Now().Add(validateCacheDuration(expiration)), - body: body, - } - - cs.mu.Lock() - cs.cache[key] = entry - cs.mu.Unlock() -} - -func (cs *cacheService) remove(key string) { - cs.mu.Lock() - delete(cs.cache, key) - cs.mu.Unlock() -} - -// GetCacheKey returns the cache key(string) from a context -// it's just the RequestURI -func GetCacheKey(ctx *Context) string { - return string(ctx.Request.URI().RequestURI()) -} - -// InvalidateCache clears the cache body for a specific key(request uri, can be retrieved by GetCacheKey(ctx)) -// -// Note that it depends on a station instance's cache service. -// Do not try to call it from default' station if you use the form of app := iris.New(), -// use the app.InvalidateCache instead of iris.InvalidateCache -// -// Example: https://github.com/iris-contrib/examples/tree/master/cache_body -func InvalidateCache(key string) { - Default.InvalidateCache(key) -} - -// InvalidateCache clears the cache body for a specific key(request uri, can be retrieved by GetCacheKey(ctx)) -// -// Note that it depends on a station instance's cache service. -// Do not try to call it from default' station if you use the form of app := iris.New(), -// use the app.Cache instead of iris.Cache -// -// Example: https://github.com/iris-contrib/examples/tree/master/cache_body -func (cs *cacheService) InvalidateCache(key string) { - cs.remove(key) -} - -// Cache is just a wrapper for a route's handler which you want to enable body caching -// Usage: iris.Get("/", iris.Cache(func(ctx *iris.Context){ -// ctx.WriteString("Hello, world!") // or a template or anything else -// }, time.Duration(10*time.Second))) // duration of expiration -// if <=time.Second then it tries to find it though request header's "cache-control" maxage value -// -// Note that it depends on a station instance's cache service. -// Do not try to call it from default' station if you use the form of app := iris.New(), -// use the app.Cache instead of iris.Cache -// -// Example: https://github.com/iris-contrib/examples/tree/master/cache_body -func Cache(bodyHandler HandlerFunc, expiration time.Duration) HandlerFunc { - return Default.Cache(bodyHandler, expiration) -} - -func getResponseContentType(ctx *Context) string { - return validateContentType(string(ctx.Response.Header.ContentType())) -} - -func getResponseStatusCode(ctx *Context) int { - return validateStatusCode(ctx.Response.StatusCode()) -} - -// Cache is just a wrapper for a route's handler which you want to enable body caching -// Usage: iris.Get("/", iris.Cache(func(ctx *iris.Context){ -// ctx.WriteString("Hello, world!") // or a template or anything else -// }, time.Duration(10*time.Second))) // duration of expiration -// if <=time.Second then it tries to find it though request header's "cache-control" maxage value -// -// Note that it depends on a station instance's cache service. -// Do not try to call it from default' station if you use the form of app := iris.New(), -// use the app.Cache instead of iris.Cache -// -// Example: https://github.com/iris-contrib/examples/tree/master/cache_body -func (cs *cacheService) Cache(bodyHandler HandlerFunc, expiration time.Duration) HandlerFunc { - expiration = validateCacheDuration(expiration) - - if cs.gcDuration == -1 || expiration < cs.gcDuration { - cs.gcDuration = expiration // the first time the gcDuration should be > minimumAllowedCacheDuration so: - } - - h := func(ctx *Context) { - key := GetCacheKey(ctx) - if v := cs.get(key); v != nil { - v.serve(ctx) - return - } - - // if not found then serve the handler and collect its results after - bodyHandler.Serve(ctx) - - if expiration <= minimumAllowedCacheDuration { - // try to set the expiraion from header - expiration = time.Duration(ctx.MaxAge()) * time.Second - } - - cType := getResponseContentType(ctx) - statusCode := getResponseStatusCode(ctx) - body := ctx.Response.Body() - // and set the cache value as its response body in a goroutine, because we want to exit from the route's handler as soon as possible - go cs.set(key, statusCode, cType, body, expiration) - } - - return h -} - -// GetRemoteCacheKey returns the context's cache key, -// differs from GetCacheKey is that this method parses the query arguments -// because this key must be sent to an external server -func GetRemoteCacheKey(ctx *Context) string { - return url.QueryEscape(ctx.Request.URI().String()) -} - -const ( - queryCacheKey = "cache_key" - queryCacheDuration = "cache_duration" - queryCacheStatusCode = "cache_status_code" - queryCacheContentType = "cache_content_type" - requestCacheTimeout = 5 * time.Second - statusCacheSucceed = StatusOK - statusCacheFailed = StatusBadRequest -) - -// RemoteCache accepts the remote server address and path of the external cache service, the body handler and optional an expiration -// the last 2 receivers works like .Cache(...) function -// -// Note: Remotecache is a global function, usage: -// app.Get("/", iris.RemoteCache("http://127.0.0.1:8888/cache", bodyHandler, time.Duration(15)*time.Second)) -// -// IT IS NOT READY FOR PRODUCTION YET, READ THE HISTORY.md for the available working cache methods -func RemoteCache(cacheServerAddr string, bodyHandler HandlerFunc, expiration time.Duration) HandlerFunc { - client := fasthttp.Client{} - // buf := utils.NewBufferPool(10) - cacheDurationStr := strconv.Itoa(int(expiration.Seconds())) - h := func(ctx *Context) { - req := fasthttp.AcquireRequest() - req.SetRequestURI(cacheServerAddr) - req.Header.SetMethodBytes(MethodGetBytes) - req.URI().QueryArgs().Add(queryCacheKey, GetRemoteCacheKey(ctx)) - - res := fasthttp.AcquireResponse() - err := client.DoTimeout(req, res, requestCacheTimeout) - if err != nil || res.StatusCode() == statusCacheFailed { - // if not found on cache, then execute the handler and save the cache to the remote server - bodyHandler.Serve(ctx) - // save to the remote cache - req.Header.SetMethodBytes(MethodPostBytes) - args := req.URI().QueryArgs() - args.Add(queryCacheDuration, cacheDurationStr) - statusCode := strconv.Itoa(ctx.Response.StatusCode()) - args.Add(queryCacheStatusCode, statusCode) - cType := string(ctx.Response.Header.Peek(contentType)) - args.Add(queryCacheContentType, cType) - - req.SetBody(ctx.Response.Body()) - go func() { - client.DoTimeout(req, res, requestCacheTimeout) - fasthttp.ReleaseRequest(req) - fasthttp.ReleaseResponse(res) - }() - - } else { - // get the status code , content type and the write the response body - statusCode := res.StatusCode() - cType := res.Header.ContentType() - ctx.SetStatusCode(statusCode) - ctx.Response.Header.SetContentTypeBytes(cType) - - ctx.RequestCtx.Write(res.Body()) - - fasthttp.ReleaseRequest(req) - fasthttp.ReleaseResponse(res) - } - - } - return h -} - -// ServeRemoteCache usage: iris.Any("/cacheservice", iris.ServeRemote()) -// client does an http request to retrieve cached body from the external/remote server which keeps the cache service. -// -// if is GET method request then gets from cache -// if it's POST method request then its saves to the cache -// if it's DELETE method request then its invalidates/removes from cache manually -// the content type and the status are setted inside the caller's handler -// this is not like cs.Cache, it's useful only when you separate your servers to achieve horizontal scaling -// -// Note that it depends on a station instance's cache service. -// Do not try to call it from default' station if you use the form of app := iris.New(), -// use the app.ServeRemoteCache instead of iris.ServeRemoteCache -func ServeRemoteCache(gcDuration time.Duration) HandlerFunc { - return Default.ServeRemoteCache(gcDuration) -} - -// ServeRemoteCache usage: iris.Any("/cacheservice", iris.ServeRemoteCache()) -// client does an http request to retrieve cached body from the external/remote server which keeps the cache service. -// -// if is GET method request then gets from cache -// if it's POST method request then its saves to the cache -// if it's DELETE method request then its invalidates/removes from cache manually -// the content type and the status are setted inside the caller's handler -// this is not like cs.Cache, it's useful only when you separate your servers to achieve horizontal scaling -// -// Note that it depends on a station instance's cache service. -// Do not try to call it from default' station if you use the form of app := iris.New(), -// use the app.ServeRemoteCache instead of iris.ServeRemoteCache -func (cs *cacheService) ServeRemoteCache(gcDuration time.Duration) HandlerFunc { - cs.gcDuration = validateCacheDuration(gcDuration) - // the service started at pre-listen state(on .Build) if gcDuration > 0 - h := func(ctx *Context) { - key := ctx.URLParam(queryCacheKey) - if key == "" { - ctx.SetStatusCode(statusCacheFailed) - return - } - - if ctx.IsGet() { - if v := cs.get(key); v != nil { - v.serve(ctx) - return - } - } else if ctx.IsPost() { - // get the cache expiration via url param - expirationSeconds, err := ctx.URLParamInt64(queryCacheDuration) - // get the body from the requested body - body := ctx.Request.Body() - if len(body) == 0 { - ctx.SetStatusCode(statusCacheFailed) - return - - } - // get the expiration from the "cache-control's maxage" if no url param is setted - if expirationSeconds <= 0 || err != nil { - expirationSeconds = ctx.MaxAge() - } - - // if not setted then try to get it via - if expirationSeconds <= 0 { - expirationSeconds = int64(minimumAllowedCacheDuration.Seconds()) - } - - cacheDuration := validateCacheDuration(time.Duration(expirationSeconds) * time.Second) - statusCode, _ := ctx.URLParamInt(queryCacheDuration) - statusCode = validateStatusCode(statusCode) - cType := validateContentType(ctx.URLParam(queryCacheContentType)) - - cs.set(key, statusCode, cType, body, cacheDuration) - - ctx.SetStatusCode(statusCacheSucceed) - return - } else if ctx.IsDelete() { - cs.remove(key) - ctx.SetStatusCode(statusCacheSucceed) - return - } - - ctx.SetStatusCode(statusCacheFailed) - } - - return h -} diff --git a/cache_test.go b/cache_test.go deleted file mode 100644 index 350503db..00000000 --- a/cache_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package iris_test - -import ( - "github.com/kataras/iris" - "github.com/kataras/iris/httptest" - "testing" - "time" -) - -var testMarkdownContents = `## Hello Markdown from Iris - -This is an example of Markdown with Iris - - - -Features --------- - -All features of Sundown are supported, including: - -* **Compatibility**. The Markdown v1.0.3 test suite passes with - the --tidy option. Without --tidy, the differences are - mostly in whitespace and entity escaping, where blackfriday is - more consistent and cleaner. - -* **Common extensions**, including table support, fenced code - blocks, autolinks, strikethroughs, non-strict emphasis, etc. - -* **Safety**. Blackfriday is paranoid when parsing, making it safe - to feed untrusted user input without fear of bad things - happening. The test suite stress tests this and there are no - known inputs that make it crash. If you find one, please let me - know and send me the input that does it. - - NOTE: "safety" in this context means *runtime safety only*. In order to - protect yourself against JavaScript injection in untrusted content, see - [this example](https://github.com/russross/blackfriday#sanitize-untrusted-content). - -* **Fast processing**. It is fast enough to render on-demand in - most web applications without having to cache the output. - -* **Thread safety**. You can run multiple parsers in different - goroutines without ill effect. There is no dependence on global - shared state. - -* **Minimal dependencies**. Blackfriday only depends on standard - library packages in Go. The source code is pretty - self-contained, so it is easy to add to any project, including - Google App Engine projects. - -* **Standards compliant**. Output successfully validates using the - W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional. - - [this is a link](https://github.com/kataras/iris) ` - -// 10 seconds test -// EXAMPLE: https://github.com/iris-contrib/examples/tree/master/cache_body -func TestCacheCanRender(t *testing.T) { - iris.ResetDefault() - - iris.Config.IsDevelopment = true - defer iris.Close() - var i = 1 - bodyHandler := func(ctx *iris.Context) { - if i%2 == 0 { // only for testing - ctx.SetStatusCode(iris.StatusNoContent) - i++ - return - } - i++ - ctx.Markdown(iris.StatusOK, testMarkdownContents) - } - - expiration := time.Duration(1 * time.Minute) - - iris.Get("/", iris.Cache(bodyHandler, expiration)) - - e := httptest.New(iris.Default, t) - - expectedBody := iris.SerializeToString("text/markdown", testMarkdownContents) - - e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(expectedBody) - time.Sleep(5 * time.Second) // let's sleep for a while in order to be saved in cache(running in goroutine) - e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(expectedBody) // the 1 minute didnt' passed so it should work - -} - -// CacheRemote IS not ready for production yet -// func TestCacheRemote(t *testing.T) { -// iris.ResetDefault() -// // setup the remote cache service listening on localhost:8888/cache -// remoteService := iris.New(iris.OptionDisableBanner(true)) -// remoteService.Any("/cache", remoteService.ServeRemoteCache(5*time.Second)) // clear the gc every 5 seconds -// defer remoteService.Close() -// go remoteService.Listen("localhost:8888") -// <-remoteService.Available -// -// app := iris.New() -// -// n := 1 -// bodyHandler := func(ctx *iris.Context) { -// n++ -// ctx.Markdown(iris.StatusOK, testMarkdownContents) -// } -// -// app.Get("/", iris.RemoteCache("http://localhost:8888/cache", bodyHandler, 10*time.Second)) -// -// e := httptest.New(app, t, httptest.Debug(false)) -// -// expectedBody := app.SerializeToString("text/markdown", testMarkdownContents) -// -// e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(expectedBody) -// time.Sleep(3 * time.Second) // let's wait a while because saving is going on a goroutine (in some ms, but travis is slow so 2 seconds wait) -// // we are in cache, so the 'n' should be 1 -// e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(expectedBody) -// if n > 1 { -// // n should be 1 because it doesn't changed after the first call -// t.Fatalf("Expected n = %d but got %d. Cache has problems!!", 1, n) -// } -// -// // let's wait 5 more seconds, the cache should be cleared now the n should be 2 -// time.Sleep(5 * time.Second) -// e.GET("/").Expect().Status(iris.StatusNoContent).Body().Empty() -// if n != 2 { -// t.Fatalf("Expected n = %d but got %d. Cache has problems!!", 2, n) -// } -// } diff --git a/context.go b/context.go index 9db8a55c..4edf792c 100644 --- a/context.go +++ b/context.go @@ -1133,7 +1133,7 @@ func (ctx *Context) MaxAge() int64 { // InvalidateCache clears the cache manually for this request uri context's handler's route func (ctx *Context) InvalidateCache() { - ctx.framework.cacheService.InvalidateCache(GetCacheKey(ctx)) + ctx.framework.InvalidateCache(ctx) } // Log logs to the iris defined logger diff --git a/iris.go b/iris.go index d5742046..de5588d4 100644 --- a/iris.go +++ b/iris.go @@ -65,6 +65,7 @@ import ( "time" "bytes" + "github.com/geekypanda/httpcache" "github.com/kataras/go-errors" "github.com/kataras/go-fs" "github.com/kataras/go-serializer" @@ -77,9 +78,9 @@ import ( const ( // IsLongTermSupport flag is true when the below version number is a long-term-support version - IsLongTermSupport = false + IsLongTermSupport = true // Version is the current version number of the Iris web framework - Version = "5.1.0" + Version = "4" banner = ` _____ _ |_ _| (_) @@ -97,9 +98,6 @@ var ( Plugins PluginContainer Router fasthttp.RequestHandler Websocket *WebsocketServer - // Look ssh.go for this field's configuration - // example: https://github.com/iris-contrib/examples/blob/master/ssh/main.go - SSH *SSHServer // Available is a channel type of bool, fired to true when the server is opened and all plugins ran // never fires false, if the .Close called then the channel is re-allocating. // the channel remains open until you close it. @@ -115,7 +113,7 @@ var ( // iris.Plugins // iris.Router // iris.Websocket -// iris.SSH and iris.Available channel +// iris.Available channel // useful mostly when you are not using the form of app := iris.New() inside your tests, to make sure that you're using a new iris instance func ResetDefault() { Default = New() @@ -124,7 +122,6 @@ func ResetDefault() { Plugins = Default.Plugins Router = Default.Router Websocket = Default.Websocket - SSH = Default.SSH Available = Default.Available } @@ -142,7 +139,6 @@ type ( // FrameworkAPI contains the main Iris Public API FrameworkAPI interface { MuxAPI - CacheServiceAPI Set(...OptionSetter) Must(error) Build() @@ -169,6 +165,8 @@ type ( TemplateString(string, interface{}, ...map[string]interface{}) string TemplateSourceString(string, interface{}) string SerializeToString(string, interface{}, ...map[string]interface{}) string + Cache(HandlerFunc, time.Duration) HandlerFunc + InvalidateCache(*Context) } // Framework is our God |\| Google.Search('Greek mythology Iris') @@ -176,7 +174,6 @@ type ( // Implements the FrameworkAPI Framework struct { *muxAPI - *cacheService // HTTP Server runtime fields is the iris' defined main server, developer can use unlimited number of servers // note: they're available after .Build, and .Serve/Listen/ListenTLS/ListenLETSENCRYPT/ListenUNIX ln net.Listener @@ -198,7 +195,6 @@ type ( Logger *log.Logger Plugins PluginContainer Websocket *WebsocketServer - SSH *SSHServer } ) @@ -216,12 +212,11 @@ func New(setters ...OptionSetter) *Framework { s := &Framework{} s.Set(setters...) - // logger, plugins & ssh + // logger & plugins { // set the Logger, which it's configuration should be declared before .Listen because the servemux and plugins needs that s.Logger = log.New(s.Config.LoggerOut, s.Config.LoggerPreffix, log.LstdFlags) s.Plugins = newPluginContainer(s.Logger) - s.SSH = NewSSHServer() } // rendering @@ -232,8 +227,6 @@ func New(setters ...OptionSetter) *Framework { "url": s.URL, "urlpath": s.Path, }) - // set the cache service - s.cacheService = newCacheService() } // websocket & sessions @@ -353,11 +346,6 @@ func (s *Framework) Build() { s.sessions.Set(s.Config.Sessions, sessions.DisableAutoGC(false)) } - // set the cache gc duration and start service - if s.cacheService.gcDuration > 0 { - s.cacheService.start() - } - if s.Config.Websocket.Endpoint != "" { // register the websocket server and listen to websocket connections when/if $instance.Websocket.OnConnection called by the dev s.Websocket.RegisterTo(s, s.Config.Websocket) @@ -404,13 +392,6 @@ func (s *Framework) Build() { } } - // - - // ssh - if s.SSH != nil && s.SSH.Enabled() { - s.SSH.bindTo(s) - } - // updates, to cover the default station's irs.Config.checkForUpdates // note: we could use the IsDevelopment configuration field to do that BUT // the developer may want to check for updates without, for example, re-build template files (comes from IsDevelopment) on each request @@ -1141,6 +1122,58 @@ func (s *Framework) SerializeToString(keyOrContentType string, obj interface{}, return res } +// Cache is just a wrapper for a route's handler which you want to enable body caching +// Usage: iris.Get("/", iris.Cache(func(ctx *iris.Context){ +// ctx.WriteString("Hello, world!") // or a template or anything else +// }, time.Duration(10*time.Second))) // duration of expiration +// if <=time.Second then it tries to find it though request header's "cache-control" maxage value +// +// Note that it depends on a station instance's cache service. +// Do not try to call it from default' station if you use the form of app := iris.New(), +// use the app.Cache instead of iris.Cache +func Cache(bodyHandler HandlerFunc, expiration time.Duration) HandlerFunc { + return Default.Cache(bodyHandler, expiration) +} + +// Cache is just a wrapper for a route's handler which you want to enable body caching +// Usage: iris.Get("/", iris.Cache(func(ctx *iris.Context){ +// ctx.WriteString("Hello, world!") // or a template or anything else +// }, time.Duration(10*time.Second))) // duration of expiration +// if <=time.Second then it tries to find it though request header's "cache-control" maxage value +// +// Note that it depends on a station instance's cache service. +// Do not try to call it from default' station if you use the form of app := iris.New(), +// use the app.Cache instead of iris.Cache +func (s *Framework) Cache(bodyHandler HandlerFunc, expiration time.Duration) HandlerFunc { + fh := httpcache.Fasthttp.Cache(func(reqCtx *fasthttp.RequestCtx) { + ctx := s.AcquireCtx(reqCtx) + bodyHandler.Serve(ctx) + s.ReleaseCtx(ctx) + }, expiration) + + return func(ctx *Context) { + fh(ctx.RequestCtx) + } +} + +// InvalidateCache clears the cache body for a specific context's url path(cache unique key) +// +// Note that it depends on a station instance's cache service. +// Do not try to call it from default' station if you use the form of app := iris.New(), +// use the app.InvalidateCache instead of iris.InvalidateCache +func InvalidateCache(ctx *Context) { + Default.InvalidateCache(ctx) +} + +// InvalidateCache clears the cache body for a specific context's url path(cache unique key) +// +// Note that it depends on a station instance's cache service. +// Do not try to call it from default' station if you use the form of app := iris.New(), +// use the app.InvalidateCache instead of iris.InvalidateCache +func (s *Framework) InvalidateCache(ctx *Context) { + httpcache.Fasthttp.Invalidate(ctx.RequestCtx) +} + // ------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------- // ----------------------------------MuxAPI implementation------------------------------ diff --git a/ssh.go b/ssh.go deleted file mode 100644 index e303e03d..00000000 --- a/ssh.go +++ /dev/null @@ -1,705 +0,0 @@ -package iris - -// Minimal management over SSH for your Iris & Q web server -// -// Declaration: -// -// iris.SSH.Host = "0.0.0.0:22" -// iris.SSH.KeyPath = "./iris_rsa" // it's auto-generated if not exists -// iris.SSH.Users = iris.Users{"kataras", []byte("pass")} -// -// -// Usage: -// via interactive command shell: -// -// $ ssh kataras@localhost -// -// or via standalone command and exit: -// -// $ ssh kataras@localhost stop -// -// -// Commands available: -// -// stop -// start -// restart -// log -// help -// exit -// -// Keep note that I will re-write this file, ssh.go because, as you can see, it's not well-written and not maintainable* - -import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "net" - "os" - "os/exec" - "path/filepath" - "runtime" - "strconv" - "strings" - "text/template" - "time" - - "log" - - "github.com/kardianos/osext" - "github.com/kardianos/service" - "github.com/kataras/go-errors" - "github.com/kataras/go-fs" - "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/terminal" -) - -// ------------------------------------------------------------------------------------- -// ------------------------------------------------------------------------------------- -// ----------------------------------Iris+SSH------------------------------------------- -// ------------------------------------------------------------------------------------- -// ------------------------------------------------------------------------------------- - -func _output(format string, a ...interface{}) func(io.Writer) { - if format[len(format)-3:] != "\n" { - format += "\n" - } - msgBytes := []byte(fmt.Sprintf(format, a...)) - - return func(w io.Writer) { - w.Write(msgBytes) - } -} - -type systemServiceWrapper struct{} - -func (w *systemServiceWrapper) Start(s service.Service) error { - return nil -} - -func (w *systemServiceWrapper) Stop(s service.Service) error { - return nil -} - -func (s *SSHServer) bindTo(station *Framework) { - if s.Enabled() && !s.IsListening() { // check if not listening because on restart this block will re-executing,but we don't want to start ssh again, ssh will never stops. - - if station.Config.IsDevelopment && s.Logger == nil { - s.Logger = station.Logger - } - - // cache the messages to be sent to the channel, no need to produce memory allocations here - statusRunningMsg := _output("The HTTP Server is running.") - statusNotRunningMsg := _output("The HTTP Server is NOT running. ") - - serverStoppedMsg := _output("The HTTP Server has been stopped.") - errServerNotReadyMsg := _output("Error: HTTP Server is not even builded yet!") - - serverStartedMsg := _output("The HTTP Server has been started.") - serverRestartedMsg := _output("The HTTP Server has been restarted.") - - loggerStartedMsg := _output("Logger has been registered to the HTTP Server.\nNew Requests will be printed here.\nYou can still type 'exit' to close this SSH Session.\n\n") - // - - sshCommands := Commands{ - Command{Name: "status", Description: "Prompts the status of the HTTP Server, is listening(started) or not(stopped).", Action: func(conn ssh.Channel) { - if station.IsRunning() { - statusRunningMsg(conn) - } else { - statusNotRunningMsg(conn) - } - execPath, err := osext.Executable() // this works fine, if the developer builded the go app, if just go run main.go then prints the temporary path which the go tool creates - if err == nil { - conn.Write([]byte("[EXEC] " + execPath + "\n")) - } - }}, - // Note for stop If you have opened a tab with Q route: - // in order to see that the http listener has closed you have to close your browser and re-navigate(browsers caches the tcp connection) - Command{Name: "stop", Description: "Stops the HTTP Server.", Action: func(conn ssh.Channel) { - if station.IsRunning() { - station.Close() - //srv.listener = nil used to reopen so let it setted - serverStoppedMsg(conn) - } else { - errServerNotReadyMsg(conn) - } - }}, - Command{Name: "start", Description: "Starts the HTTP Server.", Action: func(conn ssh.Channel) { - if !station.IsRunning() { - go station.Reserve() - } - serverStartedMsg(conn) - }}, - Command{Name: "restart", Description: "Restarts the HTTP Server.", Action: func(conn ssh.Channel) { - if station.IsRunning() { - station.Close() - //srv.listener = nil used to reopen so let it setted - } - go station.Reserve() - serverRestartedMsg(conn) - }}, - /* not ready yet - Command{Name: "service", Description: "[REQUIRES HTTP SERVER's ADMIN PRIVILEGE] Adds the web server to the system services, use it when you want to make your server to autorun on reboot", Action: func(conn ssh.Channel) { - ///TODO: - // 1. Unistall service and change the 'service' to 'install service' - // 2. Fix, this current implementation doesn't works on windows 10 it says that the service is not responding to request and start... - // 2.1 the fix is maybe add these and change the s.Install to s.Run to the $DESKTOP/some/q/main.go I will try this - // as the example shows. - - // remember: run command line as administrator > sc delete "Iris Web Server - $DATETIME" to delete the service, do it on each test. - svcConfig := &service.Config{ - Name: "Iris Web Server - " + time.Now().Format(q.TimeFormat), - DisplayName: "Iris Web Server - " + time.Now().Format(q.TimeFormat), - Description: "The web server which has been registered by SSH interface.", - } - - prg := &systemServiceWrapper{} - s, err := service.New(prg, svcConfig) - - if err != nil { - conn.Write([]byte(err.Error() + "\n")) - return - } - - err = s.Install() - if err != nil { - conn.Write([]byte(err.Error() + "\n")) - return - } - conn.Write([]byte("Service has been registered.\n")) - - }},*/ - Command{Name: "log", Description: "Adds a logger to the HTTP Server, waits for requests and prints them here.", Action: func(conn ssh.Channel) { - // the ssh user can still write commands, this is not blocking anything. - loggerMiddleware := NewLoggerHandler(conn, true) - station.UseGlobalFunc(loggerMiddleware) - - // register to the errors also - errorLoggerHandler := NewLoggerHandler(conn, false) - - for k, v := range station.mux.errorHandlers { - errorH := v - // wrap the error handler with the ssh logger middleware - station.mux.errorHandlers[k] = HandlerFunc(func(ctx *Context) { - errorH.Serve(ctx) - errorLoggerHandler(ctx) // after the error handler because that is setting the status code. - }) - } - - station.mux.build() // rebuild the mux in order the UseGlobalFunc to work at runtime - - loggerStartedMsg(conn) - // the middleware will still to run, we could remove it on exit but exit is general command I dont want to touch that - // we could make a command like 'log stop' or on 'stop' to remove the middleware...I will think about it. - }}, - } - - for _, cmd := range sshCommands { - if _, found := s.Commands.ByName(cmd.Name); !found { // yes, the user can add custom commands too, I will cover this on docs some day, it's not too hard if you see the code. - s.Commands.Add(cmd) - } - } - - go func() { - station.Must(s.Listen()) - }() - } -} - -// ------------------------------------------------------------------------------------- -// ------------------------------------------------------------------------------------- -// ----------------------------------SSH implementation--------------------------------- -// ------------------------------------------------------------------------------------- -// ------------------------------------------------------------------------------------- - -var ( - // SSHBanner is the banner goes on top of the 'ssh help message' - // it can be changed, defaults is the Iris's banner - SSHBanner = banner - - helpMessage = SSHBanner + ` - -COMMANDS: - {{ range $index, $cmd := .Commands }} - {{- $cmd.Name }} | {{ $cmd.Description }} - {{ end }} -USAGE: - ssh myusername@{{ .Hostname}} {{ .PortDeclaration }} {{ first .Commands}} - or just write the command below -VERSION: - {{ .Version }} - -` - helpTmpl *template.Template -) - -func init() { - var err error - helpTmpl = template.New("help_message").Funcs(template.FuncMap{"first": func(cmds Commands) string { - if len(cmds) > 0 { - return cmds[0].Name - } - - return "" - }}) - - helpTmpl, err = helpTmpl.Parse(helpMessage) - if err != nil { - panic(err.Error()) - } -} - -//no need of SSH prefix on these types, we don't have other commands -// use of struct and no global variables because we want each Iris instance to have its own SSH interface. - -// Action the command's handler -type Action func(ssh.Channel) - -// Command contains the registered SSH commands -// contains a Name which is the payload string -// Description which is the description of the command shows to the admin/user -// Action is the particular command's handler -type Command struct { - Name string - Description string - Action Action -} - -// Commands the SSH Commands, it's just a type of []Command -type Commands []Command - -// Add adds command(s) to the commands list -func (c *Commands) Add(cmd ...Command) { - pCommands := *c - *c = append(pCommands, cmd...) -} - -// ByName returns the command by its Name -// if not found returns a zero-value Command and false as the second output parameter. -func (c *Commands) ByName(commandName string) (cmd Command, found bool) { - pCommands := *c - for _, cmd = range pCommands { - if cmd.Name == commandName { - found = true - return - } - } - return -} - -// Users SSH.Users field, it's just map[string][]byte (username:password) -type Users map[string][]byte - -func (m Users) exists(username string, pass []byte) bool { - for k, v := range m { - if k == username && bytes.Equal(v, pass) { - return true - } - } - return false - -} - -// DefaultSSHKeyPath used if SSH.KeyPath is empty. Defaults to: "iris_rsa". It can be changed. -var DefaultSSHKeyPath = "iris_rsa" - -var errSSHExecutableNotFound = errors.New(`Cannot generate ssh private key: ssh-keygen couldn't be found. Please specify the ssh[.exe] and ssh-keygen[.exe] - path on your operating system's environment's $PATH or set the configuration field 'Bin'.\n For example, on windows, the path is: C:\\Program Files\\Git\usr\\bin. Error Trace: %q`) - -func generateSigner(keypath string, sshKeygenBin string) (ssh.Signer, error) { - if keypath == "" { - keypath = DefaultSSHKeyPath - } - if sshKeygenBin != "" { - // if empty then the user should specify the ssh-keygen bin path (if not setted already) - // on the $PATH system environment, otherwise it will panic. - if sshKeygenBin[len(sshKeygenBin)-1] != os.PathSeparator { - sshKeygenBin += string(os.PathSeparator) - } - sshKeygenBin += "ssh-keygen" - if isWindows { - sshKeygenBin += ".exe" - } - } else { - sshKeygenBin = "ssh-keygen" - } - if !fs.DirectoryExists(keypath) { - os.MkdirAll(filepath.Dir(keypath), os.ModePerm) - keygenCmd := exec.Command(sshKeygenBin, "-f", keypath, "-t", "rsa", "-N", "") - _, err := keygenCmd.Output() - if err != nil { - panic(errSSHExecutableNotFound.Format(err.Error())) - } - } - - pemBytes, err := ioutil.ReadFile(keypath) - if err != nil { - return nil, err - } - - return ssh.ParsePrivateKey(pemBytes) -} - -func validChannel(ch ssh.NewChannel) bool { - if typ := ch.ChannelType(); typ != "session" { - ch.Reject(ssh.UnknownChannelType, typ) - return false - } - return true -} - -func execCmd(cmd *exec.Cmd, ch ssh.Channel) error { - stdout, err := cmd.StdoutPipe() - if err != nil { - return err - } - - stderr, err := cmd.StderrPipe() - if err != nil { - return err - } - - input, err := cmd.StdinPipe() - if err != nil { - return err - } - - if err = cmd.Start(); err != nil { - return err - } - - go io.Copy(input, ch) - io.Copy(ch, stdout) - io.Copy(ch.Stderr(), stderr) - - if err = cmd.Wait(); err != nil { - return err - } - return nil -} - -func sendExitStatus(ch ssh.Channel) { - ch.SendRequest("exit-status", false, []byte{0, 0, 0, 0}) -} - -var errInvalidSSHCommand = errors.New("Invalid Command: '%s'") - -func parsePayload(payload string, prefix string) (string, error) { - payloadUTF8 := strings.Map(func(r rune) rune { - if r >= 32 && r < 127 { - return r - } - return -1 - }, payload) - - if prefIdx := strings.Index(payloadUTF8, prefix); prefIdx != -1 { - p := strings.TrimSpace(payloadUTF8[prefIdx+len(prefix):]) - return p, nil - } - return "", errInvalidSSHCommand.Format(payload) -} - -const ( - isWindows = runtime.GOOS == "windows" - isMac = runtime.GOOS == "darwin" -) - -// ------------------------------------------------------------------------------------- -// ------------------------------------------------------------------------------------- -// ----------------------------------SSH Server----------------------------------------- -// ------------------------------------------------------------------------------------- -// ------------------------------------------------------------------------------------- - -// SSHServer : Simple SSH interface for Iris web framework, does not implements the most secure options and code, -// but its should works -// use it at your own risk. -type SSHServer struct { - Bin string // windows: C:/Program Files/Git/usr/bin, it's the ssh[.exe] and ssh-keygen[.exe], we only need the ssh-keygen. - KeyPath string // C:/Users/kataras/.ssh/iris_rsa - Host string // host:port - listener net.Listener - Users Users // map[string][]byte]{ "username":[]byte("password"), "my_second_username" : []byte("my_second_password")} - Commands Commands // Commands{Command{Name: "restart", Description:"restarts & rebuild the server", Action: func(ssh.Channel){}}} - // note for Commands field: - // the default Iris's commands are defined at the end of this file, I tried to make this file as standalone as I can, because it will be used for Iris web framework also. - Shell bool // Set it to true to enable execute terminal's commands(system commands) via ssh if no other command is found from the Commands field. Defaults to false for security reasons - Logger *log.Logger // log.New(...)/ $qinstance.Logger, fill it when you want to receive debug and info/warnings messages -} - -// NewSSHServer returns a new empty SSHServer -func NewSSHServer() *SSHServer { - return &SSHServer{} -} - -// Enabled returns true if SSH can be started, if Host != "" -func (s *SSHServer) Enabled() bool { - if s == nil { - return false - } - return s.Host != "" -} - -// IsListening returns true if ssh server has been started -func (s *SSHServer) IsListening() bool { - return s.Enabled() && s.listener != nil -} - -func (s *SSHServer) logf(format string, a ...interface{}) { - if s.Logger != nil { - s.Logger.Printf(format, a...) - } -} - -// parsePortSSH receives an addr of form host[:port] and returns the port part of it -// ex: localhost:22 will return the `22`, mydomain.com will return the '22' -func parsePortSSH(addr string) int { - if portIdx := strings.IndexByte(addr, ':'); portIdx != -1 { - afP := addr[portIdx+1:] - p, err := strconv.Atoi(afP) - if err == nil { - return p - } - } - return 22 -} - -// commands that exists on all ssh interfaces, both Q and Iris -var standardCommands = Commands{Command{Name: "help", Description: "Opens up the assistance"}, - Command{Name: "exit", Description: "Exits from the terminal (if interactive shell)"}} - -func (s *SSHServer) writeHelp(wr io.Writer) { - port := parsePortSSH(s.Host) - hostname := ParseHostname(s.Host) - defer func() { - if r := recover(); r != nil { - // means that user-dev has old version of Go Programming Language in her/his machine, so print a message to the server terminal - // which will help the dev, NOT the client - s.logf("[IRIS SSH] Help message is disabled, please install Go Programming Language, at least version 1.7: https://golang.org/dl/") - } - }() - data := map[string]interface{}{ - "Hostname": hostname, "PortDeclaration": "-p " + strconv.Itoa(port), - "Commands": append(s.Commands, standardCommands...), - "Version": Version, - } - - helpTmpl.Execute(wr, data) -} - -var ( - errUserInvalid = errors.New("Username or Password rejected for: %q") - errServerListen = errors.New("Cannot listen to: %s, Trace: %s") -) - -// Listen starts the SSH Server -func (s *SSHServer) Listen() error { - - // get the key - privateKey, err := generateSigner(s.KeyPath, s.Bin) - if err != nil { - return err - } - // prepare the server's configuration - cfg := &ssh.ServerConfig{ - // NoClientAuth: true to allow anyone to login, nooo - PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { - username := c.User() - if !s.Users.exists(username, pass) { - return nil, errUserInvalid.Format(username) - } - return nil, nil - }} - - cfg.AddHostKey(privateKey) - - // start the server with the configuration we just made. - var lerr error - s.listener, lerr = net.Listen("tcp", s.Host) - if lerr != nil { - return errServerListen.Format(s.Host, lerr.Error()) - } - - // ready to accept incoming requests - s.logf("SSH Server is running") - for { - conn, err := s.listener.Accept() - if err != nil { - s.logf(err.Error()) - continue - } - // handshake first - - sshConn, chans, reqs, err := ssh.NewServerConn(conn, cfg) - if err != nil { - s.logf(err.Error()) - continue - } - - s.logf("New SSH Connection has been enstablish from %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion()) - - // discard all global requests - go ssh.DiscardRequests(reqs) - // accept all current chanels - go s.handleChannels(chans) - } - -} - -func (s *SSHServer) handleChannels(chans <-chan ssh.NewChannel) { - for ch := range chans { - go s.handleChannel(ch) - } -} - -var errUnsupportedReqType = errors.New("Unsupported request type: %q") - -func (s *SSHServer) handleChannel(newChannel ssh.NewChannel) { - // we working from terminal, so only type of "session" is allowed. - if !validChannel(newChannel) { - return - } - - conn, reqs, err := newChannel.Accept() - if err != nil { - s.logf(err.Error()) - return - } - - go func(in <-chan *ssh.Request) { - defer func() { - conn.Close() - //debug - s.logf("Session closed") - }() - - for req := range in { - var err error - defer func() { - if err != nil { - conn.Write([]byte(err.Error())) - } - sendExitStatus(conn) - }() - - switch req.Type { - case "pty-req": - { - s.writeHelp(conn) - req.Reply(true, nil) - } - - case "shell": - { - // comes after pty-req, this is when the user just use this form: ssh kataras@mydomain.com -p 22 - // then we want interactive shell which will execute the commands: - term := terminal.NewTerminal(conn, "> ") - - for { - line, lerr := term.ReadLine() - if lerr == io.EOF { - return - } - if lerr != nil { - err = lerr - s.logf(lerr.Error()) - continue - } - - payload, perr := parsePayload(line, "") - if perr != nil { - err = perr - return - } - - if payload == "help" { - s.writeHelp(conn) - continue - } else if payload == "exit" { - return - } - - if cmd, found := s.Commands.ByName(payload); found { - cmd.Action(conn) - } else if s.Shell { - // yes every time check that - if isWindows { - execCmd(exec.Command("cmd", "/C", payload), conn) - } else { - execCmd(exec.Command("sh", "-c", payload), conn) - } - } else { - conn.Write([]byte(errInvalidSSHCommand.Format(payload).Error() + "\n")) - } - - //s.logf(line) - } - } - - case "exec": - { - // this is the place which the user executed something like that: ssh kataras@mydomain.com -p 22 stop - // a direct command, we don' t open the interactive shell, just execute the command and exit. - payload, perr := parsePayload(string(req.Payload), "") - if perr != nil { - err = perr - return - } - - if cmd, found := s.Commands.ByName(payload); found { - cmd.Action(conn) - } else if payload == "help" { - s.writeHelp(conn) - } else if s.Shell { - // yes every time check that - if isWindows { - execCmd(exec.Command("cmd", "/C", payload), conn) - } else { - execCmd(exec.Command("sh", "-c", payload), conn) - } - } else { - err = errInvalidSSHCommand.Format(payload) - } - return - } - default: - { - err = errUnsupportedReqType.Format(req.Type) - return - } - } - } - - }(reqs) - -} - -// NewLoggerHandler is a basic Logger middleware/Handler (not an Entry Parser) -func NewLoggerHandler(writer io.Writer, calculateLatency ...bool) HandlerFunc { - shouldNext := false - if len(calculateLatency) > 0 { - shouldNext = calculateLatency[0] - } - return func(ctx *Context) { - var date, status, ip, method, path string - var latency time.Duration - var startTime, endTime time.Time - path = ctx.PathString() - method = ctx.MethodString() - - startTime = time.Now() - if shouldNext { - ctx.Next() - } - - endTime = time.Now() - latency = endTime.Sub(startTime) - date = endTime.Format("01/02 - 15:04:05") - - status = strconv.Itoa(ctx.Response.StatusCode()) - ip = ctx.RemoteAddr() - - //finally print the logs to the ssh - writer.Write([]byte(fmt.Sprintf("%s %v %4v %s %s %s \n", date, status, latency, ip, method, path))) - } -}