Matteo Scandolo | a428586 | 2020-12-01 18:10:10 -0800 | [diff] [blame^] | 1 | /* |
| 2 | Copyright 2015 The Kubernetes Authors. |
| 3 | |
| 4 | Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | you may not use this file except in compliance with the License. |
| 6 | You may obtain a copy of the License at |
| 7 | |
| 8 | http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | |
| 10 | Unless required by applicable law or agreed to in writing, software |
| 11 | distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | See the License for the specific language governing permissions and |
| 14 | limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package transport |
| 18 | |
| 19 | import ( |
| 20 | "fmt" |
| 21 | "net/http" |
| 22 | "strings" |
| 23 | "time" |
| 24 | |
| 25 | "golang.org/x/oauth2" |
| 26 | "k8s.io/klog/v2" |
| 27 | |
| 28 | utilnet "k8s.io/apimachinery/pkg/util/net" |
| 29 | ) |
| 30 | |
| 31 | // HTTPWrappersForConfig wraps a round tripper with any relevant layered |
| 32 | // behavior from the config. Exposed to allow more clients that need HTTP-like |
| 33 | // behavior but then must hijack the underlying connection (like WebSocket or |
| 34 | // HTTP2 clients). Pure HTTP clients should use the RoundTripper returned from |
| 35 | // New. |
| 36 | func HTTPWrappersForConfig(config *Config, rt http.RoundTripper) (http.RoundTripper, error) { |
| 37 | if config.WrapTransport != nil { |
| 38 | rt = config.WrapTransport(rt) |
| 39 | } |
| 40 | |
| 41 | rt = DebugWrappers(rt) |
| 42 | |
| 43 | // Set authentication wrappers |
| 44 | switch { |
| 45 | case config.HasBasicAuth() && config.HasTokenAuth(): |
| 46 | return nil, fmt.Errorf("username/password or bearer token may be set, but not both") |
| 47 | case config.HasTokenAuth(): |
| 48 | var err error |
| 49 | rt, err = NewBearerAuthWithRefreshRoundTripper(config.BearerToken, config.BearerTokenFile, rt) |
| 50 | if err != nil { |
| 51 | return nil, err |
| 52 | } |
| 53 | case config.HasBasicAuth(): |
| 54 | rt = NewBasicAuthRoundTripper(config.Username, config.Password, rt) |
| 55 | } |
| 56 | if len(config.UserAgent) > 0 { |
| 57 | rt = NewUserAgentRoundTripper(config.UserAgent, rt) |
| 58 | } |
| 59 | if len(config.Impersonate.UserName) > 0 || |
| 60 | len(config.Impersonate.Groups) > 0 || |
| 61 | len(config.Impersonate.Extra) > 0 { |
| 62 | rt = NewImpersonatingRoundTripper(config.Impersonate, rt) |
| 63 | } |
| 64 | return rt, nil |
| 65 | } |
| 66 | |
| 67 | // DebugWrappers wraps a round tripper and logs based on the current log level. |
| 68 | func DebugWrappers(rt http.RoundTripper) http.RoundTripper { |
| 69 | switch { |
| 70 | case bool(klog.V(9).Enabled()): |
| 71 | rt = newDebuggingRoundTripper(rt, debugCurlCommand, debugURLTiming, debugResponseHeaders) |
| 72 | case bool(klog.V(8).Enabled()): |
| 73 | rt = newDebuggingRoundTripper(rt, debugJustURL, debugRequestHeaders, debugResponseStatus, debugResponseHeaders) |
| 74 | case bool(klog.V(7).Enabled()): |
| 75 | rt = newDebuggingRoundTripper(rt, debugJustURL, debugRequestHeaders, debugResponseStatus) |
| 76 | case bool(klog.V(6).Enabled()): |
| 77 | rt = newDebuggingRoundTripper(rt, debugURLTiming) |
| 78 | } |
| 79 | |
| 80 | return rt |
| 81 | } |
| 82 | |
| 83 | type authProxyRoundTripper struct { |
| 84 | username string |
| 85 | groups []string |
| 86 | extra map[string][]string |
| 87 | |
| 88 | rt http.RoundTripper |
| 89 | } |
| 90 | |
| 91 | // NewAuthProxyRoundTripper provides a roundtripper which will add auth proxy fields to requests for |
| 92 | // authentication terminating proxy cases |
| 93 | // assuming you pull the user from the context: |
| 94 | // username is the user.Info.GetName() of the user |
| 95 | // groups is the user.Info.GetGroups() of the user |
| 96 | // extra is the user.Info.GetExtra() of the user |
| 97 | // extra can contain any additional information that the authenticator |
| 98 | // thought was interesting, for example authorization scopes. |
| 99 | // In order to faithfully round-trip through an impersonation flow, these keys |
| 100 | // MUST be lowercase. |
| 101 | func NewAuthProxyRoundTripper(username string, groups []string, extra map[string][]string, rt http.RoundTripper) http.RoundTripper { |
| 102 | return &authProxyRoundTripper{ |
| 103 | username: username, |
| 104 | groups: groups, |
| 105 | extra: extra, |
| 106 | rt: rt, |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | func (rt *authProxyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { |
| 111 | req = utilnet.CloneRequest(req) |
| 112 | SetAuthProxyHeaders(req, rt.username, rt.groups, rt.extra) |
| 113 | |
| 114 | return rt.rt.RoundTrip(req) |
| 115 | } |
| 116 | |
| 117 | // SetAuthProxyHeaders stomps the auth proxy header fields. It mutates its argument. |
| 118 | func SetAuthProxyHeaders(req *http.Request, username string, groups []string, extra map[string][]string) { |
| 119 | req.Header.Del("X-Remote-User") |
| 120 | req.Header.Del("X-Remote-Group") |
| 121 | for key := range req.Header { |
| 122 | if strings.HasPrefix(strings.ToLower(key), strings.ToLower("X-Remote-Extra-")) { |
| 123 | req.Header.Del(key) |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | req.Header.Set("X-Remote-User", username) |
| 128 | for _, group := range groups { |
| 129 | req.Header.Add("X-Remote-Group", group) |
| 130 | } |
| 131 | for key, values := range extra { |
| 132 | for _, value := range values { |
| 133 | req.Header.Add("X-Remote-Extra-"+headerKeyEscape(key), value) |
| 134 | } |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | func (rt *authProxyRoundTripper) CancelRequest(req *http.Request) { |
| 139 | tryCancelRequest(rt.WrappedRoundTripper(), req) |
| 140 | } |
| 141 | |
| 142 | func (rt *authProxyRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.rt } |
| 143 | |
| 144 | type userAgentRoundTripper struct { |
| 145 | agent string |
| 146 | rt http.RoundTripper |
| 147 | } |
| 148 | |
| 149 | func NewUserAgentRoundTripper(agent string, rt http.RoundTripper) http.RoundTripper { |
| 150 | return &userAgentRoundTripper{agent, rt} |
| 151 | } |
| 152 | |
| 153 | func (rt *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { |
| 154 | if len(req.Header.Get("User-Agent")) != 0 { |
| 155 | return rt.rt.RoundTrip(req) |
| 156 | } |
| 157 | req = utilnet.CloneRequest(req) |
| 158 | req.Header.Set("User-Agent", rt.agent) |
| 159 | return rt.rt.RoundTrip(req) |
| 160 | } |
| 161 | |
| 162 | func (rt *userAgentRoundTripper) CancelRequest(req *http.Request) { |
| 163 | tryCancelRequest(rt.WrappedRoundTripper(), req) |
| 164 | } |
| 165 | |
| 166 | func (rt *userAgentRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.rt } |
| 167 | |
| 168 | type basicAuthRoundTripper struct { |
| 169 | username string |
| 170 | password string |
| 171 | rt http.RoundTripper |
| 172 | } |
| 173 | |
| 174 | // NewBasicAuthRoundTripper will apply a BASIC auth authorization header to a |
| 175 | // request unless it has already been set. |
| 176 | func NewBasicAuthRoundTripper(username, password string, rt http.RoundTripper) http.RoundTripper { |
| 177 | return &basicAuthRoundTripper{username, password, rt} |
| 178 | } |
| 179 | |
| 180 | func (rt *basicAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { |
| 181 | if len(req.Header.Get("Authorization")) != 0 { |
| 182 | return rt.rt.RoundTrip(req) |
| 183 | } |
| 184 | req = utilnet.CloneRequest(req) |
| 185 | req.SetBasicAuth(rt.username, rt.password) |
| 186 | return rt.rt.RoundTrip(req) |
| 187 | } |
| 188 | |
| 189 | func (rt *basicAuthRoundTripper) CancelRequest(req *http.Request) { |
| 190 | tryCancelRequest(rt.WrappedRoundTripper(), req) |
| 191 | } |
| 192 | |
| 193 | func (rt *basicAuthRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.rt } |
| 194 | |
| 195 | // These correspond to the headers used in pkg/apis/authentication. We don't want the package dependency, |
| 196 | // but you must not change the values. |
| 197 | const ( |
| 198 | // ImpersonateUserHeader is used to impersonate a particular user during an API server request |
| 199 | ImpersonateUserHeader = "Impersonate-User" |
| 200 | |
| 201 | // ImpersonateGroupHeader is used to impersonate a particular group during an API server request. |
| 202 | // It can be repeated multiplied times for multiple groups. |
| 203 | ImpersonateGroupHeader = "Impersonate-Group" |
| 204 | |
| 205 | // ImpersonateUserExtraHeaderPrefix is a prefix for a header used to impersonate an entry in the |
| 206 | // extra map[string][]string for user.Info. The key for the `extra` map is suffix. |
| 207 | // The same key can be repeated multiple times to have multiple elements in the slice under a single key. |
| 208 | // For instance: |
| 209 | // Impersonate-Extra-Foo: one |
| 210 | // Impersonate-Extra-Foo: two |
| 211 | // results in extra["Foo"] = []string{"one", "two"} |
| 212 | ImpersonateUserExtraHeaderPrefix = "Impersonate-Extra-" |
| 213 | ) |
| 214 | |
| 215 | type impersonatingRoundTripper struct { |
| 216 | impersonate ImpersonationConfig |
| 217 | delegate http.RoundTripper |
| 218 | } |
| 219 | |
| 220 | // NewImpersonatingRoundTripper will add an Act-As header to a request unless it has already been set. |
| 221 | func NewImpersonatingRoundTripper(impersonate ImpersonationConfig, delegate http.RoundTripper) http.RoundTripper { |
| 222 | return &impersonatingRoundTripper{impersonate, delegate} |
| 223 | } |
| 224 | |
| 225 | func (rt *impersonatingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { |
| 226 | // use the user header as marker for the rest. |
| 227 | if len(req.Header.Get(ImpersonateUserHeader)) != 0 { |
| 228 | return rt.delegate.RoundTrip(req) |
| 229 | } |
| 230 | req = utilnet.CloneRequest(req) |
| 231 | req.Header.Set(ImpersonateUserHeader, rt.impersonate.UserName) |
| 232 | |
| 233 | for _, group := range rt.impersonate.Groups { |
| 234 | req.Header.Add(ImpersonateGroupHeader, group) |
| 235 | } |
| 236 | for k, vv := range rt.impersonate.Extra { |
| 237 | for _, v := range vv { |
| 238 | req.Header.Add(ImpersonateUserExtraHeaderPrefix+headerKeyEscape(k), v) |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | return rt.delegate.RoundTrip(req) |
| 243 | } |
| 244 | |
| 245 | func (rt *impersonatingRoundTripper) CancelRequest(req *http.Request) { |
| 246 | tryCancelRequest(rt.WrappedRoundTripper(), req) |
| 247 | } |
| 248 | |
| 249 | func (rt *impersonatingRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.delegate } |
| 250 | |
| 251 | type bearerAuthRoundTripper struct { |
| 252 | bearer string |
| 253 | source oauth2.TokenSource |
| 254 | rt http.RoundTripper |
| 255 | } |
| 256 | |
| 257 | // NewBearerAuthRoundTripper adds the provided bearer token to a request |
| 258 | // unless the authorization header has already been set. |
| 259 | func NewBearerAuthRoundTripper(bearer string, rt http.RoundTripper) http.RoundTripper { |
| 260 | return &bearerAuthRoundTripper{bearer, nil, rt} |
| 261 | } |
| 262 | |
| 263 | // NewBearerAuthRoundTripper adds the provided bearer token to a request |
| 264 | // unless the authorization header has already been set. |
| 265 | // If tokenFile is non-empty, it is periodically read, |
| 266 | // and the last successfully read content is used as the bearer token. |
| 267 | // If tokenFile is non-empty and bearer is empty, the tokenFile is read |
| 268 | // immediately to populate the initial bearer token. |
| 269 | func NewBearerAuthWithRefreshRoundTripper(bearer string, tokenFile string, rt http.RoundTripper) (http.RoundTripper, error) { |
| 270 | if len(tokenFile) == 0 { |
| 271 | return &bearerAuthRoundTripper{bearer, nil, rt}, nil |
| 272 | } |
| 273 | source := NewCachedFileTokenSource(tokenFile) |
| 274 | if len(bearer) == 0 { |
| 275 | token, err := source.Token() |
| 276 | if err != nil { |
| 277 | return nil, err |
| 278 | } |
| 279 | bearer = token.AccessToken |
| 280 | } |
| 281 | return &bearerAuthRoundTripper{bearer, source, rt}, nil |
| 282 | } |
| 283 | |
| 284 | func (rt *bearerAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { |
| 285 | if len(req.Header.Get("Authorization")) != 0 { |
| 286 | return rt.rt.RoundTrip(req) |
| 287 | } |
| 288 | |
| 289 | req = utilnet.CloneRequest(req) |
| 290 | token := rt.bearer |
| 291 | if rt.source != nil { |
| 292 | if refreshedToken, err := rt.source.Token(); err == nil { |
| 293 | token = refreshedToken.AccessToken |
| 294 | } |
| 295 | } |
| 296 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) |
| 297 | return rt.rt.RoundTrip(req) |
| 298 | } |
| 299 | |
| 300 | func (rt *bearerAuthRoundTripper) CancelRequest(req *http.Request) { |
| 301 | tryCancelRequest(rt.WrappedRoundTripper(), req) |
| 302 | } |
| 303 | |
| 304 | func (rt *bearerAuthRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.rt } |
| 305 | |
| 306 | // requestInfo keeps track of information about a request/response combination |
| 307 | type requestInfo struct { |
| 308 | RequestHeaders http.Header |
| 309 | RequestVerb string |
| 310 | RequestURL string |
| 311 | |
| 312 | ResponseStatus string |
| 313 | ResponseHeaders http.Header |
| 314 | ResponseErr error |
| 315 | |
| 316 | Duration time.Duration |
| 317 | } |
| 318 | |
| 319 | // newRequestInfo creates a new RequestInfo based on an http request |
| 320 | func newRequestInfo(req *http.Request) *requestInfo { |
| 321 | return &requestInfo{ |
| 322 | RequestURL: req.URL.String(), |
| 323 | RequestVerb: req.Method, |
| 324 | RequestHeaders: req.Header, |
| 325 | } |
| 326 | } |
| 327 | |
| 328 | // complete adds information about the response to the requestInfo |
| 329 | func (r *requestInfo) complete(response *http.Response, err error) { |
| 330 | if err != nil { |
| 331 | r.ResponseErr = err |
| 332 | return |
| 333 | } |
| 334 | r.ResponseStatus = response.Status |
| 335 | r.ResponseHeaders = response.Header |
| 336 | } |
| 337 | |
| 338 | // toCurl returns a string that can be run as a command in a terminal (minus the body) |
| 339 | func (r *requestInfo) toCurl() string { |
| 340 | headers := "" |
| 341 | for key, values := range r.RequestHeaders { |
| 342 | for _, value := range values { |
| 343 | headers += fmt.Sprintf(` -H %q`, fmt.Sprintf("%s: %s", key, value)) |
| 344 | } |
| 345 | } |
| 346 | |
| 347 | return fmt.Sprintf("curl -k -v -X%s %s '%s'", r.RequestVerb, headers, r.RequestURL) |
| 348 | } |
| 349 | |
| 350 | // debuggingRoundTripper will display information about the requests passing |
| 351 | // through it based on what is configured |
| 352 | type debuggingRoundTripper struct { |
| 353 | delegatedRoundTripper http.RoundTripper |
| 354 | |
| 355 | levels map[debugLevel]bool |
| 356 | } |
| 357 | |
| 358 | type debugLevel int |
| 359 | |
| 360 | const ( |
| 361 | debugJustURL debugLevel = iota |
| 362 | debugURLTiming |
| 363 | debugCurlCommand |
| 364 | debugRequestHeaders |
| 365 | debugResponseStatus |
| 366 | debugResponseHeaders |
| 367 | ) |
| 368 | |
| 369 | func newDebuggingRoundTripper(rt http.RoundTripper, levels ...debugLevel) *debuggingRoundTripper { |
| 370 | drt := &debuggingRoundTripper{ |
| 371 | delegatedRoundTripper: rt, |
| 372 | levels: make(map[debugLevel]bool, len(levels)), |
| 373 | } |
| 374 | for _, v := range levels { |
| 375 | drt.levels[v] = true |
| 376 | } |
| 377 | return drt |
| 378 | } |
| 379 | |
| 380 | func (rt *debuggingRoundTripper) CancelRequest(req *http.Request) { |
| 381 | tryCancelRequest(rt.WrappedRoundTripper(), req) |
| 382 | } |
| 383 | |
| 384 | var knownAuthTypes = map[string]bool{ |
| 385 | "bearer": true, |
| 386 | "basic": true, |
| 387 | "negotiate": true, |
| 388 | } |
| 389 | |
| 390 | // maskValue masks credential content from authorization headers |
| 391 | // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization |
| 392 | func maskValue(key string, value string) string { |
| 393 | if !strings.EqualFold(key, "Authorization") { |
| 394 | return value |
| 395 | } |
| 396 | if len(value) == 0 { |
| 397 | return "" |
| 398 | } |
| 399 | var authType string |
| 400 | if i := strings.Index(value, " "); i > 0 { |
| 401 | authType = value[0:i] |
| 402 | } else { |
| 403 | authType = value |
| 404 | } |
| 405 | if !knownAuthTypes[strings.ToLower(authType)] { |
| 406 | return "<masked>" |
| 407 | } |
| 408 | if len(value) > len(authType)+1 { |
| 409 | value = authType + " <masked>" |
| 410 | } else { |
| 411 | value = authType |
| 412 | } |
| 413 | return value |
| 414 | } |
| 415 | |
| 416 | func (rt *debuggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { |
| 417 | reqInfo := newRequestInfo(req) |
| 418 | |
| 419 | if rt.levels[debugJustURL] { |
| 420 | klog.Infof("%s %s", reqInfo.RequestVerb, reqInfo.RequestURL) |
| 421 | } |
| 422 | if rt.levels[debugCurlCommand] { |
| 423 | klog.Infof("%s", reqInfo.toCurl()) |
| 424 | |
| 425 | } |
| 426 | if rt.levels[debugRequestHeaders] { |
| 427 | klog.Infof("Request Headers:") |
| 428 | for key, values := range reqInfo.RequestHeaders { |
| 429 | for _, value := range values { |
| 430 | value = maskValue(key, value) |
| 431 | klog.Infof(" %s: %s", key, value) |
| 432 | } |
| 433 | } |
| 434 | } |
| 435 | |
| 436 | startTime := time.Now() |
| 437 | response, err := rt.delegatedRoundTripper.RoundTrip(req) |
| 438 | reqInfo.Duration = time.Since(startTime) |
| 439 | |
| 440 | reqInfo.complete(response, err) |
| 441 | |
| 442 | if rt.levels[debugURLTiming] { |
| 443 | klog.Infof("%s %s %s in %d milliseconds", reqInfo.RequestVerb, reqInfo.RequestURL, reqInfo.ResponseStatus, reqInfo.Duration.Nanoseconds()/int64(time.Millisecond)) |
| 444 | } |
| 445 | if rt.levels[debugResponseStatus] { |
| 446 | klog.Infof("Response Status: %s in %d milliseconds", reqInfo.ResponseStatus, reqInfo.Duration.Nanoseconds()/int64(time.Millisecond)) |
| 447 | } |
| 448 | if rt.levels[debugResponseHeaders] { |
| 449 | klog.Infof("Response Headers:") |
| 450 | for key, values := range reqInfo.ResponseHeaders { |
| 451 | for _, value := range values { |
| 452 | klog.Infof(" %s: %s", key, value) |
| 453 | } |
| 454 | } |
| 455 | } |
| 456 | |
| 457 | return response, err |
| 458 | } |
| 459 | |
| 460 | func (rt *debuggingRoundTripper) WrappedRoundTripper() http.RoundTripper { |
| 461 | return rt.delegatedRoundTripper |
| 462 | } |
| 463 | |
| 464 | func legalHeaderByte(b byte) bool { |
| 465 | return int(b) < len(legalHeaderKeyBytes) && legalHeaderKeyBytes[b] |
| 466 | } |
| 467 | |
| 468 | func shouldEscape(b byte) bool { |
| 469 | // url.PathUnescape() returns an error if any '%' is not followed by two |
| 470 | // hexadecimal digits, so we'll intentionally encode it. |
| 471 | return !legalHeaderByte(b) || b == '%' |
| 472 | } |
| 473 | |
| 474 | func headerKeyEscape(key string) string { |
| 475 | buf := strings.Builder{} |
| 476 | for i := 0; i < len(key); i++ { |
| 477 | b := key[i] |
| 478 | if shouldEscape(b) { |
| 479 | // %-encode bytes that should be escaped: |
| 480 | // https://tools.ietf.org/html/rfc3986#section-2.1 |
| 481 | fmt.Fprintf(&buf, "%%%02X", b) |
| 482 | continue |
| 483 | } |
| 484 | buf.WriteByte(b) |
| 485 | } |
| 486 | return buf.String() |
| 487 | } |
| 488 | |
| 489 | // legalHeaderKeyBytes was copied from net/http/lex.go's isTokenTable. |
| 490 | // See https://httpwg.github.io/specs/rfc7230.html#rule.token.separators |
| 491 | var legalHeaderKeyBytes = [127]bool{ |
| 492 | '%': true, |
| 493 | '!': true, |
| 494 | '#': true, |
| 495 | '$': true, |
| 496 | '&': true, |
| 497 | '\'': true, |
| 498 | '*': true, |
| 499 | '+': true, |
| 500 | '-': true, |
| 501 | '.': true, |
| 502 | '0': true, |
| 503 | '1': true, |
| 504 | '2': true, |
| 505 | '3': true, |
| 506 | '4': true, |
| 507 | '5': true, |
| 508 | '6': true, |
| 509 | '7': true, |
| 510 | '8': true, |
| 511 | '9': true, |
| 512 | 'A': true, |
| 513 | 'B': true, |
| 514 | 'C': true, |
| 515 | 'D': true, |
| 516 | 'E': true, |
| 517 | 'F': true, |
| 518 | 'G': true, |
| 519 | 'H': true, |
| 520 | 'I': true, |
| 521 | 'J': true, |
| 522 | 'K': true, |
| 523 | 'L': true, |
| 524 | 'M': true, |
| 525 | 'N': true, |
| 526 | 'O': true, |
| 527 | 'P': true, |
| 528 | 'Q': true, |
| 529 | 'R': true, |
| 530 | 'S': true, |
| 531 | 'T': true, |
| 532 | 'U': true, |
| 533 | 'W': true, |
| 534 | 'V': true, |
| 535 | 'X': true, |
| 536 | 'Y': true, |
| 537 | 'Z': true, |
| 538 | '^': true, |
| 539 | '_': true, |
| 540 | '`': true, |
| 541 | 'a': true, |
| 542 | 'b': true, |
| 543 | 'c': true, |
| 544 | 'd': true, |
| 545 | 'e': true, |
| 546 | 'f': true, |
| 547 | 'g': true, |
| 548 | 'h': true, |
| 549 | 'i': true, |
| 550 | 'j': true, |
| 551 | 'k': true, |
| 552 | 'l': true, |
| 553 | 'm': true, |
| 554 | 'n': true, |
| 555 | 'o': true, |
| 556 | 'p': true, |
| 557 | 'q': true, |
| 558 | 'r': true, |
| 559 | 's': true, |
| 560 | 't': true, |
| 561 | 'u': true, |
| 562 | 'v': true, |
| 563 | 'w': true, |
| 564 | 'x': true, |
| 565 | 'y': true, |
| 566 | 'z': true, |
| 567 | '|': true, |
| 568 | '~': true, |
| 569 | } |