http.go 21 KB

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