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 }