github.com/astaxie/beego@v1.12.3/context/output.go (about)

     1  // Copyright 2014 beego Author. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package context
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  	"encoding/xml"
    21  	"errors"
    22  	"fmt"
    23  	"html/template"
    24  	"io"
    25  	"mime"
    26  	"net/http"
    27  	"net/url"
    28  	"os"
    29  	"path/filepath"
    30  	"strconv"
    31  	"strings"
    32  	"time"
    33  
    34  	yaml "gopkg.in/yaml.v2"
    35  )
    36  
    37  // BeegoOutput does work for sending response header.
    38  type BeegoOutput struct {
    39  	Context    *Context
    40  	Status     int
    41  	EnableGzip bool
    42  }
    43  
    44  // NewOutput returns new BeegoOutput.
    45  // it contains nothing now.
    46  func NewOutput() *BeegoOutput {
    47  	return &BeegoOutput{}
    48  }
    49  
    50  // Reset init BeegoOutput
    51  func (output *BeegoOutput) Reset(ctx *Context) {
    52  	output.Context = ctx
    53  	output.Status = 0
    54  }
    55  
    56  // Header sets response header item string via given key.
    57  func (output *BeegoOutput) Header(key, val string) {
    58  	output.Context.ResponseWriter.Header().Set(key, val)
    59  }
    60  
    61  // Body sets response body content.
    62  // if EnableGzip, compress content string.
    63  // it sends out response body directly.
    64  func (output *BeegoOutput) Body(content []byte) error {
    65  	var encoding string
    66  	var buf = &bytes.Buffer{}
    67  	if output.EnableGzip {
    68  		encoding = ParseEncoding(output.Context.Request)
    69  	}
    70  	if b, n, _ := WriteBody(encoding, buf, content); b {
    71  		output.Header("Content-Encoding", n)
    72  		output.Header("Content-Length", strconv.Itoa(buf.Len()))
    73  	} else {
    74  		output.Header("Content-Length", strconv.Itoa(len(content)))
    75  	}
    76  	// Write status code if it has been set manually
    77  	// Set it to 0 afterwards to prevent "multiple response.WriteHeader calls"
    78  	if output.Status != 0 {
    79  		output.Context.ResponseWriter.WriteHeader(output.Status)
    80  		output.Status = 0
    81  	} else {
    82  		output.Context.ResponseWriter.Started = true
    83  	}
    84  	io.Copy(output.Context.ResponseWriter, buf)
    85  	return nil
    86  }
    87  
    88  // Cookie sets cookie value via given key.
    89  // others are ordered as cookie's max age time, path,domain, secure and httponly.
    90  func (output *BeegoOutput) Cookie(name string, value string, others ...interface{}) {
    91  	var b bytes.Buffer
    92  	fmt.Fprintf(&b, "%s=%s", sanitizeName(name), sanitizeValue(value))
    93  
    94  	//fix cookie not work in IE
    95  	if len(others) > 0 {
    96  		var maxAge int64
    97  
    98  		switch v := others[0].(type) {
    99  		case int:
   100  			maxAge = int64(v)
   101  		case int32:
   102  			maxAge = int64(v)
   103  		case int64:
   104  			maxAge = v
   105  		}
   106  
   107  		switch {
   108  		case maxAge > 0:
   109  			fmt.Fprintf(&b, "; Expires=%s; Max-Age=%d", time.Now().Add(time.Duration(maxAge)*time.Second).UTC().Format(time.RFC1123), maxAge)
   110  		case maxAge < 0:
   111  			fmt.Fprintf(&b, "; Max-Age=0")
   112  		}
   113  	}
   114  
   115  	// the settings below
   116  	// Path, Domain, Secure, HttpOnly
   117  	// can use nil skip set
   118  
   119  	// default "/"
   120  	if len(others) > 1 {
   121  		if v, ok := others[1].(string); ok && len(v) > 0 {
   122  			fmt.Fprintf(&b, "; Path=%s", sanitizeValue(v))
   123  		}
   124  	} else {
   125  		fmt.Fprintf(&b, "; Path=%s", "/")
   126  	}
   127  
   128  	// default empty
   129  	if len(others) > 2 {
   130  		if v, ok := others[2].(string); ok && len(v) > 0 {
   131  			fmt.Fprintf(&b, "; Domain=%s", sanitizeValue(v))
   132  		}
   133  	}
   134  
   135  	// default empty
   136  	if len(others) > 3 {
   137  		var secure bool
   138  		switch v := others[3].(type) {
   139  		case bool:
   140  			secure = v
   141  		default:
   142  			if others[3] != nil {
   143  				secure = true
   144  			}
   145  		}
   146  		if secure {
   147  			fmt.Fprintf(&b, "; Secure")
   148  		}
   149  	}
   150  
   151  	// default false. for session cookie default true
   152  	if len(others) > 4 {
   153  		if v, ok := others[4].(bool); ok && v {
   154  			fmt.Fprintf(&b, "; HttpOnly")
   155  		}
   156  	}
   157  
   158  	output.Context.ResponseWriter.Header().Add("Set-Cookie", b.String())
   159  }
   160  
   161  var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-")
   162  
   163  func sanitizeName(n string) string {
   164  	return cookieNameSanitizer.Replace(n)
   165  }
   166  
   167  var cookieValueSanitizer = strings.NewReplacer("\n", " ", "\r", " ", ";", " ")
   168  
   169  func sanitizeValue(v string) string {
   170  	return cookieValueSanitizer.Replace(v)
   171  }
   172  
   173  func jsonRenderer(value interface{}) Renderer {
   174  	return rendererFunc(func(ctx *Context) {
   175  		ctx.Output.JSON(value, false, false)
   176  	})
   177  }
   178  
   179  func errorRenderer(err error) Renderer {
   180  	return rendererFunc(func(ctx *Context) {
   181  		ctx.Output.SetStatus(500)
   182  		ctx.Output.Body([]byte(err.Error()))
   183  	})
   184  }
   185  
   186  // JSON writes json to response body.
   187  // if encoding is true, it converts utf-8 to \u0000 type.
   188  func (output *BeegoOutput) JSON(data interface{}, hasIndent bool, encoding bool) error {
   189  	output.Header("Content-Type", "application/json; charset=utf-8")
   190  	var content []byte
   191  	var err error
   192  	if hasIndent {
   193  		content, err = json.MarshalIndent(data, "", "  ")
   194  	} else {
   195  		content, err = json.Marshal(data)
   196  	}
   197  	if err != nil {
   198  		http.Error(output.Context.ResponseWriter, err.Error(), http.StatusInternalServerError)
   199  		return err
   200  	}
   201  	if encoding {
   202  		content = []byte(stringsToJSON(string(content)))
   203  	}
   204  	return output.Body(content)
   205  }
   206  
   207  // YAML writes yaml to response body.
   208  func (output *BeegoOutput) YAML(data interface{}) error {
   209  	output.Header("Content-Type", "application/x-yaml; charset=utf-8")
   210  	var content []byte
   211  	var err error
   212  	content, err = yaml.Marshal(data)
   213  	if err != nil {
   214  		http.Error(output.Context.ResponseWriter, err.Error(), http.StatusInternalServerError)
   215  		return err
   216  	}
   217  	return output.Body(content)
   218  }
   219  
   220  // JSONP writes jsonp to response body.
   221  func (output *BeegoOutput) JSONP(data interface{}, hasIndent bool) error {
   222  	output.Header("Content-Type", "application/javascript; charset=utf-8")
   223  	var content []byte
   224  	var err error
   225  	if hasIndent {
   226  		content, err = json.MarshalIndent(data, "", "  ")
   227  	} else {
   228  		content, err = json.Marshal(data)
   229  	}
   230  	if err != nil {
   231  		http.Error(output.Context.ResponseWriter, err.Error(), http.StatusInternalServerError)
   232  		return err
   233  	}
   234  	callback := output.Context.Input.Query("callback")
   235  	if callback == "" {
   236  		return errors.New(`"callback" parameter required`)
   237  	}
   238  	callback = template.JSEscapeString(callback)
   239  	callbackContent := bytes.NewBufferString(" if(window." + callback + ")" + callback)
   240  	callbackContent.WriteString("(")
   241  	callbackContent.Write(content)
   242  	callbackContent.WriteString(");\r\n")
   243  	return output.Body(callbackContent.Bytes())
   244  }
   245  
   246  // XML writes xml string to response body.
   247  func (output *BeegoOutput) XML(data interface{}, hasIndent bool) error {
   248  	output.Header("Content-Type", "application/xml; charset=utf-8")
   249  	var content []byte
   250  	var err error
   251  	if hasIndent {
   252  		content, err = xml.MarshalIndent(data, "", "  ")
   253  	} else {
   254  		content, err = xml.Marshal(data)
   255  	}
   256  	if err != nil {
   257  		http.Error(output.Context.ResponseWriter, err.Error(), http.StatusInternalServerError)
   258  		return err
   259  	}
   260  	return output.Body(content)
   261  }
   262  
   263  // ServeFormatted serve YAML, XML OR JSON, depending on the value of the Accept header
   264  func (output *BeegoOutput) ServeFormatted(data interface{}, hasIndent bool, hasEncode ...bool) {
   265  	accept := output.Context.Input.Header("Accept")
   266  	switch accept {
   267  	case ApplicationYAML:
   268  		output.YAML(data)
   269  	case ApplicationXML, TextXML:
   270  		output.XML(data, hasIndent)
   271  	default:
   272  		output.JSON(data, hasIndent, len(hasEncode) > 0 && hasEncode[0])
   273  	}
   274  }
   275  
   276  // Download forces response for download file.
   277  // it prepares the download response header automatically.
   278  func (output *BeegoOutput) Download(file string, filename ...string) {
   279  	// check get file error, file not found or other error.
   280  	if _, err := os.Stat(file); err != nil {
   281  		http.ServeFile(output.Context.ResponseWriter, output.Context.Request, file)
   282  		return
   283  	}
   284  
   285  	var fName string
   286  	if len(filename) > 0 && filename[0] != "" {
   287  		fName = filename[0]
   288  	} else {
   289  		fName = filepath.Base(file)
   290  	}
   291  	//https://tools.ietf.org/html/rfc6266#section-4.3
   292  	fn := url.PathEscape(fName)
   293  	if fName == fn {
   294  		fn = "filename=" + fn
   295  	} else {
   296  		/**
   297  		  The parameters "filename" and "filename*" differ only in that
   298  		  "filename*" uses the encoding defined in [RFC5987], allowing the use
   299  		  of characters not present in the ISO-8859-1 character set
   300  		  ([ISO-8859-1]).
   301  		*/
   302  		fn = "filename=" + fName + "; filename*=utf-8''" + fn
   303  	}
   304  	output.Header("Content-Disposition", "attachment; "+fn)
   305  	output.Header("Content-Description", "File Transfer")
   306  	output.Header("Content-Type", "application/octet-stream")
   307  	output.Header("Content-Transfer-Encoding", "binary")
   308  	output.Header("Expires", "0")
   309  	output.Header("Cache-Control", "must-revalidate")
   310  	output.Header("Pragma", "public")
   311  	http.ServeFile(output.Context.ResponseWriter, output.Context.Request, file)
   312  }
   313  
   314  // ContentType sets the content type from ext string.
   315  // MIME type is given in mime package.
   316  func (output *BeegoOutput) ContentType(ext string) {
   317  	if !strings.HasPrefix(ext, ".") {
   318  		ext = "." + ext
   319  	}
   320  	ctype := mime.TypeByExtension(ext)
   321  	if ctype != "" {
   322  		output.Header("Content-Type", ctype)
   323  	}
   324  }
   325  
   326  // SetStatus sets response status code.
   327  // It writes response header directly.
   328  func (output *BeegoOutput) SetStatus(status int) {
   329  	output.Status = status
   330  }
   331  
   332  // IsCachable returns boolean of this request is cached.
   333  // HTTP 304 means cached.
   334  func (output *BeegoOutput) IsCachable() bool {
   335  	return output.Status >= 200 && output.Status < 300 || output.Status == 304
   336  }
   337  
   338  // IsEmpty returns boolean of this request is empty.
   339  // HTTP 201,204 and 304 means empty.
   340  func (output *BeegoOutput) IsEmpty() bool {
   341  	return output.Status == 201 || output.Status == 204 || output.Status == 304
   342  }
   343  
   344  // IsOk returns boolean of this request runs well.
   345  // HTTP 200 means ok.
   346  func (output *BeegoOutput) IsOk() bool {
   347  	return output.Status == 200
   348  }
   349  
   350  // IsSuccessful returns boolean of this request runs successfully.
   351  // HTTP 2xx means ok.
   352  func (output *BeegoOutput) IsSuccessful() bool {
   353  	return output.Status >= 200 && output.Status < 300
   354  }
   355  
   356  // IsRedirect returns boolean of this request is redirection header.
   357  // HTTP 301,302,307 means redirection.
   358  func (output *BeegoOutput) IsRedirect() bool {
   359  	return output.Status == 301 || output.Status == 302 || output.Status == 303 || output.Status == 307
   360  }
   361  
   362  // IsForbidden returns boolean of this request is forbidden.
   363  // HTTP 403 means forbidden.
   364  func (output *BeegoOutput) IsForbidden() bool {
   365  	return output.Status == 403
   366  }
   367  
   368  // IsNotFound returns boolean of this request is not found.
   369  // HTTP 404 means not found.
   370  func (output *BeegoOutput) IsNotFound() bool {
   371  	return output.Status == 404
   372  }
   373  
   374  // IsClientError returns boolean of this request client sends error data.
   375  // HTTP 4xx means client error.
   376  func (output *BeegoOutput) IsClientError() bool {
   377  	return output.Status >= 400 && output.Status < 500
   378  }
   379  
   380  // IsServerError returns boolean of this server handler errors.
   381  // HTTP 5xx means server internal error.
   382  func (output *BeegoOutput) IsServerError() bool {
   383  	return output.Status >= 500 && output.Status < 600
   384  }
   385  
   386  func stringsToJSON(str string) string {
   387  	var jsons bytes.Buffer
   388  	for _, r := range str {
   389  		rint := int(r)
   390  		if rint < 128 {
   391  			jsons.WriteRune(r)
   392  		} else {
   393  			jsons.WriteString("\\u")
   394  			if rint < 0x100 {
   395  				jsons.WriteString("00")
   396  			} else if rint < 0x1000 {
   397  				jsons.WriteString("0")
   398  			}
   399  			jsons.WriteString(strconv.FormatInt(int64(rint), 16))
   400  		}
   401  	}
   402  	return jsons.String()
   403  }
   404  
   405  // Session sets session item value with given key.
   406  func (output *BeegoOutput) Session(name interface{}, value interface{}) {
   407  	output.Context.Input.CruSession.Set(name, value)
   408  }