diff options
author | hcpp <hcpp@ydb.tech> | 2023-11-09 20:47:31 +0300 |
---|---|---|
committer | hcpp <hcpp@ydb.tech> | 2023-11-09 21:11:21 +0300 |
commit | b7716e9978a4d1c2e548b9d53836a7d6894a8a38 (patch) | |
tree | 4ecf0ea759c7d44ed9749dc5d7beeaffc6376aac /library/go/httputil/headers | |
parent | ea9cef2dc79047c295a260f96895628b0feed43f (diff) | |
download | ydb-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.go | 259 | ||||
-rw-r--r-- | library/go/httputil/headers/accept_test.go | 309 | ||||
-rw-r--r-- | library/go/httputil/headers/authorization.go | 31 | ||||
-rw-r--r-- | library/go/httputil/headers/authorization_test.go | 30 | ||||
-rw-r--r-- | library/go/httputil/headers/content.go | 57 | ||||
-rw-r--r-- | library/go/httputil/headers/content_test.go | 41 | ||||
-rw-r--r-- | library/go/httputil/headers/cookie.go | 5 | ||||
-rw-r--r-- | library/go/httputil/headers/gotest/ya.make | 3 | ||||
-rw-r--r-- | library/go/httputil/headers/tvm.go | 8 | ||||
-rw-r--r-- | library/go/httputil/headers/user_agent.go | 5 | ||||
-rw-r--r-- | library/go/httputil/headers/warning.go | 167 | ||||
-rw-r--r-- | library/go/httputil/headers/warning_test.go | 245 | ||||
-rw-r--r-- | library/go/httputil/headers/ya.make | 23 |
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) |