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 }