-
Notifications
You must be signed in to change notification settings - Fork 390
/
utils.go
179 lines (155 loc) · 5.37 KB
/
utils.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package consoleweb
import (
"encoding/json"
"fmt"
"io"
"mime"
"net/http"
"regexp"
"strings"
"sync"
"github.com/zeebo/errs"
"storj.io/common/memory"
"storj.io/storj/satellite/console/consoleweb/consoleql"
)
// ContentLengthLimit describes 4KB limit.
const ContentLengthLimit = 4 * memory.KB
var _initAdditionalMimeTypes sync.Once
// initAdditionalMimeTypes initializes additional mime types,
// however we do it lazily to avoid needing to load OS mime types in tests.
func initAdditionalMimeTypes() {
_initAdditionalMimeTypes.Do(func() {
_ = mime.AddExtensionType(".ttf", "font/ttf")
_ = mime.AddExtensionType(".txt", "text/plain")
})
}
func typeByExtension(ext string) string {
initAdditionalMimeTypes()
return mime.TypeByExtension(ext)
}
// 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: <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
}