@@ -21,15 +21,20 @@ import (
21
21
"bytes"
22
22
"context"
23
23
"crypto/tls"
24
+ "errors"
24
25
"fmt"
25
26
"io"
27
+ "mime"
26
28
"net"
27
29
"net/http"
28
30
"net/url"
29
31
"os"
30
32
"path"
33
+ "regexp"
31
34
"strconv"
32
35
"strings"
36
+ "unicode"
37
+ "unicode/utf8"
33
38
34
39
"golang.org/x/net/http2"
35
40
"k8s.io/klog/v2"
@@ -482,3 +487,232 @@ func CloneHeader(in http.Header) http.Header {
482
487
}
483
488
return out
484
489
}
490
+
491
+ // WarningHeader contains a single RFC2616 14.46 warnings header
492
+ type WarningHeader struct {
493
+ // Codeindicates the type of warning. 299 is a miscellaneous persistent warning
494
+ Code int
495
+ // Agent contains the name or pseudonym of the server adding the Warning header.
496
+ // A single "-" is recommended when agent is unknown.
497
+ Agent string
498
+ // Warning text
499
+ Text string
500
+ }
501
+
502
+ // ParseWarningHeaders extract RFC2616 14.46 warnings headers from the specified set of header values.
503
+ // Multiple comma-separated warnings per header are supported.
504
+ // If errors are encountered on a header, the remainder of that header are skipped and subsequent headers are parsed.
505
+ // Returns successfully parsed warnings and any errors encountered.
506
+ func ParseWarningHeaders (headers []string ) ([]WarningHeader , []error ) {
507
+ var (
508
+ results []WarningHeader
509
+ errs []error
510
+ )
511
+ for _ , header := range headers {
512
+ for len (header ) > 0 {
513
+ result , remainder , err := ParseWarningHeader (header )
514
+ if err != nil {
515
+ errs = append (errs , err )
516
+ break
517
+ }
518
+ results = append (results , result )
519
+ header = remainder
520
+ }
521
+ }
522
+ return results , errs
523
+ }
524
+
525
+ var (
526
+ codeMatcher = regexp .MustCompile (`^[0-9]{3}$` )
527
+ wordDecoder = & mime.WordDecoder {}
528
+ )
529
+
530
+ // ParseWarningHeader extracts one RFC2616 14.46 warning from the specified header,
531
+ // returning an error if the header does not contain a correctly formatted warning.
532
+ // Any remaining content in the header is returned.
533
+ func ParseWarningHeader (header string ) (result WarningHeader , remainder string , err error ) {
534
+ // https://tools.ietf.org/html/rfc2616#section-14.46
535
+ // updated by
536
+ // https://tools.ietf.org/html/rfc7234#section-5.5
537
+ // https://tools.ietf.org/html/rfc7234#appendix-A
538
+ // Some requirements regarding production and processing of the Warning
539
+ // header fields have been relaxed, as it is not widely implemented.
540
+ // Furthermore, the Warning header field no longer uses RFC 2047
541
+ // encoding, nor does it allow multiple languages, as these aspects were
542
+ // not implemented.
543
+ //
544
+ // Format is one of:
545
+ // warn-code warn-agent "warn-text"
546
+ // warn-code warn-agent "warn-text" "warn-date"
547
+ //
548
+ // warn-code is a three digit number
549
+ // warn-agent is unquoted and contains no spaces
550
+ // warn-text is quoted with backslash escaping (RFC2047-encoded according to RFC2616, not encoded according to RFC7234)
551
+ // warn-date is optional, quoted, and in HTTP-date format (no embedded or escaped quotes)
552
+ //
553
+ // additional warnings can optionally be included in the same header by comma-separating them:
554
+ // warn-code warn-agent "warn-text" "warn-date"[, warn-code warn-agent "warn-text" "warn-date", ...]
555
+
556
+ // tolerate leading whitespace
557
+ header = strings .TrimSpace (header )
558
+
559
+ parts := strings .SplitN (header , " " , 3 )
560
+ if len (parts ) != 3 {
561
+ return WarningHeader {}, "" , errors .New ("invalid warning header: fewer than 3 segments" )
562
+ }
563
+ code , agent , textDateRemainder := parts [0 ], parts [1 ], parts [2 ]
564
+
565
+ // verify code format
566
+ if ! codeMatcher .Match ([]byte (code )) {
567
+ return WarningHeader {}, "" , errors .New ("invalid warning header: code segment is not 3 digits between 100-299" )
568
+ }
569
+ codeInt , _ := strconv .ParseInt (code , 10 , 64 )
570
+
571
+ // verify agent presence
572
+ if len (agent ) == 0 {
573
+ return WarningHeader {}, "" , errors .New ("invalid warning header: empty agent segment" )
574
+ }
575
+ if ! utf8 .ValidString (agent ) || hasAnyRunes (agent , unicode .IsControl ) {
576
+ return WarningHeader {}, "" , errors .New ("invalid warning header: invalid agent" )
577
+ }
578
+
579
+ // verify textDateRemainder presence
580
+ if len (textDateRemainder ) == 0 {
581
+ return WarningHeader {}, "" , errors .New ("invalid warning header: empty text segment" )
582
+ }
583
+
584
+ // extract text
585
+ text , dateAndRemainder , err := parseQuotedString (textDateRemainder )
586
+ if err != nil {
587
+ return WarningHeader {}, "" , fmt .Errorf ("invalid warning header: %v" , err )
588
+ }
589
+ // tolerate RFC2047-encoded text from warnings produced according to RFC2616
590
+ if decodedText , err := wordDecoder .DecodeHeader (text ); err == nil {
591
+ text = decodedText
592
+ }
593
+ if ! utf8 .ValidString (text ) || hasAnyRunes (text , unicode .IsControl ) {
594
+ return WarningHeader {}, "" , errors .New ("invalid warning header: invalid text" )
595
+ }
596
+ result = WarningHeader {Code : int (codeInt ), Agent : agent , Text : text }
597
+
598
+ if len (dateAndRemainder ) > 0 {
599
+ if dateAndRemainder [0 ] == '"' {
600
+ // consume date
601
+ foundEndQuote := false
602
+ for i := 1 ; i < len (dateAndRemainder ); i ++ {
603
+ if dateAndRemainder [i ] == '"' {
604
+ foundEndQuote = true
605
+ remainder = strings .TrimSpace (dateAndRemainder [i + 1 :])
606
+ break
607
+ }
608
+ }
609
+ if ! foundEndQuote {
610
+ return WarningHeader {}, "" , errors .New ("invalid warning header: unterminated date segment" )
611
+ }
612
+ } else {
613
+ remainder = dateAndRemainder
614
+ }
615
+ }
616
+ if len (remainder ) > 0 {
617
+ if remainder [0 ] == ',' {
618
+ // consume comma if present
619
+ remainder = strings .TrimSpace (remainder [1 :])
620
+ } else {
621
+ return WarningHeader {}, "" , errors .New ("invalid warning header: unexpected token after warn-date" )
622
+ }
623
+ }
624
+
625
+ return result , remainder , nil
626
+ }
627
+
628
+ func parseQuotedString (quotedString string ) (string , string , error ) {
629
+ if len (quotedString ) == 0 {
630
+ return "" , "" , errors .New ("invalid quoted string: 0-length" )
631
+ }
632
+
633
+ if quotedString [0 ] != '"' {
634
+ return "" , "" , errors .New ("invalid quoted string: missing initial quote" )
635
+ }
636
+
637
+ quotedString = quotedString [1 :]
638
+ var remainder string
639
+ escaping := false
640
+ closedQuote := false
641
+ result := & bytes.Buffer {}
642
+ loop:
643
+ for i := 0 ; i < len (quotedString ); i ++ {
644
+ b := quotedString [i ]
645
+ switch b {
646
+ case '"' :
647
+ if escaping {
648
+ result .WriteByte (b )
649
+ escaping = false
650
+ } else {
651
+ closedQuote = true
652
+ remainder = strings .TrimSpace (quotedString [i + 1 :])
653
+ break loop
654
+ }
655
+ case '\\' :
656
+ if escaping {
657
+ result .WriteByte (b )
658
+ escaping = false
659
+ } else {
660
+ escaping = true
661
+ }
662
+ default :
663
+ result .WriteByte (b )
664
+ escaping = false
665
+ }
666
+ }
667
+
668
+ if ! closedQuote {
669
+ return "" , "" , errors .New ("invalid quoted string: missing closing quote" )
670
+ }
671
+ return result .String (), remainder , nil
672
+ }
673
+
674
+ func NewWarningHeader (code int , agent , text string ) (string , error ) {
675
+ if code < 0 || code > 999 {
676
+ return "" , errors .New ("code must be between 0 and 999" )
677
+ }
678
+ if len (agent ) == 0 {
679
+ agent = "-"
680
+ } else if ! utf8 .ValidString (agent ) || strings .ContainsAny (agent , `\"` ) || hasAnyRunes (agent , unicode .IsSpace , unicode .IsControl ) {
681
+ return "" , errors .New ("agent must be valid UTF-8 and must not contain spaces, quotes, backslashes, or control characters" )
682
+ }
683
+ if ! utf8 .ValidString (text ) || hasAnyRunes (text , unicode .IsControl ) {
684
+ return "" , errors .New ("text must be valid UTF-8 and must not contain control characters" )
685
+ }
686
+ return fmt .Sprintf ("%03d %s %s" , code , agent , makeQuotedString (text )), nil
687
+ }
688
+
689
+ func hasAnyRunes (s string , runeCheckers ... func (rune ) bool ) bool {
690
+ for _ , r := range s {
691
+ for _ , checker := range runeCheckers {
692
+ if checker (r ) {
693
+ return true
694
+ }
695
+ }
696
+ }
697
+ return false
698
+ }
699
+
700
+ func makeQuotedString (s string ) string {
701
+ result := & bytes.Buffer {}
702
+ // opening quote
703
+ result .WriteRune ('"' )
704
+ for _ , c := range s {
705
+ switch c {
706
+ case '"' , '\\' :
707
+ // escape " and \
708
+ result .WriteRune ('\\' )
709
+ result .WriteRune (c )
710
+ default :
711
+ // write everything else as-is
712
+ result .WriteRune (c )
713
+ }
714
+ }
715
+ // closing quote
716
+ result .WriteRune ('"' )
717
+ return result .String ()
718
+ }
0 commit comments