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 }