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 <vladimir@thecybershadow.net>
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 
23 /// AA-like superset structure with the purpose of maintaining
24 /// compatibility with the old HTTP string[string] headers field
25 struct Headers
26 {
27 	struct Header { string name, value; }
28 
29 	private Header[][CIAsciiString] headers;
30 
31 	this(string[string] aa)
32 	{
33 		foreach (k, v; aa)
34 			this.add(k, v);
35 	}
36 
37 	this(string[][string] aa)
38 	{
39 		foreach (k, vals; aa)
40 			foreach (v; vals)
41 				this.add(k, v);
42 	}
43 
44 	/// If multiple headers with this name are present,
45 	/// only the first one is returned.
46 	ref inout(string) opIndex(string name) inout
47 	{
48 		return headers[CIAsciiString(name)][0].value;
49 	}
50 
51 	string opIndexAssign(string value, string name)
52 	{
53 		headers[CIAsciiString(name)] = [Header(name, value)];
54 		return value;
55 	}
56 
57 	inout(string)* opBinaryRight(string op)(string name) inout @nogc
58 	if (op == "in")
59 	{
60 		auto pvalues = CIAsciiString(name) in headers;
61 		if (pvalues && (*pvalues).length)
62 			return &(*pvalues)[0].value;
63 		return null;
64 	}
65 
66 	void remove(string name)
67 	{
68 		headers.remove(CIAsciiString(name));
69 	}
70 
71 	// D forces these to be "ref"
72 	int opApply(int delegate(ref string name, ref string value) dg)
73 	{
74 		int ret;
75 		outer:
76 		foreach (key, values; headers)
77 			foreach (header; values)
78 			{
79 				ret = dg(header.name, header.value);
80 				if (ret)
81 					break outer;
82 			}
83 		return ret;
84 	}
85 
86 	// Copy-paste because of https://issues.dlang.org/show_bug.cgi?id=7543
87 	int opApply(int delegate(ref const(string) name, ref const(string) value) dg) const
88 	{
89 		int ret;
90 		outer:
91 		foreach (name, values; headers)
92 			foreach (header; values)
93 			{
94 				ret = dg(header.name, header.value);
95 				if (ret)
96 					break outer;
97 			}
98 		return ret;
99 	}
100 
101 	void add(string name, string value)
102 	{
103 		auto key = CIAsciiString(name);
104 		if (key !in headers)
105 			headers[key] = [Header(name, value)];
106 		else
107 			headers[key] ~= Header(name, value);
108 	}
109 
110 	string get(string key, string def) const
111 	{
112 		return getLazy(key, def);
113 	}
114 
115 	string getLazy(string key, lazy string def) const
116 	{
117 		auto pvalue = key in this;
118 		return pvalue ? *pvalue : def;
119 	}
120 
121 	inout(string)[] getAll(string key) inout
122 	{
123 		inout(string)[] result;
124 		foreach (header; headers.get(CIAsciiString(key), null))
125 			result ~= header.value;
126 		return result;
127 	}
128 
129 	/// Warning: discards repeating headers
130 	string[string] opCast(T)() const
131 		if (is(T == string[string]))
132 	{
133 		string[string] result;
134 		foreach (key, value; this)
135 			result[key] = value;
136 		return result;
137 	}
138 
139 	string[][string] opCast(T)() inout
140 		if (is(T == string[][string]))
141 	{
142 		string[][string] result;
143 		foreach (k, v; this)
144 			result[k] ~= v;
145 		return result;
146 	}
147 
148 	@property Headers dup()
149 	{
150 		Headers c;
151 		foreach (k, v; this)
152 			c.add(k, v);
153 		return c;
154 	}
155 
156 	@property size_t length() const
157 	{
158 		return headers.length;
159 	}
160 }
161 
162 unittest
163 {
164 	Headers headers;
165 	headers["test"] = "test";
166 
167 	void test(T)(T headers)
168 	{
169 		assert("TEST" in headers);
170 		assert(headers["TEST"] == "test");
171 
172 		foreach (k, v; headers)
173 			assert(k == "test" && v == "test");
174 
175 		auto aas = cast(string[string])headers;
176 		assert(aas == ["test" : "test"]);
177 
178 		auto aaa = cast(string[][string])headers;
179 		assert(aaa == ["test" : ["test"]]);
180 	}
181 
182 	test(headers);
183 
184 	const constHeaders = headers;
185 	test(constHeaders);
186 }
187 
188 /// Normalize capitalization
189 string normalizeHeaderName(string header) pure
190 {
191 	alias std.ascii.toUpper toUpper;
192 	alias std.ascii.toLower toLower;
193 
194 	auto s = header.dup;
195 	auto segments = s.split("-");
196 	foreach (segment; segments)
197 	{
198 		foreach (ref c; segment)
199 			c = cast(char)toUpper(c);
200 		switch (segment)
201 		{
202 			case "ID":
203 			case "IP":
204 			case "NNTP":
205 			case "TE":
206 			case "WWW":
207 				continue;
208 			case "ETAG":
209 				segment[] = "ETag";
210 				break;
211 			default:
212 				foreach (ref c; segment[1..$])
213 					c = cast(char)toLower(c);
214 				break;
215 		}
216 	}
217 	return s;
218 }
219 
220 unittest
221 {
222 	assert(normalizeHeaderName("X-ORIGINATING-IP") == "X-Originating-IP");
223 }
224 
225 struct TokenHeader
226 {
227 	string value;
228 	string[string] properties;
229 }
230 
231 TokenHeader decodeTokenHeader(string s)
232 {
233 	string take(char until)
234 	{
235 		string result;
236 		auto p = s.indexOf(until);
237 		if (p < 0)
238 			result = s,
239 			s = null;
240 		else
241 			result = s[0..p],
242 			s = asciiStrip(s[p+1..$]);
243 		return result;
244 	}
245 
246 	TokenHeader result;
247 	result.value = take(';');
248 
249 	while (s.length)
250 	{
251 		string name = take('=').toLower();
252 		string value;
253 		if (s.length && s[0] == '"')
254 		{
255 			s = s[1..$];
256 			value = take('"');
257 			take(';');
258 		}
259 		else
260 			value = take(';');
261 		result.properties[name] = value;
262 	}
263 
264 	return result;
265 }