1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-27 06:47:08 +00:00

20 days of unstoppable work. Waiting fo go 1.8, I didn't finish yet, some touches remains.

Former-commit-id: ed84f99c89f43fe5e980a8e6d0ee22c186f0e1b9
This commit is contained in:
Gerasimos (Makis) Maropoulos
2017-02-14 05:54:11 +02:00
parent 2b2a205e63
commit 244a59e055
108 changed files with 9016 additions and 7596 deletions

View File

@@ -17,12 +17,14 @@ import (
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/iris-contrib/formBinder"
"github.com/kataras/go-errors"
"github.com/kataras/go-fs"
"github.com/kataras/go-sessions"
"github.com/kataras/go-template"
)
const (
@@ -119,6 +121,85 @@ func (r *requestValues) Reset() {
*r = (*r)[:0]
}
type (
// ContextPool is a set of temporary *Context that may be individually saved and
// retrieved.
//
// Any item stored in the Pool may be removed automatically at any time without
// notification. If the Pool holds the only reference when this happens, the
// item might be deallocated.
//
// The ContextPool is safe for use by multiple goroutines simultaneously.
//
// ContextPool's purpose is to cache allocated but unused Contexts for later reuse,
// relieving pressure on the garbage collector.
ContextPool interface {
// Acquire returns a Context from pool.
// See Release.
Acquire(w http.ResponseWriter, r *http.Request) *Context
// Release puts a Context back to its pull, this function releases its resources.
// See Acquire.
Release(ctx *Context)
// Framework is never used, except when you're in a place where you don't have access to the *iris.Framework station
// but you need to fire a func or check its Config.
//
// Used mostly inside external routers to take the .Config.VHost
// without the need of other param receivers and refactors when changes
//
// note: we could make a variable inside contextPool which would be received by newContextPool
// but really doesn't need, we just need to borrow a context: we are in pre-build state
// so the server is not actually running yet, no runtime performance cost.
Framework() *Framework
// Run is a combination of Acquire and Release , between these two the `runner` runs,
// when `runner` finishes its job then the Context is being released.
Run(w http.ResponseWriter, r *http.Request, runner func(ctx *Context))
}
contextPool struct {
pool sync.Pool
}
)
var _ ContextPool = &contextPool{}
func (c *contextPool) Acquire(w http.ResponseWriter, r *http.Request) *Context {
ctx := c.pool.Get().(*Context)
ctx.ResponseWriter = acquireResponseWriter(w)
ctx.Request = r
return ctx
}
func (c *contextPool) Release(ctx *Context) {
// flush the body (on recorder) or just the status code (on basic response writer)
// when all finished
ctx.ResponseWriter.flushResponse()
ctx.Middleware = nil
ctx.session = nil
ctx.Request = nil
///TODO:
ctx.ResponseWriter.releaseMe()
ctx.values.Reset()
c.pool.Put(ctx)
}
func (c *contextPool) Framework() *Framework {
ctx := c.pool.Get().(*Context)
s := ctx.framework
c.pool.Put(ctx)
return s
}
func (c *contextPool) Run(w http.ResponseWriter, r *http.Request, runner func(*Context)) {
ctx := c.Acquire(w, r)
runner(ctx)
c.Release(ctx)
}
type (
// Map is just a conversion for a map[string]interface{}
@@ -198,17 +279,17 @@ func (ctx *Context) GetHandlerName() string {
// it can validate paths, has sessions, path parameters and all.
//
// You can find the Route by iris.Lookup("theRouteName")
// you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName")
// you can set a route name as: myRoute := iris.Default.Get("/mypath", handler)("theRouteName")
// that will set a name to the route and returns its iris.Route instance for further usage.
//
// It doesn't changes the global state, if a route was "offline" it remains offline.
//
// see ExecRouteAgainst(routeName, againstRequestPath string),
// iris.None(...) and iris.SetRouteOnline/SetRouteOffline
// iris.Default.None(...) and iris.Default.SetRouteOnline/SetRouteOffline
// For more details look: https://github.com/kataras/iris/issues/585
//
// Example: https://github.com/iris-contrib/examples/tree/master/route_state
func (ctx *Context) ExecRoute(r Route) *Context {
func (ctx *Context) ExecRoute(r RouteInfo) *Context {
return ctx.ExecRouteAgainst(r, ctx.Path())
}
@@ -219,23 +300,27 @@ func (ctx *Context) ExecRoute(r Route) *Context {
// it can validate paths, has sessions, path parameters and all.
//
// You can find the Route by iris.Lookup("theRouteName")
// you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName")
// you can set a route name as: myRoute := iris.Default.Get("/mypath", handler)("theRouteName")
// that will set a name to the route and returns its iris.Route instance for further usage.
//
// It doesn't changes the global state, if a route was "offline" it remains offline.
//
// see ExecRoute(routeName),
// iris.None(...) and iris.SetRouteOnline/SetRouteOffline
// iris.Default.None(...) and iris.Default.SetRouteOnline/SetRouteOffline
// For more details look: https://github.com/kataras/iris/issues/585
//
// Example: https://github.com/iris-contrib/examples/tree/master/route_state
func (ctx *Context) ExecRouteAgainst(r Route, againstRequestPath string) *Context {
func (ctx *Context) ExecRouteAgainst(r RouteInfo, againstRequestPath string) *Context {
if r != nil {
context := &(*ctx)
context.Middleware = context.Middleware[0:0]
context.values.Reset()
tree := ctx.framework.muxAPI.mux.getTree(r.Method(), r.Subdomain())
tree.entry.get(againstRequestPath, context)
context.Request.RequestURI = againstRequestPath
context.Request.URL.Path = againstRequestPath
context.Request.URL.RawPath = againstRequestPath
ctx.framework.policies.RouterReversionPolicy.RouteContextLinker(r, context)
// tree := ctx.framework.muxAPI.mux.getTree(r.Method(), r.Subdomain())
// tree.entry.get(againstRequestPath, context)
if len(context.Middleware) > 0 {
context.Do()
return context
@@ -251,16 +336,17 @@ func (ctx *Context) ExecRouteAgainst(r Route, againstRequestPath string) *Contex
// then use the: if c := ExecRoute(r); c == nil { /* move to the next, the route is not valid */ }
//
// You can find the Route by iris.Lookup("theRouteName")
// you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName")
// you can set a route name as: myRoute := iris.Default.Get("/mypath", handler)("theRouteName")
// that will set a name to the route and returns its iris.Route instance for further usage.
//
// if the route found then it executes that and don't continue to the next handler
// if not found then continue to the next handler
func Prioritize(r Route) HandlerFunc {
func Prioritize(r RouteInfo) HandlerFunc {
if r != nil {
return func(ctx *Context) {
reqPath := ctx.Path()
if strings.HasPrefix(reqPath, r.StaticPath()) {
staticPath := ctx.framework.policies.RouterReversionPolicy.StaticPath(r.Path())
if strings.HasPrefix(reqPath, staticPath) {
newctx := ctx.ExecRouteAgainst(r, reqPath)
if newctx == nil { // route not found.
ctx.EmitError(StatusNotFound)
@@ -317,7 +403,7 @@ func (ctx *Context) Subdomain() (subdomain string) {
func (ctx *Context) VirtualHostname() string {
realhost := ctx.Host()
hostname := realhost
virtualhost := ctx.framework.mux.hostname
virtualhost := ctx.framework.Config.VHost
if portIdx := strings.IndexByte(hostname, ':'); portIdx > 0 {
hostname = hostname[0:portIdx]
@@ -496,7 +582,7 @@ var (
// -------------------------------------------------------------------------------------
// NOTE: No default max body size http package has some built'n protection for DoS attacks
// See iris.Config.MaxBytesReader, https://github.com/golang/go/issues/2093#issuecomment-66057813
// See iris.Default.Config.MaxBytesReader, https://github.com/golang/go/issues/2093#issuecomment-66057813
// and https://github.com/golang/go/issues/2093#issuecomment-66057824
// LimitRequestBodySize is a middleware which sets a request body size limit for all next handlers
@@ -555,7 +641,7 @@ func (u UnmarshalerFunc) Unmarshal(data []byte, v interface{}) error {
// Examples of usage: context.ReadJSON, context.ReadXML
func (ctx *Context) UnmarshalBody(v interface{}, unmarshaler Unmarshaler) error {
if ctx.Request.Body == nil {
return errors.New("Empty body, please send request body!")
return errors.New("unmarshal: empty body")
}
rawData, err := ioutil.ReadAll(ctx.Request.Body)
@@ -635,14 +721,12 @@ func (ctx *Context) Redirect(urlToRedirect string, statusHeader ...int) {
ctx.StopExecution()
httpStatus := StatusFound // a 'temporary-redirect-like' which works better than for our purpose
if statusHeader != nil && len(statusHeader) > 0 && statusHeader[0] > 0 {
if len(statusHeader) > 0 && statusHeader[0] > 0 {
httpStatus = statusHeader[0]
}
if urlToRedirect == ctx.Path() {
if ctx.framework.Config.IsDevelopment {
ctx.Log("Trying to redirect to itself. FROM: %s TO: %s", ctx.Path(), urlToRedirect)
}
ctx.Log(DevMode, "trying to redirect to itself. FROM: %s TO: %s", ctx.Path(), urlToRedirect)
}
http.Redirect(ctx.ResponseWriter, ctx.Request, urlToRedirect, httpStatus)
}
@@ -739,46 +823,68 @@ func (ctx *Context) TryWriteGzip(b []byte) (int, error) {
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// renderSerialized renders contents with a serializer with status OK which you can change using RenderWithStatus or ctx.SetStatusCode(iris.StatusCode)
func (ctx *Context) renderSerialized(contentType string, obj interface{}, options ...map[string]interface{}) error {
s := ctx.framework.serializers
finalResult, err := s.Serialize(contentType, obj, options...)
if err != nil {
return err
const (
// NoLayout to disable layout for a particular template file
NoLayout = template.NoLayout
// TemplateLayoutContextKey is the name of the user values which can be used to set a template layout from a middleware and override the parent's
TemplateLayoutContextKey = "templateLayout"
)
// getGzipOption receives a default value and the render options map and returns if gzip is enabled for this render action
func getGzipOption(defaultValue bool, options map[string]interface{}) bool {
gzipOpt := options["gzip"] // we only need that, so don't create new map to keep the options.
if b, isBool := gzipOpt.(bool); isBool {
return b
}
return defaultValue
}
// gtCharsetOption receives a default value and the render options map and returns the correct charset for this render action
func getCharsetOption(defaultValue string, options map[string]interface{}) string {
charsetOpt := options["charset"]
if s, isString := charsetOpt.(string); isString {
return s
}
return defaultValue
}
func (ctx *Context) fastRenderWithStatus(status int, cType string, data []byte) (err error) {
if _, shouldFirstStatusCode := ctx.ResponseWriter.(*responseWriter); shouldFirstStatusCode {
ctx.SetStatusCode(status)
}
gzipEnabled := ctx.framework.Config.Gzip
charset := ctx.framework.Config.Charset
if len(options) > 0 {
gzipEnabled = getGzipOption(gzipEnabled, options[0]) // located to the template.go below the RenderOptions
charset = getCharsetOption(charset, options[0])
}
ctype := contentType
if ctype == contentMarkdown { // remember the text/markdown is just a custom internal iris content type, which in reallity renders html
ctype = contentHTML
if cType != contentBinary {
cType += "; charset=" + charset
}
if ctype != contentBinary { // set the charset only on non-binary data
ctype += "; charset=" + charset
}
ctx.SetContentType(ctype)
if gzipEnabled {
ctx.TryWriteGzip(finalResult)
// add the content type to the response
ctx.SetContentType(cType)
var out io.Writer
if gzipEnabled && ctx.clientAllowsGzip() {
ctx.ResponseWriter.Header().Add(varyHeader, acceptEncodingHeader)
ctx.SetHeader(contentEncodingHeader, "gzip")
gzipWriter := fs.AcquireGzipWriter(ctx.ResponseWriter)
defer fs.ReleaseGzipWriter(gzipWriter)
out = gzipWriter
} else {
ctx.ResponseWriter.Write(finalResult)
}
ctx.SetStatusCode(StatusOK)
return nil
}
// RenderTemplateSource serves a template source(raw string contents) from the first template engines which supports raw parsing returns its result as string
func (ctx *Context) RenderTemplateSource(status int, src string, binding interface{}, options ...map[string]interface{}) error {
err := ctx.framework.templates.renderSource(ctx, src, binding, options...)
if err == nil {
ctx.SetStatusCode(status)
out = ctx.ResponseWriter
}
return err
// no need to loop through the RenderPolicy, these types must be fast as possible
// with the features like gzip and custom charset too.
_, err = out.Write(data)
// we don't care for the last one it will not be written more than one if we have the *responseWriter
///TODO:
// if we have ResponseRecorder order doesn't matters but I think the transactions have bugs,
// temporary let's keep it here because it 'fixes' one of them...
ctx.SetStatusCode(status)
return
}
// RenderWithStatus builds up the response from the specified template or a serialize engine.
@@ -789,11 +895,55 @@ func (ctx *Context) RenderWithStatus(status int, name string, binding interface{
ctx.SetStatusCode(status)
}
if strings.IndexByte(name, '.') > -1 { //we have template
err = ctx.framework.templates.renderFile(ctx, name, binding, options...)
} else {
err = ctx.renderSerialized(name, binding, options...)
// we do all these because we don't want to initialize a new map for each execution...
gzipEnabled := ctx.framework.Config.Gzip
charset := ctx.framework.Config.Charset
if len(options) > 0 {
gzipEnabled = getGzipOption(gzipEnabled, options[0])
charset = getCharsetOption(charset, options[0])
}
ctxLayout := ctx.GetString(TemplateLayoutContextKey)
if ctxLayout != "" {
if len(options) > 0 {
options[0]["layout"] = ctxLayout
} else {
options = []map[string]interface{}{{"layout": ctxLayout}}
}
}
// Find Content type
// if it the name is not a template file, then take that as the content type.
cType := contentHTML
if !strings.Contains(name, ".") {
// remember the text/markdown is just a custom internal
// iris content type, which in reallity renders html
if name != contentMarkdown {
cType = name
}
}
if cType != contentBinary {
cType += "; charset=" + charset
}
// add the content type to the response
ctx.SetContentType(cType)
var out io.Writer
if gzipEnabled && ctx.clientAllowsGzip() {
ctx.ResponseWriter.Header().Add(varyHeader, acceptEncodingHeader)
ctx.SetHeader(contentEncodingHeader, "gzip")
gzipWriter := fs.AcquireGzipWriter(ctx.ResponseWriter)
defer fs.ReleaseGzipWriter(gzipWriter)
out = gzipWriter
} else {
out = ctx.ResponseWriter
}
err = ctx.framework.Render(out, name, binding, options...)
// we don't care for the last one it will not be written more than one if we have the *responseWriter
///TODO:
// if we have ResponseRecorder order doesn't matters but I think the transactions have bugs , for now let's keep it here because it 'fixes' one of them...
@@ -817,32 +967,45 @@ func (ctx *Context) Render(name string, binding interface{}, options ...map[stri
// Note: the options: "gzip" and "charset" are built'n support by Iris, so you can pass these on any template engine or serialize engine
func (ctx *Context) MustRender(name string, binding interface{}, options ...map[string]interface{}) {
if err := ctx.Render(name, binding, options...); err != nil {
ctx.HTML(StatusServiceUnavailable, fmt.Sprintf("<h2>Template: %s</h2><b>%s</b>", name, err.Error()))
if ctx.framework.Config.IsDevelopment {
ctx.framework.Logger.Printf("MustRender panics on template: %s.Trace: %s\n", name, err)
htmlErr := ctx.HTML(StatusServiceUnavailable,
fmt.Sprintf("<h2>Template: %s</h2><b>%s</b>", name, err.Error()))
ctx.Log(DevMode, "MustRender failed to render '%s', trace: %s\n",
name, err)
if htmlErr != nil {
ctx.Log(DevMode, "MustRender also failed to render the html fallback: %s",
htmlErr.Error())
}
}
}
// TemplateString accepts a template filename, its context data and returns the result of the parsed template (string)
// if any error returns empty string
func (ctx *Context) TemplateString(name string, binding interface{}, options ...map[string]interface{}) string {
return ctx.framework.TemplateString(name, binding, options...)
}
// HTML writes html string with a http status
func (ctx *Context) HTML(status int, htmlContents string) {
if err := ctx.RenderWithStatus(status, contentHTML, htmlContents); err != nil {
// if no serialize engine found for text/html
ctx.SetContentType(contentHTML + "; charset=" + ctx.framework.Config.Charset)
ctx.SetStatusCode(status)
ctx.WriteString(htmlContents)
}
}
// Data writes out the raw bytes as binary data.
func (ctx *Context) Data(status int, v []byte) error {
return ctx.RenderWithStatus(status, contentBinary, v)
//
// RenderPolicy does NOT apply to context.HTML, context.Text and context.Data
// To change their default behavior users should use
// the context.RenderWithStatus(statusCode, contentType, content, options...) instead.
func (ctx *Context) Data(status int, data []byte) error {
return ctx.fastRenderWithStatus(status, contentBinary, data)
}
// Text writes out a string as plain text.
//
// RenderPolicy does NOT apply to context.HTML, context.Text and context.Data
// To change their default behavior users should use
// the context.RenderWithStatus(statusCode, contentType, content, options...) instead.
func (ctx *Context) Text(status int, text string) error {
return ctx.fastRenderWithStatus(status, contentBinary, []byte(text))
}
// HTML writes html string with a http status
//
// RenderPolicy does NOT apply to context.HTML, context.Text and context.Data
// To change their default behavior users should use
// the context.RenderWithStatus(statusCode, contentType, content, options...) instead.
func (ctx *Context) HTML(status int, htmlContents string) error {
return ctx.fastRenderWithStatus(status, contentHTML, []byte(htmlContents))
}
// JSON marshals the given interface object and writes the JSON response.
@@ -855,11 +1018,6 @@ func (ctx *Context) JSONP(status int, callback string, v interface{}) error {
return ctx.RenderWithStatus(status, contentJSONP, v, map[string]interface{}{"callback": callback})
}
// Text writes out a string as plain text.
func (ctx *Context) Text(status int, v string) error {
return ctx.RenderWithStatus(status, contentText, v)
}
// XML marshals the given interface object and writes the XML response.
func (ctx *Context) XML(status int, v interface{}) error {
return ctx.RenderWithStatus(status, contentXML, v)
@@ -867,15 +1025,20 @@ func (ctx *Context) XML(status int, v interface{}) error {
// MarkdownString parses the (dynamic) markdown string and returns the converted html string
func (ctx *Context) MarkdownString(markdownText string) string {
return ctx.framework.SerializeToString(contentMarkdown, markdownText)
out := &bytes.Buffer{}
_, ok := ctx.framework.policies.RenderPolicy(out, contentMarkdown, markdownText)
if ok {
return out.String()
}
return ""
}
// Markdown parses and renders to the client a particular (dynamic) markdown string
// accepts two parameters
// first is the http status code
// second is the markdown string
func (ctx *Context) Markdown(status int, markdown string) {
ctx.HTML(status, ctx.MarkdownString(markdown))
func (ctx *Context) Markdown(status int, markdown string) error {
return ctx.HTML(status, ctx.MarkdownString(markdown))
}
// -------------------------------------------------------------------------------------
@@ -898,9 +1061,9 @@ func (ctx *Context) staticCachePassed(modtime time.Time) bool {
// SetClientCachedBody like SetBody but it sends with an expiration datetime
// which is managed by the client-side (all major browsers supports this feature)
func (ctx *Context) SetClientCachedBody(status int, bodyContent []byte, cType string, modtime time.Time) {
func (ctx *Context) SetClientCachedBody(status int, bodyContent []byte, cType string, modtime time.Time) error {
if ctx.staticCachePassed(modtime) {
return
return nil
}
modtimeFormatted := modtime.UTC().Format(ctx.framework.Config.TimeFormat)
@@ -909,7 +1072,8 @@ func (ctx *Context) SetClientCachedBody(status int, bodyContent []byte, cType st
ctx.ResponseWriter.Header().Set(lastModified, modtimeFormatted)
ctx.SetStatusCode(status)
ctx.ResponseWriter.Write(bodyContent)
_, err := ctx.ResponseWriter.Write(bodyContent)
return err
}
// ServeContent serves content, headers are autoset
@@ -973,9 +1137,9 @@ func (ctx *Context) ServeFile(filename string, gzipCompression bool) error {
// SendFile sends file for force-download to the client
//
// Use this instead of ServeFile to 'force-download' bigger files to the client
func (ctx *Context) SendFile(filename string, destinationName string) {
func (ctx *Context) SendFile(filename string, destinationName string) error {
ctx.ResponseWriter.Header().Set(contentDisposition, "attachment;filename="+destinationName)
ctx.ServeFile(filename, false)
return ctx.ServeFile(filename, false)
}
// StreamWriter registers the given stream writer for populating
@@ -1134,12 +1298,11 @@ func (ctx *Context) ParamInt64(key string) (int64, error) {
// hasthe form of key1=value1,key2=value2...
func (ctx *Context) ParamsSentence() string {
var buff bytes.Buffer
ctx.VisitValues(func(kb string, vg interface{}) {
v, ok := vg.(string)
ctx.VisitValues(func(k string, vi interface{}) {
v, ok := vi.(string)
if !ok {
return
}
k := string(kb)
buff.WriteString(k)
buff.WriteString("=")
buff.WriteString(v)
@@ -1444,9 +1607,7 @@ func (ctx *Context) BeginTransaction(pipe func(transaction *Transaction)) {
t := newTransaction(ctx)
defer func() {
if err := recover(); err != nil {
if ctx.framework.Config.IsDevelopment {
ctx.Log(errTransactionInterrupted.Format(err).Error())
}
ctx.Log(DevMode, errTransactionInterrupted.Format(err).Error())
// complete (again or not , doesn't matters) the scope without loud
t.Complete(nil)
// we continue as normal, no need to return here*
@@ -1465,8 +1626,8 @@ func (ctx *Context) BeginTransaction(pipe func(transaction *Transaction)) {
}
// Log logs to the iris defined logger
func (ctx *Context) Log(format string, a ...interface{}) {
ctx.framework.Logger.Printf(format, a...)
func (ctx *Context) Log(mode LogMode, format string, a ...interface{}) {
ctx.framework.Log(mode, fmt.Sprintf(format, a...))
}
// Framework returns the Iris instance, containing the configuration and all other fields