1 /**
2 * HTTP / mail / etc. headers
3 *
4 * License:
5 * This Source Code Form is subject to the terms of
6 * the Mozilla Public License, v. 2.0. If a copy of
7 * the MPL was not distributed with this file, You
8 * can obtain one at http://mozilla.org/MPL/2.0/.
9 *
10 * Authors:
11 * Vladimir Panteleev <ae@cy.md>
12 */
13
14 module ae.net.ietf.headers;
15
16 import std.algorithm;
17 import std.string;
18 import std.ascii;
19 import std.exception;
20
21 import ae.utils.text;
22 import ae.utils.aa;
23
24 /// AA-like structure for storing headers, allowing for case
25 /// insensitivity and multiple values per key.
26 struct Headers
27 {
28 private struct Header { string name, value; }
29
30 private Header[][CIAsciiString] headers;
31
32 /// Initialize from a D associative array.
33 this(string[string] aa)
34 {
35 foreach (k, v; aa)
36 this.add(k, v);
37 }
38
39 /// ditto
40 this(string[][string] aa)
41 {
42 foreach (k, vals; aa)
43 foreach (v; vals)
44 this.add(k, v);
45 }
46
47 /// If multiple headers with this name are present,
48 /// only the first one is returned.
49 ref inout(string) opIndex(string name) inout
50 {
51 return headers[CIAsciiString(name)][0].value;
52 }
53
54 /// Sets the given header to the given value, overwriting any previous values.
55 string opIndexAssign(string value, string name)
56 {
57 headers[CIAsciiString(name)] = [Header(name, value)];
58 return value;
59 }
60
61 /// If the given header exists, return a pointer to the first value.
62 /// Otherwise, return null.
63 inout(string)* opBinaryRight(string op)(string name) inout @nogc
64 if (op == "in")
65 {
66 auto pvalues = CIAsciiString(name) in headers;
67 if (pvalues && (*pvalues).length)
68 return &(*pvalues)[0].value;
69 return null;
70 }
71
72 /// Remove the given header.
73 /// Does nothing if the header was not present.
74 void remove(string name)
75 {
76 headers.remove(CIAsciiString(name));
77 }
78
79 /// Iterate over all headers, including multiple instances of the seame header.
80 int opApply(int delegate(ref string name, ref string value) dg)
81 {
82 int ret;
83 outer:
84 foreach (key, values; headers)
85 foreach (header; values)
86 {
87 ret = dg(header.name, header.value);
88 if (ret)
89 break outer;
90 }
91 return ret;
92 }
93
94 // Copy-paste because of https://issues.dlang.org/show_bug.cgi?id=7543
95 /// ditto
96 int opApply(int delegate(ref const(string) name, ref const(string) value) dg) const
97 {
98 int ret;
99 outer:
100 foreach (name, values; headers)
101 foreach (header; values)
102 {
103 ret = dg(header.name, header.value);
104 if (ret)
105 break outer;
106 }
107 return ret;
108 }
109
110 /// Add a value for the given header.
111 /// Adds a new instance of the header if one already existed.
112 void add(string name, string value)
113 {
114 auto key = CIAsciiString(name);
115 if (key !in headers)
116 headers[key] = [Header(name, value)];
117 else
118 headers[key] ~= Header(name, value);
119 }
120
121 /// Retrieve the value of the given header if it is present, otherwise return `def`.
122 string get(string key, string def) const
123 {
124 return getLazy(key, def);
125 }
126
127 /// Lazy version of `get`.
128 string getLazy(string key, lazy string def) const
129 {
130 auto pvalue = key in this;
131 return pvalue ? *pvalue : def;
132 }
133
134 /// Retrieve all values of the given header.
135 inout(string)[] getAll(string key) inout
136 {
137 inout(string)[] result;
138 foreach (header; headers.get(CIAsciiString(key), null))
139 result ~= header.value;
140 return result;
141 }
142
143 /// If the given header is not yet present, add it with the given value.
144 ref string require(string key, lazy string value)
145 {
146 return headers.require(CIAsciiString(key), [Header(key, value)])[0].value;
147 }
148
149 /// True-ish if any headers have been set.
150 bool opCast(T)() const
151 if (is(T == bool))
152 {
153 return !!headers;
154 }
155
156 /// Converts to a D associative array,
157 /// with at most one value per header.
158 /// Warning: discards repeating headers!
159 string[string] opCast(T)() const
160 if (is(T == string[string]))
161 {
162 string[string] result;
163 foreach (key, value; this)
164 result[key] = value;
165 return result;
166 }
167
168 /// Converts to a D associative array.
169 string[][string] opCast(T)() inout
170 if (is(T == string[][string]))
171 {
172 string[][string] result;
173 foreach (k, v; this)
174 result[k] ~= v;
175 return result;
176 }
177
178 /// Creates and returns a copy of this `Headers` instance.
179 @property Headers dup() const
180 {
181 Headers c;
182 foreach (k, v; this)
183 c.add(k, v);
184 return c;
185 }
186
187 /// Returns the number of headers and values (including duplicate headers).
188 @property size_t length() const
189 {
190 return headers.length;
191 }
192 }
193
194 unittest
195 {
196 Headers headers;
197 headers["test"] = "test";
198
199 void test(T)(T headers)
200 {
201 assert("TEST" in headers);
202 assert(headers["TEST"] == "test");
203
204 foreach (k, v; headers)
205 assert(k == "test" && v == "test");
206
207 auto aas = cast(string[string])headers;
208 assert(aas == ["test" : "test"]);
209
210 auto aaa = cast(string[][string])headers;
211 assert(aaa == ["test" : ["test"]]);
212 }
213
214 test(headers);
215
216 const constHeaders = headers;
217 test(constHeaders);
218 }
219
220 /// Attempts to normalize the capitalization of a header name to a
221 /// likely form.
222 /// This involves capitalizing all words, plus rules to uppercase
223 /// common acronyms used in header names, such as "IP" and "ETag".
224 string normalizeHeaderName(string header) pure
225 {
226 alias std.ascii.toUpper toUpper;
227 alias std.ascii.toLower toLower;
228
229 auto s = header.dup;
230 auto segments = s.split("-");
231 foreach (segment; segments)
232 {
233 foreach (ref c; segment)
234 c = cast(char)toUpper(c);
235 switch (segment)
236 {
237 case "ID":
238 case "IP":
239 case "NNTP":
240 case "TE":
241 case "WWW":
242 continue;
243 case "ETAG":
244 segment[] = "ETag";
245 break;
246 default:
247 foreach (ref c; segment[1..$])
248 c = cast(char)toLower(c);
249 break;
250 }
251 }
252 return s;
253 }
254
255 unittest
256 {
257 assert(normalizeHeaderName("X-ORIGINATING-IP") == "X-Originating-IP");
258 }
259
260 /// Decodes headers of the form
261 /// `"main-value; param1=value1; param2=value2"`
262 struct TokenHeader
263 {
264 string value; /// The main header value.
265 string[string] properties; /// Following properties, as a D associative array.
266 }
267
268 /// ditto
269 TokenHeader decodeTokenHeader(string s)
270 {
271 string take(char until)
272 {
273 string result;
274 auto p = s.indexOf(until);
275 if (p < 0)
276 result = s,
277 s = null;
278 else
279 result = s[0..p],
280 s = asciiStrip(s[p+1..$]);
281 return result;
282 }
283
284 TokenHeader result;
285 result.value = take(';');
286
287 while (s.length)
288 {
289 string name = take('=').toLower();
290 string value;
291 if (s.length && s[0] == '"')
292 {
293 s = s[1..$];
294 value = take('"');
295 take(';');
296 }
297 else
298 value = take(';');
299 result.properties[name] = value;
300 }
301
302 return result;
303 }