// Copyright (C) 2019 Storj Labs, Inc. // See LICENSE for copying information. package consoleweb import ( "encoding/json" "fmt" "io" "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 := io.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: , , // 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 }