Prepare for 4.0.0 gopkg.in for-ever package (All 20+ other repositories refactored) including gitbook and examples

This commit is contained in:
Gerasimos Maropoulos 2016-10-31 08:19:00 +02:00
parent 6f22da7622
commit 32e3cbede1
7 changed files with 69 additions and 1262 deletions

View File

@ -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`. **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` :) ) - **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 // use the app.Cache instead of iris.Cache
Cache(bodyHandler HandlerFunc, expiration time.Duration) HandlerFunc 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. // 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(), // 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 // use the app.InvalidateCache instead of iris.InvalidateCache
InvalidateCache(key string) 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 ```go
package main 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`) - **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. - **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. - **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 > 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 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 - 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 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. - **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) - **FIX**: Websocket panic on non-websocket connection[*](https://github.com/kataras/iris/issues/367)

View File

@ -19,7 +19,7 @@
<br/> <br/>
<a href="https://github.com/kataras/iris/releases"><img src="https://img.shields.io/badge/%20version%20-%205.1.0%20-blue.svg?style=flat-square" alt="Releases"></a> <a href="https://github.com/kataras/iris/releases"><img src="https://img.shields.io/badge/%20version%20-%204%20LTS%20-blue.svg?style=flat-square" alt="Releases"></a>
<a href="https://github.com/iris-contrib/examples"><img src="https://img.shields.io/badge/%20examples-repository-3362c2.svg?style=flat-square" alt="Examples"></a> <a href="https://github.com/iris-contrib/examples"><img src="https://img.shields.io/badge/%20examples-repository-3362c2.svg?style=flat-square" alt="Examples"></a>
@ -71,6 +71,7 @@ Ideally suited for both experienced and novice Developers.
- Limit request body - Limit request body
- Localization i18N - Localization i18N
- Serve static files - Serve static files
- Cache
- Log requests - Log requests
- Define your format and output for the logger - Define your format and output for the logger
- Define custom HTTP errors handlers - Define custom HTTP errors handlers
@ -882,7 +883,7 @@ I recommend writing your API tests using this new library, [httpexpect](https://
Versioning Versioning
------------ ------------
Current: **5.1.0** Current: **v4 LTS**
Todo Todo
------------ ------------
@ -921,7 +922,7 @@ under the Apache Version 2 license found in the [LICENSE file](LICENSE).
[Travis]: http://travis-ci.org/kataras/iris [Travis]: http://travis-ci.org/kataras/iris
[License Widget]: https://img.shields.io/badge/license-Apache%20Version%202-E91E63.svg?style=flat-square [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 [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 [Release]: https://github.com/kataras/iris/releases
[Chat Widget]: https://img.shields.io/badge/community-chat%20-00BCD4.svg?style=flat-square [Chat Widget]: https://img.shields.io/badge/community-chat%20-00BCD4.svg?style=flat-square
[Chat]: https://kataras.rocket.chat/channel/iris [Chat]: https://kataras.rocket.chat/channel/iris

384
cache.go
View File

@ -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
}

View File

@ -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)
// }
// }

View File

