1 /** 2 * HTTP utilities 3 * 4 * Copyright: 5 * (C) 2013 Jack Lloyd 6 * (C) 2014-2015 Etienne Cimon 7 * 8 * License: 9 * Botan is released under the Simplified BSD License (see LICENSE.md) 10 */ 11 module botan.utils.http_util.http_util; 12 13 import botan.utils.types; 14 import memutils.hashmap; 15 import botan.utils.parsing; 16 import botan.codec.hex; 17 import std.datetime; 18 import std.stdio; 19 import std.conv; 20 import std..string; 21 import std.array : Appender; 22 23 struct HTTPResponse 24 { 25 public: 26 27 this(uint status_code, in string status_message, 28 in string _body, HashMapRef!(string, string) headers) 29 { 30 m_status_code = status_code; 31 m_status_message = status_message; 32 m_body = _body; 33 m_headers = headers; 34 } 35 36 uint statusCode() const { return m_status_code; } 37 38 string _body() const { return m_body; } 39 40 const(HashMapRef!(string, string)) headers() const { return m_headers; } 41 42 string statusMessage() const { return m_status_message; } 43 44 void throwUnlessOk() 45 { 46 if (statusCode() != 200) 47 throw new Exception("HTTP error: " ~ statusMessage()); 48 } 49 50 string toString() 51 { 52 Appender!string output; 53 output ~= "HTTP " ~ statusCode().to!string ~ " " ~ statusMessage() ~ "\n"; 54 foreach (const ref string k, const ref string v; headers()) 55 output ~= "Header '" ~ k ~ "' = '" ~ v ~ "'\n"; 56 output ~= "Body " ~ to!string(_body().length) ~ " bytes:\n"; 57 output ~= cast(string) _body(); 58 return output.data; 59 } 60 61 private: 62 uint m_status_code; 63 string m_status_message = "Uninitialized"; 64 string m_body; 65 HashMapRef!(string, string) m_headers; 66 } 67 68 HTTPResponse httpSync()(in string verb, 69 in string url, 70 in string content_type, 71 auto const ref Vector!ubyte _body, 72 size_t allowable_redirects) 73 { 74 HashMapRef!(string, string) headers; 75 if (!tcp_message_handler) 76 throw new Exception("No HTTP Handler Defined"); 77 const auto protocol_host_sep = url.indexOf("://"); 78 if (protocol_host_sep == -1) 79 throw new Exception("Invalid URL " ~ url); 80 const string protocol = url[0 .. protocol_host_sep]; 81 82 string buff = url[protocol_host_sep + 3 .. $]; 83 84 const auto host_loc_sep = buff.indexOf('/'); 85 86 string hostname, loc; 87 88 if (host_loc_sep == -1) 89 { 90 hostname = buff[0 .. $]; 91 loc = "/"; 92 } 93 else 94 { 95 hostname = buff[0 .. host_loc_sep]; 96 loc = url[host_loc_sep .. $]; 97 } 98 99 import std.array : Appender; 100 Appender!string outbuf; 101 102 outbuf ~= verb ~ " " ~ loc ~ " HTTP/1.0\r\n"; 103 outbuf ~= "Host: " ~ hostname ~ "\r\n"; 104 105 if (verb == "GET") 106 { 107 outbuf ~= "Accept: */*\r\n"; 108 outbuf ~= "Cache-Control: no-cache\r\n"; 109 } 110 else if (verb == "POST") 111 outbuf ~= "Content-Length: " ~ _body.length.to!string ~ "\r\n"; 112 113 if (content_type != "") 114 outbuf ~= "Content-Type: " ~ content_type ~ "\r\n"; 115 116 outbuf ~= "Connection: close\r\n\r\n"; 117 outbuf ~= cast(string) _body[]; 118 119 auto reply = tcp_message_handler(hostname, outbuf.data); 120 121 if (reply.length == 0) 122 throw new Exception("No response"); 123 124 string http_version; 125 uint status_code; 126 string status_message; 127 128 ptrdiff_t idx = reply.indexOf(' '); 129 130 if (idx == -1) 131 throw new Exception("Not an HTTP response"); 132 133 http_version = reply[0 .. idx]; 134 135 if (http_version.length == 0 || http_version[0 .. 5] != "HTTP/") 136 throw new Exception("Not an HTTP response"); 137 138 string reply_front = reply[idx + 1 .. $]; 139 status_code = parse!uint(reply_front); 140 141 idx = reply.indexOf('\n'); 142 143 if (idx == -1) 144 throw new Exception("Not an HTTP response"); 145 146 status_message = reply[status_code.to!string.length + http_version.to!string.length + 2 .. idx].strip; 147 148 reply = reply[idx + 1 .. $]; 149 150 string header_line; 151 while (true) 152 { 153 idx = reply.indexOf("\n"); 154 if (idx < 0) 155 throw new Exception("Unterminated HTTP headers"); 156 header_line = reply[0 .. idx].strip; 157 if (!header_line.length) 158 break; 159 160 auto sep = header_line.indexOf(':'); 161 if (sep == -1) 162 throw new Exception("Invalid HTTP header " ~ header_line); 163 const string key = header_line[0 .. sep].strip; 164 const string val = header_line[sep + 1 .. $].strip; 165 headers[key] = val; 166 167 reply = reply[idx + 1 .. $]; 168 } 169 170 if (status_code == 301 && headers.get("Location") != "") 171 { 172 if (allowable_redirects == 0) 173 throw new Exception("HTTP redirection count exceeded"); 174 return GET_sync(headers["Location"], allowable_redirects - 1); 175 } 176 177 string resp_body = reply[idx + 1 .. $]; 178 179 const string header_size = headers.get("Content-Length"); 180 181 if (header_size != "") 182 { 183 if (resp_body.length != to!size_t(header_size)) 184 throw new Exception("Content-Length disagreement, header says " ~ 185 header_size ~ " got " ~ to!string(resp_body.length)); 186 } 187 188 return HTTPResponse(status_code, status_message, resp_body, headers); 189 } 190 191 string urlEncode(in string input) 192 { 193 import std.array : Appender; 194 Appender!string output; 195 196 foreach (c; input) 197 { 198 if (c >= 'A' && c <= 'Z') 199 output ~= c; 200 else if (c >= 'a' && c <= 'z') 201 output ~= c; 202 else if (c >= '0' && c <= '9') 203 output ~= c; 204 else if (c == '-' || c == '_' || c == '.' || c == '~') 205 output ~= c; 206 else { 207 char[2] buf; 208 hexEncode(buf.ptr, cast(const(ubyte)*) &c, 1); 209 output ~= '%' ~ buf.ptr[0 .. 2]; 210 } 211 } 212 213 return output.data; 214 } 215 216 HTTPResponse GET_sync(in string url, size_t allowable_redirects = 1) 217 { 218 return httpSync("GET", url, "", Vector!ubyte(), allowable_redirects); 219 } 220 221 HTTPResponse POST_sync(ALLOC)(in string url, in string content_type, 222 auto const ref Vector!(ubyte, ALLOC) _body, 223 size_t allowable_redirects = 1) 224 { 225 return httpSync("POST", url, content_type, _body, allowable_redirects); 226 } 227 228 string delegate(in string hostname, in string message) tcp_message_handler;