github.com/wangkui503/aero@v1.0.0/Context.go (about)

     1  package aero
     2  
     3  import (
     4  	"bytes"
     5  	"compress/gzip"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"log"
    10  	"mime"
    11  	"net/http"
    12  	"path/filepath"
    13  	"strconv"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/aerogo/session"
    18  	"github.com/fatih/color"
    19  	jsoniter "github.com/json-iterator/go"
    20  	"github.com/julienschmidt/httprouter"
    21  	"github.com/tomasen/realip"
    22  )
    23  
    24  // This should be close to the MTU size of a TCP packet.
    25  // Regarding performance it makes no sense to compress smaller files.
    26  // Bandwidth can be saved however the savings are minimal for small files
    27  // and the overhead of compressing can lead up to a 75% reduction
    28  // in server speed under high load. Therefore in this case
    29  // we're trying to optimize for performance, not bandwidth.
    30  const gzipThreshold = 1450
    31  
    32  const (
    33  	cacheControlHeader            = "Cache-Control"
    34  	cacheControlAlwaysValidate    = "must-revalidate"
    35  	cacheControlMedia             = "public, max-age=13824000"
    36  	contentTypeOptionsHeader      = "X-Content-Type-Options"
    37  	contentTypeOptions            = "nosniff"
    38  	xssProtectionHeader           = "X-XSS-Protection"
    39  	xssProtection                 = "1; mode=block"
    40  	etagHeader                    = "ETag"
    41  	contentTypeHeader             = "Content-Type"
    42  	contentTypeHTML               = "text/html; charset=utf-8"
    43  	contentTypeCSS                = "text/css; charset=utf-8"
    44  	contentTypeJavaScript         = "application/javascript; charset=utf-8"
    45  	contentTypeJSON               = "application/json; charset=utf-8"
    46  	contentTypeJSONLD             = "application/ld+json; charset=utf-8"
    47  	contentTypePlainText          = "text/plain; charset=utf-8"
    48  	contentTypeEventStream        = "text/event-stream; charset=utf-8"
    49  	contentEncodingHeader         = "Content-Encoding"
    50  	contentEncodingGzip           = "gzip"
    51  	acceptEncodingHeader          = "Accept-Encoding"
    52  	contentLengthHeader           = "Content-Length"
    53  	ifNoneMatchHeader             = "If-None-Match"
    54  	referrerPolicyHeader          = "Referrer-Policy"
    55  	referrerPolicySameOrigin      = "no-referrer"
    56  	strictTransportSecurityHeader = "Strict-Transport-Security"
    57  	strictTransportSecurity       = "max-age=31536000; includeSubDomains; preload"
    58  	contentSecurityPolicyHeader   = "Content-Security-Policy"
    59  
    60  	// responseTimeHeader            = "X-Response-Time"
    61  	// xFrameOptionsHeader           = "X-Frame-Options"
    62  	// xFrameOptions                 = "SAMEORIGIN"
    63  	// serverHeader                  = "Server"
    64  	// server                        = "Aero"
    65  )
    66  
    67  // Push options describes the headers that are sent
    68  // to our server to retrieve the push response.
    69  var pushOptions = http.PushOptions{
    70  	Method: "GET",
    71  	Header: http.Header{
    72  		acceptEncodingHeader: []string{"gzip"},
    73  	},
    74  }
    75  
    76  // Context represents a single request & response.
    77  type Context struct {
    78  	// net/http
    79  	request  *http.Request
    80  	response http.ResponseWriter
    81  	params   httprouter.Params
    82  
    83  	// Responded tells if the request has been dealt with already
    84  	responded bool
    85  
    86  	// A pointer to the application this request occurred on.
    87  	App *Application
    88  
    89  	// Status code
    90  	StatusCode int
    91  
    92  	// Error message
    93  	ErrorMessage string
    94  
    95  	// Custom data
    96  	Data interface{}
    97  
    98  	// User session
    99  	session *session.Session
   100  }
   101  
   102  // Request returns the HTTP request.
   103  func (ctx *Context) Request() Request {
   104  	return Request{
   105  		inner: ctx.request,
   106  	}
   107  }
   108  
   109  // Response returns the HTTP response.
   110  func (ctx *Context) Response() Response {
   111  	return Response{
   112  		inner: ctx.response,
   113  	}
   114  }
   115  
   116  // Session returns the session of the context or creates and caches a new session.
   117  func (ctx *Context) Session() *session.Session {
   118  	// Return cached session if available.
   119  	if ctx.session != nil {
   120  		return ctx.session
   121  	}
   122  
   123  	// Check if the client has a session cookie already.
   124  	cookie, err := ctx.request.Cookie("sid")
   125  
   126  	if err == nil {
   127  		sid := cookie.Value
   128  
   129  		if session.IsValidID(sid) {
   130  			ctx.session, err = ctx.App.Sessions.Store.Get(sid)
   131  
   132  			if err != nil {
   133  				color.Red(err.Error())
   134  			}
   135  
   136  			if ctx.session != nil {
   137  				return ctx.session
   138  			}
   139  		}
   140  	}
   141  
   142  	// Create a new session
   143  	ctx.session = ctx.App.Sessions.New()
   144  
   145  	// Create a session cookie in the client
   146  	ctx.createSessionCookie()
   147  
   148  	return ctx.session
   149  }
   150  
   151  // createSessionCookie creates a session cookie in the client.
   152  func (ctx *Context) createSessionCookie() {
   153  	sessionCookie := http.Cookie{
   154  		Name:     "sid",
   155  		Value:    ctx.session.ID(),
   156  		HttpOnly: true,
   157  		Secure:   true,
   158  		MaxAge:   ctx.App.Sessions.Duration,
   159  		Path:     "/",
   160  	}
   161  
   162  	http.SetCookie(ctx.response, &sessionCookie)
   163  }
   164  
   165  // HasSession indicates whether the client has a valid session or not.
   166  func (ctx *Context) HasSession() bool {
   167  	if ctx.session != nil {
   168  		return true
   169  	}
   170  
   171  	cookie, err := ctx.request.Cookie("sid")
   172  
   173  	if err != nil || !session.IsValidID(cookie.Value) {
   174  		return false
   175  	}
   176  
   177  	ctx.session, err = ctx.App.Sessions.Store.Get(cookie.Value)
   178  
   179  	if err != nil {
   180  		return false
   181  	}
   182  
   183  	return ctx.session != nil
   184  }
   185  
   186  // JSON encodes the object to a JSON string and responds.
   187  func (ctx *Context) JSON(value interface{}) string {
   188  	ctx.response.Header().Set(contentTypeHeader, contentTypeJSON)
   189  
   190  	bytes, err := jsoniter.Marshal(value)
   191  
   192  	if err != nil {
   193  		ctx.StatusCode = http.StatusInternalServerError
   194  		return `{"error": "Could not encode object to JSON"}`
   195  	}
   196  
   197  	return string(bytes)
   198  }
   199  
   200  // JSONLinkedData encodes the object to a JSON linked data string and responds.
   201  func (ctx *Context) JSONLinkedData(value interface{}) string {
   202  	ctx.response.Header().Set(contentTypeHeader, contentTypeJSONLD)
   203  
   204  	bytes, err := jsoniter.Marshal(value)
   205  
   206  	if err != nil {
   207  		ctx.StatusCode = http.StatusInternalServerError
   208  		return `{"error": "Could not encode object to JSON"}`
   209  	}
   210  
   211  	return string(bytes)
   212  }
   213  
   214  // HTML sends a HTML string.
   215  func (ctx *Context) HTML(html string) string {
   216  	ctx.response.Header().Set(contentTypeHeader, contentTypeHTML)
   217  	ctx.response.Header().Set(contentTypeOptionsHeader, contentTypeOptions)
   218  	ctx.response.Header().Set(xssProtectionHeader, xssProtection)
   219  	// ctx.response.Header().Set(xFrameOptionsHeader, xFrameOptions)
   220  	ctx.response.Header().Set(referrerPolicyHeader, referrerPolicySameOrigin)
   221  
   222  	if ctx.App.Security.Certificate != "" {
   223  		ctx.response.Header().Set(strictTransportSecurityHeader, strictTransportSecurity)
   224  		ctx.response.Header().Set(contentSecurityPolicyHeader, ctx.App.ContentSecurityPolicy.String())
   225  	}
   226  
   227  	return html
   228  }
   229  
   230  // Text sends a plain text string.
   231  func (ctx *Context) Text(text string) string {
   232  	ctx.response.Header().Set(contentTypeHeader, contentTypePlainText)
   233  	return text
   234  }
   235  
   236  // CSS sends a style sheet.
   237  func (ctx *Context) CSS(text string) string {
   238  	ctx.response.Header().Set(contentTypeHeader, contentTypeCSS)
   239  	return text
   240  }
   241  
   242  // JavaScript sends a script.
   243  func (ctx *Context) JavaScript(code string) string {
   244  	ctx.response.Header().Set(contentTypeHeader, contentTypeJavaScript)
   245  	return code
   246  }
   247  
   248  // EventStream sends server events to the client.
   249  func (ctx *Context) EventStream(stream *EventStream) string {
   250  	defer close(stream.Closed)
   251  
   252  	// Flush
   253  	flusher, ok := ctx.response.(http.Flusher)
   254  
   255  	if !ok {
   256  		return ctx.Error(http.StatusNotImplemented, "Flushing not supported")
   257  	}
   258  
   259  	// Catch disconnect events
   260  	disconnected := ctx.request.Context().Done()
   261  
   262  	// Send headers
   263  	header := ctx.response.Header()
   264  	header.Set(contentTypeHeader, contentTypeEventStream)
   265  	header.Set(cacheControlHeader, "no-cache")
   266  	header.Set("Connection", "keep-alive")
   267  	header.Set("Access-Control-Allow-Origin", "*")
   268  	ctx.response.WriteHeader(200)
   269  	ctx.responded = true
   270  
   271  	for {
   272  		select {
   273  		case <-disconnected:
   274  			return ""
   275  
   276  		case event := <-stream.Events:
   277  			if event != nil {
   278  				data := event.Data
   279  
   280  				switch data.(type) {
   281  				case string, []byte:
   282  					// Do nothing with the data if it's already a string or byte slice.
   283  				default:
   284  					data, _ = jsoniter.Marshal(data)
   285  				}
   286  
   287  				fmt.Fprintf(ctx.response, "event: %s\ndata: %s\n\n", event.Name, data)
   288  				flusher.Flush()
   289  			}
   290  
   291  		case <-time.After(5 * time.Second):
   292  			// Send one byte to keep alive the connection
   293  			// which will also check for disconnection.
   294  			ctx.response.Write([]byte("\n"))
   295  			flusher.Flush()
   296  		}
   297  	}
   298  }
   299  
   300  // File sends the contents of a local file and determines its mime type by extension.
   301  func (ctx *Context) File(file string) string {
   302  	extension := filepath.Ext(file)
   303  	contentType := mime.TypeByExtension(extension)
   304  
   305  	// Cache control header
   306  	if IsMediaType(contentType) {
   307  		ctx.response.Header().Set(cacheControlHeader, cacheControlMedia)
   308  	}
   309  
   310  	http.ServeFile(ctx.response, ctx.request, file)
   311  	ctx.responded = true
   312  	return ""
   313  }
   314  
   315  // ReadAll returns the contents of the reader.
   316  // This will create an in-memory copy and calculate the E-Tag before sending the data.
   317  // Compression will be applied if necessary.
   318  func (ctx *Context) ReadAll(reader io.Reader) string {
   319  	data, err := ioutil.ReadAll(reader)
   320  
   321  	if err != nil {
   322  		return ctx.Error(http.StatusInternalServerError, err)
   323  	}
   324  
   325  	return BytesToStringUnsafe(data)
   326  }
   327  
   328  // Reader sends the contents of the io.Reader without creating an in-memory copy.
   329  // E-Tags will not be generated for the content and compression will not be applied.
   330  // Use this function if your reader contains huge amounts of data.
   331  func (ctx *Context) Reader(reader io.Reader) string {
   332  	io.Copy(ctx.response, reader)
   333  	ctx.responded = true
   334  	return ""
   335  }
   336  
   337  // ReadSeeker sends the contents of the io.ReadSeeker without creating an in-memory copy.
   338  // E-Tags will not be generated for the content and compression will not be applied.
   339  // Use this function if your reader contains huge amounts of data.
   340  func (ctx *Context) ReadSeeker(reader io.ReadSeeker) string {
   341  	http.ServeContent(ctx.response, ctx.request, "", time.Time{}, reader)
   342  	ctx.responded = true
   343  	return ""
   344  }
   345  
   346  // Error should be used for sending error messages to the user.
   347  func (ctx *Context) Error(statusCode int, errors ...interface{}) string {
   348  	ctx.StatusCode = statusCode
   349  	ctx.response.Header().Set(contentTypeHeader, contentTypeHTML)
   350  
   351  	message := bytes.Buffer{}
   352  
   353  	if len(errors) == 0 {
   354  		message.WriteString(fmt.Sprintf("Unknown error: %d", statusCode))
   355  	} else {
   356  		for index, param := range errors {
   357  			switch err := param.(type) {
   358  			case string:
   359  				message.WriteString(err)
   360  			case error:
   361  				message.WriteString(err.Error())
   362  			default:
   363  				continue
   364  			}
   365  
   366  			if index != len(errors)-1 {
   367  				message.WriteString(": ")
   368  			}
   369  		}
   370  	}
   371  
   372  	ctx.ErrorMessage = message.String()
   373  	color.Red(ctx.ErrorMessage)
   374  	return ctx.ErrorMessage
   375  }
   376  
   377  // URI returns the relative path, e.g. /blog/post/123.
   378  func (ctx *Context) URI() string {
   379  	return ctx.request.URL.Path
   380  }
   381  
   382  // SetURI sets the relative path, e.g. /blog/post/123.
   383  func (ctx *Context) SetURI(b string) {
   384  	ctx.request.URL.Path = b
   385  }
   386  
   387  // Get retrieves an URL parameter.
   388  func (ctx *Context) Get(param string) string {
   389  	return strings.TrimPrefix(ctx.params.ByName(param), "/")
   390  }
   391  
   392  // GetInt retrieves an URL parameter as an integer.
   393  func (ctx *Context) GetInt(param string) (int, error) {
   394  	return strconv.Atoi(ctx.Get(param))
   395  }
   396  
   397  // RealIP tries to determine the real IP address of the request.
   398  func (ctx *Context) RealIP() string {
   399  	return strings.Trim(realip.RealIP(ctx.request), "[]")
   400  }
   401  
   402  // UserAgent retrieves the user agent for the given request.
   403  func (ctx *Context) UserAgent() string {
   404  	ctx.request.URL.Query()
   405  	return ctx.request.UserAgent()
   406  }
   407  
   408  // Query retrieves the value for the given URL query parameter.
   409  func (ctx *Context) Query(param string) string {
   410  	return ctx.request.URL.Query().Get(param)
   411  }
   412  
   413  // Redirect redirects to the given URL using status code 302.
   414  func (ctx *Context) Redirect(url string) string {
   415  	ctx.StatusCode = http.StatusFound
   416  	ctx.response.Header().Set("Location", url)
   417  	return ""
   418  }
   419  
   420  // RedirectPermanently redirects to the given URL and indicates that this is a permanent change using status code 301.
   421  func (ctx *Context) RedirectPermanently(url string) string {
   422  	ctx.StatusCode = http.StatusMovedPermanently
   423  	ctx.response.Header().Set("Location", url)
   424  	return ""
   425  }
   426  
   427  // IsMediaType returns whether the given content type is a media type.
   428  func IsMediaType(contentType string) bool {
   429  	return strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/") || strings.HasPrefix(contentType, "audio/")
   430  }
   431  
   432  // pushResources will push the given resources to the HTTP response.
   433  func (ctx *Context) pushResources() {
   434  	// Check if all the conditions for a push are met
   435  	for _, pushCondition := range ctx.App.pushConditions {
   436  		if !pushCondition(ctx) {
   437  			return
   438  		}
   439  	}
   440  
   441  	// OnPush callbacks
   442  	for _, callback := range ctx.App.onPush {
   443  		callback(ctx)
   444  	}
   445  
   446  	// Check if we can push
   447  	pusher, ok := ctx.response.(http.Pusher)
   448  
   449  	if !ok {
   450  		return
   451  	}
   452  
   453  	// Push every resource defined in config.json
   454  	for _, resource := range ctx.App.Config.Push {
   455  		if err := pusher.Push(resource, &pushOptions); err != nil {
   456  			log.Printf("Failed to push %s: %v", resource, err)
   457  		}
   458  	}
   459  }
   460  
   461  // respond responds either with raw code or gzipped if the
   462  // code length is greater than the gzip threshold.
   463  func (ctx *Context) respond(code string) {
   464  	// If the request has been dealt with already,
   465  	// or if the request has been canceled by the client,
   466  	// there's nothing to do here.
   467  	if ctx.responded || ctx.request.Context().Err() != nil {
   468  		return
   469  	}
   470  
   471  	ctx.respondBytes(StringToBytesUnsafe(code))
   472  }
   473  
   474  // respondBytes responds either with raw code or gzipped if the
   475  // code length is greater than the gzip threshold. Requires a byte slice.
   476  func (ctx *Context) respondBytes(b []byte) {
   477  	response := ctx.response
   478  	header := response.Header()
   479  	contentType := header.Get(contentTypeHeader)
   480  	isMedia := IsMediaType(contentType)
   481  
   482  	// Cache control header
   483  	if isMedia {
   484  		header.Set(cacheControlHeader, cacheControlMedia)
   485  	} else {
   486  		header.Set(cacheControlHeader, cacheControlAlwaysValidate)
   487  	}
   488  
   489  	// Push
   490  	if contentType == contentTypeHTML && len(ctx.App.Config.Push) > 0 {
   491  		ctx.pushResources()
   492  	}
   493  
   494  	// Small response
   495  	if len(b) < gzipThreshold {
   496  		header.Set(contentLengthHeader, strconv.Itoa(len(b)))
   497  		response.WriteHeader(ctx.StatusCode)
   498  		response.Write(b)
   499  		return
   500  	}
   501  
   502  	// ETag generation
   503  	etag := ETag(b)
   504  
   505  	// If client cache is up to date, send 304 with no response body.
   506  	clientETag := ctx.request.Header.Get(ifNoneMatchHeader)
   507  
   508  	if etag == clientETag {
   509  		response.WriteHeader(304)
   510  		return
   511  	}
   512  
   513  	// Set ETag
   514  	header.Set(etagHeader, etag)
   515  
   516  	// No GZip?
   517  	supportsGZip := strings.Contains(ctx.request.Header.Get(acceptEncodingHeader), "gzip")
   518  
   519  	if !ctx.App.Config.GZip || !supportsGZip || isMedia {
   520  		header.Set(contentLengthHeader, strconv.Itoa(len(b)))
   521  		response.WriteHeader(ctx.StatusCode)
   522  		response.Write(b)
   523  		return
   524  	}
   525  
   526  	// GZip
   527  	header.Set(contentEncodingHeader, contentEncodingGzip)
   528  	response.WriteHeader(ctx.StatusCode)
   529  
   530  	// Write response body
   531  	writer, _ := gzip.NewWriterLevel(response, gzip.BestCompression)
   532  	writer.Write(b)
   533  	writer.Flush()
   534  }