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.string;
17 import std.ascii;
18 import std.exception;
19 
20 /// AA-like superset structure with the purpose of maintaining
21 /// compatibility with the old HTTP string[string] headers field
22 struct Headers
23 {
24 	// All keys are internally upper-case.
25 	private string[][string] headers;
26 
27 	/// If multiple headers with this name are present,
28 	/// only the first one is returned.
29 	ref inout(string) opIndex(string name) inout
30 	{
31 		return headers[toUpper(name)][0];
32 	}
33 
34 	string opIndexAssign(string value, string name)
35 	{
36 		headers[toUpper(name)] = [value];
37 		return value;
38 	}
39 
40 	inout(string)* opIn_r(string name) inout
41 	{
42 		auto pvalues = toUpper(name) in headers;
43 		if (pvalues && (*pvalues).length)
44 			return (*pvalues).ptr;
45 		return null;
46 	}
47 
48 	void remove(string name)
49 	{
50 		headers.remove(toUpper(name));
51 	}
52 
53 	// D forces these to be "ref"
54 	int opApply(int delegate(ref string name, ref string value) dg)
55 	{
56 		int ret;
57 		outer:
58 		foreach (name, values; headers)
59 			foreach (value; values)
60 			{
61 				auto normName = normalizeHeaderName(name);
62 				ret = dg(normName, value);
63 				if (ret)
64 					break outer;
65 			}
66 		return ret;
67 	}
68 
69 	// Copy-paste because of https://issues.dlang.org/show_bug.cgi?id=7543
70 	int opApply(int delegate(ref string name, ref const(string) value) dg) const
71 	{
72 		int ret;
73 		outer:
74 		foreach (name, values; headers)
75 			foreach (value; values)
76 			{
77 				auto normName = normalizeHeaderName(name);
78 				ret = dg(normName, value);
79 				if (ret)
80 					break outer;
81 			}
82 		return ret;
83 	}
84 
85 	void add(string name, string value)
86 	{
87 		name = toUpper(name);
88 		if (name !in headers)
89 			headers[name] = [value];
90 		else
91 			headers[name] ~= value;
92 	}
93 
94 	string get(string key, string def) const
95 	{
96 		return getLazy(key, def);
97 	}
98 
99 	string getLazy(string key, lazy string def) const
100 	{
101 		auto pvalue = key in this;
102 		return pvalue ? *pvalue : def;
103 	}
104 
105 	inout(string)[] getAll(string key) inout
106 	{
107 		return headers[toUpper(key)];
108 	}
109 
110 	/// Warning: discards repeating headers
111 	string[string] opCast(T)() const
112 		if (is(T == string[string]))
113 	{
114 		string[string] result;
115 		foreach (key, value; this)
116 			result[key] = value;
117 		return result;
118 	}
119 
120 	string[][string] opCast(T)() inout
121 		if (is(T == string[][string]))
122 	{
123 		string[][string] result;
124 		foreach (k, v; this)
125 			result[k] ~= v;
126 		return result;
127 	}
128 }
129 
130 unittest
131 {
132 	Headers headers;
133 	headers["test"] = "test";
134 
135 	void test(T)(T headers)
136 	{
137 		assert("TEST" in headers);
138 		assert(headers["TEST"] == "test");
139 
140 		foreach (k, v; headers)
141 			assert(k == "Test" && v == "test");
142 
143 		auto aas = cast(string[string])headers;
144 		assert(aas == ["Test" : "test"]);
145 
146 		auto aaa = cast(string[][string])headers;
147 		assert(aaa == ["Test" : ["test"]]);
148 	}
149 
150 	test(headers);
151 
152 	const constHeaders = headers;
153 	test(constHeaders);
154 }
155 
156 /// Normalize capitalization
157 string normalizeHeaderName(string header)
158 {
159 	alias std.ascii.toUpper toUpper;
160 	alias std.ascii.toLower toLower;
161 
162 	auto s = header.dup;
163 	auto segments = s.split("-");
164 	foreach (segment; segments)
165 	{
166 		foreach (ref c; segment)
167 			c = cast(char)toUpper(c);
168 		switch (segment)
169 		{
170 			case "ID":
171 			case "IP":
172 			case "NNTP":
173 			case "TE":
174 			case "WWW":
175 				continue;
176 			case "ETAG":
177 				segment[] = "ETag";
178 				break;
179 			default:
180 				foreach (ref c; segment[1..$])
181 					c = cast(char)toLower(c);
182 				break;
183 		}
184 	}
185 	return assumeUnique(s);
186 }
187 
188 unittest
189 {
190 	assert(normalizeHeaderName("X-ORIGINATING-IP") == "X-Originating-IP");
191 }