github.com/spotmaxtech/k8s-apimachinery-v0260@v0.0.1/pkg/util/net/http.go (about) 1 /* 2 Copyright 2016 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 net 18 19 import ( 20 "bytes" 21 "context" 22 "crypto/tls" 23 "errors" 24 "fmt" 25 "io" 26 "mime" 27 "net" 28 "net/http" 29 "net/url" 30 "os" 31 "path" 32 "regexp" 33 "strconv" 34 "strings" 35 "time" 36 "unicode" 37 "unicode/utf8" 38 39 "golang.org/x/net/http2" 40 "k8s.io/klog/v2" 41 netutils "k8s.io/utils/net" 42 ) 43 44 // JoinPreservingTrailingSlash does a path.Join of the specified elements, 45 // preserving any trailing slash on the last non-empty segment 46 func JoinPreservingTrailingSlash(elem ...string) string { 47 // do the basic path join 48 result := path.Join(elem...) 49 50 // find the last non-empty segment 51 for i := len(elem) - 1; i >= 0; i-- { 52 if len(elem[i]) > 0 { 53 // if the last segment ended in a slash, ensure our result does as well 54 if strings.HasSuffix(elem[i], "/") && !strings.HasSuffix(result, "/") { 55 result += "/" 56 } 57 break 58 } 59 } 60 61 return result 62 } 63 64 // IsTimeout returns true if the given error is a network timeout error 65 func IsTimeout(err error) bool { 66 var neterr net.Error 67 if errors.As(err, &neterr) { 68 return neterr != nil && neterr.Timeout() 69 } 70 return false 71 } 72 73 // IsProbableEOF returns true if the given error resembles a connection termination 74 // scenario that would justify assuming that the watch is empty. 75 // These errors are what the Go http stack returns back to us which are general 76 // connection closure errors (strongly correlated) and callers that need to 77 // differentiate probable errors in connection behavior between normal "this is 78 // disconnected" should use the method. 79 func IsProbableEOF(err error) bool { 80 if err == nil { 81 return false 82 } 83 var uerr *url.Error 84 if errors.As(err, &uerr) { 85 err = uerr.Err 86 } 87 msg := err.Error() 88 switch { 89 case err == io.EOF: 90 return true 91 case err == io.ErrUnexpectedEOF: 92 return true 93 case msg == "http: can't write HTTP request on broken connection": 94 return true 95 case strings.Contains(msg, "http2: server sent GOAWAY and closed the connection"): 96 return true 97 case strings.Contains(msg, "connection reset by peer"): 98 return true 99 case strings.Contains(strings.ToLower(msg), "use of closed network connection"): 100 return true 101 } 102 return false 103 } 104 105 var defaultTransport = http.DefaultTransport.(*http.Transport) 106 107 // SetOldTransportDefaults applies the defaults from http.DefaultTransport 108 // for the Proxy, Dial, and TLSHandshakeTimeout fields if unset 109 func SetOldTransportDefaults(t *http.Transport) *http.Transport { 110 if t.Proxy == nil || isDefault(t.Proxy) { 111 // http.ProxyFromEnvironment doesn't respect CIDRs and that makes it impossible to exclude things like pod and service IPs from proxy settings 112 // ProxierWithNoProxyCIDR allows CIDR rules in NO_PROXY 113 t.Proxy = NewProxierWithNoProxyCIDR(http.ProxyFromEnvironment) 114 } 115 // If no custom dialer is set, use the default context dialer 116 //lint:file-ignore SA1019 Keep supporting deprecated Dial method of custom transports 117 if t.DialContext == nil && t.Dial == nil { 118 t.DialContext = defaultTransport.DialContext 119 } 120 if t.TLSHandshakeTimeout == 0 { 121 t.TLSHandshakeTimeout = defaultTransport.TLSHandshakeTimeout 122 } 123 if t.IdleConnTimeout == 0 { 124 t.IdleConnTimeout = defaultTransport.IdleConnTimeout 125 } 126 return t 127 } 128 129 // SetTransportDefaults applies the defaults from http.DefaultTransport 130 // for the Proxy, Dial, and TLSHandshakeTimeout fields if unset 131 func SetTransportDefaults(t *http.Transport) *http.Transport { 132 t = SetOldTransportDefaults(t) 133 // Allow clients to disable http2 if needed. 134 if s := os.Getenv("DISABLE_HTTP2"); len(s) > 0 { 135 klog.Info("HTTP2 has been explicitly disabled") 136 } else if allowsHTTP2(t) { 137 if err := configureHTTP2Transport(t); err != nil { 138 klog.Warningf("Transport failed http2 configuration: %v", err) 139 } 140 } 141 return t 142 } 143 144 func readIdleTimeoutSeconds() int { 145 ret := 30 146 // User can set the readIdleTimeout to 0 to disable the HTTP/2 147 // connection health check. 148 if s := os.Getenv("HTTP2_READ_IDLE_TIMEOUT_SECONDS"); len(s) > 0 { 149 i, err := strconv.Atoi(s) 150 if err != nil { 151 klog.Warningf("Illegal HTTP2_READ_IDLE_TIMEOUT_SECONDS(%q): %v."+ 152 " Default value %d is used", s, err, ret) 153 return ret 154 } 155 ret = i 156 } 157 return ret 158 } 159 160 func pingTimeoutSeconds() int { 161 ret := 15 162 if s := os.Getenv("HTTP2_PING_TIMEOUT_SECONDS"); len(s) > 0 { 163 i, err := strconv.Atoi(s) 164 if err != nil { 165 klog.Warningf("Illegal HTTP2_PING_TIMEOUT_SECONDS(%q): %v."+ 166 " Default value %d is used", s, err, ret) 167 return ret 168 } 169 ret = i 170 } 171 return ret 172 } 173 174 func configureHTTP2Transport(t *http.Transport) error { 175 t2, err := http2.ConfigureTransports(t) 176 if err != nil { 177 return err 178 } 179 // The following enables the HTTP/2 connection health check added in 180 // https://github.com/golang/net/pull/55. The health check detects and 181 // closes broken transport layer connections. Without the health check, 182 // a broken connection can linger too long, e.g., a broken TCP 183 // connection will be closed by the Linux kernel after 13 to 30 minutes 184 // by default, which caused 185 // https://github.com/kubernetes/client-go/issues/374 and 186 // https://github.com/kubernetes/kubernetes/issues/87615. 187 t2.ReadIdleTimeout = time.Duration(readIdleTimeoutSeconds()) * time.Second 188 t2.PingTimeout = time.Duration(pingTimeoutSeconds()) * time.Second 189 return nil 190 } 191 192 func allowsHTTP2(t *http.Transport) bool { 193 if t.TLSClientConfig == nil || len(t.TLSClientConfig.NextProtos) == 0 { 194 // the transport expressed no NextProto preference, allow 195 return true 196 } 197 for _, p := range t.TLSClientConfig.NextProtos { 198 if p == http2.NextProtoTLS { 199 // the transport explicitly allowed http/2 200 return true 201 } 202 } 203 // the transport explicitly set NextProtos and excluded http/2 204 return false 205 } 206 207 type RoundTripperWrapper interface { 208 http.RoundTripper 209 WrappedRoundTripper() http.RoundTripper 210 } 211 212 type DialFunc func(ctx context.Context, net, addr string) (net.Conn, error) 213 214 func DialerFor(transport http.RoundTripper) (DialFunc, error) { 215 if transport == nil { 216 return nil, nil 217 } 218 219 switch transport := transport.(type) { 220 case *http.Transport: 221 // transport.DialContext takes precedence over transport.Dial 222 if transport.DialContext != nil { 223 return transport.DialContext, nil 224 } 225 // adapt transport.Dial to the DialWithContext signature 226 if transport.Dial != nil { 227 return func(ctx context.Context, net, addr string) (net.Conn, error) { 228 return transport.Dial(net, addr) 229 }, nil 230 } 231 // otherwise return nil 232 return nil, nil 233 case RoundTripperWrapper: 234 return DialerFor(transport.WrappedRoundTripper()) 235 default: 236 return nil, fmt.Errorf("unknown transport type: %T", transport) 237 } 238 } 239 240 // CloseIdleConnectionsFor close idles connections for the Transport. 241 // If the Transport is wrapped it iterates over the wrapped round trippers 242 // until it finds one that implements the CloseIdleConnections method. 243 // If the Transport does not have a CloseIdleConnections method 244 // then this function does nothing. 245 func CloseIdleConnectionsFor(transport http.RoundTripper) { 246 if transport == nil { 247 return 248 } 249 type closeIdler interface { 250 CloseIdleConnections() 251 } 252 253 switch transport := transport.(type) { 254 case closeIdler: 255 transport.CloseIdleConnections() 256 case RoundTripperWrapper: 257 CloseIdleConnectionsFor(transport.WrappedRoundTripper()) 258 default: 259 klog.Warningf("unknown transport type: %T", transport) 260 } 261 } 262 263 type TLSClientConfigHolder interface { 264 TLSClientConfig() *tls.Config 265 } 266 267 func TLSClientConfig(transport http.RoundTripper) (*tls.Config, error) { 268 if transport == nil { 269 return nil, nil 270 } 271 272 switch transport := transport.(type) { 273 case *http.Transport: 274 return transport.TLSClientConfig, nil 275 case TLSClientConfigHolder: 276 return transport.TLSClientConfig(), nil 277 case RoundTripperWrapper: 278 return TLSClientConfig(transport.WrappedRoundTripper()) 279 default: 280 return nil, fmt.Errorf("unknown transport type: %T", transport) 281 } 282 } 283 284 func FormatURL(scheme string, host string, port int, path string) *url.URL { 285 return &url.URL{ 286 Scheme: scheme, 287 Host: net.JoinHostPort(host, strconv.Itoa(port)), 288 Path: path, 289 } 290 } 291 292 func GetHTTPClient(req *http.Request) string { 293 if ua := req.UserAgent(); len(ua) != 0 { 294 return ua 295 } 296 return "unknown" 297 } 298 299 // SourceIPs splits the comma separated X-Forwarded-For header and joins it with 300 // the X-Real-Ip header and/or req.RemoteAddr, ignoring invalid IPs. 301 // The X-Real-Ip is omitted if it's already present in the X-Forwarded-For chain. 302 // The req.RemoteAddr is always the last IP in the returned list. 303 // It returns nil if all of these are empty or invalid. 304 func SourceIPs(req *http.Request) []net.IP { 305 var srcIPs []net.IP 306 307 hdr := req.Header 308 // First check the X-Forwarded-For header for requests via proxy. 309 hdrForwardedFor := hdr.Get("X-Forwarded-For") 310 if hdrForwardedFor != "" { 311 // X-Forwarded-For can be a csv of IPs in case of multiple proxies. 312 // Use the first valid one. 313 parts := strings.Split(hdrForwardedFor, ",") 314 for _, part := range parts { 315 ip := netutils.ParseIPSloppy(strings.TrimSpace(part)) 316 if ip != nil { 317 srcIPs = append(srcIPs, ip) 318 } 319 } 320 } 321 322 // Try the X-Real-Ip header. 323 hdrRealIp := hdr.Get("X-Real-Ip") 324 if hdrRealIp != "" { 325 ip := netutils.ParseIPSloppy(hdrRealIp) 326 // Only append the X-Real-Ip if it's not already contained in the X-Forwarded-For chain. 327 if ip != nil && !containsIP(srcIPs, ip) { 328 srcIPs = append(srcIPs, ip) 329 } 330 } 331 332 // Always include the request Remote Address as it cannot be easily spoofed. 333 var remoteIP net.IP 334 // Remote Address in Go's HTTP server is in the form host:port so we need to split that first. 335 host, _, err := net.SplitHostPort(req.RemoteAddr) 336 if err == nil { 337 remoteIP = netutils.ParseIPSloppy(host) 338 } 339 // Fallback if Remote Address was just IP. 340 if remoteIP == nil { 341 remoteIP = netutils.ParseIPSloppy(req.RemoteAddr) 342 } 343 344 // Don't duplicate remote IP if it's already the last address in the chain. 345 if remoteIP != nil && (len(srcIPs) == 0 || !remoteIP.Equal(srcIPs[len(srcIPs)-1])) { 346 srcIPs = append(srcIPs, remoteIP) 347 } 348 349 return srcIPs 350 } 351 352 // Checks whether the given IP address is contained in the list of IPs. 353 func containsIP(ips []net.IP, ip net.IP) bool { 354 for _, v := range ips { 355 if v.Equal(ip) { 356 return true 357 } 358 } 359 return false 360 } 361 362 // Extracts and returns the clients IP from the given request. 363 // Looks at X-Forwarded-For header, X-Real-Ip header and request.RemoteAddr in that order. 364 // Returns nil if none of them are set or is set to an invalid value. 365 func GetClientIP(req *http.Request) net.IP { 366 ips := SourceIPs(req) 367 if len(ips) == 0 { 368 return nil 369 } 370 return ips[0] 371 } 372 373 // Prepares the X-Forwarded-For header for another forwarding hop by appending the previous sender's 374 // IP address to the X-Forwarded-For chain. 375 func AppendForwardedForHeader(req *http.Request) { 376 // Copied from net/http/httputil/reverseproxy.go: 377 if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { 378 // If we aren't the first proxy retain prior 379 // X-Forwarded-For information as a comma+space 380 // separated list and fold multiple headers into one. 381 if prior, ok := req.Header["X-Forwarded-For"]; ok { 382 clientIP = strings.Join(prior, ", ") + ", " + clientIP 383 } 384 req.Header.Set("X-Forwarded-For", clientIP) 385 } 386 } 387 388 var defaultProxyFuncPointer = fmt.Sprintf("%p", http.ProxyFromEnvironment) 389 390 // isDefault checks to see if the transportProxierFunc is pointing to the default one 391 func isDefault(transportProxier func(*http.Request) (*url.URL, error)) bool { 392 transportProxierPointer := fmt.Sprintf("%p", transportProxier) 393 return transportProxierPointer == defaultProxyFuncPointer 394 } 395 396 // NewProxierWithNoProxyCIDR constructs a Proxier function that respects CIDRs in NO_PROXY and delegates if 397 // no matching CIDRs are found 398 func NewProxierWithNoProxyCIDR(delegate func(req *http.Request) (*url.URL, error)) func(req *http.Request) (*url.URL, error) { 399 // we wrap the default method, so we only need to perform our check if the NO_PROXY (or no_proxy) envvar has a CIDR in it 400 noProxyEnv := os.Getenv("NO_PROXY") 401 if noProxyEnv == "" { 402 noProxyEnv = os.Getenv("no_proxy") 403 } 404 noProxyRules := strings.Split(noProxyEnv, ",") 405 406 cidrs := []*net.IPNet{} 407 for _, noProxyRule := range noProxyRules { 408 _, cidr, _ := netutils.ParseCIDRSloppy(noProxyRule) 409 if cidr != nil { 410 cidrs = append(cidrs, cidr) 411 } 412 } 413 414 if len(cidrs) == 0 { 415 return delegate 416 } 417 418 return func(req *http.Request) (*url.URL, error) { 419 ip := netutils.ParseIPSloppy(req.URL.Hostname()) 420 if ip == nil { 421 return delegate(req) 422 } 423 424 for _, cidr := range cidrs { 425 if cidr.Contains(ip) { 426 return nil, nil 427 } 428 } 429 430 return delegate(req) 431 } 432 } 433 434 // DialerFunc implements Dialer for the provided function. 435 type DialerFunc func(req *http.Request) (net.Conn, error) 436 437 func (fn DialerFunc) Dial(req *http.Request) (net.Conn, error) { 438 return fn(req) 439 } 440 441 // Dialer dials a host and writes a request to it. 442 type Dialer interface { 443 // Dial connects to the host specified by req's URL, writes the request to the connection, and 444 // returns the opened net.Conn. 445 Dial(req *http.Request) (net.Conn, error) 446 } 447 448 // CloneRequest creates a shallow copy of the request along with a deep copy of the Headers. 449 func CloneRequest(req *http.Request) *http.Request { 450 r := new(http.Request) 451 452 // shallow clone 453 *r = *req 454 455 // deep copy headers 456 r.Header = CloneHeader(req.Header) 457 458 return r 459 } 460 461 // CloneHeader creates a deep copy of an http.Header. 462 func CloneHeader(in http.Header) http.Header { 463 out := make(http.Header, len(in)) 464 for key, values := range in { 465 newValues := make([]string, len(values)) 466 copy(newValues, values) 467 out[key] = newValues 468 } 469 return out 470 } 471 472 // WarningHeader contains a single RFC2616 14.46 warnings header 473 type WarningHeader struct { 474 // Codeindicates the type of warning. 299 is a miscellaneous persistent warning 475 Code int 476 // Agent contains the name or pseudonym of the server adding the Warning header. 477 // A single "-" is recommended when agent is unknown. 478 Agent string 479 // Warning text 480 Text string 481 } 482 483 // ParseWarningHeaders extract RFC2616 14.46 warnings headers from the specified set of header values. 484 // Multiple comma-separated warnings per header are supported. 485 // If errors are encountered on a header, the remainder of that header are skipped and subsequent headers are parsed. 486 // Returns successfully parsed warnings and any errors encountered. 487 func ParseWarningHeaders(headers []string) ([]WarningHeader, []error) { 488 var ( 489 results []WarningHeader 490 errs []error 491 ) 492 for _, header := range headers { 493 for len(header) > 0 { 494 result, remainder, err := ParseWarningHeader(header) 495 if err != nil { 496 errs = append(errs, err) 497 break 498 } 499 results = append(results, result) 500 header = remainder 501 } 502 } 503 return results, errs 504 } 505 506 var ( 507 codeMatcher = regexp.MustCompile(`^[0-9]{3}$`) 508 wordDecoder = &mime.WordDecoder{} 509 ) 510 511 // ParseWarningHeader extracts one RFC2616 14.46 warning from the specified header, 512 // returning an error if the header does not contain a correctly formatted warning. 513 // Any remaining content in the header is returned. 514 func ParseWarningHeader(header string) (result WarningHeader, remainder string, err error) { 515 // https://tools.ietf.org/html/rfc2616#section-14.46 516 // updated by 517 // https://tools.ietf.org/html/rfc7234#section-5.5 518 // https://tools.ietf.org/html/rfc7234#appendix-A 519 // Some requirements regarding production and processing of the Warning 520 // header fields have been relaxed, as it is not widely implemented. 521 // Furthermore, the Warning header field no longer uses RFC 2047 522 // encoding, nor does it allow multiple languages, as these aspects were 523 // not implemented. 524 // 525 // Format is one of: 526 // warn-code warn-agent "warn-text" 527 // warn-code warn-agent "warn-text" "warn-date" 528 // 529 // warn-code is a three digit number 530 // warn-agent is unquoted and contains no spaces 531 // warn-text is quoted with backslash escaping (RFC2047-encoded according to RFC2616, not encoded according to RFC7234) 532 // warn-date is optional, quoted, and in HTTP-date format (no embedded or escaped quotes) 533 // 534 // additional warnings can optionally be included in the same header by comma-separating them: 535 // warn-code warn-agent "warn-text" "warn-date"[, warn-code warn-agent "warn-text" "warn-date", ...] 536 537 // tolerate leading whitespace 538 header = strings.TrimSpace(header) 539 540 parts := strings.SplitN(header, " ", 3) 541 if len(parts) != 3 { 542 return WarningHeader{}, "", errors.New("invalid warning header: fewer than 3 segments") 543 } 544 code, agent, textDateRemainder := parts[0], parts[1], parts[2] 545 546 // verify code format 547 if !codeMatcher.Match([]byte(code)) { 548 return WarningHeader{}, "", errors.New("invalid warning header: code segment is not 3 digits between 100-299") 549 } 550 codeInt, _ := strconv.ParseInt(code, 10, 64) 551 552 // verify agent presence 553 if len(agent) == 0 { 554 return WarningHeader{}, "", errors.New("invalid warning header: empty agent segment") 555 } 556 if !utf8.ValidString(agent) || hasAnyRunes(agent, unicode.IsControl) { 557 return WarningHeader{}, "", errors.New("invalid warning header: invalid agent") 558 } 559 560 // verify textDateRemainder presence 561 if len(textDateRemainder) == 0 { 562 return WarningHeader{}, "", errors.New("invalid warning header: empty text segment") 563 } 564 565 // extract text 566 text, dateAndRemainder, err := parseQuotedString(textDateRemainder) 567 if err != nil { 568 return WarningHeader{}, "", fmt.Errorf("invalid warning header: %v", err) 569 } 570 // tolerate RFC2047-encoded text from warnings produced according to RFC2616 571 if decodedText, err := wordDecoder.DecodeHeader(text); err == nil { 572 text = decodedText 573 } 574 if !utf8.ValidString(text) || hasAnyRunes(text, unicode.IsControl) { 575 return WarningHeader{}, "", errors.New("invalid warning header: invalid text") 576 } 577 result = WarningHeader{Code: int(codeInt), Agent: agent, Text: text} 578 579 if len(dateAndRemainder) > 0 { 580 if dateAndRemainder[0] == '"' { 581 // consume date 582 foundEndQuote := false 583 for i := 1; i < len(dateAndRemainder); i++ { 584 if dateAndRemainder[i] == '"' { 585 foundEndQuote = true 586 remainder = strings.TrimSpace(dateAndRemainder[i+1:]) 587 break 588 } 589 } 590 if !foundEndQuote { 591 return WarningHeader{}, "", errors.New("invalid warning header: unterminated date segment") 592 } 593 } else { 594 remainder = dateAndRemainder 595 } 596 } 597 if len(remainder) > 0 { 598 if remainder[0] == ',' { 599 // consume comma if present 600 remainder = strings.TrimSpace(remainder[1:]) 601 } else { 602 return WarningHeader{}, "", errors.New("invalid warning header: unexpected token after warn-date") 603 } 604 } 605 606 return result, remainder, nil 607 } 608 609 func parseQuotedString(quotedString string) (string, string, error) { 610 if len(quotedString) == 0 { 611 return "", "", errors.New("invalid quoted string: 0-length") 612 } 613 614 if quotedString[0] != '"' { 615 return "", "", errors.New("invalid quoted string: missing initial quote") 616 } 617 618 quotedString = quotedString[1:] 619 var remainder string 620 escaping := false 621 closedQuote := false 622 result := &strings.Builder{} 623 loop: 624 for i := 0; i < len(quotedString); i++ { 625 b := quotedString[i] 626 switch b { 627 case '"': 628 if escaping { 629 result.WriteByte(b) 630 escaping = false 631 } else { 632 closedQuote = true 633 remainder = strings.TrimSpace(quotedString[i+1:]) 634 break loop 635 } 636 case '\\': 637 if escaping { 638 result.WriteByte(b) 639 escaping = false 640 } else { 641 escaping = true 642 } 643 default: 644 result.WriteByte(b) 645 escaping = false 646 } 647 } 648 649 if !closedQuote { 650 return "", "", errors.New("invalid quoted string: missing closing quote") 651 } 652 return result.String(), remainder, nil 653 } 654 655 func NewWarningHeader(code int, agent, text string) (string, error) { 656 if code < 0 || code > 999 { 657 return "", errors.New("code must be between 0 and 999") 658 } 659 if len(agent) == 0 { 660 agent = "-" 661 } else if !utf8.ValidString(agent) || strings.ContainsAny(agent, `\"`) || hasAnyRunes(agent, unicode.IsSpace, unicode.IsControl) { 662 return "", errors.New("agent must be valid UTF-8 and must not contain spaces, quotes, backslashes, or control characters") 663 } 664 if !utf8.ValidString(text) || hasAnyRunes(text, unicode.IsControl) { 665 return "", errors.New("text must be valid UTF-8 and must not contain control characters") 666 } 667 return fmt.Sprintf("%03d %s %s", code, agent, makeQuotedString(text)), nil 668 } 669 670 func hasAnyRunes(s string, runeCheckers ...func(rune) bool) bool { 671 for _, r := range s { 672 for _, checker := range runeCheckers { 673 if checker(r) { 674 return true 675 } 676 } 677 } 678 return false 679 } 680 681 func makeQuotedString(s string) string { 682 result := &bytes.Buffer{} 683 // opening quote 684 result.WriteRune('"') 685 for _, c := range s { 686 switch c { 687 case '"', '\\': 688 // escape " and \ 689 result.WriteRune('\\') 690 result.WriteRune(c) 691 default: 692 // write everything else as-is 693 result.WriteRune(c) 694 } 695 } 696 // closing quote 697 result.WriteRune('"') 698 return result.String() 699 }