grafana/pkg/web/context.go

208 lines
5.4 KiB
Go
Raw Normal View History

// 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/util/errutil"
"github.com/grafana/grafana/pkg/util/errutil/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
}
Alerting: Add limits and filters to Prometheus Rules API (#66627) This commit adds support for limits and filters to the Prometheus Rules API. Limits: It adds a number of limits to the Grafana flavour of the Prometheus Rules API: - `limit` limits the maximum number of Rule Groups returned - `limit_rules` limits the maximum number of rules per Rule Group - `limit_alerts` limits the maximum number of alerts per rule It sorts Rule Groups and rules within Rule Groups such that data in the response is stable across requests. It also returns summaries (totals) for all Rule Groups, individual Rule Groups and rules. Filters: Alerts can be filtered by state with the `state` query string. An example of an HTTP request asking for just firing alerts might be `/api/prometheus/grafana/api/v1/rules?state=alerting`. A request can filter by two or more states by adding additional `state` query strings to the URL. For example `?state=alerting&state=normal`. Like the alert list panel, the `firing`, `pending` and `normal` state are first compared against the state of each alert rule. All other states are ignored. If the alert rule matches then its alert instances are filtered against states once more. Alerts can also be filtered by labels using the `matcher` query string. Like `state`, multiple matchers can be provided by adding additional `matcher` query strings to the URL. The match expression should be parsed using existing regular expression and sent to the API as URL-encoded JSON in the format: { "name": "test", "value": "value1", "isRegex": false, "isEqual": true } The `isRegex` and `isEqual` options work as follows: | IsEqual | IsRegex | Operator | | ------- | -------- | -------- | | true | false | = | | true | true | =~ | | false | true | !~ | | false | false | != |
2023-04-17 11:45:06 -05:00
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
}