cloudflare/cloudflared

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2021.12.4

Branches

Tags

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

Clone

HTTPS

Download ZIP

connection/h2mux_header.go

128lines · modecode

1package connection
2
3import (
4 "fmt"
5 "net/http"
6 "net/url"
7 "strconv"
8 "strings"
9
10 "github.com/pkg/errors"
11
12 "github.com/cloudflare/cloudflared/h2mux"
13)
14
15// H2RequestHeadersToH1Request converts the HTTP/2 headers coming from origintunneld
16// to an HTTP/1 Request object destined for the local origin web service.
17// This operation includes conversion of the pseudo-headers into their closest
18// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
19func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
20 for _, header := range h2 {
21 name := strings.ToLower(header.Name)
22 if !IsH2muxControlRequestHeader(name) {
23 continue
24 }
25
26 switch name {
27 case ":method":
28 h1.Method = header.Value
29 case ":scheme":
30 // noop - use the preexisting scheme from h1.URL
31 case ":authority":
32 // Otherwise the host header will be based on the origin URL
33 h1.Host = header.Value
34 case ":path":
35 // We don't want to be an "opinionated" proxy, so ideally we would use :path as-is.
36 // However, this HTTP/1 Request object belongs to the Go standard library,
37 // whose URL package makes some opinionated decisions about the encoding of
38 // URL characters: see the docs of https://godoc.org/net/url#URL,
39 // in particular the EscapedPath method https://godoc.org/net/url#URL.EscapedPath,
40 // which is always used when computing url.URL.String(), whether we'd like it or not.
41 //
42 // Well, not *always*. We could circumvent this by using url.URL.Opaque. But
43 // that would present unusual difficulties when using an HTTP proxy: url.URL.Opaque
44 // is treated differently when HTTP_PROXY is set!
45 // See https://github.com/golang/go/issues/5684#issuecomment-66080888
46 //
47 // This means we are subject to the behavior of net/url's function `shouldEscape`
48 // (as invoked with mode=encodePath): https://github.com/golang/go/blob/go1.12.7/src/net/url/url.go#L101
49
50 if header.Value == "*" {
51 h1.URL.Path = "*"
52 continue
53 }
54 // Due to the behavior of validation.ValidateUrl, h1.URL may
55 // already have a partial value, with or without a trailing slash.
56 base := h1.URL.String()
57 base = strings.TrimRight(base, "/")
58 // But we know :path begins with '/', because we handled '*' above - see RFC7540
59 requestURL, err := url.Parse(base + header.Value)
60 if err != nil {
61 return errors.Wrap(err, fmt.Sprintf("invalid path '%v'", header.Value))
62 }
63 h1.URL = requestURL
64 case "content-length":
65 contentLength, err := strconv.ParseInt(header.Value, 10, 64)
66 if err != nil {
67 return fmt.Errorf("unparseable content length")
68 }
69 h1.ContentLength = contentLength
70 case RequestUserHeaders:
71 // Do not forward the serialized headers to the origin -- deserialize them, and ditch the serialized version
72 // Find and parse user headers serialized into a single one
73 userHeaders, err := DeserializeHeaders(header.Value)
74 if err != nil {
75 return errors.Wrap(err, "Unable to parse user headers")
76 }
77 for _, userHeader := range userHeaders {
78 h1.Header.Add(userHeader.Name, userHeader.Value)
79 }
80 default:
81 // All other control headers shall just be proxied transparently
82 h1.Header.Add(header.Name, header.Value)
83 }
84 }
85
86 return nil
87}
88
89func H1ResponseToH2ResponseHeaders(status int, h1 http.Header) (h2 []h2mux.Header) {
90 h2 = []h2mux.Header{
91 {Name: ":status", Value: strconv.Itoa(status)},
92 }
93 userHeaders := make(http.Header, len(h1))
94 for header, values := range h1 {
95 h2name := strings.ToLower(header)
96 if h2name == "content-length" {
97 // This header has meaning in HTTP/2 and will be used by the edge,
98 // so it should be sent as an HTTP/2 response header.
99
100 // Since these are http2 headers, they're required to be lowercase
101 h2 = append(h2, h2mux.Header{Name: "content-length", Value: values[0]})
102 } else if !IsH2muxControlResponseHeader(h2name) || IsWebsocketClientHeader(h2name) {
103 // User headers, on the other hand, must all be serialized so that
104 // HTTP/2 header validation won't be applied to HTTP/1 header values
105 userHeaders[header] = values
106 }
107 }
108
109 // Perform user header serialization and set them in the single header
110 h2 = append(h2, h2mux.Header{Name: ResponseUserHeaders, Value: SerializeHeaders(userHeaders)})
111 return h2
112}
113
114// IsH2muxControlRequestHeader is called in the direction of eyeball -> origin.
115func IsH2muxControlRequestHeader(headerName string) bool {
116 return headerName == "content-length" ||
117 headerName == "connection" || headerName == "upgrade" || // Websocket request headers
118 strings.HasPrefix(headerName, ":") ||
119 strings.HasPrefix(headerName, "cf-")
120}
121
122// IsH2muxControlResponseHeader is called in the direction of eyeball <- origin.
123func IsH2muxControlResponseHeader(headerName string) bool {
124 return headerName == "content-length" ||
125 strings.HasPrefix(headerName, ":") ||
126 strings.HasPrefix(headerName, "cf-int-") ||
127 strings.HasPrefix(headerName, "cf-cloudflared-")
128}
129