7aaab3c4c4
Trace all the requests that the HTTP API endpoints receive. We want to trace them with Monkit because we want to break them down by request type and response code for seeing if they succeeded or failed. Also log them with DEBUG level with the IP client. Change-Id: Ia7b013351c788f131e775818f27091f3014ea861
175 lines
5.1 KiB
Go
175 lines
5.1 KiB
Go
// Copyright (C) 2019 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package consoleweb
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"mime"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/zeebo/errs"
|
|
|
|
"storj.io/common/memory"
|
|
"storj.io/storj/satellite/console/consoleweb/consoleql"
|
|
)
|
|
|
|
// ContentLengthLimit describes 4KB limit.
|
|
const ContentLengthLimit = 4 * memory.KB
|
|
|
|
func init() {
|
|
err := mime.AddExtensionType(".ttf", "font/ttf")
|
|
if err != nil {
|
|
panic(err.Error())
|
|
}
|
|
|
|
err = mime.AddExtensionType(".txt", "text/plain")
|
|
if err != nil {
|
|
panic(err.Error())
|
|
}
|
|
}
|
|
|
|
// JSON request from graphql clients.
|
|
type graphqlJSON struct {
|
|
Query string
|
|
OperationName string
|
|
Variables map[string]interface{}
|
|
}
|
|
|
|
// getQuery retrieves graphql query from request.
|
|
func getQuery(w http.ResponseWriter, req *http.Request) (query graphqlJSON, err error) {
|
|
switch req.Method {
|
|
case http.MethodGet:
|
|
query.Query = req.URL.Query().Get(consoleql.Query)
|
|
return query, nil
|
|
case http.MethodPost:
|
|
return queryPOST(w, req)
|
|
default:
|
|
return query, errs.New("wrong http request type")
|
|
}
|
|
}
|
|
|
|
// queryPOST retrieves graphql query from POST request.
|
|
func queryPOST(w http.ResponseWriter, req *http.Request) (query graphqlJSON, err error) {
|
|
limitedReader := http.MaxBytesReader(w, req.Body, ContentLengthLimit.Int64())
|
|
switch typ := req.Header.Get(contentType); typ {
|
|
case applicationGraphql:
|
|
body, err := ioutil.ReadAll(limitedReader)
|
|
query.Query = string(body)
|
|
return query, errs.Combine(err, limitedReader.Close())
|
|
case applicationJSON:
|
|
err := json.NewDecoder(limitedReader).Decode(&query)
|
|
return query, errs.Combine(err, limitedReader.Close())
|
|
default:
|
|
return query, errs.New("can't parse request body of type %s", typ)
|
|
}
|
|
}
|
|
|
|
// getClientIPRegExp is used by the function getClientIP.
|
|
var getClientIPRegExp = regexp.MustCompile(`(?i:(?:^|;)for=([^,; ]+))`)
|
|
|
|
// getClientIP gets the IP of the proxy (that's the value of the field
|
|
// r.RemoteAddr) and the client from the first exiting header in this order:
|
|
// 'Forwarded', 'X-Forwarded-For', or 'X-Real-Ip'.
|
|
// It returns a string of the format "{{proxy ip}} ({{client ip}})" or if there
|
|
// isn't any of those headers then it returns "{{client ip}}" where "client ip"
|
|
// is the value of the field r.RemoteAddr.
|
|
//
|
|
// The 'for' field of the 'Forwarded' may contain the IP with a port, as defined
|
|
// in the RFC 7239. When the header contains the IP with a port, the port is
|
|
// striped, so only the IP is returned.
|
|
//
|
|
// NOTE: it doesn't check that the IP value get from wherever source is a well
|
|
// formatted IP v4 nor v6; an invalid formatted IP will return an undefined
|
|
// result.
|
|
func getClientIP(r *http.Request) string {
|
|
requestIPs := func(clientIP string) string {
|
|
return fmt.Sprintf("%s (%s)", r.RemoteAddr, clientIP)
|
|
}
|
|
|
|
h := r.Header.Get("Forwarded")
|
|
if h != "" {
|
|
// Get the first value of the 'for' identifier present in the header because
|
|
// its the one that contains the client IP.
|
|
// see: https://datatracker.ietf.org/doc/html/rfc7230
|
|
matches := getClientIPRegExp.FindStringSubmatch(h)
|
|
if len(matches) > 1 {
|
|
ip := strings.Trim(matches[1], `"`)
|
|
ip = stripPort(ip)
|
|
if ip[0] == '[' {
|
|
ip = ip[1 : len(ip)-1]
|
|
}
|
|
|
|
return requestIPs(ip)
|
|
}
|
|
}
|
|
|
|
h = r.Header.Get("X-Forwarded-For")
|
|
if h != "" {
|
|
// Get the first the value IP because it's the client IP.
|
|
// Header sysntax: X-Forwarded-For: <client>, <proxy1>, <proxy2>
|
|
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
|
|
ips := strings.SplitN(h, ",", 2)
|
|
if len(ips) > 0 {
|
|
return requestIPs(ips[0])
|
|
}
|
|
}
|
|
|
|
h = r.Header.Get("X-Real-Ip")
|
|
if h != "" {
|
|
// Get the value of the header because its value is just the client IP.
|
|
// This header is mostly sent by NGINX.
|
|
// See https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/
|
|
return requestIPs(h)
|
|
}
|
|
|
|
return r.RemoteAddr
|
|
}
|
|
|
|
// stripPort strips the port from addr when it has it and return the host
|
|
// part. A host can be a hostname or an IP v4 or an IP v6.
|
|
//
|
|
// NOTE: this function expects a well-formatted address. When it's hostname or
|
|
// IP v4, the port at the end and separated with a colon, nor hostname or IP can
|
|
// have colons; when it's a IP v6 with port the IP part is enclosed with square
|
|
// brackets (.i.e []) and the port separated with a colon, otherwise the IP
|
|
// isn't enclosed by square brackets.
|
|
// An invalid addr produce an unspecified value.
|
|
func stripPort(addr string) string {
|
|
// Ensure to strip the port out if r.RemoteAddr has it.
|
|
// We don't use net.SplitHostPort because the function returns an error if the
|
|
// address doesn't contain the port and the returned host is an empty string,
|
|
// besides it doesn't return an error that can be distinguished from others
|
|
// unless that the error message is compared, which is discouraging.
|
|
if addr == "" {
|
|
return ""
|
|
}
|
|
|
|
// It's an IP v6 with port.
|
|
if addr[0] == '[' {
|
|
idx := strings.LastIndex(addr, ":")
|
|
if idx <= 1 {
|
|
return addr
|
|
}
|
|
|
|
return addr[1 : idx-1]
|
|
}
|
|
|
|
// It's a IP v4 with port.
|
|
if strings.Count(addr, ":") == 1 {
|
|
idx := strings.LastIndex(addr, ":")
|
|
if idx < 0 {
|
|
return addr
|
|
}
|
|
|
|
return addr[0:idx]
|
|
}
|
|
|
|
// It's a IP v4 or v6 without port.
|
|
return addr
|
|
}
|