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 }