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, const(ubyte)[] 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, scope void delegate(in ubyte[]) sink)
159 	{
160 		// Get total file size
161 		DWORD bytesTotal = 0;
162 		try
163 			bytesTotal = hUrl.I!httpQueryNumber(HTTP_QUERY_CONTENT_LENGTH);
164 		catch (Exception e) {}
165 
166 		DWORD bytesReadTotal;
167 
168 		while (true)
169 		{
170 			DWORD bytesRead;
171 			InternetReadFile(hUrl, buf.ptr, buf.length, &bytesRead);
172 			if (bytesRead==0)
173 				break;
174 			sink(buf[0..bytesRead]);
175 			bytesReadTotal += bytesRead;
176 		}
177 
178 		enforce(!bytesTotal || bytesReadTotal == bytesTotal,
179 			"Failed to download the whole object (got %s out of %s bytes)".format(bytesReadTotal, bytesTotal));
180 	}
181 
182 	final static DWORD urlFlags(string url)
183 	{
184 		return url.startsWith("https://") ? INTERNET_FLAG_SECURE : 0;
185 	}
186 
187 public:
188 	override void downloadFile(string url, string target)
189 	{
190 		import std.stdio : File;
191 		auto f = File(target, "wb");
192 		auto hNet = open();
193 		auto hReq = hNet.I!openUrl(url);
194 		hReq.I!checkOK();
195 		hReq.I!doDownload(&f.rawWrite!ubyte);
196 	} ///
197 
198 	override ubyte[] getFile(string url)
199 	{
200 		auto result = appender!(ubyte[]);
201 		auto hNet = open();
202 		auto hReq = hNet.I!openUrl(url);
203 		hReq.I!checkOK();
204 		hReq.I!doDownload(&result.put!(const(ubyte)[]));
205 		return result.data;
206 	} ///
207 
208 	override ubyte[] post(string url, const(ubyte)[] data)
209 	{
210 		auto request = new HttpRequest(url);
211 
212 		auto hNet = open();
213 		auto hCon = hNet.I!connect(request.host, request.port);
214 		auto hReq = hCon.I!openRequest("POST", request.resource, urlFlags(url));
215 		hReq.I!sendRequest(null, data);
216 		hReq.I!checkOK();
217 
218 		auto result = appender!(ubyte[]);
219 		hReq.I!doDownload(&result.put!(const(ubyte)[]));
220 		return result.data;
221 	} ///
222 
223 	override bool urlOK(string url)
224 	{
225 		try
226 		{
227 			auto request = new HttpRequest(url);
228 
229 			auto hNet = open();
230 			auto hCon = hNet.I!connect(request.host, request.port);
231 			auto hReq = hCon.I!openRequest("HEAD", request.resource, urlFlags(url));
232 			hReq.I!sendRequest();
233 
234 			return hReq.I!httpQueryNumber(HTTP_QUERY_STATUS_CODE) == 200;
235 		}
236 		catch (Exception e)
237 			return false;
238 	} ///
239 
240 	override string resolveRedirect(string url)
241 	{
242 		auto request = new HttpRequest(url);
243 
244 		auto hNet = open(INTERNET_FLAG_NO_AUTO_REDIRECT);
245 		auto hCon = hNet.I!connect(request.host, request.port);
246 		auto hReq = hCon.I!openRequest("HEAD", request.resource, INTERNET_FLAG_NO_AUTO_REDIRECT | urlFlags(url));
247 		hReq.I!sendRequest();
248 
249 		auto location = hReq.I!httpQueryString(HTTP_QUERY_LOCATION);
250 		return location ? url.applyRelativeURL(location) : null;
251 	} ///
252 
253 	override HttpResponse httpRequest(HttpRequest request)
254 	{
255 		string requestHeaders;
256 		foreach (name, value; request.headers)
257 			requestHeaders ~= name ~ ": " ~ value ~ "\r\n";
258 		auto hNet = open(INTERNET_FLAG_NO_AUTO_REDIRECT);
259 		auto hCon = hNet.I!connect(request.host, request.port);
260 		auto hReq = hCon.I!openRequest(request.method, request.resource,
261 			INTERNET_FLAG_NO_AUTO_REDIRECT | urlFlags(request.url));
262 		auto requestData = request.data.joinData;
263 		hReq.I!sendRequest(requestHeaders, requestData.contents);
264 
265 		auto response = new HttpResponse;
266 		response.status = cast(HttpStatusCode)hReq.I!httpQueryNumber(HTTP_QUERY_STATUS_CODE).to!(OriginalType!HttpStatusCode);
267 		response.statusMessage = hReq.I!httpQueryString(HTTP_QUERY_STATUS_TEXT);
268 
269 		auto responseHeaders = hReq.I!httpQueryString(HTTP_QUERY_RAW_HEADERS);
270 		foreach (header; responseHeaders.splitter('\0').dropOne)
271 			if (header.length)
272 			{
273 				auto parts = header.findSplit(":");
274 				response.headers.add(parts[0].strip, parts[2].strip);
275 			}
276 
277 		hReq.I!doDownload((scope const(ubyte)[] bytes) { response.data ~= Data(bytes); });
278 		return response;
279 	} ///
280 }
281 
282 static this()
283 {
284 	net = new WinINetNetwork();
285 }