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 version (Have_vibe_d) { 24 import vibe.core.net; 25 import vibe.core.stream; 26 import vibe.stream.operations : readAll; 27 } else {*/ 28 import std.socket; 29 import std.stream; 30 import std.socketstream; 31 //} 32 // import string; 33 34 struct HTTPResponse 35 { 36 public: 37 38 this(uint status_code, in string status_message, 39 in string _body, HashMapRef!(string, string) headers) 40 { 41 m_status_code = status_code; 42 m_status_message = status_message; 43 m_body = _body; 44 m_headers = headers; 45 } 46 47 uint statusCode() const { return m_status_code; } 48 49 string _body() const { return m_body; } 50 51 const(HashMapRef!(string, string)) headers() const { return m_headers; } 52 53 string statusMessage() const { return m_status_message; } 54 55 void throwUnlessOk() 56 { 57 if (statusCode() != 200) 58 throw new Exception("HTTP error: " ~ statusMessage()); 59 } 60 61 string toString() 62 { 63 Appender!string output; 64 output ~= "HTTP " ~ statusCode().to!string ~ " " ~ statusMessage() ~ "\n"; 65 foreach (const ref string k, const ref string v; headers()) 66 output ~= "Header '" ~ k ~ "' = '" ~ v ~ "'\n"; 67 output ~= "Body " ~ to!string(_body().length) ~ " bytes:\n"; 68 output ~= cast(string) _body(); 69 return output.data; 70 } 71 72 private: 73 uint m_status_code; 74 string m_status_message = "Uninitialized"; 75 string m_body; 76 HashMapRef!(string, string) m_headers; 77 } 78 79 HTTPResponse httpSync()(in string verb, 80 in string url, 81 in string content_type, 82 auto const ref Vector!ubyte _body, 83 size_t allowable_redirects) 84 { 85 const auto protocol_host_sep = url.indexOf("://"); 86 if (protocol_host_sep == -1) 87 throw new Exception("Invalid URL " ~ url); 88 const string protocol = url[0 .. protocol_host_sep]; 89 90 string buff = url[protocol_host_sep + 3 .. $]; 91 92 const auto host_loc_sep = buff.indexOf('/'); 93 94 string hostname, loc; 95 96 if (host_loc_sep == -1) 97 { 98 hostname = buff[0 .. $]; 99 loc = "/"; 100 } 101 else 102 { 103 hostname = buff[0 .. host_loc_sep]; 104 loc = url[host_loc_sep .. $]; 105 } 106 107 import std.array : Appender; 108 Appender!string outbuf; 109 110 outbuf ~= verb ~ " " ~ loc ~ " HTTP/1.0\r"; 111 outbuf ~= "Host: " ~ hostname ~ "\r"; 112 113 if (verb == "GET") 114 { 115 outbuf ~= "Accept: */*\r"; 116 outbuf ~= "Cache-Control: no-cache\r"; 117 } 118 else if (verb == "POST") 119 outbuf ~= "Content-Length: " ~ _body.length.to!string ~ "\r"; 120 121 if (content_type != "") 122 outbuf ~= "Content-Type: " ~ content_type ~ "\r"; 123 124 outbuf ~= "Connection: close\r\r"; 125 outbuf ~= cast(string) _body[]; 126 127 auto reply = httpTransact(hostname, outbuf.data); 128 129 if (reply.length == 0) 130 throw new Exception("No response"); 131 132 string http_version; 133 uint status_code; 134 string status_message; 135 136 ptrdiff_t idx = reply.indexOf(' '); 137 138 if (idx == -1) 139 throw new Exception("Not an HTTP response"); 140 141 http_version = reply[0 .. idx]; 142 143 if (http_version.length == 0 || http_version[0 .. 5] != "HTTP/") 144 throw new Exception("Not an HTTP response"); 145 146 string reply_front = reply[idx + 1 .. $]; 147 status_code = parse!uint(reply_front); 148 149 idx = reply.indexOf('\r'); 150 151 if (idx == -1) 152 throw new Exception("Not an HTTP response"); 153 154 status_message = reply[status_code.to!string.length + http_version.to!string.length + 2 .. idx]; 155 156 reply = reply[idx + 1 .. $]; 157 158 HashMapRef!(string, string) headers; 159 string header_line; 160 while (reply[0] != '\r') 161 { 162 idx = reply.indexOf('\r'); 163 header_line = reply[0 .. idx]; 164 165 auto sep = header_line.indexOf(": "); 166 if (sep == -1 || sep > header_line.length - 2) 167 throw new Exception("Invalid HTTP header " ~ header_line); 168 const string key = header_line[0 .. sep]; 169 170 if (sep + 2 < header_line.length - 1) 171 { 172 const string val = header_line[sep + 2 .. $]; 173 headers[key] = val; 174 } 175 176 reply = reply[idx + 1 .. $]; 177 } 178 179 if (status_code == 301 && headers.get("Location") != "") 180 { 181 if (allowable_redirects == 0) 182 throw new Exception("HTTP redirection count exceeded"); 183 return GET_sync(headers["Location"], allowable_redirects - 1); 184 } 185 186 string resp_body = reply[1 .. $]; 187 188 const string header_size = headers.get("Content-Length"); 189 190 if (header_size != "") 191 { 192 if (resp_body.length != to!size_t(header_size)) 193 throw new Exception("Content-Length disagreement, header says " ~ 194 header_size ~ " got " ~ to!string(resp_body.length)); 195 } 196 197 return HTTPResponse(status_code, status_message, resp_body, headers); 198 } 199 200 string urlEncode(in string input) 201 { 202 import std.array : Appender; 203 Appender!string output; 204 205 foreach (c; input) 206 { 207 if (c >= 'A' && c <= 'Z') 208 output ~= c; 209 else if (c >= 'a' && c <= 'z') 210 output ~= c; 211 else if (c >= '0' && c <= '9') 212 output ~= c; 213 else if (c == '-' || c == '_' || c == '.' || c == '~') 214 output ~= c; 215 else { 216 char[2] buf; 217 hexEncode(buf.ptr, cast(const(ubyte)*) &c, 1); 218 output ~= '%' ~ buf.ptr[0 .. 2]; 219 } 220 } 221 222 return output.data; 223 } 224 225 HTTPResponse GET_sync(in string url, size_t allowable_redirects = 1) 226 { 227 return httpSync("GET", url, "", Vector!ubyte(), allowable_redirects); 228 } 229 230 HTTPResponse POST_sync(ALLOC)(in string url, in string content_type, 231 auto const ref Vector!(ubyte, ALLOC) _body, 232 size_t allowable_redirects = 1) 233 { 234 return httpSync("POST", url, content_type, _body, allowable_redirects); 235 } 236 237 238 239 string httpTransact(in string hostname, in string message) 240 { 241 242 /* version (Have_vibe_d) { 243 244 TCPConnection stream = connectTCP(hostname, 80); 245 scope(exit) stream.close(); 246 stream.write(message); 247 stream.finished(); 248 return stream.readAll(); 249 } else {*/ 250 Socket socket = new TcpSocket(new InternetAddress(hostname, 80)); 251 scope(exit) socket.close(); 252 SocketStream stream = new SocketStream(socket); 253 stream.writeString(message); 254 255 Appender!string in_buf; 256 // Skip HTTP header. 257 while (true) 258 { 259 auto line = stream.readLine(); 260 if (!line.length) 261 break; 262 in_buf ~= line; 263 } 264 return in_buf.data; 265 // } 266 }