mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 18:51:03 +01:00
57e5b77853
Former-commit-id: 67ea92337f120f03e8bb96d3daa43529b10af2f2
1697 lines
60 KiB
Go
1697 lines
60 KiB
Go
package iris
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"mime/multipart"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"reflect"
|
|
"regexp"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/iris-contrib/formBinder"
|
|
"github.com/kataras/go-errors"
|
|
"github.com/kataras/go-fs"
|
|
"github.com/kataras/go-template"
|
|
)
|
|
|
|
const (
|
|
// ContentType represents the header["Content-Type"]
|
|
contentType = "Content-Type"
|
|
// ContentLength represents the header["Content-Length"]
|
|
contentLength = "Content-Length"
|
|
// contentEncodingHeader represents the header["Content-Encoding"]
|
|
contentEncodingHeader = "Content-Encoding"
|
|
// varyHeader represents the header "Vary"
|
|
varyHeader = "Vary"
|
|
// acceptEncodingHeader represents the header key & value "Accept-Encoding"
|
|
acceptEncodingHeader = "Accept-Encoding"
|
|
// ContentHTML is the string of text/html response headers
|
|
contentHTML = "text/html"
|
|
// ContentBinary header value for binary data.
|
|
contentBinary = "application/octet-stream"
|
|
// ContentJSON header value for JSON data.
|
|
contentJSON = "application/json"
|
|
// ContentJSONP header value for JSONP & Javascript data.
|
|
contentJSONP = "application/javascript"
|
|
// ContentJavascript header value for Javascript/JSONP
|
|
// conversional
|
|
contentJavascript = "application/javascript"
|
|
// ContentText header value for Text data.
|
|
contentText = "text/plain"
|
|
// ContentXML header value for XML data.
|
|
contentXML = "text/xml"
|
|
|
|
// contentMarkdown custom key/content type, the real is the text/html
|
|
contentMarkdown = "text/markdown"
|
|
|
|
// LastModified "Last-Modified"
|
|
lastModified = "Last-Modified"
|
|
// IfModifiedSince "If-Modified-Since"
|
|
ifModifiedSince = "If-Modified-Since"
|
|
// ContentDisposition "Content-Disposition"
|
|
contentDisposition = "Content-Disposition"
|
|
// CacheControl "Cache-Control"
|
|
cacheControl = "Cache-Control"
|
|
|
|
// stopExecutionPosition used inside the Context, is the number which shows us that the context's middleware manually stop the execution
|
|
stopExecutionPosition = 255
|
|
)
|
|
|
|
type (
|
|
requestValue struct {
|
|
key []byte
|
|
value interface{}
|
|
}
|
|
requestValues []requestValue
|
|
)
|
|
|
|
func (r *requestValues) Set(key string, value interface{}) {
|
|
args := *r
|
|
n := len(args)
|
|
for i := 0; i < n; i++ {
|
|
kv := &args[i]
|
|
if string(kv.key) == key {
|
|
kv.value = value
|
|
return
|
|
}
|
|
}
|
|
|
|
c := cap(args)
|
|
if c > n {
|
|
args = args[:n+1]
|
|
kv := &args[n]
|
|
kv.key = append(kv.key[:0], key...)
|
|
kv.value = value
|
|
*r = args
|
|
return
|
|
}
|
|
|
|
kv := requestValue{}
|
|
kv.key = append(kv.key[:0], key...)
|
|
kv.value = value
|
|
*r = append(args, kv)
|
|
}
|
|
|
|
func (r *requestValues) Get(key string) interface{} {
|
|
args := *r
|
|
n := len(args)
|
|
for i := 0; i < n; i++ {
|
|
kv := &args[i]
|
|
if string(kv.key) == key {
|
|
return kv.value
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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{}
|
|
// should not be used inside Render when PongoEngine is used.
|
|
Map map[string]interface{}
|
|
|
|
// Context is resetting every time a request is coming to the server
|
|
// it is not good practice to use this object in goroutines, for these cases use the .Clone()
|
|
Context struct {
|
|
ResponseWriter // *responseWriter by default, when record is on then *ResponseRecorder
|
|
Request *http.Request
|
|
values requestValues
|
|
framework *Framework
|
|
//keep track all registered middleware (handlers)
|
|
Middleware Middleware // exported because is useful for debugging
|
|
session Session
|
|
// Pos is the position number of the Context, look .Next to understand
|
|
Pos int // exported because is useful for debugging
|
|
}
|
|
)
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -----------------------------Handler(s) Execution------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
// Do calls the first handler only, it's like Next with negative pos, used only on Router&MemoryRouter
|
|
func (ctx *Context) Do() {
|
|
ctx.Pos = 0
|
|
ctx.Middleware[0].Serve(ctx)
|
|
}
|
|
|
|
// Next calls all the next handler from the middleware stack, it used inside a middleware
|
|
func (ctx *Context) Next() {
|
|
//set position to the next
|
|
ctx.Pos++
|
|
//run the next
|
|
if ctx.Pos < len(ctx.Middleware) {
|
|
ctx.Middleware[ctx.Pos].Serve(ctx)
|
|
}
|
|
}
|
|
|
|
// NextHandler returns the next handler in the chain (ctx.Middleware)
|
|
// otherwise nil.
|
|
// Notes:
|
|
// If the result of NextHandler() will be executed then
|
|
// the ctx.Pos (which is exported for these reasons) should be manually increment(++)
|
|
// otherwise your app will visit twice the same handler.
|
|
func (ctx *Context) NextHandler() Handler {
|
|
nextPos := ctx.Pos + 1
|
|
// check if it has a next middleware
|
|
if nextPos < len(ctx.Middleware) {
|
|
return ctx.Middleware[nextPos]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// StopExecution just sets the .pos to 255 in order to not move to the next middlewares(if any)
|
|
func (ctx *Context) StopExecution() {
|
|
ctx.Pos = stopExecutionPosition
|
|
}
|
|
|
|
// IsStopped checks and returns true if the current position of the Context is 255, means that the StopExecution has called
|
|
func (ctx *Context) IsStopped() bool {
|
|
return ctx.Pos == stopExecutionPosition
|
|
}
|
|
|
|
// GetHandlerName as requested returns the stack-name of the function which the Middleware is setted from
|
|
func (ctx *Context) GetHandlerName() string {
|
|
return runtime.FuncForPC(reflect.ValueOf(ctx.Middleware[len(ctx.Middleware)-1]).Pointer()).Name()
|
|
}
|
|
|
|
// ExecRoute calls any route (mostly "offline" route) like it was requested by the user, but it is not.
|
|
// Offline means that the route is registered to the iris and have all features that a normal route has
|
|
// BUT it isn't available by browsing, its handlers executed only when other handler's context call them
|
|
// 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.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.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 RouteInfo) *Context {
|
|
return ctx.ExecRouteAgainst(r, ctx.Path())
|
|
}
|
|
|
|
// ExecRouteAgainst calls any iris.Route against a 'virtually' request path
|
|
// like it was requested by the user, but it is not.
|
|
// Offline means that the route is registered to the iris and have all features that a normal route has
|
|
// BUT it isn't available by browsing, its handlers executed only when other handler's context call them
|
|
// 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.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.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 RouteInfo, againstRequestPath string) *Context {
|
|
if r != nil {
|
|
context := &(*ctx)
|
|
context.Middleware = context.Middleware[0:0]
|
|
context.values.Reset()
|
|
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
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Prioritize is a middleware which executes a route against this path
|
|
// when the request's Path has a prefix of the route's STATIC PART
|
|
// is not executing ExecRoute to determinate if it's valid, for performance reasons
|
|
// if this function is not enough for you and you want to test more than one parameterized path
|
|
// 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.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 RouteInfo) HandlerFunc {
|
|
if r != nil {
|
|
return func(ctx *Context) {
|
|
reqPath := ctx.Path()
|
|
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)
|
|
}
|
|
return
|
|
}
|
|
// execute the next handler if no prefix
|
|
// here look, the only error we catch is the 404,
|
|
// we can't go ctx.Next() and believe that the next handler will manage the error
|
|
// because it will not, we are not on the router.
|
|
ctx.Next()
|
|
}
|
|
}
|
|
return func(ctx *Context) { ctx.Next() }
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -----------------------------Request URL, Method, IP & Headers getters---------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
// Method returns the http request method
|
|
// same as *http.Request.Method
|
|
func (ctx *Context) Method() string {
|
|
return ctx.Request.Method
|
|
}
|
|
|
|
// Host returns the host part of the current url
|
|
func (ctx *Context) Host() string {
|
|
h := ctx.Request.URL.Host
|
|
if h == "" {
|
|
h = ctx.Request.Host
|
|
}
|
|
return h
|
|
}
|
|
|
|
// ServerHost returns the server host taken by *http.Request.Host
|
|
func (ctx *Context) ServerHost() string {
|
|
return ctx.Request.Host
|
|
}
|
|
|
|
// Subdomain returns the subdomain (string) of this request, if any
|
|
func (ctx *Context) Subdomain() (subdomain string) {
|
|
host := ctx.Host()
|
|
if index := strings.IndexByte(host, '.'); index > 0 {
|
|
subdomain = host[0:index]
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// VirtualHostname returns the hostname that user registers,
|
|
// host path maybe differs from the real which is the Host(), which taken from a net.listener
|
|
func (ctx *Context) VirtualHostname() string {
|
|
realhost := ctx.Host()
|
|
hostname := realhost
|
|
virtualhost := ctx.framework.Config.VHost
|
|
|
|
if portIdx := strings.IndexByte(hostname, ':'); portIdx > 0 {
|
|
hostname = hostname[0:portIdx]
|
|
}
|
|
if idxDotAnd := strings.LastIndexByte(hostname, '.'); idxDotAnd > 0 {
|
|
s := hostname[idxDotAnd:]
|
|
// means that we have the request's host mymachine.com or 127.0.0.1/0.0.0.0, but for the second option we will need to replace it with the hostname that the dev was registered
|
|
// this needed to parse correct the {{ url }} iris global template engine's function
|
|
if s == ".1" {
|
|
hostname = strings.Replace(hostname, "127.0.0.1", virtualhost, 1)
|
|
} else if s == ".0" {
|
|
hostname = strings.Replace(hostname, "0.0.0.0", virtualhost, 1)
|
|
}
|
|
//
|
|
} else {
|
|
hostname = strings.Replace(hostname, "localhost", virtualhost, 1)
|
|
}
|
|
|
|
return hostname
|
|
}
|
|
|
|
// Path returns the full escaped path as string
|
|
// for unescaped use: ctx.RequestCtx.RequestURI() or RequestPath(escape bool)
|
|
func (ctx *Context) Path() string {
|
|
return ctx.RequestPath(ctx.framework.Config.EnablePathEscape)
|
|
}
|
|
|
|
// RequestPath returns the requested path
|
|
func (ctx *Context) RequestPath(escape bool) string {
|
|
if escape {
|
|
// NOTE: for example:
|
|
// DecodeURI decodes %2F to '/'
|
|
// DecodeQuery decodes any %20 to whitespace
|
|
// here we choose to be query-decoded only
|
|
// and with context.ParamDecoded the user receives a URI decoded path parameter.
|
|
// see https://github.com/iris-contrib/examples/tree/master/named_parameters_pathescape
|
|
// and https://github.com/iris-contrib/examples/tree/master/pathescape
|
|
|
|
return DecodeQuery(ctx.Request.URL.EscapedPath())
|
|
}
|
|
return ctx.Request.URL.Path
|
|
}
|
|
|
|
// RemoteAddr tries to return the real client's request IP
|
|
func (ctx *Context) RemoteAddr() string {
|
|
header := ctx.RequestHeader("X-Real-Ip")
|
|
realIP := strings.TrimSpace(header)
|
|
if realIP != "" {
|
|
return realIP
|
|
}
|
|
realIP = ctx.RequestHeader("X-Forwarded-For")
|
|
idx := strings.IndexByte(realIP, ',')
|
|
if idx >= 0 {
|
|
realIP = realIP[0:idx]
|
|
}
|
|
realIP = strings.TrimSpace(realIP)
|
|
if realIP != "" {
|
|
return realIP
|
|
}
|
|
addr := strings.TrimSpace(ctx.Request.RemoteAddr)
|
|
if len(addr) == 0 {
|
|
return ""
|
|
}
|
|
// if addr has port use the net.SplitHostPort otherwise(error occurs) take as it is
|
|
if ip, _, err := net.SplitHostPort(addr); err == nil {
|
|
return ip
|
|
}
|
|
return addr
|
|
}
|
|
|
|
// RequestHeader returns the request header's value
|
|
// accepts one parameter, the key of the header (string)
|
|
// returns string
|
|
func (ctx *Context) RequestHeader(k string) string {
|
|
return ctx.Request.Header.Get(k)
|
|
}
|
|
|
|
// IsAjax returns true if this request is an 'ajax request'( XMLHttpRequest)
|
|
//
|
|
// Read more at: http://www.w3schools.com/ajax/
|
|
func (ctx *Context) IsAjax() bool {
|
|
return ctx.RequestHeader("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest"
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -----------------------------GET & POST arguments------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
// URLParam returns the get parameter from a request , if any
|
|
func (ctx *Context) URLParam(key string) string {
|
|
return ctx.Request.URL.Query().Get(key)
|
|
}
|
|
|
|
// URLParams returns a map of GET query parameters separated by comma if more than one
|
|
// it returns an empty map if nothing founds
|
|
func (ctx *Context) URLParams() map[string]string {
|
|
values := map[string]string{}
|
|
|
|
q := ctx.URLParamsAsMulti()
|
|
if q != nil {
|
|
for k, v := range q {
|
|
values[k] = strings.Join(v, ",")
|
|
}
|
|
}
|
|
|
|
return values
|
|
}
|
|
|
|
// URLParamsAsMulti returns a map of list contains the url get parameters
|
|
func (ctx *Context) URLParamsAsMulti() map[string][]string {
|
|
return ctx.Request.URL.Query()
|
|
}
|
|
|
|
// URLParamInt returns the url query parameter as int value from a request , returns error on parse fail
|
|
func (ctx *Context) URLParamInt(key string) (int, error) {
|
|
return strconv.Atoi(ctx.URLParam(key))
|
|
}
|
|
|
|
// URLParamInt64 returns the url query parameter as int64 value from a request , returns error on parse fail
|
|
func (ctx *Context) URLParamInt64(key string) (int64, error) {
|
|
return strconv.ParseInt(ctx.URLParam(key), 10, 64)
|
|
}
|
|
|
|
func (ctx *Context) askParseForm() error {
|
|
if ctx.Request.Form == nil {
|
|
if err := ctx.Request.ParseForm(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FormValues returns all post data values with their keys
|
|
// form data, get, post & put query arguments
|
|
//
|
|
// NOTE: A check for nil is necessary for zero results
|
|
func (ctx *Context) FormValues() map[string][]string {
|
|
// we skip the check of multipart form, takes too much memory, if user wants it can do manually now.
|
|
if err := ctx.askParseForm(); err != nil {
|
|
return nil
|
|
}
|
|
return ctx.Request.Form // nothing more to do, it's already contains both query and post & put args.
|
|
}
|
|
|
|
// FormValue returns a single form value by its name/key
|
|
func (ctx *Context) FormValue(name string) string {
|
|
return ctx.Request.FormValue(name)
|
|
}
|
|
|
|
// PostValue returns a form's only-post value by its name
|
|
// same as Request.PostFormValue
|
|
func (ctx *Context) PostValue(name string) string {
|
|
return ctx.Request.PostFormValue(name)
|
|
}
|
|
|
|
// FormFile returns the first file for the provided form key.
|
|
// FormFile calls ctx.Request.ParseMultipartForm and ParseForm if necessary.
|
|
//
|
|
// same as Request.FormFile
|
|
func (ctx *Context) FormFile(key string) (multipart.File, *multipart.FileHeader, error) {
|
|
return ctx.Request.FormFile(key)
|
|
}
|
|
|
|
var (
|
|
errTemplateExecute = errors.New("Unable to execute a template. Trace: %s")
|
|
errReadBody = errors.New("While trying to read %s from the request body. Trace %s")
|
|
errServeContent = errors.New("While trying to serve content to the client. Trace %s")
|
|
)
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -----------------------------Request Body Binders/Readers----------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
// NOTE: No default max body size http package has some built'n protection for DoS attacks
|
|
// 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
|
|
// should be registered before all other handlers
|
|
var LimitRequestBodySize = func(maxRequestBodySizeBytes int64) HandlerFunc {
|
|
return func(ctx *Context) {
|
|
ctx.SetMaxRequestBodySize(maxRequestBodySizeBytes)
|
|
ctx.Next()
|
|
}
|
|
}
|
|
|
|
// SetMaxRequestBodySize sets a limit to the request body size
|
|
// should be called before reading the request body from the client
|
|
func (ctx *Context) SetMaxRequestBodySize(limitOverBytes int64) {
|
|
ctx.Request.Body = http.MaxBytesReader(ctx.ResponseWriter, ctx.Request.Body, limitOverBytes)
|
|
}
|
|
|
|
// BodyDecoder is an interface which any struct can implement in order to customize the decode action
|
|
// from ReadJSON and ReadXML
|
|
//
|
|
// Trivial example of this could be:
|
|
// type User struct { Username string }
|
|
//
|
|
// func (u *User) Decode(data []byte) error {
|
|
// return json.Unmarshal(data, u)
|
|
// }
|
|
//
|
|
// the 'context.ReadJSON/ReadXML(&User{})' will call the User's
|
|
// Decode option to decode the request body
|
|
//
|
|
// Note: This is totally optionally, the default decoders
|
|
// for ReadJSON is the encoding/json and for ReadXML is the encoding/xml
|
|
type BodyDecoder interface {
|
|
Decode(data []byte) error
|
|
}
|
|
|
|
// Unmarshaler is the interface implemented by types that can unmarshal any raw data
|
|
// TIP INFO: Any v object which implements the BodyDecoder can be override the unmarshaler
|
|
type Unmarshaler interface {
|
|
Unmarshal(data []byte, v interface{}) error
|
|
}
|
|
|
|
// UnmarshalerFunc a shortcut for the Unmarshaler interface
|
|
//
|
|
// See 'Unmarshaler' and 'BodyDecoder' for more
|
|
type UnmarshalerFunc func(data []byte, v interface{}) error
|
|
|
|
// Unmarshal parses the X-encoded data and stores the result in the value pointed to by v.
|
|
// Unmarshal uses the inverse of the encodings that Marshal uses, allocating maps,
|
|
// slices, and pointers as necessary.
|
|
func (u UnmarshalerFunc) Unmarshal(data []byte, v interface{}) error {
|
|
return u(data, v)
|
|
}
|
|
|
|
// UnmarshalBody reads the request's body and binds it to a value or pointer of any type
|
|
// Examples of usage: context.ReadJSON, context.ReadXML
|
|
func (ctx *Context) UnmarshalBody(v interface{}, unmarshaler Unmarshaler) error {
|
|
if ctx.Request.Body == nil {
|
|
return errors.New("unmarshal: empty body")
|
|
}
|
|
|
|
rawData, err := ioutil.ReadAll(ctx.Request.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if ctx.framework.Config.DisableBodyConsumptionOnUnmarshal {
|
|
// * remember, Request.Body has no Bytes(), we have to consume them first
|
|
// and after re-set them to the body, this is the only solution.
|
|
ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(rawData))
|
|
}
|
|
|
|
// check if the v contains its own decode
|
|
// in this case the v should be a pointer also,
|
|
// but this is up to the user's custom Decode implementation*
|
|
//
|
|
// See 'BodyDecoder' for more
|
|
if decoder, isDecoder := v.(BodyDecoder); isDecoder {
|
|
return decoder.Decode(rawData)
|
|
}
|
|
|
|
// check if v is already a pointer, if yes then pass as it's
|
|
if reflect.TypeOf(v).Kind() == reflect.Ptr {
|
|
return unmarshaler.Unmarshal(rawData, v)
|
|
}
|
|
// finally, if the v doesn't contains a self-body decoder and it's not a pointer
|
|
// use the custom unmarshaler to bind the body
|
|
return unmarshaler.Unmarshal(rawData, &v)
|
|
}
|
|
|
|
// ReadJSON reads JSON from request's body and binds it to a value of any json-valid type
|
|
func (ctx *Context) ReadJSON(jsonObject interface{}) error {
|
|
return ctx.UnmarshalBody(jsonObject, UnmarshalerFunc(json.Unmarshal))
|
|
}
|
|
|
|
// ReadXML reads XML from request's body and binds it to a value of any xml-valid type
|
|
func (ctx *Context) ReadXML(xmlObject interface{}) error {
|
|
return ctx.UnmarshalBody(xmlObject, UnmarshalerFunc(xml.Unmarshal))
|
|
}
|
|
|
|
// ReadForm binds the formObject with the form data
|
|
// it supports any kind of struct
|
|
func (ctx *Context) ReadForm(formObject interface{}) error {
|
|
values := ctx.FormValues()
|
|
if values == nil {
|
|
return errors.New("An empty form passed on context.ReadForm")
|
|
}
|
|
return errReadBody.With(formBinder.Decode(values, formObject))
|
|
}
|
|
|
|
/* Response */
|
|
|
|
// SetContentType sets the response writer's header key 'Content-Type' to a given value(s)
|
|
func (ctx *Context) SetContentType(s string) {
|
|
ctx.ResponseWriter.Header().Set(contentType, s)
|
|
}
|
|
|
|
// SetHeader write to the response writer's header to a given key the given value
|
|
func (ctx *Context) SetHeader(k string, v string) {
|
|
ctx.ResponseWriter.Header().Add(k, v)
|
|
}
|
|
|
|
// SetStatusCode sets the status code header to the response
|
|
//
|
|
// same as .WriteHeader, iris takes cares of your status code seriously
|
|
func (ctx *Context) SetStatusCode(statusCode int) {
|
|
ctx.ResponseWriter.WriteHeader(statusCode)
|
|
}
|
|
|
|
// Redirect redirect sends a redirect response the client
|
|
// accepts 2 parameters string and an optional int
|
|
// first parameter is the url to redirect
|
|
// second parameter is the http status should send, default is 302 (StatusFound),
|
|
// you can set it to 301 (Permant redirect), if that's nessecery
|
|
func (ctx *Context) Redirect(urlToRedirect string, statusHeader ...int) {
|
|
ctx.StopExecution()
|
|
|
|
httpStatus := StatusFound // a 'temporary-redirect-like' which works better than for our purpose
|
|
if len(statusHeader) > 0 && statusHeader[0] > 0 {
|
|
httpStatus = statusHeader[0]
|
|
}
|
|
|
|
if urlToRedirect == ctx.Path() {
|
|
ctx.Log(DevMode, "trying to redirect to itself. FROM: %s TO: %s", ctx.Path(), urlToRedirect)
|
|
}
|
|
http.Redirect(ctx.ResponseWriter, ctx.Request, urlToRedirect, httpStatus)
|
|
}
|
|
|
|
// RedirectTo does the same thing as Redirect but instead of receiving a uri or path it receives a route name
|
|
func (ctx *Context) RedirectTo(routeName string, args ...interface{}) {
|
|
s := ctx.framework.URL(routeName, args...)
|
|
if s != "" {
|
|
ctx.Redirect(s, StatusFound)
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -----------------------------(Custom) Errors-----------------------------------------
|
|
// ----------------------Look iris.OnError/EmitError for more---------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
// NotFound emits an error 404 to the client, using the custom http errors
|
|
// if no custom errors provided then it sends the default error message
|
|
func (ctx *Context) NotFound() {
|
|
ctx.framework.EmitError(StatusNotFound, ctx)
|
|
}
|
|
|
|
// Panic emits an error 500 to the client, using the custom http errors
|
|
// if no custom errors rpovided then it sends the default error message
|
|
func (ctx *Context) Panic() {
|
|
ctx.framework.EmitError(StatusInternalServerError, ctx)
|
|
}
|
|
|
|
// EmitError executes the custom error by the http status code passed to the function
|
|
func (ctx *Context) EmitError(statusCode int) {
|
|
ctx.framework.EmitError(statusCode, ctx)
|
|
ctx.StopExecution()
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------Context's gzip inline response writer ----------------------
|
|
// ---------------------Look template.go & iris.go for more options---------------------
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
var (
|
|
errClientDoesNotSupportGzip = errors.New("Client doesn't supports gzip compression")
|
|
)
|
|
|
|
func (ctx *Context) clientAllowsGzip() bool {
|
|
if h := ctx.RequestHeader(acceptEncodingHeader); h != "" {
|
|
for _, v := range strings.Split(h, ";") {
|
|
if strings.Contains(v, "gzip") { // we do Contains because sometimes browsers has the q=, we don't use it atm. || strings.Contains(v,"deflate"){
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// WriteGzip accepts bytes, which are compressed to gzip format and sent to the client.
|
|
// returns the number of bytes written and an error ( if the client doesn' supports gzip compression)
|
|
func (ctx *Context) WriteGzip(b []byte) (int, error) {
|
|
if ctx.clientAllowsGzip() {
|
|
ctx.ResponseWriter.Header().Add(varyHeader, acceptEncodingHeader)
|
|
|
|
gzipWriter := fs.AcquireGzipWriter(ctx.ResponseWriter)
|
|
n, err := gzipWriter.Write(b)
|
|
fs.ReleaseGzipWriter(gzipWriter)
|
|
|
|
if err == nil {
|
|
ctx.SetHeader(contentEncodingHeader, "gzip")
|
|
} // else write the contents as it is? no let's create a new func for this
|
|
return n, err
|
|
}
|
|
|
|
return 0, errClientDoesNotSupportGzip
|
|
}
|
|
|
|
// TryWriteGzip accepts bytes, which are compressed to gzip format and sent to the client.
|
|
// If client does not supprots gzip then the contents are written as they are, uncompressed.
|
|
func (ctx *Context) TryWriteGzip(b []byte) (int, error) {
|
|
n, err := ctx.WriteGzip(b)
|
|
if err != nil {
|
|
// check if the error came from gzip not allowed and not the writer itself
|
|
if _, ok := err.(*errors.Error); ok {
|
|
// client didn't supported gzip, write them uncompressed:
|
|
return ctx.ResponseWriter.Write(b)
|
|
}
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -----------------------------Render and powerful content negotiation-----------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
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 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
|
|
}
|
|
|
|
// 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.
|
|
// Note: the options: "gzip" and "charset" are built'n support by Iris, so you can pass these on any template engine or serialize engines
|
|
func (ctx *Context) RenderWithStatus(status int, name string, binding interface{}, options ...map[string]interface{}) (err error) {
|
|
|
|
if _, shouldFirstStatusCode := ctx.ResponseWriter.(*responseWriter); shouldFirstStatusCode {
|
|
ctx.SetStatusCode(status)
|
|
}
|
|
|
|
// 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...
|
|
ctx.SetStatusCode(status)
|
|
|
|
return
|
|
}
|
|
|
|
// Render same as .RenderWithStatus but with status to iris.StatusOK (200) if no previous status exists
|
|
// builds up the response from the specified template or a serialize engine.
|
|
// 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) Render(name string, binding interface{}, options ...map[string]interface{}) error {
|
|
errCode := ctx.ResponseWriter.StatusCode()
|
|
if errCode <= 0 {
|
|
errCode = StatusOK
|
|
}
|
|
return ctx.RenderWithStatus(errCode, name, binding, options...)
|
|
}
|
|
|
|
// MustRender same as .Render but returns 503 service unavailable http status with a (html) message if render failed
|
|
// 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 {
|
|
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())
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// Data writes out the raw bytes as binary data.
|
|
//
|
|
// 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.
|
|
func (ctx *Context) JSON(status int, v interface{}) error {
|
|
return ctx.RenderWithStatus(status, contentJSON, v)
|
|
}
|
|
|
|
// JSONP marshals the given interface object and writes the JSON response.
|
|
func (ctx *Context) JSONP(status int, callback string, v interface{}) error {
|
|
return ctx.RenderWithStatus(status, contentJSONP, v, map[string]interface{}{"callback": callback})
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// MarkdownString parses the (dynamic) markdown string and returns the converted html string
|
|
func (ctx *Context) MarkdownString(markdownText string) string {
|
|
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) error {
|
|
return ctx.HTML(status, ctx.MarkdownString(markdown))
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// --------------------Static content serve by context implementation-------------------
|
|
// --------------------Look iris.go for more useful Static web system methods-----------
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
// staticCachePassed checks the IfModifiedSince header and
|
|
// returns true if (client-side) duration has expired
|
|
func (ctx *Context) staticCachePassed(modtime time.Time) bool {
|
|
if t, err := time.Parse(ctx.framework.Config.TimeFormat, ctx.RequestHeader(ifModifiedSince)); err == nil && modtime.Before(t.Add(StaticCacheDuration)) {
|
|
ctx.ResponseWriter.Header().Del(contentType)
|
|
ctx.ResponseWriter.Header().Del(contentLength)
|
|
ctx.SetStatusCode(StatusNotModified)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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) error {
|
|
if ctx.staticCachePassed(modtime) {
|
|
return nil
|
|
}
|
|
|
|
modtimeFormatted := modtime.UTC().Format(ctx.framework.Config.TimeFormat)
|
|
|
|
ctx.ResponseWriter.Header().Set(contentType, cType)
|
|
ctx.ResponseWriter.Header().Set(lastModified, modtimeFormatted)
|
|
ctx.SetStatusCode(status)
|
|
|
|
_, err := ctx.ResponseWriter.Write(bodyContent)
|
|
return err
|
|
}
|
|
|
|
// ServeContent serves content, headers are autoset
|
|
// receives three parameters, it's low-level function, instead you can use .ServeFile(string,bool)/SendFile(string,string)
|
|
//
|
|
// You can define your own "Content-Type" header also, after this function call
|
|
// Doesn't implements resuming (by range), use ctx.SendFile instead
|
|
func (ctx *Context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error {
|
|
if t, err := time.Parse(ctx.framework.Config.TimeFormat, ctx.RequestHeader(ifModifiedSince)); err == nil && modtime.Before(t.Add(1*time.Second)) {
|
|
ctx.ResponseWriter.Header().Del(contentType)
|
|
ctx.ResponseWriter.Header().Del(contentLength)
|
|
ctx.SetStatusCode(StatusNotModified)
|
|
return nil
|
|
}
|
|
|
|
ctx.ResponseWriter.Header().Set(contentType, fs.TypeByExtension(filename))
|
|
ctx.ResponseWriter.Header().Set(lastModified, modtime.UTC().Format(ctx.framework.Config.TimeFormat))
|
|
ctx.SetStatusCode(StatusOK)
|
|
var out io.Writer
|
|
if gzipCompression && 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 := io.Copy(out, content)
|
|
return errServeContent.With(err)
|
|
}
|
|
|
|
// ServeFile serves a view file, to send a file ( zip for example) to the client you should use the SendFile(serverfilename,clientfilename)
|
|
// receives two parameters
|
|
// filename/path (string)
|
|
// gzipCompression (bool)
|
|
//
|
|
// You can define your own "Content-Type" header also, after this function call
|
|
// This function doesn't implement resuming (by range), use ctx.SendFile instead
|
|
//
|
|
// Use it when you want to serve css/js/... files to the client, for bigger files and 'force-download' use the SendFile
|
|
func (ctx *Context) ServeFile(filename string, gzipCompression bool) error {
|
|
f, err := os.Open(filename)
|
|
if err != nil {
|
|
return fmt.Errorf("%d", 404)
|
|
}
|
|
defer f.Close()
|
|
fi, _ := f.Stat()
|
|
if fi.IsDir() {
|
|
filename = path.Join(filename, "index.html")
|
|
f, err = os.Open(filename)
|
|
if err != nil {
|
|
return fmt.Errorf("%d", 404)
|
|
}
|
|
fi, _ = f.Stat()
|
|
}
|
|
return ctx.ServeContent(f, fi.Name(), fi.ModTime(), gzipCompression)
|
|
}
|
|
|
|
// 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) error {
|
|
ctx.ResponseWriter.Header().Set(contentDisposition, "attachment;filename="+destinationName)
|
|
return ctx.ServeFile(filename, false)
|
|
}
|
|
|
|
// StreamWriter registers the given stream writer for populating
|
|
// response body.
|
|
//
|
|
// Access to context's and/or its' members is forbidden from writer.
|
|
//
|
|
// This function may be used in the following cases:
|
|
//
|
|
// * if response body is too big (more than iris.LimitRequestBodySize(if setted)).
|
|
// * if response body is streamed from slow external sources.
|
|
// * if response body must be streamed to the client in chunks.
|
|
// (aka `http server push`).
|
|
//
|
|
// receives a function which receives the response writer
|
|
// and returns false when it should stop writing, otherwise true in order to continue
|
|
func (ctx *Context) StreamWriter(writer func(w io.Writer) bool) {
|
|
w := ctx.ResponseWriter
|
|
notifyClosed := w.CloseNotify()
|
|
for {
|
|
select {
|
|
// response writer forced to close, exit.
|
|
case <-notifyClosed:
|
|
return
|
|
default:
|
|
shouldContinue := writer(w)
|
|
w.Flush()
|
|
if !shouldContinue {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// --------------------------------Storage----------------------------------------------
|
|
// -----------------------User Values & Path parameters--------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
// ValuesLen returns the total length of the user values storage, some of them maybe path parameters
|
|
func (ctx *Context) ValuesLen() (n int) {
|
|
return len(ctx.values)
|
|
}
|
|
|
|
// Get returns the user's value from a key
|
|
// if doesn't exists returns nil
|
|
func (ctx *Context) Get(key string) interface{} {
|
|
return ctx.values.Get(key)
|
|
}
|
|
|
|
// GetFmt returns a value which has this format: func(format string, args ...interface{}) string
|
|
// if doesn't exists returns a function which returns an empty string.
|
|
//
|
|
// "translate" is the key of the i18n middlweare
|
|
// for more plaese look: https://github.com/iris-contrib/examples/tree/master/middleware_internationalization_i18n
|
|
func (ctx *Context) getFmt(key string) func(format string, args ...interface{}) string {
|
|
if v, ok := ctx.Get(key).(func(format string, args ...interface{}) string); ok {
|
|
return v
|
|
}
|
|
return func(format string, args ...interface{}) string { return "" }
|
|
}
|
|
|
|
// TranslateLanguageContextKey & TranslateFunctionContextKey are used by i18n handlers/middleware
|
|
// currently we have only one: https://github.com/iris-contrib/middleware/tree/master/i18n
|
|
// but you can use these keys to override the i18n's cookie name (TranslateLanguageContextKey)
|
|
// or to store new translate function by using the ctx.Set(iris.TranslateFunctionContextKey, theTrFunc)
|
|
var (
|
|
TranslateLanguageContextKey = "language"
|
|
TranslateFunctionContextKey = "translate"
|
|
)
|
|
|
|
// Translate is the i18n (localization) middleware's function, it just
|
|
// calls the ctx.getFmt("translate").
|
|
// "translate" is the key of the i18n middlweare
|
|
// for more plaese look: https://github.com/iris-contrib/examples/tree/master/middleware_internationalization_i18n
|
|
func (ctx *Context) Translate(format string, args ...interface{}) string {
|
|
return ctx.getFmt(TranslateFunctionContextKey)(format, args...)
|
|
}
|
|
|
|
// GetString same as Get but returns the value as string
|
|
// if nothing founds returns empty string ""
|
|
func (ctx *Context) GetString(key string) string {
|
|
if v, ok := ctx.Get(key).(string); ok {
|
|
return v
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
var errIntParse = errors.New("Unable to find or parse the integer, found: %#v")
|
|
|
|
// GetInt same as Get but tries to convert the return value as integer
|
|
// if nothing found or canno be parsed to integer it returns an error
|
|
func (ctx *Context) GetInt(key string) (int, error) {
|
|
v := ctx.Get(key)
|
|
if vint, ok := v.(int); ok {
|
|
return vint, nil
|
|
} else if vstring, sok := v.(string); sok {
|
|
return strconv.Atoi(vstring)
|
|
}
|
|
|
|
return -1, errIntParse.Format(v)
|
|
}
|
|
|
|
// Set sets a value to a key in the values map
|
|
func (ctx *Context) Set(key string, value interface{}) {
|
|
ctx.values.Set(key, value)
|
|
}
|
|
|
|
// VisitValues calls visitor for each existing context's temp values.
|
|
//
|
|
// visitor must not retain references to key and value after returning.
|
|
// Make key and/or value copies if you need storing them after returning.
|
|
func (ctx *Context) VisitValues(visitor func(string, interface{})) {
|
|
for i, n := 0, len(ctx.values); i < n; i++ {
|
|
kv := &ctx.values[i]
|
|
visitor(string(kv.key), kv.value)
|
|
}
|
|
}
|
|
|
|
// ParamsLen tries to return all the stored values which values are string, probably most of them will be the path parameters
|
|
func (ctx *Context) ParamsLen() (n int) {
|
|
ctx.VisitValues(func(kb string, vg interface{}) {
|
|
if _, ok := vg.(string); ok {
|
|
n++
|
|
}
|
|
|
|
})
|
|
return
|
|
}
|
|
|
|
// Param returns the string representation of the key's path named parameter's value
|
|
// same as GetString
|
|
func (ctx *Context) Param(key string) string {
|
|
return ctx.GetString(key)
|
|
}
|
|
|
|
// ParamDecoded returns a url-query-decoded path parameter's value
|
|
func (ctx *Context) ParamDecoded(key string) string {
|
|
return DecodeQuery(DecodeQuery(ctx.Param(key)))
|
|
}
|
|
|
|
// ParamInt returns the int representation of the key's path named parameter's value
|
|
// same as GetInt
|
|
func (ctx *Context) ParamInt(key string) (int, error) {
|
|
return ctx.GetInt(key)
|
|
}
|
|
|
|
// ParamInt64 returns the int64 representation of the key's path named parameter's value
|
|
func (ctx *Context) ParamInt64(key string) (int64, error) {
|
|
return strconv.ParseInt(ctx.Param(key), 10, 64)
|
|
}
|
|
|
|
// ParamsSentence returns a string implementation of all parameters that this context keeps
|
|
// hasthe form of key1=value1,key2=value2...
|
|
func (ctx *Context) ParamsSentence() string {
|
|
var buff bytes.Buffer
|
|
ctx.VisitValues(func(k string, vi interface{}) {
|
|
v, ok := vi.(string)
|
|
if !ok {
|
|
return
|
|
}
|
|
buff.WriteString(k)
|
|
buff.WriteString("=")
|
|
buff.WriteString(v)
|
|
// we don't know where that (yet) stops so...
|
|
buff.WriteString(",")
|
|
|
|
})
|
|
result := buff.String()
|
|
if len(result) < 2 {
|
|
return ""
|
|
}
|
|
|
|
return result[0 : len(result)-1]
|
|
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -----------https://github.com/golang/net/blob/master/context/context.go--------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
// Deadline returns the time when work done on behalf of this context
|
|
// should be canceled. Deadline returns ok==false when no deadline is
|
|
// set. Successive calls to Deadline return the same results.
|
|
func (ctx *Context) Deadline() (deadline time.Time, ok bool) {
|
|
return
|
|
}
|
|
|
|
// Done returns a channel that's closed when work done on behalf of this
|
|
// context should be canceled. Done may return nil if this context can
|
|
// never be canceled. Successive calls to Done return the same value.
|
|
//
|
|
// WithCancel arranges for Done to be closed when cancel is called;
|
|
// WithDeadline arranges for Done to be closed when the deadline
|
|
// expires; WithTimeout arranges for Done to be closed when the timeout
|
|
// elapses.
|
|
//
|
|
// Done is provided for use in select statements:
|
|
//
|
|
// // Stream generates values with DoSomething and sends them to out
|
|
// // until DoSomething returns an error or ctx.Done is closed.
|
|
// func Stream(ctx context.Context, out chan<- Value) error {
|
|
// for {
|
|
// v, err := DoSomething(ctx)
|
|
// if err != nil {
|
|
// return err
|
|
// }
|
|
// select {
|
|
// case <-ctx.Done():
|
|
// return ctx.Err()
|
|
// case out <- v:
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// See http://blog.golang.org/pipelines for more examples of how to use
|
|
// a Done channel for cancelation.
|
|
func (ctx *Context) Done() <-chan struct{} {
|
|
return nil
|
|
}
|
|
|
|
// Err returns a non-nil error value after Done is closed. Err returns
|
|
// Canceled if the context was canceled or DeadlineExceeded if the
|
|
// context's deadline passed. No other values for Err are defined.
|
|
// After Done is closed, successive calls to Err return the same value.
|
|
func (ctx *Context) Err() error {
|
|
return nil
|
|
}
|
|
|
|
// Value returns the value associated with this context for key, or nil
|
|
// if no value is associated with key. Successive calls to Value with
|
|
// the same key returns the same result.
|
|
//
|
|
// Use context values only for request-scoped data that transits
|
|
// processes and API boundaries, not for passing optional parameters to
|
|
// functions.
|
|
//
|
|
// A key identifies a specific value in a Context. Functions that wish
|
|
// to store values in Context typically allocate a key in a global
|
|
// variable then use that key as the argument to context.WithValue and
|
|
// Context.Value. A key can be any type that supports equality;
|
|
// packages should define keys as an unexported type to avoid
|
|
// collisions.
|
|
//
|
|
// Packages that define a Context key should provide type-safe accessors
|
|
// for the values stores using that key:
|
|
//
|
|
// // Package user defines a User type that's stored in Contexts.
|
|
// package user
|
|
//
|
|
// import "golang.org/x/net/context"
|
|
//
|
|
// // User is the type of value stored in the Contexts.
|
|
// type User struct {...}
|
|
//
|
|
// // key is an unexported type for keys defined in this package.
|
|
// // This prevents collisions with keys defined in other packages.
|
|
// type key int
|
|
//
|
|
// // userKey is the key for user.User values in Contexts. It is
|
|
// // unexported; clients use user.NewContext and user.FromContext
|
|
// // instead of using this key directly.
|
|
// var userKey key = 0
|
|
//
|
|
// // NewContext returns a new Context that carries value u.
|
|
// func NewContext(ctx context.Context, u *User) context.Context {
|
|
// return context.WithValue(ctx, userKey, u)
|
|
// }
|
|
//
|
|
// // FromContext returns the User value stored in ctx, if any.
|
|
// func FromContext(ctx context.Context) (*User, bool) {
|
|
// u, ok := ctx.Value(userKey).(*User)
|
|
// return u, ok
|
|
// }
|
|
func (ctx *Context) Value(key interface{}) interface{} {
|
|
if key == 0 {
|
|
return ctx.Request
|
|
}
|
|
if k, ok := key.(string); ok {
|
|
return ctx.GetString(k)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// --------------------------------Session & Cookies------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
// VisitAllCookies takes a visitor which loops on each (request's) cookie key and value
|
|
func (ctx *Context) VisitAllCookies(visitor func(key string, value string)) {
|
|
for _, cookie := range ctx.Request.Cookies() {
|
|
visitor(cookie.Name, cookie.Value)
|
|
}
|
|
}
|
|
|
|
// GetCookie returns cookie's value by it's name
|
|
// returns empty string if nothing was found
|
|
func (ctx *Context) GetCookie(name string) string {
|
|
cookie, err := ctx.Request.Cookie(name)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return cookie.Value
|
|
}
|
|
|
|
// SetCookie adds a cookie
|
|
func (ctx *Context) SetCookie(cookie *http.Cookie) {
|
|
http.SetCookie(ctx.ResponseWriter, cookie)
|
|
}
|
|
|
|
// SetCookieKV adds a cookie, receives just a key(string) and a value(string)
|
|
//
|
|
// Expires on 2 hours by default(unchable)
|
|
// use ctx.SetCookie or http.SetCookie instead for more control.
|
|
func (ctx *Context) SetCookieKV(name, value string) {
|
|
c := &http.Cookie{}
|
|
c.Name = name
|
|
c.Value = value
|
|
c.HttpOnly = true
|
|
c.Expires = time.Now().Add(time.Duration(120) * time.Minute)
|
|
ctx.SetCookie(c)
|
|
}
|
|
|
|
// RemoveCookie deletes a cookie by it's name/key
|
|
func (ctx *Context) RemoveCookie(name string) {
|
|
c := &http.Cookie{}
|
|
c.Name = name
|
|
c.Value = ""
|
|
c.Path = "/"
|
|
c.HttpOnly = true
|
|
exp := time.Now().Add(-time.Duration(1) * time.Minute) //RFC says 1 second, but let's do it 1 minute to make sure is working...
|
|
c.Expires = exp
|
|
c.MaxAge = -1
|
|
ctx.SetCookie(c)
|
|
// delete request's cookie also, which is temporary available
|
|
ctx.Request.Header.Set("Cookie", "")
|
|
}
|
|
|
|
var errSessionsPolicyIsMissing = errors.New(
|
|
`
|
|
manually call of context.Session() for client IP: '%s' without specified SessionsPolicy!
|
|
Please .Adapt one of the available session managers inside 'kataras/iris/adaptors'.
|
|
|
|
Edit your main .go source file to adapt one of these and restart your app.
|
|
i.e: lines (<---) were missing.
|
|
-------------------------------------------------------------------
|
|
import (
|
|
"github.com/kataras/iris"
|
|
"github.com/kataras/iris/adaptors/httprouter" // or gorillamux
|
|
"github.com/kataras/iris/adaptors/sessions" // <--- this line
|
|
)
|
|
|
|
func main(){
|
|
app := iris.New()
|
|
// right below the iris.New()
|
|
app.Adapt(httprouter.New()) // or gorillamux.New()
|
|
|
|
mySessions := sessions.New(sessions.Config{
|
|
// Cookie string, the session's client cookie name, for example: "mysessionid"
|
|
//
|
|
// Defaults to "gosessionid"
|
|
Cookie: "mysessionid",
|
|
// base64 urlencoding,
|
|
// if you have strange name cookie name enable this
|
|
DecodeCookie: false,
|
|
// it's time.Duration, from the time cookie is created, how long it can be alive?
|
|
// 0 means no expire.
|
|
Expires: 0,
|
|
// the length of the sessionid's cookie's value
|
|
CookieLength: 32,
|
|
// if you want to invalid cookies on different subdomains
|
|
// of the same host, then enable it
|
|
DisableSubdomainPersistence: false,
|
|
})
|
|
|
|
// OPTIONALLY:
|
|
// import "gopkg.in/kataras/iris.v6/adaptors/sessions/sessiondb/redis"
|
|
// or import "github.com/kataras/go-sessions/sessiondb/$any_available_community_database"
|
|
// mySessions.UseDatabase(redis.New(...))
|
|
|
|
app.Adapt(mySessions) // <--- and this line were missing.
|
|
|
|
// the rest of your source code...
|
|
// ...
|
|
|
|
app.Listen("%s")
|
|
}
|
|
-------------------------------------------------------------------
|
|
`)
|
|
|
|
// Session returns the current Session.
|
|
//
|
|
// if SessionsPolicy is missing then a detailed how-to-fix message
|
|
// will be visible to the user (DevMode)
|
|
// and the return value will be NILL.
|
|
func (ctx *Context) Session() Session {
|
|
policy := ctx.framework.policies.SessionsPolicy
|
|
if policy.Start == nil {
|
|
ctx.framework.Log(DevMode,
|
|
errSessionsPolicyIsMissing.Format(ctx.RemoteAddr(), ctx.framework.Config.VHost).Error())
|
|
return nil
|
|
}
|
|
|
|
if ctx.session == nil {
|
|
ctx.session = policy.Start(ctx.ResponseWriter, ctx.Request)
|
|
}
|
|
|
|
return ctx.session
|
|
}
|
|
|
|
// SessionDestroy destroys the whole session, calls the provider's destroy and remove the cookie
|
|
func (ctx *Context) SessionDestroy() {
|
|
if sess := ctx.Session(); sess != nil {
|
|
ctx.framework.policies.SessionsPolicy.Destroy(ctx.ResponseWriter, ctx.Request)
|
|
}
|
|
}
|
|
|
|
var maxAgeExp = regexp.MustCompile(`maxage=(\d+)`)
|
|
|
|
// MaxAge returns the "cache-control" request header's value
|
|
// seconds as int64
|
|
// if header not found or parse failed then it returns -1
|
|
func (ctx *Context) MaxAge() int64 {
|
|
header := ctx.RequestHeader(cacheControl)
|
|
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
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// ---------------------------Transactions & Response Writer Recording------------------
|
|
// -------------------------------------------------------------------------------------
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
// Record transforms the context's basic and direct responseWriter to a ResponseRecorder
|
|
// which can be used to reset the body, reset headers, get the body,
|
|
// get & set the status code at any time and more
|
|
func (ctx *Context) Record() {
|
|
if w, ok := ctx.ResponseWriter.(*responseWriter); ok {
|
|
ctx.ResponseWriter = acquireResponseRecorder(w)
|
|
}
|
|
}
|
|
|
|
// Recorder returns the context's ResponseRecorder
|
|
// if not recording then it starts recording and returns the new context's ResponseRecorder
|
|
func (ctx *Context) Recorder() *ResponseRecorder {
|
|
ctx.Record()
|
|
return ctx.ResponseWriter.(*ResponseRecorder)
|
|
}
|
|
|
|
// IsRecording returns the response recorder and a true value
|
|
// when the response writer is recording the status code, body, headers and so on,
|
|
// else returns nil and false
|
|
func (ctx *Context) IsRecording() (*ResponseRecorder, bool) {
|
|
//NOTE:
|
|
// two return values in order to minimize the if statement:
|
|
// if (Recording) then writer = Recorder()
|
|
// instead we do: recorder,ok = Recording()
|
|
rr, ok := ctx.ResponseWriter.(*ResponseRecorder)
|
|
return rr, ok
|
|
}
|
|
|
|
// skipTransactionsContextKey set this to any value to stop executing next transactions
|
|
// it's a context-key in order to be used from anywhere, set it by calling the SkipTransactions()
|
|
const skipTransactionsContextKey = "__IRIS_TRANSACTIONS_SKIP___"
|
|
|
|
// SkipTransactions if called then skip the rest of the transactions
|
|
// or all of them if called before the first transaction
|
|
func (ctx *Context) SkipTransactions() {
|
|
ctx.Set(skipTransactionsContextKey, 1)
|
|
}
|
|
|
|
// TransactionsSkipped returns true if the transactions skipped or canceled at all.
|
|
func (ctx *Context) TransactionsSkipped() bool {
|
|
if n, err := ctx.GetInt(skipTransactionsContextKey); err == nil && n == 1 {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// non-detailed error log for transacton unexpected panic
|
|
var errTransactionInterrupted = errors.New("Transaction Interrupted, recovery from panic:\n%s")
|
|
|
|
// BeginTransaction starts a scoped transaction.
|
|
//
|
|
// Can't say a lot here because it will take more than 200 lines to write about.
|
|
// You can search third-party articles or books on how Business Transaction works (it's quite simple, especially here).
|
|
//
|
|
// Note that this is unique and new
|
|
// (=I haver never seen any other examples or code in Golang on this subject, so far, as with the most of iris features...)
|
|
// it's not covers all paths,
|
|
// such as databases, this should be managed by the libraries you use to make your database connection,
|
|
// this transaction scope is only for context's response.
|
|
// Transactions have their own middleware ecosystem also, look iris.go:UseTransaction.
|
|
//
|
|
// See https://github.com/iris-contrib/examples/tree/master/transactions for more
|
|
func (ctx *Context) BeginTransaction(pipe func(transaction *Transaction)) {
|
|
// SILLY NOTE: use of manual pipe type in order of TransactionFunc
|
|
// in order to help editors complete the sentence here...
|
|
|
|
// do NOT begin a transaction when the previous transaction has been failed
|
|
// and it was requested scoped or SkipTransactions called manually.
|
|
if ctx.TransactionsSkipped() {
|
|
return
|
|
}
|
|
|
|
// start recording in order to be able to control the full response writer
|
|
ctx.Record()
|
|
|
|
// get a transaction scope from the pool by passing the temp context/
|
|
t := newTransaction(ctx)
|
|
defer func() {
|
|
if err := recover(); err != nil {
|
|
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*
|
|
}
|
|
|
|
// write the temp contents to the original writer
|
|
t.Context.ResponseWriter.writeTo(ctx.ResponseWriter)
|
|
|
|
// give back to the transaction the original writer (SetBeforeFlush works this way and only this way)
|
|
// this is tricky but nessecery if we want ctx.EmitError to work inside transactions
|
|
t.Context.ResponseWriter = ctx.ResponseWriter
|
|
}()
|
|
|
|
// run the worker with its context clone inside.
|
|
pipe(t)
|
|
}
|
|
|
|
// Log logs to the iris defined logger
|
|
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
|
|
func (ctx *Context) Framework() *Framework {
|
|
return ctx.framework
|
|
}
|