grafana/pkg/web/context.go

214 lines
5.6 KiB
Go

// Copyright 2014 The Macaron Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package web
import (
"encoding/json"
"errors"
"fmt"
"html/template"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"syscall"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/util/errhttp"
)
// Context represents the runtime context of current request of Macaron instance.
// It is the integration of most frequently used middlewares and helper methods.
type Context struct {
mws []Middleware
Req *http.Request
Resp ResponseWriter
template *template.Template
}
var errMissingWrite = errutil.Internal("web.missingWrite")
func (ctx *Context) run() {
h := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
for i := len(ctx.mws) - 1; i >= 0; i-- {
h = ctx.mws[i](h)
}
rw := ctx.Resp
h.ServeHTTP(ctx.Resp, ctx.Req)
// Prevent the handler chain from not writing anything.
// This indicates nearly always that a middleware is misbehaving and not calling its next.ServeHTTP().
// In rare cases where a blank http.StatusOK without any body is wished, explicitly state that using w.WriteStatus(http.StatusOK)
if !rw.Written() {
errhttp.Write(
ctx.Req.Context(),
errMissingWrite.Errorf("chain did not write HTTP response: %s", ctx.Req.URL.Path),
rw,
)
}
}
// RemoteAddr returns more real IP address.
func (ctx *Context) RemoteAddr() string {
return RemoteAddr(ctx.Req)
}
func RemoteAddr(req *http.Request) string {
addr := req.Header.Get("X-Real-IP")
if len(addr) == 0 {
// X-Forwarded-For may contain multiple IP addresses, separated by
// commas.
addr = strings.TrimSpace(strings.Split(req.Header.Get("X-Forwarded-For"), ",")[0])
}
// parse user inputs from headers to prevent log forgery
if len(addr) > 0 {
if parsedIP := net.ParseIP(addr); parsedIP == nil {
// if parsedIP is nil we clean addr and populate with RemoteAddr below
addr = ""
}
}
if len(addr) == 0 {
addr = req.RemoteAddr
if i := strings.LastIndex(addr, ":"); i > -1 {
addr = addr[:i]
}
}
return addr
}
const (
headerContentType = "Content-Type"
contentTypeJSON = "application/json; charset=UTF-8"
contentTypeHTML = "text/html; charset=UTF-8"
)
// HTML renders the HTML with default template set.
func (ctx *Context) HTML(status int, name string, data any) {
ctx.Resp.Header().Set(headerContentType, contentTypeHTML)
ctx.Resp.WriteHeader(status)
if err := ctx.template.ExecuteTemplate(ctx.Resp, name, data); err != nil {
if errors.Is(err, syscall.EPIPE) { // Client has stopped listening.
return
}
panic(fmt.Sprintf("Context.HTML - Error rendering template: %s. You may need to build frontend assets \n %s", name, err.Error()))
}
}
func (ctx *Context) JSON(status int, data any) {
ctx.Resp.Header().Set(headerContentType, contentTypeJSON)
ctx.Resp.WriteHeader(status)
enc := json.NewEncoder(ctx.Resp)
if Env != PROD {
enc.SetIndent("", " ")
}
if err := enc.Encode(data); err != nil {
panic("Context.JSON: " + err.Error())
}
}
// Redirect sends a redirect response
func (ctx *Context) Redirect(location string, status ...int) {
code := http.StatusFound
if len(status) == 1 {
code = status[0]
}
http.Redirect(ctx.Resp, ctx.Req, location, code)
}
// MaxMemory is the maximum amount of memory to use when parsing a multipart form.
// Set this to whatever value you prefer; default is 10 MB.
var MaxMemory = int64(1024 * 1024 * 10)
func (ctx *Context) parseForm() {
if ctx.Req.Form != nil {
return
}
contentType := ctx.Req.Header.Get(headerContentType)
if (ctx.Req.Method == "POST" || ctx.Req.Method == "PUT") &&
len(contentType) > 0 && strings.Contains(contentType, "multipart/form-data") {
_ = ctx.Req.ParseMultipartForm(MaxMemory)
} else {
_ = ctx.Req.ParseForm()
}
}
// Query querys form parameter.
func (ctx *Context) Query(name string) string {
ctx.parseForm()
return ctx.Req.Form.Get(name)
}
// QueryStrings returns a list of results by given query name.
func (ctx *Context) QueryStrings(name string) []string {
ctx.parseForm()
vals, ok := ctx.Req.Form[name]
if !ok {
return []string{}
}
return vals
}
// QueryBool returns query result in bool type.
func (ctx *Context) QueryBool(name string) bool {
v, _ := strconv.ParseBool(ctx.Query(name))
return v
}
// QueryInt returns query result in int type.
func (ctx *Context) QueryInt(name string) int {
n, _ := strconv.Atoi(ctx.Query(name))
return n
}
// QueryInt64 returns query result in int64 type.
func (ctx *Context) QueryInt64(name string) int64 {
n, _ := strconv.ParseInt(ctx.Query(name), 10, 64)
return n
}
func (ctx *Context) QueryInt64WithDefault(name string, d int64) int64 {
n, err := strconv.ParseInt(ctx.Query(name), 10, 64)
if err != nil {
return d
}
return n
}
// GetCookie returns given cookie value from request header.
func (ctx *Context) GetCookie(name string) string {
cookie, err := ctx.Req.Cookie(name)
if err != nil {
return ""
}
val, _ := url.QueryUnescape(cookie.Value)
return val
}
// QueryFloat64 returns query result in float64 type.
func (ctx *Context) QueryFloat64(name string) float64 {
n, _ := strconv.ParseFloat(ctx.Query(name), 64)
return n
}