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