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;