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 }