1 /**
2  * ae.sys.net implementation using WinINet
3  * Note: Requires Windows.
4  *
5  * License:
6  *   This Source Code Form is subject to the terms of
7  *   the Mozilla Public License, v. 2.0. If a copy of
8  *   the MPL was not distributed with this file, You
9  *   can obtain one at http://mozilla.org/MPL/2.0/.
10  *
11  * Authors:
12  *   Vladimir Panteleev <vladimir@thecybershadow.net>
13  */
14 
15 module ae.sys.net.wininet;
16 version(Windows):
17 
18 import std.algorithm.iteration;
19 import std.algorithm.searching;
20 import std.array;
21 import std.conv : to;
22 import std.exception;
23 import std.range : dropOne;
24 import std.string;
25 import std.traits : OriginalType;
26 import std.typecons : RefCounted;
27 
28 import ae.net.http.common : HttpRequest, HttpResponse, HttpStatusCode;
29 import ae.net.ietf.url;
30 import ae.sys.data;
31 import ae.sys.dataset;
32 import ae.sys.net;
33 import ae.sys.windows.dll;
34 import ae.sys.windows.exception;
35 import ae.utils.meta;
36 
37 import ae.sys.windows.imports;
38 mixin(importWin32!q{winbase});
39 mixin(importWin32!q{windef});
40 mixin(importWin32!q{wininet});
41 
42 class WinINetNetwork : Network
43 {
44 private:
45 	// Don't require wininet.lib
46 	mixin DynamicLoadMulti!("wininet.dll",
47 		HttpQueryInfoA,
48 		HttpOpenRequestA,
49 		HttpSendRequestA,
50 		InternetCloseHandle,
51 		InternetConnectA,
52 		InternetOpenA,
53 		InternetOpenUrlA,
54 		InternetReadFile,
55 	);
56 
57 protected:
58 	struct HNetImpl
59 	{
60 		HINTERNET hNet;
61 		alias hNet this;
62 		@disable this(this);
63 		~this() { if (hNet != hNet.init) InternetCloseHandle(hNet); }
64 	}
65 	alias HNet = RefCounted!HNetImpl;
66 
67 	final static HNet open(DWORD flags = 0)
68 	{
69 		auto hNet = InternetOpenA("ae.sys.net.wininet", INTERNET_OPEN_TYPE_PRECONFIG, null, null, flags)
70 			.wenforce("InternetOpen");
71 		return HNet(hNet);
72 	}
73 
74 	final static HNet openUrl(ref HNet hNet, string url)
75 	{
76 		auto hUrl = InternetOpenUrlA(hNet, url.toStringz(), null, 0xFFFFFFFF, INTERNET_FLAG_RELOAD, 0)
77 			.wenforce("InternetOpenUrl");
78 		return HNet(hUrl);
79 	}
80 
81 	final static HNet connect(ref HNet hNet, string serverName, INTERNET_PORT port)
82 	{
83 		auto hCon = InternetConnectA(hNet, serverName.toStringz(), port, null, null, INTERNET_SERVICE_HTTP, 0, 0)
84 			.wenforce("InternetConnect");
85 		return HNet(hCon);
86 	}
87 
88 	final static HNet openRequest(ref HNet hCon, string method, string resource, DWORD flags = 0)
89 	{
90 		auto hReq = HttpOpenRequestA(hCon, method.toStringz(), resource.toStringz(), null, null, null, flags, 0)
91 			.wenforce("HttpOpenRequest");
92 		return HNet(hReq);
93 	}
94 
95 	final static void sendRequest(ref HNet hReq, string headers = null, in void[] optionalData = null)
96 	{
97 		HttpSendRequestA(hReq, headers.ptr, headers.length.to!DWORD, cast(void*)optionalData.ptr, optionalData.length.to!DWORD)
98 			.wenforce("HttpSendRequest");
99 	}
100 
101 	static ubyte[0x10000] buf = void;
102 
103 	final static void[][] httpQuery(ref HNet hUrl, uint infoLevel)
104 	{
105 		DWORD index = 0;
106 
107 		void[][] result;
108 		while (true)
109 		{
110 			DWORD size = buf.sizeof;
111 			if (HttpQueryInfoA(hUrl, infoLevel, buf.ptr, &size, &index))
112 			{
113 				if (size == buf.sizeof && (infoLevel & HTTP_QUERY_FLAG_NUMBER))
114 					size = DWORD.sizeof;
115 				result ~= buf[0..size].dup;
116 			}
117 			else
118 			if (GetLastError() == ERROR_HTTP_HEADER_NOT_FOUND)
119 				return result;
120 			else
121 				wenforce(false, "HttpQueryInfo");
122 
123 			if (index == 0)
124 				return result;
125 		}
126 	}
127 
128 	final static void[] httpQueryOne(ref HNet hUrl, uint infoLevel)
129 	{
130 		auto results = hUrl.I!httpQuery(infoLevel);
131 		enforce(results.length <= 1, "Multiple results for HTTP info query");
132 		return results.length ? results[0] : null;
133 	}
134 
135 	final static string httpQueryString(ref HNet hUrl, uint infoLevel)
136 	{
137 		return cast(string)hUrl.I!httpQueryOne(infoLevel);
138 	}
139 
140 	final static uint httpQueryNumber(ref HNet hUrl, uint infoLevel)
141 	{
142 		DWORD[] results = cast(DWORD[])hUrl.I!httpQueryOne(infoLevel | HTTP_QUERY_FLAG_NUMBER);
143 		enforce(results.length, "No result for HTTP info query");
144 		return results[0];
145 	}
146 
147 	final static void checkOK(HNet hUrl)
148 	{
149 		auto statusCode = hUrl.I!httpQueryNumber(HTTP_QUERY_STATUS_CODE);
150 		if (statusCode != 200)
151 		{
152 			auto statusText = hUrl.I!httpQueryString(HTTP_QUERY_STATUS_TEXT);
153 			throw new Exception("Bad HTTP status code: %d (%s)".format(statusCode, statusText));
154 		}
155 	}
156 
157 	final static void doDownload(HNet hUrl, void delegate(ubyte[]) sink)
158 	{
159 		doDownload(hUrl, cast(void delegate(const ubyte[])) sink);
160 	}
161 
162 	final static void doDownload(HNet hUrl, void delegate(const ubyte[]) sink)
163 	{
164 		// Get total file size
165 		DWORD bytesTotal = 0;
166 		try
167 			bytesTotal = hUrl.I!httpQueryNumber(HTTP_QUERY_CONTENT_LENGTH);
168 		catch (Exception e) {}
169 
170 		DWORD bytesReadTotal;
171 
172 		while (true)
173 		{
174 			DWORD bytesRead;
175 			InternetReadFile(hUrl, buf.ptr, buf.length, &bytesRead);
176 			if (bytesRead==0)
177 				break;
178 			sink(buf[0..bytesRead]);
179 			bytesReadTotal += bytesRead;
180 		}
181 
182 		enforce(!bytesTotal || bytesReadTotal == bytesTotal,
183 			"Failed to download the whole object (got %s out of %s bytes)".format(bytesReadTotal, bytesTotal));
184 	}
185 
186 	final static DWORD urlFlags(string url)
187 	{
188 		return url.startsWith("https://") ? INTERNET_FLAG_SECURE : 0;
189 	}
190 
191 public:
192 	override void downloadFile(string url, string target)
193 	{
194 		import std.stdio;
195 		auto f = File(target, "wb");
196 		auto hNet = open();
197 		auto hReq = hNet.I!openUrl(url);
198 		hReq.I!checkOK();
199 		hReq.I!doDownload(&f.rawWrite!ubyte);
200 	}
201 
202 	override void[] getFile(string url)
203 	{
204 		auto result = appender!(ubyte[]);
205 		auto hNet = open();
206 		auto hReq = hNet.I!openUrl(url);
207 		hReq.I!checkOK();
208 		hReq.I!doDownload(&result.put!(ubyte[]));
209 		return result.data;
210 	}
211 
212 	override void[] post(string url, in void[] data)
213 	{
214 		auto request = new HttpRequest(url);
215 
216 		auto hNet = open();
217 		auto hCon = hNet.I!connect(request.host, request.port);
218 		auto hReq = hCon.I!openRequest("POST", request.resource, urlFlags(url));
219 		hReq.I!sendRequest(null, data);
220 		hReq.I!checkOK();
221 
222 		auto result = appender!(ubyte[]);
223 		hReq.I!doDownload(&result.put!(ubyte[]));
224 		return result.data;
225 	}
226 
227 	override bool urlOK(string url)
228 	{
229 		try
230 		{
231 			auto request = new HttpRequest(url);
232 
233 			auto hNet = open();
234 			auto hCon = hNet.I!connect(request.host, request.port);
235 			auto hReq = hCon.I!openRequest("HEAD", request.resource, urlFlags(url));
236 			hReq.I!sendRequest();
237 
238 			return hReq.I!httpQueryNumber(HTTP_QUERY_STATUS_CODE) == 200;
239 		}
240 		catch (Exception e)
241 			return false;
242 	}
243 
244 	override string resolveRedirect(string url)
245 	{
246 		auto request = new HttpRequest(url);
247 
248 		auto hNet = open(INTERNET_FLAG_NO_AUTO_REDIRECT);
249 		auto hCon = hNet.I!connect(request.host, request.port);
250 		auto hReq = hCon.I!openRequest("HEAD", request.resource, INTERNET_FLAG_NO_AUTO_REDIRECT | urlFlags(url));
251 		hReq.I!sendRequest();
252 
253 		auto location = hReq.I!httpQueryString(HTTP_QUERY_LOCATION);
254 		return location ? url.applyRelativeURL(location) : null;
255 	}
256 
257 	override HttpResponse httpRequest(HttpRequest request)
258 	{
259 		string requestHeaders;
260 		foreach (name, value; request.headers)
261 			requestHeaders ~= name ~ ": " ~ value ~ "\r\n";
262 		auto hNet = open(INTERNET_FLAG_NO_AUTO_REDIRECT);
263 		auto hCon = hNet.I!connect(request.host, request.port);
264 		auto hReq = hCon.I!openRequest(request.method, request.resource,
265 			INTERNET_FLAG_NO_AUTO_REDIRECT | urlFlags(request.url));
266 		auto requestData = request.data.joinData;
267 		hReq.I!sendRequest(requestHeaders, requestData.contents);
268 
269 		auto response = new HttpResponse;
270 		response.status = cast(HttpStatusCode)hReq.I!httpQueryNumber(HTTP_QUERY_STATUS_CODE).to!(OriginalType!HttpStatusCode);
271 		response.statusMessage = hReq.I!httpQueryString(HTTP_QUERY_STATUS_TEXT);
272 
273 		auto responseHeaders = hReq.I!httpQueryString(HTTP_QUERY_RAW_HEADERS);
274 		foreach (header; responseHeaders.splitter('\0').dropOne)
275 			if (header.length)
276 			{
277 				auto parts = header.findSplit(":");
278 				response.headers.add(parts[0].strip, parts[2].strip);
279 			}
280 
281 		hReq.I!doDownload((ubyte[] bytes) { response.data ~= Data(bytes, true); });
282 		return response;
283 	}
284 }
285 
286 static this()
287 {
288 	net = new WinINetNetwork();
289 }