-
Notifications
You must be signed in to change notification settings - Fork 0
/
headerset.go
216 lines (175 loc) · 5.47 KB
/
headerset.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
package httransform
import (
"bufio"
"bytes"
"fmt"
"net/textproto"
"strings"
"golang.org/x/xerrors"
)
// Header is a structure to present both key and value of the header.
type Header struct {
// HTTP headers are case-insensitive so we need to have some case
// insensitive ID to be able to refer them. ID usually stores
// lowercased key.
ID string
// Key stores the name of the header (with correct case).
Key []byte
// Value stores value of the header (with correct case).
Value []byte
}
// String method returns some string representation of this
// datastructure so *Header implements fmt.Stringer interface.
func (h *Header) String() string {
return fmt.Sprintf("%s: %s", string(h.Key), string(h.Value))
}
// HeaderSet presents collection of headers keeping their order.
//
// If you delete a header, then other headers will keep their positions.
// If you update the value again, then original position is going to be
// restored.
//
// hs := HeaderSet{}
// hs.SetString("Connection", "close")
// hs.GetString("connection")
//
// This will return you "close". Please pay attention to the fact that
// we've used lowercased version. It works. And it works even for updates.
//
// hs := HeaderSet{}
// hs.SetString("Connection", "close")
// hs.SetString("connection", "keep-alive")
// hs.GetString("Connection")
//
// This will return you "keep-alive". What will you see on the wire?
// Header name will be "Connection" because we've seen such case in the
// first place.
type HeaderSet struct {
index map[string]int
values []*Header
removedHeaders map[string]struct{}
}
// SetString sets key and value of the header. If we've already seen
// such header (ignoring its case), we'll update it value, otherwise
// header will be appended to the list. If header was deleted, it would
// be restored keeping the original position in header set.
func (hs *HeaderSet) SetString(key, value string) {
hs.SetBytes([]byte(key), []byte(value))
}
// SetBytes is the version of SetString which works with bytes.
func (hs *HeaderSet) SetBytes(key []byte, value []byte) {
if hs.index == nil {
hs.index = map[string]int{}
}
if hs.values == nil {
hs.values = []*Header{}
}
if hs.removedHeaders == nil {
hs.removedHeaders = map[string]struct{}{}
}
lowerKey := string(bytes.ToLower(key))
if position, ok := hs.index[lowerKey]; ok {
hs.values[position].Value = value
} else {
newHeader := getHeader()
newHeader.ID = lowerKey
newHeader.Key = append(newHeader.Key, key...)
newHeader.Value = append(newHeader.Value, value...)
hs.values = append(hs.values, newHeader)
hs.index[lowerKey] = len(hs.values) - 1
}
delete(hs.removedHeaders, lowerKey)
}
// DeleteString removes (marks as deleted) a header with the given key
// from header set.
func (hs *HeaderSet) DeleteString(key string) {
hs.removedHeaders[strings.ToLower(key)] = struct{}{}
}
// DeleteBytes is a version of DeleteString which accepts bytes.
func (hs *HeaderSet) DeleteBytes(key []byte) {
hs.removedHeaders[string(bytes.ToLower(key))] = struct{}{}
}
// GetString returns a value of the header with the given name (ignoring
// the case). If no such header exist, then second parameter is false.
//
// Semantically, this method is similar to accessing map's key.
func (hs *HeaderSet) GetString(key string) (string, bool) {
lowerKey := strings.ToLower(key)
_, ok := hs.removedHeaders[lowerKey]
if ok {
return "", false
}
if pos, ok := hs.index[lowerKey]; ok {
return string(hs.values[pos].Value), true
}
return "", false
}
// GetBytes is the version of GetString which works with bytes.
func (hs *HeaderSet) GetBytes(key []byte) ([]byte, bool) {
key = bytes.ToLower(key)
if _, ok := hs.removedHeaders[string(key)]; ok {
return nil, false
}
if pos, ok := hs.index[string(key)]; ok {
return hs.values[pos].Value, true
}
return nil, false
}
// Items returns a list of headers from header set in the correct order.
func (hs *HeaderSet) Items() []*Header {
headers := make([]*Header, 0, len(hs.values))
for _, v := range hs.values {
if _, ok := hs.removedHeaders[v.ID]; !ok {
headers = append(headers, v)
}
}
return headers
}
// String returns a string representation of the header set. So,
// *HeaderSet implements fmt.Stringer interface.
func (hs *HeaderSet) String() string {
builder := strings.Builder{}
for _, v := range hs.Items() {
builder.WriteString(v.String())
builder.WriteString("\n")
}
return builder.String()
}
// Clear drops internal state of the headerset.
func (hs *HeaderSet) Clear() {
for k := range hs.index {
delete(hs.index, k)
}
for k := range hs.removedHeaders {
delete(hs.removedHeaders, k)
}
for _, v := range hs.values {
releaseHeader(v)
}
hs.values = hs.values[:0]
}
// ParseHeaders parses raw HTTP headers into the headerset.
func ParseHeaders(hset *HeaderSet, rawHeaders []byte) error {
reader := textproto.NewReader(bufio.NewReader(bytes.NewReader(rawHeaders)))
// skip first line of HTTP
reader.ReadContinuedLineBytes() // nolint: errcheck
for {
line, err := reader.ReadContinuedLineBytes()
if len(line) == 0 || err != nil {
return err
}
colPosition := bytes.IndexByte(line, ':')
if colPosition < 0 {
return xerrors.Errorf("Malformed header %s", string(line))
}
endKey := colPosition
for endKey > 0 && line[endKey-1] == ' ' {
endKey--
}
colPosition++
for colPosition < len(line) && (line[colPosition] == ' ' || line[colPosition] == '\t') {
colPosition++
}
hset.SetBytes(line[:endKey], line[colPosition:])
}
}