mirror of
https://github.com/kataras/iris.git
synced 2025-03-14 08:16:28 +01:00
Prepare for 4.0.0 gopkg.in for-ever package (All 20+ other repositories refactored) including gitbook and examples
This commit is contained in:
parent
6f22da7622
commit
32e3cbede1
21
HISTORY.md
21
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`.
|
**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)
|
||||||
|
|
|
@ -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
384
cache.go
|
@ -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
|
|
||||||
}
|
|
127
cache_test.go
127
cache_test.go
|
@ -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)
|
|
||||||
// }
|
|
||||||
// }
|
|
|
@ -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
85
iris.go
|
@ -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
705
ssh.go
|
@ -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)))
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user