cloudflare/cloudflared
Publicmirrored from https://github.com/cloudflare/cloudflaredAvailable
connection/header.go
251lines · modecode
| 1 | package connection |
| 2 | |
| 3 | import ( |
| 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 | |
| 16 | var ( |
| 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 | |
| 27 | var ( |
| 28 | // pre-generate possible values for res |
| 29 | responseMetaHeaderCfd = mustInitRespMetaHeader("cloudflared") |
| 30 | responseMetaHeaderOrigin = mustInitRespMetaHeader("origin") |
| 31 | ) |
| 32 | |
| 33 | type responseMetaHeader struct { |
| 34 | Source string `json:"src"` |
| 35 | } |
| 36 | |
| 37 | func 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 | |
| 45 | var headerEncoding = base64.RawStdEncoding |
| 46 | |
| 47 | // note: all h2mux headers should be lower-case (http/2 style) |
| 48 | const () |
| 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 |
| 54 | func 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 | |
| 124 | func 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 | |
| 131 | func 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 |
| 139 | func IsWebsocketClientHeader(headerName string) bool { |
| 140 | return headerName == "sec-websocket-accept" || |
| 141 | headerName == "connection" || |
| 142 | headerName == "upgrade" |
| 143 | } |
| 144 | |
| 145 | func 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;] |
| 172 | func 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` |
| 218 | func 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 | |