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