@ -1133,7 +1133,7 @@ func (ctx *Context) MaxAge() int64 {
// InvalidateCache clears the cache manually for this request uri context's handler's route // InvalidateCache clears the cache manually for this request uri context's handler's route
func (ctx *Context) InvalidateCache() { func (ctx *Context) InvalidateCache() {
ctx.framework.cacheService.InvalidateCache(GetCacheKey(ctx)) ctx.framework.InvalidateCache(ctx)
} }
// Log logs to the iris defined logger // Log logs to the iris defined logger

85
iris.go
View File

@ -65,6 +65,7 @@ import (
"time" "time"
"bytes" "bytes"
"github.com/geekypanda/httpcache"
"github.com/kataras/go-errors" "github.com/kataras/go-errors"
"github.com/kataras/go-fs" "github.com/kataras/go-fs"
"github.com/kataras/go-serializer" "github.com/kataras/go-serializer"
@ -77,9 +78,9 @@ import (
const ( const (
// IsLongTermSupport flag is true when the below version number is a long-term-support version // 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 is the current version number of the Iris web framework
Version = "5.1.0" Version = "4"
banner = ` _____ _ banner = ` _____ _
|_ _| (_) |_ _| (_)
@ -97,9 +98,6 @@ var (
Plugins PluginContainer Plugins PluginContainer
Router fasthttp.RequestHandler Router fasthttp.RequestHandler
Websocket *WebsocketServer 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 // 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. // never fires false, if the .Close called then the channel is re-allocating.
// the channel remains open until you close it. // the channel remains open until you close it.
@ -115,7 +113,7 @@ var (
// iris.Plugins // iris.Plugins
// iris.Router // iris.Router
// iris.Websocket // 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 // 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() { func ResetDefault() {
Default = New() Default = New()
@ -124,7 +122,6 @@ func ResetDefault() {
Plugins = Default.Plugins Plugins = Default.Plugins
Router = Default.Router Router = Default.Router
Websocket = Default.Websocket Websocket = Default.Websocket
SSH = Default.SSH
Available = Default.Available Available = Default.Available
} }
@ -142,7 +139,6 @@ type (
// FrameworkAPI contains the main Iris Public API // FrameworkAPI contains the main Iris Public API
FrameworkAPI interface { FrameworkAPI interface {
MuxAPI MuxAPI
CacheServiceAPI
Set(...OptionSetter) Set(...OptionSetter)
Must(error) Must(error)
Build() Build()
@ -169,6 +165,8 @@ type (
TemplateString(string, interface{}, ...map[string]interface{}) string TemplateString(string, interface{}, ...map[string]interface{}) string
TemplateSourceString(string, interface{}) string TemplateSourceString(string, interface{}) string
SerializeToString(string, interface{}, ...map[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') // Framework is our God |\| Google.Search('Greek mythology Iris')
@ -176,7 +174,6 @@ type (
// Implements the FrameworkAPI // Implements the FrameworkAPI
Framework struct { Framework struct {
*muxAPI *muxAPI
*cacheService
// HTTP Server runtime fields is the iris' defined main server, developer can use unlimited number of servers // 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 // note: they're available after .Build, and .Serve/Listen/ListenTLS/ListenLETSENCRYPT/ListenUNIX
ln net.Listener ln net.Listener
@ -198,7 +195,6 @@ type (
Logger *log.Logger Logger *log.Logger
Plugins PluginContainer Plugins PluginContainer
Websocket *WebsocketServer Websocket *WebsocketServer
SSH *SSHServer
} }
) )
@ -216,12 +212,11 @@ func New(setters ...OptionSetter) *Framework {
s := &Framework{} s := &Framework{}
s.Set(setters...) 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 // 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.Logger = log.New(s.Config.LoggerOut, s.Config.LoggerPreffix, log.LstdFlags)
s.Plugins = newPluginContainer(s.Logger) s.Plugins = newPluginContainer(s.Logger)
s.SSH = NewSSHServer()
} }
// rendering // rendering
@ -232,8 +227,6 @@ func New(setters ...OptionSetter) *Framework {
"url": s.URL, "url": s.URL,
"urlpath": s.Path, "urlpath": s.Path,
}) })
// set the cache service
s.cacheService = newCacheService()
} }
// websocket & sessions // websocket & sessions
@ -353,11 +346,6 @@ func (s *Framework) Build() {
s.sessions.Set(s.Config.Sessions, sessions.DisableAutoGC(false)) 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 != "" { if s.Config.Websocket.Endpoint != "" {
// register the websocket server and listen to websocket connections when/if $instance.Websocket.OnConnection called by the dev // 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) 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 // updates, to cover the default station's irs.Config.checkForUpdates
// note: we could use the IsDevelopment configuration field to do that BUT // 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 // 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 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------------------------------ // ----------------------------------MuxAPI implementation------------------------------

705
ssh.go
View File

@ -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)))
}
}