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 pure
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) pure
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) pure
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) pure
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 pure nothrow @nogc
123 	{
124 		auto pvalue = key in this;
125 		return pvalue ? *pvalue : def;
126 	}
127 
128 	/// Lazy version of `get`.
129 	string getLazy(string key, lazy string def) const pure /*nothrow*/ /*@nogc*/
130 	{
131 		auto pvalue = key in this;
132 		return pvalue ? *pvalue : def;
133 	}
134 
135 	/// Retrieve all values of the given header.
136 	inout(string)[] getAll(string key) inout pure
137 	{
138 		inout(string)[] result;
139 		foreach (header; headers.get(CIAsciiString(key), null))
140 			result ~= header.value;
141 		return result;
142 	}
143 
144 	/// If the given header is not yet present, add it with the given value.
145 	ref string require(string key, lazy string value) pure
146 	{
147 		return headers.require(CIAsciiString(key), [Header(key, value)])[0].value;
148 	}
149 
150 	/// True-ish if any headers have been set.
151 	bool opCast(T)() const pure nothrow @nogc
152 		if (is(T == bool))
153 	{
154 		return !!headers;
155 	}
156 
157 	/// Converts to a D associative array,
158 	/// with at most one value per header.
159 	/// Warning: discards repeating headers!
160 	string[string] opCast(T)() const
161 		if (is(T == string[string]))
162 	{
163 		string[string] result;
164 		foreach (key, value; this)
165 			result[key] = value;
166 		return result;
167 	}
168 
169 	/// Converts to a D associative array.
170 	string[][string] opCast(T)() inout
171 		if (is(T == string[][string]))
172 	{
173 		string[][string] result;
174 		foreach (k, v; this)
175 			result[k] ~= v;
176 		return result;
177 	}
178 
179 	/// Creates and returns a copy of this `Headers` instance.
180 	@property Headers dup() const
181 	{
182 		Headers c;
183 		foreach (k, v; this)
184 			c.add(k, v);
185 		return c;
186 	}
187 
188 	/// Returns the number of headers and values (including duplicate headers).
189 	@property size_t length() const pure nothrow @nogc
190 	{
191 		return headers.length;
192 	}
193 }
194 
195 unittest
196 {
197 	Headers headers;
198 	headers["test"] = "test";
199 
200 	void test(T)(T headers)
201 	{
202 		assert("TEST" in headers);
203 		assert(headers["TEST"] == "test");
204 
205 		foreach (k, v; headers)
206 			assert(k == "test" && v == "test");
207 
208 		auto aas = cast(string[string])headers;
209 		assert(aas == ["test" : "test"]);
210 
211 		auto aaa = cast(string[][string])headers;
212 		assert(aaa == ["test" : ["test"]]);
213 	}
214 
215 	test(headers);
216 
217 	const constHeaders = headers;
218 	test(constHeaders);
219 }
220 
221 /// Attempts to normalize the capitalization of a header name to a
222 /// likely form.
223 /// This involves capitalizing all words, plus rules to uppercase
224 /// common acronyms used in header names, such as "IP" and "ETag".
225 string normalizeHeaderName(string header) pure
226 {
227 	alias std.ascii.toUpper toUpper;
228 	alias std.ascii.toLower toLower;
229 
230 	auto s = header.dup;
231 	auto segments = s.split("-");
232 	foreach (segment; segments)
233 	{
234 		foreach (ref c; segment)
235 			c = cast(char)toUpper(c);
236 		switch (segment)
237 		{
238 			case "ID":
239 			case "IP":
240 			case "NNTP":
241 			case "TE":
242 			case "WWW":
243 				continue;
244 			case "ETAG":
245 				segment[] = "ETag";
246 				break;
247 			default:
248 				foreach (ref c; segment[1..$])
249 					c = cast(char)toLower(c);
250 				break;
251 		}
252 	}
253 	return s;
254 }
255 
256 unittest
257 {
258 	assert(normalizeHeaderName("X-ORIGINATING-IP") == "X-Originating-IP");
259 }
260 
261 /// Decodes headers of the form
262 /// `"main-value; param1=value1; param2=value2"`
263 struct TokenHeader
264 {
265 	string value; /// The main header value.
266 	string[string] properties; /// Following properties, as a D associative array.
267 }
268 
269 /// ditto
270 TokenHeader decodeTokenHeader(string s)
271 {
272 	string take(char until)
273 	{
274 		string result;
275 		auto p = s.indexOf(until);
276 		if (p < 0)
277 			result = s,
278 			s = null;
279 		else
280 			result = s[0..p],
281 			s = asciiStrip(s[p+1..$]);
282 		return result;
283 	}
284 
285 	TokenHeader result;
286 	result.value = take(';');
287 
288 	while (s.length)
289 	{
290 		string name = take('=').toLower();
291 		string value;
292 		if (s.length && s[0] == '"')
293 		{
294 			s = s[1..$];
295 			value = take('"');
296 			take(';');
297 		}
298 		else
299 			value = take(';');
300 		result.properties[name] = value;
301 	}
302 
303 	return result;
304 }