aboutsummaryrefslogtreecommitdiffstats
path: root/library/go/httputil/headers
diff options
context:
space:
mode:
authorhcpp <hcpp@ydb.tech>2023-11-09 20:47:31 +0300
committerhcpp <hcpp@ydb.tech>2023-11-09 21:11:21 +0300
commitb7716e9978a4d1c2e548b9d53836a7d6894a8a38 (patch)
tree4ecf0ea759c7d44ed9749dc5d7beeaffc6376aac /library/go/httputil/headers
parentea9cef2dc79047c295a260f96895628b0feed43f (diff)
downloadydb-b7716e9978a4d1c2e548b9d53836a7d6894a8a38.tar.gz
YQ Connector:metrics (one more time)
custom httppuller has been added Revert "Revert "metrics have been added"" This reverts commit e2a874f25a443edf946bab9a7f077239ba569ab0, reversing changes made to 2dbbc3a1a033dd09ad29f0c168d8ea7fef97309e.
Diffstat (limited to 'library/go/httputil/headers')
-rw-r--r--library/go/httputil/headers/accept.go259
-rw-r--r--library/go/httputil/headers/accept_test.go309
-rw-r--r--library/go/httputil/headers/authorization.go31
-rw-r--r--library/go/httputil/headers/authorization_test.go30
-rw-r--r--library/go/httputil/headers/content.go57
-rw-r--r--library/go/httputil/headers/content_test.go41
-rw-r--r--library/go/httputil/headers/cookie.go5
-rw-r--r--library/go/httputil/headers/gotest/ya.make3
-rw-r--r--library/go/httputil/headers/tvm.go8
-rw-r--r--library/go/httputil/headers/user_agent.go5
-rw-r--r--library/go/httputil/headers/warning.go167
-rw-r--r--library/go/httputil/headers/warning_test.go245
-rw-r--r--library/go/httputil/headers/ya.make23
13 files changed, 1183 insertions, 0 deletions
diff --git a/library/go/httputil/headers/accept.go b/library/go/httputil/headers/accept.go
new file mode 100644
index 0000000000..394bed7360
--- /dev/null
+++ b/library/go/httputil/headers/accept.go
@@ -0,0 +1,259 @@
+package headers
+
+import (
+ "fmt"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+const (
+ AcceptKey = "Accept"
+ AcceptEncodingKey = "Accept-Encoding"
+)
+
+type AcceptableEncodings []AcceptableEncoding
+
+type AcceptableEncoding struct {
+ Encoding ContentEncoding
+ Weight float32
+
+ pos int
+}
+
+func (as AcceptableEncodings) IsAcceptable(encoding ContentEncoding) bool {
+ for _, ae := range as {
+ if ae.Encoding == encoding {
+ return ae.Weight != 0
+ }
+ }
+ return false
+}
+
+func (as AcceptableEncodings) String() string {
+ if len(as) == 0 {
+ return ""
+ }
+
+ var b strings.Builder
+ for i, ae := range as {
+ b.WriteString(ae.Encoding.String())
+
+ if ae.Weight > 0.0 && ae.Weight < 1.0 {
+ b.WriteString(";q=" + strconv.FormatFloat(float64(ae.Weight), 'f', 1, 32))
+ }
+
+ if i < len(as)-1 {
+ b.WriteString(", ")
+ }
+ }
+ return b.String()
+}
+
+type AcceptableTypes []AcceptableType
+
+func (as AcceptableTypes) IsAcceptable(contentType ContentType) bool {
+ for _, ae := range as {
+ if ae.Type == contentType {
+ return ae.Weight != 0
+ }
+ }
+ return false
+}
+
+type AcceptableType struct {
+ Type ContentType
+ Weight float32
+ Extension map[string]string
+
+ pos int
+}
+
+func (as AcceptableTypes) String() string {
+ if len(as) == 0 {
+ return ""
+ }
+
+ var b strings.Builder
+ for i, at := range as {
+ b.WriteString(at.Type.String())
+
+ if at.Weight > 0.0 && at.Weight < 1.0 {
+ b.WriteString(";q=" + strconv.FormatFloat(float64(at.Weight), 'f', 1, 32))
+ }
+
+ for k, v := range at.Extension {
+ b.WriteString(";" + k + "=" + v)
+ }
+
+ if i < len(as)-1 {
+ b.WriteString(", ")
+ }
+ }
+ return b.String()
+}
+
+// ParseAccept parses Accept HTTP header.
+// It will sort acceptable types by weight, specificity and position.
+// See: https://tools.ietf.org/html/rfc2616#section-14.1
+func ParseAccept(headerValue string) (AcceptableTypes, error) {
+ if headerValue == "" {
+ return nil, nil
+ }
+
+ parsedValues, err := parseAcceptFamilyHeader(headerValue)
+ if err != nil {
+ return nil, err
+ }
+ ah := make(AcceptableTypes, 0, len(parsedValues))
+ for _, parsedValue := range parsedValues {
+ ah = append(ah, AcceptableType{
+ Type: ContentType(parsedValue.Value),
+ Weight: parsedValue.Weight,
+ Extension: parsedValue.Extension,
+ pos: parsedValue.pos,
+ })
+ }
+
+ sort.Slice(ah, func(i, j int) bool {
+ // sort by weight only
+ if ah[i].Weight != ah[j].Weight {
+ return ah[i].Weight > ah[j].Weight
+ }
+
+ // sort by most specific if types are equal
+ if ah[i].Type == ah[j].Type {
+ return len(ah[i].Extension) > len(ah[j].Extension)
+ }
+
+ // move counterpart up if one of types is ANY
+ if ah[i].Type == ContentTypeAny {
+ return false
+ }
+ if ah[j].Type == ContentTypeAny {
+ return true
+ }
+
+ // i type has j type as prefix
+ if strings.HasSuffix(string(ah[j].Type), "/*") &&
+ strings.HasPrefix(string(ah[i].Type), string(ah[j].Type)[:len(ah[j].Type)-1]) {
+ return true
+ }
+
+ // j type has i type as prefix
+ if strings.HasSuffix(string(ah[i].Type), "/*") &&
+ strings.HasPrefix(string(ah[j].Type), string(ah[i].Type)[:len(ah[i].Type)-1]) {
+ return false
+ }
+
+ // sort by position if nothing else left
+ return ah[i].pos < ah[j].pos
+ })
+
+ return ah, nil
+}
+
+// ParseAcceptEncoding parses Accept-Encoding HTTP header.
+// It will sort acceptable encodings by weight and position.
+// See: https://tools.ietf.org/html/rfc2616#section-14.3
+func ParseAcceptEncoding(headerValue string) (AcceptableEncodings, error) {
+ if headerValue == "" {
+ return nil, nil
+ }
+
+ // e.g. gzip;q=1.0, compress, identity
+ parsedValues, err := parseAcceptFamilyHeader(headerValue)
+ if err != nil {
+ return nil, err
+ }
+ acceptableEncodings := make(AcceptableEncodings, 0, len(parsedValues))
+ for _, parsedValue := range parsedValues {
+ acceptableEncodings = append(acceptableEncodings, AcceptableEncoding{
+ Encoding: ContentEncoding(parsedValue.Value),
+ Weight: parsedValue.Weight,
+ pos: parsedValue.pos,
+ })
+ }
+ sort.Slice(acceptableEncodings, func(i, j int) bool {
+ // sort by weight only
+ if acceptableEncodings[i].Weight != acceptableEncodings[j].Weight {
+ return acceptableEncodings[i].Weight > acceptableEncodings[j].Weight
+ }
+
+ // move counterpart up if one of encodings is ANY
+ if acceptableEncodings[i].Encoding == EncodingAny {
+ return false
+ }
+ if acceptableEncodings[j].Encoding == EncodingAny {
+ return true
+ }
+
+ // sort by position if nothing else left
+ return acceptableEncodings[i].pos < acceptableEncodings[j].pos
+ })
+
+ return acceptableEncodings, nil
+}
+
+type acceptHeaderValue struct {
+ Value string
+ Weight float32
+ Extension map[string]string
+
+ pos int
+}
+
+// parseAcceptFamilyHeader parses family of Accept* HTTP headers
+// See: https://tools.ietf.org/html/rfc2616#section-14.1
+func parseAcceptFamilyHeader(header string) ([]acceptHeaderValue, error) {
+ headerValues := strings.Split(header, ",")
+
+ parsedValues := make([]acceptHeaderValue, 0, len(headerValues))
+ for i, headerValue := range headerValues {
+ valueParams := strings.Split(headerValue, ";")
+
+ parsedValue := acceptHeaderValue{
+ Value: strings.TrimSpace(valueParams[0]),
+ Weight: 1.0,
+ pos: i,
+ }
+
+ // parse quality factor and/or accept extension
+ if len(valueParams) > 1 {
+ for _, rawParam := range valueParams[1:] {
+ rawParam = strings.TrimSpace(rawParam)
+ params := strings.SplitN(rawParam, "=", 2)
+ key := strings.TrimSpace(params[0])
+
+ // quality factor
+ if key == "q" {
+ if len(params) != 2 {
+ return nil, fmt.Errorf("invalid quality factor format: %q", rawParam)
+ }
+
+ w, err := strconv.ParseFloat(params[1], 32)
+ if err != nil {
+ return nil, err
+ }
+ parsedValue.Weight = float32(w)
+
+ continue
+ }
+
+ // extension
+ if parsedValue.Extension == nil {
+ parsedValue.Extension = make(map[string]string)
+ }
+
+ var value string
+ if len(params) == 2 {
+ value = strings.TrimSpace(params[1])
+ }
+ parsedValue.Extension[key] = value
+ }
+ }
+
+ parsedValues = append(parsedValues, parsedValue)
+ }
+ return parsedValues, nil
+}
diff --git a/library/go/httputil/headers/accept_test.go b/library/go/httputil/headers/accept_test.go
new file mode 100644
index 0000000000..09d3da086f
--- /dev/null
+++ b/library/go/httputil/headers/accept_test.go
@@ -0,0 +1,309 @@
+package headers_test
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/ydb-platform/ydb/library/go/httputil/headers"
+)
+
+// examples for tests taken from https://tools.ietf.org/html/rfc2616#section-14.3
+func TestParseAcceptEncoding(t *testing.T) {
+ testCases := []struct {
+ name string
+ input string
+ expected headers.AcceptableEncodings
+ expectedErr error
+ }{
+ {
+ "ietf_example_1",
+ "compress, gzip",
+ headers.AcceptableEncodings{
+ {Encoding: headers.ContentEncoding("compress"), Weight: 1.0},
+ {Encoding: headers.ContentEncoding("gzip"), Weight: 1.0},
+ },
+ nil,
+ },
+ {
+ "ietf_example_2",
+ "",
+ nil,
+ nil,
+ },
+ {
+ "ietf_example_3",
+ "*",
+ headers.AcceptableEncodings{
+ {Encoding: headers.ContentEncoding("*"), Weight: 1.0},
+ },
+ nil,
+ },
+ {
+ "ietf_example_4",
+ "compress;q=0.5, gzip;q=1.0",
+ headers.AcceptableEncodings{
+ {Encoding: headers.ContentEncoding("gzip"), Weight: 1.0},
+ {Encoding: headers.ContentEncoding("compress"), Weight: 0.5},
+ },
+ nil,
+ },
+ {
+ "ietf_example_5",
+ "gzip;q=1.0, identity; q=0.5, *;q=0",
+ headers.AcceptableEncodings{
+ {Encoding: headers.ContentEncoding("gzip"), Weight: 1.0},
+ {Encoding: headers.ContentEncoding("identity"), Weight: 0.5},
+ {Encoding: headers.ContentEncoding("*"), Weight: 0},
+ },
+ nil,
+ },
+ {
+ "solomon_headers",
+ "zstd,lz4,gzip,deflate",
+ headers.AcceptableEncodings{
+ {Encoding: headers.ContentEncoding("zstd"), Weight: 1.0},
+ {Encoding: headers.ContentEncoding("lz4"), Weight: 1.0},
+ {Encoding: headers.ContentEncoding("gzip"), Weight: 1.0},
+ {Encoding: headers.ContentEncoding("deflate"), Weight: 1.0},
+ },
+ nil,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ acceptableEncodings, err := headers.ParseAcceptEncoding(tc.input)
+
+ if tc.expectedErr != nil {
+ assert.EqualError(t, err, tc.expectedErr.Error())
+ } else {
+ assert.NoError(t, err)
+ }
+
+ require.Len(t, acceptableEncodings, len(tc.expected))
+
+ opt := cmpopts.IgnoreUnexported(headers.AcceptableEncoding{})
+ assert.True(t, cmp.Equal(tc.expected, acceptableEncodings, opt), cmp.Diff(tc.expected, acceptableEncodings, opt))
+ })
+ }
+}
+
+func TestParseAccept(t *testing.T) {
+ testCases := []struct {
+ name string
+ input string
+ expected headers.AcceptableTypes
+ expectedErr error
+ }{
+ {
+ "empty_header",
+ "",
+ nil,
+ nil,
+ },
+ {
+ "accept_any",
+ "*/*",
+ headers.AcceptableTypes{
+ {Type: headers.ContentTypeAny, Weight: 1.0},
+ },
+ nil,
+ },
+ {
+ "accept_single",
+ "application/json",
+ headers.AcceptableTypes{
+ {Type: headers.TypeApplicationJSON, Weight: 1.0},
+ },
+ nil,
+ },
+ {
+ "accept_multiple",
+ "application/json, application/protobuf",
+ headers.AcceptableTypes{
+ {Type: headers.TypeApplicationJSON, Weight: 1.0},
+ {Type: headers.TypeApplicationProtobuf, Weight: 1.0},
+ },
+ nil,
+ },
+ {
+ "accept_multiple_weighted",
+ "application/json;q=0.8, application/protobuf",
+ headers.AcceptableTypes{
+ {Type: headers.TypeApplicationProtobuf, Weight: 1.0},
+ {Type: headers.TypeApplicationJSON, Weight: 0.8},
+ },
+ nil,
+ },
+ {
+ "accept_multiple_weighted_unsorted",
+ "text/plain;q=0.5, application/protobuf, application/json;q=0.5",
+ headers.AcceptableTypes{
+ {Type: headers.TypeApplicationProtobuf, Weight: 1.0},
+ {Type: headers.TypeTextPlain, Weight: 0.5},
+ {Type: headers.TypeApplicationJSON, Weight: 0.5},
+ },
+ nil,
+ },
+ {
+ "unknown_type",
+ "custom/type, unknown/my_type;q=0.2",
+ headers.AcceptableTypes{
+ {Type: headers.ContentType("custom/type"), Weight: 1.0},
+ {Type: headers.ContentType("unknown/my_type"), Weight: 0.2},
+ },
+ nil,
+ },
+ {
+ "yabro_19.6.0",
+ "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3",
+ headers.AcceptableTypes{
+ {Type: headers.ContentType("text/html"), Weight: 1.0},
+ {Type: headers.ContentType("application/xhtml+xml"), Weight: 1.0},
+ {Type: headers.ContentType("image/webp"), Weight: 1.0},
+ {Type: headers.ContentType("image/apng"), Weight: 1.0},
+ {Type: headers.ContentType("application/signed-exchange"), Weight: 1.0, Extension: map[string]string{"v": "b3"}},
+ {Type: headers.ContentType("application/xml"), Weight: 0.9},
+ {Type: headers.ContentType("*/*"), Weight: 0.8},
+ },
+ nil,
+ },
+ {
+ "chrome_81.0.4044",
+ "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
+ headers.AcceptableTypes{
+ {Type: headers.ContentType("text/html"), Weight: 1.0},
+ {Type: headers.ContentType("application/xhtml+xml"), Weight: 1.0},
+ {Type: headers.ContentType("image/webp"), Weight: 1.0},
+ {Type: headers.ContentType("image/apng"), Weight: 1.0},
+ {Type: headers.ContentType("application/xml"), Weight: 0.9},
+ {Type: headers.ContentType("application/signed-exchange"), Weight: 0.9, Extension: map[string]string{"v": "b3"}},
+ {Type: headers.ContentType("*/*"), Weight: 0.8},
+ },
+ nil,
+ },
+ {
+ "firefox_77.0b3",
+ "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
+ headers.AcceptableTypes{
+ {Type: headers.ContentType("text/html"), Weight: 1.0},
+ {Type: headers.ContentType("application/xhtml+xml"), Weight: 1.0},
+ {Type: headers.ContentType("image/webp"), Weight: 1.0},
+ {Type: headers.ContentType("application/xml"), Weight: 0.9},
+ {Type: headers.ContentType("*/*"), Weight: 0.8},
+ },
+ nil,
+ },
+ {
+ "sort_by_most_specific",
+ "text/*, text/html, */*, text/html;level=1",
+ headers.AcceptableTypes{
+ {Type: headers.ContentType("text/html"), Weight: 1.0, Extension: map[string]string{"level": "1"}},
+ {Type: headers.ContentType("text/html"), Weight: 1.0},
+ {Type: headers.ContentType("text/*"), Weight: 1.0},
+ {Type: headers.ContentType("*/*"), Weight: 1.0},
+ },
+ nil,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ at, err := headers.ParseAccept(tc.input)
+
+ if tc.expectedErr != nil {
+ assert.EqualError(t, err, tc.expectedErr.Error())
+ } else {
+ assert.NoError(t, err)
+ }
+
+ require.Len(t, at, len(tc.expected))
+
+ opt := cmpopts.IgnoreUnexported(headers.AcceptableType{})
+ assert.True(t, cmp.Equal(tc.expected, at, opt), cmp.Diff(tc.expected, at, opt))
+ })
+ }
+}
+
+func TestAcceptableTypesString(t *testing.T) {
+ testCases := []struct {
+ name string
+ types headers.AcceptableTypes
+ expected string
+ }{
+ {
+ "empty",
+ headers.AcceptableTypes{},
+ "",
+ },
+ {
+ "single",
+ headers.AcceptableTypes{
+ {Type: headers.TypeApplicationJSON},
+ },
+ "application/json",
+ },
+ {
+ "single_weighted",
+ headers.AcceptableTypes{
+ {Type: headers.TypeApplicationJSON, Weight: 0.8},
+ },
+ "application/json;q=0.8",
+ },
+ {
+ "multiple",
+ headers.AcceptableTypes{
+ {Type: headers.TypeApplicationJSON},
+ {Type: headers.TypeApplicationProtobuf},
+ },
+ "application/json, application/protobuf",
+ },
+ {
+ "multiple_weighted",
+ headers.AcceptableTypes{
+ {Type: headers.TypeApplicationProtobuf},
+ {Type: headers.TypeApplicationJSON, Weight: 0.8},
+ },
+ "application/protobuf, application/json;q=0.8",
+ },
+ {
+ "multiple_weighted_with_extension",
+ headers.AcceptableTypes{
+ {Type: headers.TypeApplicationProtobuf},
+ {Type: headers.TypeApplicationJSON, Weight: 0.8},
+ {Type: headers.TypeApplicationXML, Weight: 0.5, Extension: map[string]string{"label": "1"}},
+ },
+ "application/protobuf, application/json;q=0.8, application/xml;q=0.5;label=1",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ assert.Equal(t, tc.expected, tc.types.String())
+ })
+ }
+}
+
+func BenchmarkParseAccept(b *testing.B) {
+ benchCases := []string{
+ "",
+ "*/*",
+ "application/json",
+ "application/json, application/protobuf",
+ "application/json;q=0.8, application/protobuf",
+ "text/plain;q=0.5, application/protobuf, application/json;q=0.5",
+ "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3",
+ "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
+ "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
+ "text/*, text/html, */*, text/html;level=1",
+ }
+
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, _ = headers.ParseAccept(benchCases[i%len(benchCases)])
+ }
+}
diff --git a/library/go/httputil/headers/authorization.go b/library/go/httputil/headers/authorization.go
new file mode 100644
index 0000000000..145e04f931
--- /dev/null
+++ b/library/go/httputil/headers/authorization.go
@@ -0,0 +1,31 @@
+package headers
+
+import "strings"
+
+const (
+ AuthorizationKey = "Authorization"
+
+ TokenTypeBearer TokenType = "bearer"
+ TokenTypeMAC TokenType = "mac"
+)
+
+type TokenType string
+
+// String implements stringer interface
+func (tt TokenType) String() string {
+ return string(tt)
+}
+
+func AuthorizationTokenType(token string) TokenType {
+ if len(token) > len(TokenTypeBearer) &&
+ strings.ToLower(token[:len(TokenTypeBearer)]) == TokenTypeBearer.String() {
+ return TokenTypeBearer
+ }
+
+ if len(token) > len(TokenTypeMAC) &&
+ strings.ToLower(token[:len(TokenTypeMAC)]) == TokenTypeMAC.String() {
+ return TokenTypeMAC
+ }
+
+ return TokenType("unknown")
+}
diff --git a/library/go/httputil/headers/authorization_test.go b/library/go/httputil/headers/authorization_test.go
new file mode 100644
index 0000000000..4e93aac1cd
--- /dev/null
+++ b/library/go/httputil/headers/authorization_test.go
@@ -0,0 +1,30 @@
+package headers_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/ydb-platform/ydb/library/go/httputil/headers"
+)
+
+func TestAuthorizationTokenType(t *testing.T) {
+ testCases := []struct {
+ name string
+ token string
+ expected headers.TokenType
+ }{
+ {"bearer", "bearer ololo.trololo", headers.TokenTypeBearer},
+ {"Bearer", "Bearer ololo.trololo", headers.TokenTypeBearer},
+ {"BEARER", "BEARER ololo.trololo", headers.TokenTypeBearer},
+ {"mac", "mac ololo.trololo", headers.TokenTypeMAC},
+ {"Mac", "Mac ololo.trololo", headers.TokenTypeMAC},
+ {"MAC", "MAC ololo.trololo", headers.TokenTypeMAC},
+ {"unknown", "shimba ololo.trololo", headers.TokenType("unknown")},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ assert.Equal(t, tc.expected, headers.AuthorizationTokenType(tc.token))
+ })
+ }
+}
diff --git a/library/go/httputil/headers/content.go b/library/go/httputil/headers/content.go
new file mode 100644
index 0000000000..b92e013cc3
--- /dev/null
+++ b/library/go/httputil/headers/content.go
@@ -0,0 +1,57 @@
+package headers
+
+type ContentType string
+
+// String implements stringer interface
+func (ct ContentType) String() string {
+ return string(ct)
+}
+
+type ContentEncoding string
+
+// String implements stringer interface
+func (ce ContentEncoding) String() string {
+ return string(ce)
+}
+
+const (
+ ContentTypeKey = "Content-Type"
+ ContentLength = "Content-Length"
+ ContentEncodingKey = "Content-Encoding"
+
+ ContentTypeAny ContentType = "*/*"
+
+ TypeApplicationJSON ContentType = "application/json"
+ TypeApplicationXML ContentType = "application/xml"
+ TypeApplicationOctetStream ContentType = "application/octet-stream"
+ TypeApplicationProtobuf ContentType = "application/protobuf"
+ TypeApplicationMsgpack ContentType = "application/msgpack"
+ TypeApplicationXSolomonSpack ContentType = "application/x-solomon-spack"
+
+ EncodingAny ContentEncoding = "*"
+ EncodingZSTD ContentEncoding = "zstd"
+ EncodingLZ4 ContentEncoding = "lz4"
+ EncodingGZIP ContentEncoding = "gzip"
+ EncodingDeflate ContentEncoding = "deflate"
+
+ TypeTextPlain ContentType = "text/plain"
+ TypeTextHTML ContentType = "text/html"
+ TypeTextCSV ContentType = "text/csv"
+ TypeTextCmd ContentType = "text/cmd"
+ TypeTextCSS ContentType = "text/css"
+ TypeTextXML ContentType = "text/xml"
+ TypeTextMarkdown ContentType = "text/markdown"
+
+ TypeImageAny ContentType = "image/*"
+ TypeImageJPEG ContentType = "image/jpeg"
+ TypeImageGIF ContentType = "image/gif"
+ TypeImagePNG ContentType = "image/png"
+ TypeImageSVG ContentType = "image/svg+xml"
+ TypeImageTIFF ContentType = "image/tiff"
+ TypeImageWebP ContentType = "image/webp"
+
+ TypeVideoMPEG ContentType = "video/mpeg"
+ TypeVideoMP4 ContentType = "video/mp4"
+ TypeVideoOgg ContentType = "video/ogg"
+ TypeVideoWebM ContentType = "video/webm"
+)
diff --git a/library/go/httputil/headers/content_test.go b/library/go/httputil/headers/content_test.go
new file mode 100644
index 0000000000..36c7b8ea8f
--- /dev/null
+++ b/library/go/httputil/headers/content_test.go
@@ -0,0 +1,41 @@
+package headers_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/ydb-platform/ydb/library/go/httputil/headers"
+)
+
+func TestContentTypeConsts(t *testing.T) {
+ assert.Equal(t, headers.ContentTypeKey, "Content-Type")
+
+ assert.Equal(t, headers.ContentTypeAny, headers.ContentType("*/*"))
+
+ assert.Equal(t, headers.TypeApplicationJSON, headers.ContentType("application/json"))
+ assert.Equal(t, headers.TypeApplicationXML, headers.ContentType("application/xml"))
+ assert.Equal(t, headers.TypeApplicationOctetStream, headers.ContentType("application/octet-stream"))
+ assert.Equal(t, headers.TypeApplicationProtobuf, headers.ContentType("application/protobuf"))
+ assert.Equal(t, headers.TypeApplicationMsgpack, headers.ContentType("application/msgpack"))
+
+ assert.Equal(t, headers.TypeTextPlain, headers.ContentType("text/plain"))
+ assert.Equal(t, headers.TypeTextHTML, headers.ContentType("text/html"))
+ assert.Equal(t, headers.TypeTextCSV, headers.ContentType("text/csv"))
+ assert.Equal(t, headers.TypeTextCmd, headers.ContentType("text/cmd"))
+ assert.Equal(t, headers.TypeTextCSS, headers.ContentType("text/css"))
+ assert.Equal(t, headers.TypeTextXML, headers.ContentType("text/xml"))
+ assert.Equal(t, headers.TypeTextMarkdown, headers.ContentType("text/markdown"))
+
+ assert.Equal(t, headers.TypeImageAny, headers.ContentType("image/*"))
+ assert.Equal(t, headers.TypeImageJPEG, headers.ContentType("image/jpeg"))
+ assert.Equal(t, headers.TypeImageGIF, headers.ContentType("image/gif"))
+ assert.Equal(t, headers.TypeImagePNG, headers.ContentType("image/png"))
+ assert.Equal(t, headers.TypeImageSVG, headers.ContentType("image/svg+xml"))
+ assert.Equal(t, headers.TypeImageTIFF, headers.ContentType("image/tiff"))
+ assert.Equal(t, headers.TypeImageWebP, headers.ContentType("image/webp"))
+
+ assert.Equal(t, headers.TypeVideoMPEG, headers.ContentType("video/mpeg"))
+ assert.Equal(t, headers.TypeVideoMP4, headers.ContentType("video/mp4"))
+ assert.Equal(t, headers.TypeVideoOgg, headers.ContentType("video/ogg"))
+ assert.Equal(t, headers.TypeVideoWebM, headers.ContentType("video/webm"))
+}
diff --git a/library/go/httputil/headers/cookie.go b/library/go/httputil/headers/cookie.go
new file mode 100644
index 0000000000..bcc685c474
--- /dev/null
+++ b/library/go/httputil/headers/cookie.go
@@ -0,0 +1,5 @@
+package headers
+
+const (
+ CookieKey = "Cookie"
+)
diff --git a/library/go/httputil/headers/gotest/ya.make b/library/go/httputil/headers/gotest/ya.make
new file mode 100644
index 0000000000..467fc88ca4
--- /dev/null
+++ b/library/go/httputil/headers/gotest/ya.make
@@ -0,0 +1,3 @@
+GO_TEST_FOR(library/go/httputil/headers)
+
+END()
diff --git a/library/go/httputil/headers/tvm.go b/library/go/httputil/headers/tvm.go
new file mode 100644
index 0000000000..1737cc69d7
--- /dev/null
+++ b/library/go/httputil/headers/tvm.go
@@ -0,0 +1,8 @@
+package headers
+
+const (
+ // XYaServiceTicket is http header that should be used for service ticket transfer.
+ XYaServiceTicketKey = "X-Ya-Service-Ticket"
+ // XYaUserTicket is http header that should be used for user ticket transfer.
+ XYaUserTicketKey = "X-Ya-User-Ticket"
+)
diff --git a/library/go/httputil/headers/user_agent.go b/library/go/httputil/headers/user_agent.go
new file mode 100644
index 0000000000..366606a01d
--- /dev/null
+++ b/library/go/httputil/headers/user_agent.go
@@ -0,0 +1,5 @@
+package headers
+
+const (
+ UserAgentKey = "User-Agent"
+)
diff --git a/library/go/httputil/headers/warning.go b/library/go/httputil/headers/warning.go
new file mode 100644
index 0000000000..20df80e664
--- /dev/null
+++ b/library/go/httputil/headers/warning.go
@@ -0,0 +1,167 @@
+package headers
+
+import (
+ "errors"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/ydb-platform/ydb/library/go/core/xerrors"
+)
+
+const (
+ WarningKey = "Warning"
+
+ WarningResponseIsStale = 110 // RFC 7234, 5.5.1
+ WarningRevalidationFailed = 111 // RFC 7234, 5.5.2
+ WarningDisconnectedOperation = 112 // RFC 7234, 5.5.3
+ WarningHeuristicExpiration = 113 // RFC 7234, 5.5.4
+ WarningMiscellaneousWarning = 199 // RFC 7234, 5.5.5
+ WarningTransformationApplied = 214 // RFC 7234, 5.5.6
+ WarningMiscellaneousPersistentWarning = 299 // RFC 7234, 5.5.7
+)
+
+var warningStatusText = map[int]string{
+ WarningResponseIsStale: "Response is Stale",
+ WarningRevalidationFailed: "Revalidation Failed",
+ WarningDisconnectedOperation: "Disconnected Operation",
+ WarningHeuristicExpiration: "Heuristic Expiration",
+ WarningMiscellaneousWarning: "Miscellaneous Warning",
+ WarningTransformationApplied: "Transformation Applied",
+ WarningMiscellaneousPersistentWarning: "Miscellaneous Persistent Warning",
+}
+
+// WarningText returns a text for the warning header code. It returns the empty
+// string if the code is unknown.
+func WarningText(warn int) string {
+ return warningStatusText[warn]
+}
+
+// AddWarning adds Warning to http.Header with proper formatting
+// see: https://tools.ietf.org/html/rfc7234#section-5.5
+func AddWarning(h http.Header, warn int, agent, reason string, date time.Time) {
+ values := make([]string, 0, 4)
+ values = append(values, strconv.Itoa(warn))
+
+ if agent != "" {
+ values = append(values, agent)
+ } else {
+ values = append(values, "-")
+ }
+
+ if reason != "" {
+ values = append(values, strconv.Quote(reason))
+ }
+
+ if !date.IsZero() {
+ values = append(values, strconv.Quote(date.Format(time.RFC1123)))
+ }
+
+ h.Add(WarningKey, strings.Join(values, " "))
+}
+
+type WarningHeader struct {
+ Code int
+ Agent string
+ Reason string
+ Date time.Time
+}
+
+// ParseWarnings reads and parses Warning headers from http.Header
+func ParseWarnings(h http.Header) ([]WarningHeader, error) {
+ warnings, ok := h[WarningKey]
+ if !ok {
+ return nil, nil
+ }
+
+ res := make([]WarningHeader, 0, len(warnings))
+ for _, warn := range warnings {
+ wh, err := parseWarning(warn)
+ if err != nil {
+ return nil, xerrors.Errorf("cannot parse '%s' header: %w", warn, err)
+ }
+ res = append(res, wh)
+ }
+
+ return res, nil
+}
+
+func parseWarning(warn string) (WarningHeader, error) {
+ var res WarningHeader
+
+ // parse code
+ {
+ codeSP := strings.Index(warn, " ")
+
+ // fast path - code only warning
+ if codeSP == -1 {
+ code, err := strconv.Atoi(warn)
+ res.Code = code
+ return res, err
+ }
+
+ code, err := strconv.Atoi(warn[:codeSP])
+ if err != nil {
+ return WarningHeader{}, err
+ }
+ res.Code = code
+
+ warn = strings.TrimSpace(warn[codeSP+1:])
+ }
+
+ // parse agent
+ {
+ agentSP := strings.Index(warn, " ")
+
+ // fast path - no data after agent
+ if agentSP == -1 {
+ res.Agent = warn
+ return res, nil
+ }
+
+ res.Agent = warn[:agentSP]
+ warn = strings.TrimSpace(warn[agentSP+1:])
+ }
+
+ // parse reason
+ {
+ if len(warn) == 0 {
+ return res, nil
+ }
+
+ // reason must by quoted, so we search for second quote
+ reasonSP := strings.Index(warn[1:], `"`)
+
+ // fast path - bad reason
+ if reasonSP == -1 {
+ return WarningHeader{}, errors.New("bad reason formatting")
+ }
+
+ res.Reason = warn[1 : reasonSP+1]
+ warn = strings.TrimSpace(warn[reasonSP+2:])
+ }
+
+ // parse date
+ {
+ if len(warn) == 0 {
+ return res, nil
+ }
+
+ // optional date must by quoted, so we search for second quote
+ dateSP := strings.Index(warn[1:], `"`)
+
+ // fast path - bad date
+ if dateSP == -1 {
+ return WarningHeader{}, errors.New("bad date formatting")
+ }
+
+ dt, err := time.Parse(time.RFC1123, warn[1:dateSP+1])
+ if err != nil {
+ return WarningHeader{}, err
+ }
+ res.Date = dt
+ }
+
+ return res, nil
+}
diff --git a/library/go/httputil/headers/warning_test.go b/library/go/httputil/headers/warning_test.go
new file mode 100644
index 0000000000..9decb2f52f
--- /dev/null
+++ b/library/go/httputil/headers/warning_test.go
@@ -0,0 +1,245 @@
+package headers
+
+import (
+ "net/http"
+ "strconv"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestWarningText(t *testing.T) {
+ testCases := []struct {
+ code int
+ expect string
+ }{
+ {WarningResponseIsStale, "Response is Stale"},
+ {WarningRevalidationFailed, "Revalidation Failed"},
+ {WarningDisconnectedOperation, "Disconnected Operation"},
+ {WarningHeuristicExpiration, "Heuristic Expiration"},
+ {WarningMiscellaneousWarning, "Miscellaneous Warning"},
+ {WarningTransformationApplied, "Transformation Applied"},
+ {WarningMiscellaneousPersistentWarning, "Miscellaneous Persistent Warning"},
+ {42, ""},
+ {1489, ""},
+ }
+
+ for _, tc := range testCases {
+ t.Run(strconv.Itoa(tc.code), func(t *testing.T) {
+ assert.Equal(t, tc.expect, WarningText(tc.code))
+ })
+ }
+}
+
+func TestAddWarning(t *testing.T) {
+ type args struct {
+ warn int
+ agent string
+ reason string
+ date time.Time
+ }
+
+ testCases := []struct {
+ name string
+ args args
+ expect http.Header
+ }{
+ {
+ name: "code_only",
+ args: args{warn: WarningResponseIsStale, agent: "", reason: "", date: time.Time{}},
+ expect: http.Header{
+ WarningKey: []string{
+ "110 -",
+ },
+ },
+ },
+ {
+ name: "code_agent",
+ args: args{warn: WarningResponseIsStale, agent: "ololo/trololo", reason: "", date: time.Time{}},
+ expect: http.Header{
+ WarningKey: []string{
+ "110 ololo/trololo",
+ },
+ },
+ },
+ {
+ name: "code_agent_reason",
+ args: args{warn: WarningResponseIsStale, agent: "ololo/trololo", reason: "shimba-boomba", date: time.Time{}},
+ expect: http.Header{
+ WarningKey: []string{
+ `110 ololo/trololo "shimba-boomba"`,
+ },
+ },
+ },
+ {
+ name: "code_agent_reason_date",
+ args: args{
+ warn: WarningResponseIsStale,
+ agent: "ololo/trololo",
+ reason: "shimba-boomba",
+ date: time.Date(2019, time.January, 14, 10, 50, 43, 0, time.UTC),
+ },
+ expect: http.Header{
+ WarningKey: []string{
+ `110 ololo/trololo "shimba-boomba" "Mon, 14 Jan 2019 10:50:43 UTC"`,
+ },
+ },
+ },
+ {
+ name: "code_reason_date",
+ args: args{
+ warn: WarningResponseIsStale,
+ agent: "",
+ reason: "shimba-boomba",
+ date: time.Date(2019, time.January, 14, 10, 50, 43, 0, time.UTC),
+ },
+ expect: http.Header{
+ WarningKey: []string{
+ `110 - "shimba-boomba" "Mon, 14 Jan 2019 10:50:43 UTC"`,
+ },
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ h := http.Header{}
+ AddWarning(h, tc.args.warn, tc.args.agent, tc.args.reason, tc.args.date)
+ assert.Equal(t, tc.expect, h)
+ })
+ }
+}
+
+func TestParseWarnings(t *testing.T) {
+ testCases := []struct {
+ name string
+ h http.Header
+ expect []WarningHeader
+ expectErr bool
+ }{
+ {
+ name: "no_warnings",
+ h: http.Header{},
+ expect: nil,
+ expectErr: false,
+ },
+ {
+ name: "single_code_only",
+ h: http.Header{
+ WarningKey: []string{
+ "110",
+ },
+ },
+ expect: []WarningHeader{
+ {
+ Code: 110,
+ Agent: "",
+ Reason: "",
+ Date: time.Time{},
+ },
+ },
+ },
+ {
+ name: "single_code_and_empty_agent",
+ h: http.Header{
+ WarningKey: []string{
+ "110 -",
+ },
+ },
+ expect: []WarningHeader{
+ {
+ Code: 110,
+ Agent: "-",
+ Reason: "",
+ Date: time.Time{},
+ },
+ },
+ },
+ {
+ name: "single_code_and_agent",
+ h: http.Header{
+ WarningKey: []string{
+ "110 shimba/boomba",
+ },
+ },
+ expect: []WarningHeader{
+ {
+ Code: 110,
+ Agent: "shimba/boomba",
+ Reason: "",
+ Date: time.Time{},
+ },
+ },
+ },
+ {
+ name: "single_code_agent_and_reason",
+ h: http.Header{
+ WarningKey: []string{
+ `110 shimba/boomba "looken tooken"`,
+ },
+ },
+ expect: []WarningHeader{
+ {
+ Code: 110,
+ Agent: "shimba/boomba",
+ Reason: "looken tooken",
+ Date: time.Time{},
+ },
+ },
+ },
+ {
+ name: "single_full",
+ h: http.Header{
+ WarningKey: []string{
+ `110 shimba/boomba "looken tooken" "Mon, 14 Jan 2019 10:50:43 UTC"`,
+ },
+ },
+ expect: []WarningHeader{
+ {
+ Code: 110,
+ Agent: "shimba/boomba",
+ Reason: "looken tooken",
+ Date: time.Date(2019, time.January, 14, 10, 50, 43, 0, time.UTC),
+ },
+ },
+ },
+ {
+ name: "multiple_full",
+ h: http.Header{
+ WarningKey: []string{
+ `110 shimba/boomba "looken tooken" "Mon, 14 Jan 2019 10:50:43 UTC"`,
+ `112 chiken "cooken" "Mon, 15 Jan 2019 10:51:43 UTC"`,
+ },
+ },
+ expect: []WarningHeader{
+ {
+ Code: 110,
+ Agent: "shimba/boomba",
+ Reason: "looken tooken",
+ Date: time.Date(2019, time.January, 14, 10, 50, 43, 0, time.UTC),
+ },
+ {
+ Code: 112,
+ Agent: "chiken",
+ Reason: "cooken",
+ Date: time.Date(2019, time.January, 15, 10, 51, 43, 0, time.UTC),
+ },
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ got, err := ParseWarnings(tc.h)
+
+ if tc.expectErr {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ }
+
+ assert.Equal(t, tc.expect, got)
+ })
+ }
+}
diff --git a/library/go/httputil/headers/ya.make b/library/go/httputil/headers/ya.make
new file mode 100644
index 0000000000..d249197dc3
--- /dev/null
+++ b/library/go/httputil/headers/ya.make
@@ -0,0 +1,23 @@
+GO_LIBRARY()
+
+SRCS(
+ accept.go
+ authorization.go
+ content.go
+ cookie.go
+ tvm.go
+ user_agent.go
+ warning.go
+)
+
+GO_TEST_SRCS(warning_test.go)
+
+GO_XTEST_SRCS(
+ accept_test.go
+ authorization_test.go
+ content_test.go
+)
+
+END()
+
+RECURSE(gotest)