1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-18 02:17:05 +00:00

Publish the new version ✈️ | Look description please!

# FAQ

### Looking for free support?

	http://support.iris-go.com
    https://kataras.rocket.chat/channel/iris

### Looking for previous versions?

    https://github.com/kataras/iris#version

### Should I upgrade my Iris?

Developers are not forced to upgrade if they don't really need it. Upgrade whenever you feel ready.
> Iris uses the [vendor directory](https://docs.google.com/document/d/1Bz5-UB7g2uPBdOx-rw5t9MxJwkfpx90cqG9AFL0JAYo) feature, so you get truly reproducible builds, as this method guards against upstream renames and deletes.

**How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris`.
For further installation support, please click [here](http://support.iris-go.com/d/16-how-to-install-iris-web-framework).

### About our new home page
    http://iris-go.com

Thanks to [Santosh Anand](https://github.com/santoshanand) the http://iris-go.com has been upgraded and it's really awesome!

[Santosh](https://github.com/santoshanand) is a freelancer, he has a great knowledge of nodejs and express js, Android, iOS, React Native, Vue.js etc, if you need a developer to find or create a solution for your problem or task, please contact with him.

The amount of the next two or three donations you'll send they will be immediately transferred to his own account balance, so be generous please!

Read more at https://github.com/kataras/iris/blob/master/HISTORY.md


Former-commit-id: eec2d71bbe011d6b48d2526eb25919e36e5ad94e
This commit is contained in:
kataras
2017-06-03 23:22:52 +03:00
parent 03bcadadec
commit 5e4b63acb2
330 changed files with 35786 additions and 17316 deletions

27
cache/LICENSE vendored Normal file
View File

@@ -0,0 +1,27 @@
Copyright (c) 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Gerasimos Maropoulos nor the name of his
username, kataras, may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

70
cache/cache.go vendored Normal file
View File

@@ -0,0 +1,70 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/* Package cache provides cache capabilities with rich support of options and rules.
Example code:
import (
"time"
"github.com/kataras/iris"
"github.com/kataras/iris/cache"
"github.com/kataras/iris/context"
)
func main(){
app := iris.Default()
cachedHandler := cache.CacheHandler(h, 2 *time.Minute)
app.Get("/hello", cachedHandler)
app.Run(iris.Addr(":8080"))
}
func h(ctx context.Context) {
ctx.HTML("<h1> Hello, this should be cached. Every 2 minutes it will be refreshed, check your browser's inspector</h1>")
}
*/
package cache
import (
"time"
"github.com/kataras/iris/cache/client"
"github.com/kataras/iris/context"
)
// Cache accepts two parameters
// first is the context.Handler which you want to cache its result
// the second is, optional, the cache Entry's expiration duration
// if the expiration <=2 seconds then expiration is taken by the "cache-control's maxage" header
// returns context.Handler, which you can use as your default router or per-route handler
//
// All type of responses are cached, templates, json, text, anything.
//
// You can add validators with this function
func Cache(bodyHandler context.Handler, expiration time.Duration) *client.Handler {
return client.NewHandler(bodyHandler, expiration)
}
// CacheHandler accepts two parameters
// first is the context.Handler which you want to cache its result
// the second is, optional, the cache Entry's expiration duration
// if the expiration <=2 seconds then expiration is taken by the "cache-control's maxage" header
// returns context.Handler, which you can use as your default router or per-route handler
//
// All type of responses are cached, templates, json, text, anything.
//
// it returns the context.Handler, for more options use the .Cache .
func CacheHandler(bodyHandler context.Handler, expiration time.Duration) context.Handler {
return Cache(bodyHandler, expiration).ServeHTTP
}
var (
// NoCache called when a particular handler is not valid for cache.
// If this function called inside a handler then the handler is not cached
// even if it's surrounded with the Cache/CacheFunc/CacheRemote wrappers.
NoCache = client.NoCache
)

29
cache/cfg/cfg.go vendored Normal file
View File

@@ -0,0 +1,29 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cfg
import "time"
// The constants be used by both client and server
var (
FailStatus = 400
SuccessStatus = 200
ContentHTML = "text/html; charset=utf-8"
ContentTypeHeader = "Content-Type"
StatusCodeHeader = "Status"
QueryCacheKey = "cache_key"
QueryCacheDuration = "cache_duration"
QueryCacheStatusCode = "cache_status_code"
QueryCacheContentType = "cache_content_type"
RequestCacheTimeout = 5 * time.Second
)
// NoCacheHeader is the static header key which is setted to the response when NoCache is called,
// used inside nethttp and fhttp Skippers.
var NoCacheHeader = "X-No-Cache"
// MinimumCacheDuration is the minimum duration from time.Now
// which is allowed between cache save and cache clear
var MinimumCacheDuration = 2 * time.Second

173
cache/client/client.go vendored Normal file
View File

@@ -0,0 +1,173 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package client
import (
"bytes"
"io/ioutil"
"net/http"
"time"
"github.com/kataras/iris/cache/cfg"
"github.com/kataras/iris/cache/client/rule"
"github.com/kataras/iris/cache/uri"
"github.com/kataras/iris/context"
)
// ClientHandler is the client-side handler
// for each of the cached route paths's response
// register one client handler per route.
//
// it's just calls a remote cache service server/handler,
// which lives on other, external machine.
//
type ClientHandler struct {
// bodyHandler the original route's handler
bodyHandler context.Handler
// Rule optional validators for pre cache and post cache actions
//
// See more at ruleset.go
rule rule.Rule
life time.Duration
remoteHandlerURL string
}
// NewClientHandler returns a new remote client handler
// which asks the remote handler the cached entry's response
// with a GET request, or add a response with POST request
// these all are done automatically, users can use this
// handler as they use the local.go/NewHandler
//
// the ClientHandler is useful when user
// wants to apply horizontal scaling to the app and
// has a central http server which handles
func NewClientHandler(bodyHandler context.Handler, life time.Duration, remote string) *ClientHandler {
return &ClientHandler{
bodyHandler: bodyHandler,
rule: DefaultRuleSet,
life: life,
remoteHandlerURL: remote,
}
}
// Rule sets the ruleset for this handler,
// see internal/net/http/ruleset.go for more information.
//
// returns itself.
func (h *ClientHandler) Rule(r rule.Rule) *ClientHandler {
if r == nil {
// if nothing passed then use the allow-everything rule
r = rule.Satisfied()
}
h.rule = r
return h
}
// AddRule adds a rule in the chain, the default rules are executed first.
//
// returns itself.
func (h *ClientHandler) AddRule(r rule.Rule) *ClientHandler {
if r == nil {
return h
}
h.rule = rule.Chained(h.rule, r)
return h
}
// Client is used inside the global Request function
// this client is an exported to give you a freedom of change its Transport, Timeout and so on(in case of ssl)
var Client = &http.Client{Timeout: cfg.RequestCacheTimeout}
const (
methodGet = "GET"
methodPost = "POST"
)
// ServeHTTP , or remote cache client whatever you like, it's the client-side function of the ServeHTTP
// sends a request to the server-side remote cache Service and sends the cached response to the frontend client
// it is used only when you achieved something like horizontal scaling (separate machines)
// look ../remote/remote.ServeHTTP for more
//
// if cache din't find then it sends a POST request and save the bodyHandler's body to the remote cache.
//
// It takes 3 parameters
// the first is the remote address (it's the address you started your http server which handled by the Service.ServeHTTP)
// the second is the handler (or the mux) you want to cache
// and the third is the, optionally, cache expiration,
// which is used to set cache duration of this specific cache entry to the remote cache service
// if <=minimumAllowedCacheDuration then the server will try to parse from "cache-control" header
//
// client-side function
func (h *ClientHandler) ServeHTTP(ctx context.Context) {
// check for deniers, if at least one of them return true
// for this specific request, then skip the whole cache
if !h.rule.Claim(ctx) {
h.bodyHandler(ctx)
return
}
uri := &uri.URIBuilder{}
uri.ServerAddr(h.remoteHandlerURL).ClientURI(ctx.Request().URL.RequestURI()).ClientMethod(ctx.Request().Method)
// set the full url here because below we have other issues, probably net/http bugs
request, err := http.NewRequest(methodGet, uri.String(), nil)
if err != nil {
//// println("error when requesting to the remote service: " + err.Error())
// somehing very bad happens, just execute the user's handler and return
h.bodyHandler(ctx)
return
}
// println("GET Do to the remote cache service with the url: " + request.URL.String())
response, err := Client.Do(request)
if err != nil || response.StatusCode == cfg.FailStatus {
// if not found on cache, then execute the handler and save the cache to the remote server
recorder := ctx.Recorder()
h.bodyHandler(ctx)
// check if it's a valid response, if it's not then just return.
if !h.rule.Valid(ctx) {
return
}
// save to the remote cache
// we re-create the request for any case
body := recorder.Body()[0:]
if len(body) == 0 {
//// println("Request: len body is zero, do nothing")
return
}
uri.StatusCode(recorder.StatusCode())
uri.Lifetime(h.life)
uri.ContentType(recorder.Header().Get(cfg.ContentTypeHeader))
request, err = http.NewRequest(methodPost, uri.String(), bytes.NewBuffer(body)) // yes new buffer every time
// println("POST Do to the remote cache service with the url: " + request.URL.String())
if err != nil {
//// println("Request: error on method Post of request to the remote: " + err.Error())
return
}
// go Client.Do(request)
Client.Do(request)
} else {
// get the status code , content type and the write the response body
ctx.ContentType(response.Header.Get(cfg.ContentTypeHeader))
ctx.StatusCode(response.StatusCode)
responseBody, err := ioutil.ReadAll(response.Body)
response.Body.Close()
if err != nil {
return
}
ctx.Write(responseBody)
}
}

116
cache/client/handler.go vendored Normal file
View File

@@ -0,0 +1,116 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package client
import (
"time"
"github.com/kataras/iris/cache/cfg"
"github.com/kataras/iris/cache/client/rule"
"github.com/kataras/iris/cache/entry"
"github.com/kataras/iris/context"
)
// Handler the local cache service handler contains
// the original bodyHandler, the memory cache entry and
// the validator for each of the incoming requests and post responses
type Handler struct {
// bodyHandler the original route's handler
bodyHandler context.Handler
// Rule optional validators for pre cache and post cache actions
//
// See more at ruleset.go
rule rule.Rule
// entry is the memory cache entry
entry *entry.Entry
}
// NewHandler returns a new cached handler
func NewHandler(bodyHandler context.Handler,
expireDuration time.Duration) *Handler {
e := entry.NewEntry(expireDuration)
return &Handler{
bodyHandler: bodyHandler,
rule: DefaultRuleSet,
entry: e,
}
}
// Rule sets the ruleset for this handler.
//
// returns itself.
func (h *Handler) Rule(r rule.Rule) *Handler {
if r == nil {
// if nothing passed then use the allow-everything rule
r = rule.Satisfied()
}
h.rule = r
return h
}
// AddRule adds a rule in the chain, the default rules are executed first.
//
// returns itself.
func (h *Handler) AddRule(r rule.Rule) *Handler {
if r == nil {
return h
}
h.rule = rule.Chained(h.rule, r)
return h
}
func (h *Handler) ServeHTTP(ctx context.Context) {
// check for pre-cache validators, if at least one of them return false
// for this specific request, then skip the whole cache
if !h.rule.Claim(ctx) {
h.bodyHandler(ctx)
return
}
// check if we have a stored response( it is not expired)
res, exists := h.entry.Response()
if !exists {
// if it's not exists, then execute the original handler
// with our custom response recorder response writer
// because the net/http doesn't give us
// a built'n way to get the status code & body
recorder := ctx.Recorder()
h.bodyHandler(ctx)
// now that we have recordered the response,
// we are ready to check if that specific response is valid to be stored.
// check if it's a valid response, if it's not then just return.
if !h.rule.Valid(ctx) {
return
}
// no need to copy the body, its already done inside
body := recorder.Body()
if len(body) == 0 {
// if no body then just exit
return
}
// check for an expiration time if the
// given expiration was not valid then check for GetMaxAge &
// update the response & release the recorder
h.entry.Reset(recorder.StatusCode(), recorder.Header().Get(cfg.ContentTypeHeader), body, GetMaxAge(ctx.Request()))
return
}
// if it's valid then just write the cached results
ctx.ContentType(res.ContentType())
ctx.StatusCode(res.StatusCode())
ctx.Write(res.Body())
}

113
cache/client/response_recorder.go vendored Normal file
View File

@@ -0,0 +1,113 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package client
import (
"net/http"
"sync"
)
var rpool = sync.Pool{}
// AcquireResponseRecorder returns a ResponseRecorder
func AcquireResponseRecorder(underline http.ResponseWriter) *ResponseRecorder {
v := rpool.Get()
var res *ResponseRecorder
if v != nil {
res = v.(*ResponseRecorder)
} else {
res = &ResponseRecorder{}
}
res.underline = underline
return res
}
// ReleaseResponseRecorder releases a ResponseRecorder which has been previously received by AcquireResponseRecorder
func ReleaseResponseRecorder(res *ResponseRecorder) {
res.underline = nil
res.statusCode = 0
res.chunks = res.chunks[0:0]
rpool.Put(res)
}
// ResponseRecorder is used by httpcache to be able to get the Body and the StatusCode of a request handler
type ResponseRecorder struct {
underline http.ResponseWriter
chunks [][]byte // 2d because .Write can be called more than one time in the same handler and we want to cache all of them
statusCode int // the saved status code which will be used from the cache service
}
// Body joins the chunks to one []byte slice, this is the full body
func (res *ResponseRecorder) Body() []byte {
var body []byte
for i := range res.chunks {
body = append(body, res.chunks[i]...)
}
return body
}
// ContentType returns the header's value of "Content-Type"
func (res *ResponseRecorder) ContentType() string {
return res.Header().Get("Content-Type")
}
// StatusCode returns the status code, if not given then returns 200
// but doesn't changes the existing behavior
func (res *ResponseRecorder) StatusCode() int {
if res.statusCode == 0 {
return 200
}
return res.statusCode
}
// Header returns the header map that will be sent by
// WriteHeader. Changing the header after a call to
// WriteHeader (or Write) has no effect unless the modified
// headers were declared as trailers by setting the
// "Trailer" header before the call to WriteHeader (see example).
// To suppress implicit response headers, set their value to nil.
func (res *ResponseRecorder) Header() http.Header {
return res.underline.Header()
}
// Write writes the data to the connection as part of an HTTP reply.
//
// If WriteHeader has not yet been called, Write calls
// WriteHeader(http.StatusOK) before writing the data. If the Header
// does not contain a Content-Type line, Write adds a Content-Type set
// to the result of passing the initial 512 bytes of written data to
// DetectContentType.
//
// Depending on the HTTP protocol version and the client, calling
// Write or WriteHeader may prevent future reads on the
// Request.Body. For HTTP/1.x requests, handlers should read any
// needed request body data before writing the response. Once the
// headers have been flushed (due to either an explicit Flusher.Flush
// call or writing enough data to trigger a flush), the request body
// may be unavailable. For HTTP/2 requests, the Go HTTP server permits
// handlers to continue to read the request body while concurrently
// writing the response. However, such behavior may not be supported
// by all HTTP/2 clients. Handlers should read before writing if
// possible to maximize compatibility.
func (res *ResponseRecorder) Write(contents []byte) (int, error) {
if res.statusCode == 0 { // if not setted set it here
res.WriteHeader(http.StatusOK)
}
res.chunks = append(res.chunks, contents)
return res.underline.Write(contents)
}
// WriteHeader sends an HTTP response header with status code.
// If WriteHeader is not called explicitly, the first call to Write
// will trigger an implicit WriteHeader(http.StatusOK).
// Thus explicit calls to WriteHeader are mainly used to
// send error codes.
func (res *ResponseRecorder) WriteHeader(statusCode int) {
if res.statusCode == 0 { // set it only if not setted already, we don't want logs about multiple sends
res.statusCode = statusCode
res.underline.WriteHeader(statusCode)
}
}

60
cache/client/rule/chained.go vendored Normal file
View File

@@ -0,0 +1,60 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package rule
import (
"github.com/kataras/iris/context"
)
// chainedRule is a Rule with next Rule
type chainedRule struct {
Rule
next Rule
}
var _ Rule = &chainedRule{}
// chainedSingle returns a new rule witch has a next rule too
func chainedSingle(rule Rule, next Rule) Rule {
if next == nil {
next = Satisfied()
}
return &chainedRule{
Rule: rule,
next: next,
}
}
// Chained returns a new rule which has more than one coming next ruleset
func Chained(rule Rule, next ...Rule) Rule {
if len(next) == 0 {
return chainedSingle(rule, nil)
}
c := chainedSingle(rule, next[0])
for i := 1; i < len(next); i++ {
c = chainedSingle(c, next[i])
}
return c
}
// Claim validator
func (c *chainedRule) Claim(ctx context.Context) bool {
if !c.Rule.Claim(ctx) {
return false
}
return c.next.Claim(ctx)
}
// Valid validator
func (c *chainedRule) Valid(ctx context.Context) bool {
if !c.Rule.Valid(ctx) {
return false
}
return c.next.Valid(ctx)
}

53
cache/client/rule/conditional.go vendored Normal file
View File

@@ -0,0 +1,53 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package rule
import (
"github.com/kataras/iris/context"
)
// Conditional is a Rule witch adds a predicate in order to its methods to execute
type conditionalRule struct {
claimPredicate func() bool
validPredicate func() bool
}
var emptyConditionalPredicate = func() bool {
return true
}
var _ Rule = &conditionalRule{}
// Conditional returns a new rule witch has conditionals
func Conditional(claimPredicate func() bool, validPredicate func() bool) Rule {
if claimPredicate == nil {
claimPredicate = emptyConditionalPredicate
}
if validPredicate == nil {
validPredicate = emptyConditionalPredicate
}
return &conditionalRule{
claimPredicate: claimPredicate,
validPredicate: validPredicate,
}
}
// Claim validator
func (c *conditionalRule) Claim(ctx context.Context) bool {
if !c.claimPredicate() {
return false
}
return true
}
// Valid validator
func (c *conditionalRule) Valid(ctx context.Context) bool {
if !c.validPredicate() {
return false
}
return true
}

59
cache/client/rule/header.go vendored Normal file
View File

@@ -0,0 +1,59 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package rule
import (
"github.com/kataras/iris/context"
"github.com/kataras/iris/cache/ruleset"
)
// The HeaderPredicate should be alived on each of $package/rule BUT GOLANG DOESN'T SUPPORT type alias and I don't want to have so many copies around
// read more at ../../ruleset.go
// headerRule is a Rule witch receives and checks for a header predicates
// request headers on Claim and response headers on Valid.
type headerRule struct {
claim ruleset.HeaderPredicate
valid ruleset.HeaderPredicate
}
var _ Rule = &headerRule{}
// Header returns a new rule witch claims and execute the post validations trough headers
func Header(claim ruleset.HeaderPredicate, valid ruleset.HeaderPredicate) Rule {
if claim == nil {
claim = ruleset.EmptyHeaderPredicate
}
if valid == nil {
valid = ruleset.EmptyHeaderPredicate
}
return &headerRule{
claim: claim,
valid: valid,
}
}
// HeaderClaim returns a header rule which cares only about claiming (pre-validation)
func HeaderClaim(claim ruleset.HeaderPredicate) Rule {
return Header(claim, nil)
}
// HeaderValid returns a header rule which cares only about valid (post-validation)
func HeaderValid(valid ruleset.HeaderPredicate) Rule {
return Header(nil, valid)
}
// Claim validator
func (h *headerRule) Claim(ctx context.Context) bool {
return h.claim(ctx.Request().Header.Get)
}
// Valid validator
func (h *headerRule) Valid(ctx context.Context) bool {
return h.valid(ctx.ResponseWriter().Header().Get)
}

26
cache/client/rule/not_satisfied.go vendored Normal file
View File

@@ -0,0 +1,26 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package rule
import (
"github.com/kataras/iris/context"
)
type notSatisfiedRule struct{}
var _ Rule = &notSatisfiedRule{}
// NotSatisfied returns a rule which allows nothing
func NotSatisfied() Rule {
return &notSatisfiedRule{}
}
func (n *notSatisfiedRule) Claim(context.Context) bool {
return false
}
func (n *notSatisfiedRule) Valid(context.Context) bool {
return false
}

15
cache/client/rule/rule.go vendored Normal file
View File

@@ -0,0 +1,15 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package rule
import (
"github.com/kataras/iris/context"
)
// Rule a superset of validators
type Rule interface {
Claim(ctx context.Context) bool
Valid(ctx context.Context) bool
}

28
cache/client/rule/satisfied.go vendored Normal file
View File

@@ -0,0 +1,28 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package rule
import (
"github.com/kataras/iris/context"
)
type satisfiedRule struct{}
var _ Rule = &satisfiedRule{}
// Satisfied returns a rule which allows anything,
// it's usually the last rule on chained rules if no next rule is given,
// but it can be used outside of a chain too as a default allow-all rule.
func Satisfied() Rule {
return &satisfiedRule{}
}
func (n *satisfiedRule) Claim(context.Context) bool {
return true
}
func (n *satisfiedRule) Valid(context.Context) bool {
return true
}

97
cache/client/rule/validator.go vendored Normal file
View File

@@ -0,0 +1,97 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package rule
import (
"github.com/kataras/iris/context"
)
// Validators are introduced to implement the RFC about cache (https://tools.ietf.org/html/rfc7234#section-1.1).
// PreValidator like middleware, executes before the cache action begins, if a callback returns false
// then this specific cache action, with specific request, is ignored and the real (original)
// handler is executed instead.
//
// I'll not add all specifications here I'll give the opportunity (public API in the httpcache package-level)
// to the end-user to specify her/his ignore rules too (ignore-only for now).
//
// Each package, nethttp and fhttp should implement their own encapsulations because of different request object.
//
// One function, accepts the request and returns false if should be denied/ignore, otherwise true.
// if at least one return false then the original handler will execute as it's
// and the whole cache action(set & get) should be ignored, it will be never go to the step of post-cache validations.
type PreValidator func(context.Context) bool
// PostValidator type is is introduced to implement the second part of the RFC about cache.
//
// Q: What's the difference between this and a PreValidator?
// A: PreValidator runs BEFORE trying to get the cache, it cares only for the request
// and if at least one PreValidator returns false then it just runs the original handler and stop there, at the other hand
// a PostValidator runs if all PreValidators returns true and original handler is executed but with a response recorder,
// also the PostValidator should return true to store the cached response.
// Last, a PostValidator accepts a context
// in order to be able to catch the original handler's response,
// the PreValidator checks only for request.
//
// If a function of type of PostValidator returns true then the (shared-always) cache is allowed to be stored.
type PostValidator func(context.Context) bool
// validatorRule is a rule witch receives PreValidators and PostValidators
// it's a 'complete set of rules', you can call it as a Responsible Validator,
// it's used when you the user wants to check for special things inside a request and a response.
type validatorRule struct {
// preValidators a list of PreValidator functions, execute before real cache begins
// if at least one of them returns false then the original handler will execute as it's
// and the whole cache action(set & get) will be skipped for this specific client's request.
//
// Read-only 'runtime'
preValidators []PreValidator
// postValidators a list of PostValidator functions, execute after the original handler is executed with the response recorder
// and exactly before this cached response is saved,
// if at least one of them returns false then the response will be not saved for this specific client's request.
//
// Read-only 'runtime'
postValidators []PostValidator
}
var _ Rule = &validatorRule{}
// DefaultValidator returns a new validator which contains the default pre and post cache validators
func DefaultValidator() Rule { return Validator(nil, nil) }
// Validator receives the preValidators and postValidators and returns a new Validator rule
func Validator(preValidators []PreValidator, postValidators []PostValidator) Rule {
return &validatorRule{
preValidators: preValidators,
postValidators: postValidators,
}
}
// Claim returns true if incoming request can claim for a cached handler
// the original handler should run as it is and exit
func (v *validatorRule) Claim(ctx context.Context) bool {
// check for pre-cache validators, if at least one of them return false
// for this specific request, then skip the whole cache
for _, shouldProcess := range v.preValidators {
if !shouldProcess(ctx) {
return false
}
}
return true
}
// Valid returns true if incoming request and post-response from the original handler
// is valid to be store to the cache, if not(false) then the consumer should just exit
// otherwise(true) the consumer should store the cached response
func (v *validatorRule) Valid(ctx context.Context) bool {
// check if it's a valid response, if it's not then just return.
for _, valid := range v.postValidators {
if !valid(ctx) {
return false
}
}
return true
}

38
cache/client/ruleset.go vendored Normal file
View File

@@ -0,0 +1,38 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package client
import (
"github.com/kataras/iris/cache/cfg"
"github.com/kataras/iris/cache/client/rule"
"github.com/kataras/iris/cache/ruleset"
"github.com/kataras/iris/context"
)
// DefaultRuleSet is a list of the default pre-cache validators
// which exists in ALL handlers, local and remote.
var DefaultRuleSet = rule.Chained(
// #1 A shared cache MUST NOT use a cached response to a request with an
// Authorization header field
rule.HeaderClaim(ruleset.AuthorizationRule),
// #2 "must-revalidate" and/or
// "s-maxage" response directives are not allowed to be served stale
// (Section 4.2.4) by shared caches. In particular, a response with
// either "max-age=0, must-revalidate" or "s-maxage=0" cannot be used to
// satisfy a subsequent request without revalidating it on the origin
// server.
rule.HeaderClaim(ruleset.MustRevalidateRule),
rule.HeaderClaim(ruleset.ZeroMaxAgeRule),
// #3 custom No-Cache header used inside this library
// for BOTH request and response (after get-cache action)
rule.Header(ruleset.NoCacheRule, ruleset.NoCacheRule),
)
// NoCache called when a particular handler is not valid for cache.
// If this function called inside a handler then the handler is not cached
// even if it's surrounded with the Cache/CacheFunc wrappers.
func NoCache(ctx context.Context) {
ctx.Header(cfg.NoCacheHeader, "true")
}

24
cache/client/utils.go vendored Normal file
View File

@@ -0,0 +1,24 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package client
import (
"net/http"
"time"
"github.com/kataras/iris/cache/entry"
)
// GetMaxAge parses the "Cache-Control" header
// and returns a LifeChanger which can be passed
// to the response's Reset
func GetMaxAge(r *http.Request) entry.LifeChanger {
return func() time.Duration {
cacheControlHeader := r.Header.Get("Cache-Control")
// headerCacheDur returns the seconds
headerCacheDur := entry.ParseMaxAge(cacheControlHeader)
return time.Duration(headerCacheDur) * time.Second
}
}

109
cache/entry/entry.go vendored Normal file
View File

@@ -0,0 +1,109 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package entry
import (
"time"
"github.com/kataras/iris/cache/cfg"
)
// Entry is the cache entry
// contains the expiration datetime and the response
type Entry struct {
life time.Duration
// ExpiresAt is the time which this cache will not be available
expiresAt time.Time
// Response the response should be served to the client
response *Response
// but we need the key to invalidate manually...xmm
// let's see for that later, maybe we make a slice instead
// of store map
}
// NewEntry returns a new cache entry
// it doesn't sets the expiresAt & the response
// because these are setting each time on Reset
func NewEntry(duration time.Duration) *Entry {
// if given duration is not <=0 (which means finds from the headers)
// then we should check for the MinimumCacheDuration here
if duration >= 0 && duration < cfg.MinimumCacheDuration {
duration = cfg.MinimumCacheDuration
}
return &Entry{
life: duration,
response: &Response{},
}
}
// Response gets the cache response contents
// if it's valid returns them with a true value
// otherwise returns nil, false
func (e *Entry) Response() (*Response, bool) {
if !e.valid() {
// it has been expired
return nil, false
}
return e.response, true
}
// valid returns true if this entry's response is still valid
// or false if the expiration time passed
func (e *Entry) valid() bool {
return !time.Now().After(e.expiresAt)
}
// LifeChanger is the function which returns
// a duration which will be compared with the current
// entry's (cache life) duration
// and execute the LifeChanger func
// to set the new life time
type LifeChanger func() time.Duration
// ChangeLifetime modifies the life field
// which is the life duration of the cached response
// of this cache entry
//
// useful when we find a max-age header from the handler
func (e *Entry) ChangeLifetime(fdur LifeChanger) {
if e.life < cfg.MinimumCacheDuration {
newLifetime := fdur()
if newLifetime > e.life {
e.life = newLifetime
} else {
// if even the new lifetime is less than MinimumCacheDuration
// then change set it explicitly here
e.life = cfg.MinimumCacheDuration
}
}
}
// Reset called each time the entry is expired
// and the handler calls this after the original handler executed
// to re-set the response with the new handler's content result
func (e *Entry) Reset(statusCode int, contentType string,
body []byte, lifeChanger LifeChanger) {
if e.response == nil {
e.response = &Response{}
}
if statusCode > 0 {
e.response.statusCode = statusCode
}
if contentType != "" {
e.response.contentType = contentType
}
e.response.body = body
// check if a given life changer provided
// and if it does then execute the change life time
if lifeChanger != nil {
e.ChangeLifetime(lifeChanger)
}
e.expiresAt = time.Now().Add(e.life)
}

39
cache/entry/response.go vendored Normal file
View File

@@ -0,0 +1,39 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package entry
// Response is the cached response will be send to the clients
// its fields setted at runtime on each of the non-cached executions
// non-cached executions = first execution, and each time after
// cache expiration datetime passed
type Response struct {
// statusCode for the response cache handler
statusCode int
// contentType for the response cache handler
contentType string
// body is the contents will be served by the cache handler
body []byte
}
// StatusCode returns a valid status code
func (r *Response) StatusCode() int {
if r.statusCode <= 0 {
r.statusCode = 200
}
return r.statusCode
}
// ContentType returns a valid content type
func (r *Response) ContentType() string {
if r.contentType == "" {
r.contentType = "text/html; charset=utf-8"
}
return r.contentType
}
// Body returns contents will be served by the cache handler
func (r *Response) Body() []byte {
return r.body
}

28
cache/entry/util.go vendored Normal file
View File

@@ -0,0 +1,28 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package entry
import (
"regexp"
"strconv"
)
var maxAgeExp = regexp.MustCompile(`maxage=(\d+)`)
// ParseMaxAge parses the max age from the receiver parameter, "cache-control" header
// returns seconds as int64
// if header not found or parse failed then it returns -1
func ParseMaxAge(header string) int64 {
if header == "" {
return -1
}
m := maxAgeExp.FindStringSubmatch(header)
if len(m) == 2 {
if v, err := strconv.Atoi(m[1]); err == nil {
return int64(v)
}
}
return -1
}

47
cache/ruleset/ruleset.go vendored Normal file
View File

@@ -0,0 +1,47 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package ruleset provides the basics rules which are being extended by rules.
package ruleset
// The shared header-mostly rules for both nethttp and fasthttp
var (
AuthorizationRule = func(header GetHeader) bool {
return header("Authorization") == "" &&
header("Proxy-Authenticate") == ""
}
MustRevalidateRule = func(header GetHeader) bool {
return header("Must-Revalidate") == ""
}
ZeroMaxAgeRule = func(header GetHeader) bool {
return header("S-Maxage") != "0" &&
header("Max-Age") != "0"
}
NoCacheRule = func(header GetHeader) bool {
return header("No-Cache") != "true"
}
)
// THESE ARE HERE BECAUSE THE GOLANG DOESN'T SUPPORTS THE F.... INTERFACE ALIAS, THIS SHOULD EXISTS ONLY ON /$package/rule
// or somehow move interface generic rules (such as conditional, header) here, because the code sharing is exactly THE SAME
// except the -end interface, this on other language can be designing very very nice but here no OOP so we stuck on this,
// it's better than before but not as I wanted to be.
// They will make me to forget my software design skills,
// they (the language's designers) rollback the feature of type alias, BLOG POSTING that is an UNUSEFUL feature.....
// GetHeader is a type of func which receives a func of string which returns a string
// used to get headers values, both request's and response's.
type GetHeader func(string) string
// HeaderPredicate is a type of func which receives a func of string which returns a string and a boolean,
// used to get headers values, both request's and response's.
type HeaderPredicate func(GetHeader) bool
// EmptyHeaderPredicate returns always true
var EmptyHeaderPredicate = func(GetHeader) bool {
return true
}

118
cache/uri/uribuilder.go vendored Normal file
View File

@@ -0,0 +1,118 @@
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uri
import (
"net/url"
"strconv"
"strings"
"time"
"github.com/kataras/iris/cache/cfg"
)
// URIBuilder is the requested url builder
// which keeps all the information the server
// should know to save or retrieve a cached entry's response
// used on client-side only
// both from net/http and valyala/fasthttp
type URIBuilder struct {
serverAddr,
clientMethod,
clientURI string
cacheLifetime time.Duration
cacheStatuscode int
cacheContentType string
}
// ServerAddr sets the server address for the final request url
func (r *URIBuilder) ServerAddr(s string) *URIBuilder {
r.serverAddr = s
return r
}
// ClientMethod sets the method which the client should call the remote server's handler
// used to build the final request url too
func (r *URIBuilder) ClientMethod(s string) *URIBuilder {
r.clientMethod = s
return r
}
// ClientURI sets the client path for the final request url
func (r *URIBuilder) ClientURI(s string) *URIBuilder {
r.clientURI = s
return r
}
// Lifetime sets the cache lifetime for the final request url
func (r *URIBuilder) Lifetime(d time.Duration) *URIBuilder {
r.cacheLifetime = d
return r
}
// StatusCode sets the cache status code for the final request url
func (r *URIBuilder) StatusCode(code int) *URIBuilder {
r.cacheStatuscode = code
return r
}
// ContentType sets the cache content type for the final request url
func (r *URIBuilder) ContentType(s string) *URIBuilder {
r.cacheContentType = s
return r
}
// String returns the full url which should be passed to get a cache entry response back
// (it could be setted by server too but we need some client-freedom on the requested key)
// in order to be sure that the registered cache entries are unique among different clients with the same key
// note1: we do it manually*,
// note2: on fasthttp that is not required because the query args added as expected but we will use it there too to be align with net/http
func (r URIBuilder) String() string {
return r.build()
}
func (r URIBuilder) build() string {
remoteURL := r.serverAddr
// fasthttp appends the "/" in the last uri (with query args also, that's probably a fasthttp bug which I'll fix later)
// for now lets make that check:
if !strings.HasSuffix(remoteURL, "/") {
remoteURL += "/"
}
scheme := "http://"
// validate the remoteURL, should contains a scheme, if not then check if the client has given a scheme and if so
// use that for the server too
if !strings.Contains(remoteURL, "://") {
if strings.Contains(remoteURL, ":443") || strings.Contains(remoteURL, ":https") {
remoteURL = "https://" + remoteURL
} else {
remoteURL = scheme + "://" + remoteURL
}
}
var cacheDurationStr, statusCodeStr string
if r.cacheLifetime.Seconds() > 0 {
cacheDurationStr = strconv.Itoa(int(r.cacheLifetime.Seconds()))
}
if r.cacheStatuscode > 0 {
statusCodeStr = strconv.Itoa(r.cacheStatuscode)
}
s := remoteURL + "?" + cfg.QueryCacheKey + "=" + url.QueryEscape(r.clientMethod+scheme+r.clientURI)
if cacheDurationStr != "" {
s += "&" + cfg.QueryCacheDuration + "=" + url.QueryEscape(cacheDurationStr)
}
if statusCodeStr != "" {
s += "&" + cfg.QueryCacheStatusCode + "=" + url.QueryEscape(statusCodeStr)
}
if r.cacheContentType != "" {
s += "&" + cfg.QueryCacheContentType + "=" + url.QueryEscape(r.cacheContentType)
}
return s
}