cloudflare/cloudflared

Public

mirrored from https://github.com/cloudflare/cloudflaredAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2021.10.5

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

connection/header.go

251lines · modecode

1package connection
2
3import (
4 "encoding/base64"
5 "fmt"
6 "net/http"
7 "net/url"
8 "strconv"
9 "strings"
10
11 "github.com/pkg/errors"
12
13 "github.com/cloudflare/cloudflared/h2mux"
14)
15
16var (
17 // h2mux-style special headers
18 RequestUserHeaders = "cf-cloudflared-request-headers"
19 ResponseUserHeaders = "cf-cloudflared-response-headers"
20 ResponseMetaHeader = "cf-cloudflared-response-meta"
21
22 // h2mux-style special headers
23 CanonicalResponseUserHeaders = http.CanonicalHeaderKey(ResponseUserHeaders)
24 CanonicalResponseMetaHeader = http.CanonicalHeaderKey(ResponseMetaHeader)
25)
26
27var (
28 // pre-generate possible values for res
29 responseMetaHeaderCfd = mustInitRespMetaHeader("cloudflared")
30 responseMetaHeaderOrigin = mustInitRespMetaHeader("origin")
31)
32
33type responseMetaHeader struct {
34 Source string `json:"src"`
35}
36
37func mustInitRespMetaHeader(src string) string {
38 header, err := json.Marshal(responseMetaHeader{Source: src})
39 if err != nil {
40 panic(fmt.Sprintf("Failed to serialize response meta header = %s, err: %v", src, err))
41 }
42 return string(header)
43}
44
45var headerEncoding = base64.RawStdEncoding
46
47// note: all h2mux headers should be lower-case (http/2 style)
48const ()
49
50// H2RequestHeadersToH1Request converts the HTTP/2 headers coming from origintunneld
51// to an HTTP/1 Request object destined for the local origin web service.
52// This operation includes conversion of the pseudo-headers into their closest
53// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
54func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
55 for _, header := range h2 {
56 name := strings.ToLower(header.Name)
57 if !IsControlRequestHeader(name) {
58 continue
59 }
60
61 switch name {
62 case ":method":
63 h1.Method = header.Value
64 case ":scheme":
65 // noop - use the preexisting scheme from h1.URL
66 case ":authority":
67 // Otherwise the host header will be based on the origin URL
68 h1.Host = header.Value
69 case ":path":
70 // We don't want to be an "opinionated" proxy, so ideally we would use :path as-is.
71 // However, this HTTP/1 Request object belongs to the Go standard library,
72 // whose URL package makes some opinionated decisions about the encoding of
73 // URL characters: see the docs of https://godoc.org/net/url#URL,
74 // in particular the EscapedPath method https://godoc.org/net/url#URL.EscapedPath,
75 // which is always used when computing url.URL.String(), whether we'd like it or not.
76 //
77 // Well, not *always*. We could circumvent this by using url.URL.Opaque. But
78 // that would present unusual difficulties when using an HTTP proxy: url.URL.Opaque
79 // is treated differently when HTTP_PROXY is set!
80 // See https://github.com/golang/go/issues/5684#issuecomment-66080888
81 //
82 // This means we are subject to the behavior of net/url's function `shouldEscape`
83 // (as invoked with mode=encodePath): https://github.com/golang/go/blob/go1.12.7/src/net/url/url.go#L101
84
85 if header.Value == "*" {
86 h1.URL.Path = "*"
87 continue
88 }
89 // Due to the behavior of validation.ValidateUrl, h1.URL may
90 // already have a partial value, with or without a trailing slash.
91 base := h1.URL.String()
92 base = strings.TrimRight(base, "/")
93 // But we know :path begins with '/', because we handled '*' above - see RFC7540
94 requestURL, err := url.Parse(base + header.Value)
95 if err != nil {
96 return errors.Wrap(err, fmt.Sprintf("invalid path '%v'", header.Value))
97 }
98 h1.URL = requestURL
99 case "content-length":
100 contentLength, err := strconv.ParseInt(header.Value, 10, 64)
101 if err != nil {
102 return fmt.Errorf("unparseable content length")
103 }
104 h1.ContentLength = contentLength
105 case RequestUserHeaders:
106 // Do not forward the serialized headers to the origin -- deserialize them, and ditch the serialized version
107 // Find and parse user headers serialized into a single one
108 userHeaders, err := DeserializeHeaders(header.Value)
109 if err != nil {
110 return errors.Wrap(err, "Unable to parse user headers")
111 }
112 for _, userHeader := range userHeaders {
113 h1.Header.Add(userHeader.Name, userHeader.Value)
114 }
115 default:
116 // All other control headers shall just be proxied transparently
117 h1.Header.Add(header.Name, header.Value)
118 }
119 }
120
121 return nil
122}
123
124func IsControlRequestHeader(headerName string) bool {
125 return headerName == "content-length" ||
126 headerName == "connection" || headerName == "upgrade" || // Websocket request headers
127 strings.HasPrefix(headerName, ":") ||
128 strings.HasPrefix(headerName, "cf-")
129}
130
131func IsControlResponseHeader(headerName string) bool {
132 return headerName == "content-length" ||
133 strings.HasPrefix(headerName, ":") ||
134 strings.HasPrefix(headerName, "cf-int-") ||
135 strings.HasPrefix(headerName, "cf-cloudflared-")
136}
137
138// isWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
139func IsWebsocketClientHeader(headerName string) bool {
140 return headerName == "sec-websocket-accept" ||
141 headerName == "connection" ||
142 headerName == "upgrade"
143}
144
145func H1ResponseToH2ResponseHeaders(status int, h1 http.Header) (h2 []h2mux.Header) {
146 h2 = []h2mux.Header{
147 {Name: ":status", Value: strconv.Itoa(status)},
148 }
149 userHeaders := make(http.Header, len(h1))
150 for header, values := range h1 {
151 h2name := strings.ToLower(header)
152 if h2name == "content-length" {
153 // This header has meaning in HTTP/2 and will be used by the edge,
154 // so it should be sent as an HTTP/2 response header.
155
156 // Since these are http2 headers, they're required to be lowercase
157 h2 = append(h2, h2mux.Header{Name: "content-length", Value: values[0]})
158 } else if !IsControlResponseHeader(h2name) || IsWebsocketClientHeader(h2name) {
159 // User headers, on the other hand, must all be serialized so that
160 // HTTP/2 header validation won't be applied to HTTP/1 header values
161 userHeaders[header] = values
162 }
163 }
164
165 // Perform user header serialization and set them in the single header
166 h2 = append(h2, h2mux.Header{Name: ResponseUserHeaders, Value: SerializeHeaders(userHeaders)})
167 return h2
168}
169
170// Serialize HTTP1.x headers by base64-encoding each header name and value,
171// and then joining them in the format of [key:value;]
172func SerializeHeaders(h1Headers http.Header) string {
173 // compute size of the fully serialized value and largest temp buffer we will need
174 serializedLen := 0
175 maxTempLen := 0
176 for headerName, headerValues := range h1Headers {
177 for _, headerValue := range headerValues {
178 nameLen := headerEncoding.EncodedLen(len(headerName))
179 valueLen := headerEncoding.EncodedLen(len(headerValue))
180 const delims = 2
181 serializedLen += delims + nameLen + valueLen
182 if nameLen > maxTempLen {
183 maxTempLen = nameLen
184 }
185 if valueLen > maxTempLen {
186 maxTempLen = valueLen
187 }
188 }
189 }
190 var buf strings.Builder
191 buf.Grow(serializedLen)
192
193 temp := make([]byte, maxTempLen)
194 writeB64 := func(s string) {
195 n := headerEncoding.EncodedLen(len(s))
196 if n > len(temp) {
197 temp = make([]byte, n)
198 }
199 headerEncoding.Encode(temp[:n], []byte(s))
200 buf.Write(temp[:n])
201 }
202
203 for headerName, headerValues := range h1Headers {
204 for _, headerValue := range headerValues {
205 if buf.Len() > 0 {
206 buf.WriteByte(';')
207 }
208 writeB64(headerName)
209 buf.WriteByte(':')
210 writeB64(headerValue)
211 }
212 }
213
214 return buf.String()
215}
216
217// Deserialize headers serialized by `SerializeHeader`
218func DeserializeHeaders(serializedHeaders string) ([]h2mux.Header, error) {
219 const unableToDeserializeErr = "Unable to deserialize headers"
220
221 var deserialized []h2mux.Header
222 for _, serializedPair := range strings.Split(serializedHeaders, ";") {
223 if len(serializedPair) == 0 {
224 continue
225 }
226
227 serializedHeaderParts := strings.Split(serializedPair, ":")
228 if len(serializedHeaderParts) != 2 {
229 return nil, errors.New(unableToDeserializeErr)
230 }
231
232 serializedName := serializedHeaderParts[0]
233 serializedValue := serializedHeaderParts[1]
234 deserializedName := make([]byte, headerEncoding.DecodedLen(len(serializedName)))
235 deserializedValue := make([]byte, headerEncoding.DecodedLen(len(serializedValue)))
236
237 if _, err := headerEncoding.Decode(deserializedName, []byte(serializedName)); err != nil {
238 return nil, errors.Wrap(err, unableToDeserializeErr)
239 }
240 if _, err := headerEncoding.Decode(deserializedValue, []byte(serializedValue)); err != nil {
241 return nil, errors.Wrap(err, unableToDeserializeErr)
242 }
243
244 deserialized = append(deserialized, h2mux.Header{
245 Name: string(deserializedName),
246 Value: string(deserializedValue),
247 })
248 }
249
250 return deserialized, nil
251}
252