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