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.array;
19 import std.exception;
20 import std.string;
21 import std.typecons : RefCounted;
22 
23 import ae.net.http.common : HttpRequest;
24 import ae.net.ietf.url;
25 import ae.sys.net;
26 import ae.sys.windows.dll;
27 import ae.sys.windows.exception;
28 import ae.utils.meta;
29 
30 import ae.sys.windows.imports;
31 mixin(importWin32!q{winbase});
32 mixin(importWin32!q{windef});
33 mixin(importWin32!q{wininet});
34 
35 class WinINetNetwork : Network
36 {
37 private:
38 	// Don't require wininet.lib
39 	mixin DynamicLoadMulti!("wininet.dll",
40 		HttpQueryInfoA,
41 		HttpOpenRequestA,
42 		HttpSendRequestA,
43 		InternetCloseHandle,
44 		InternetConnectA,
45 		InternetOpenA,
46 		InternetOpenUrlA,
47 		InternetReadFile,
48 	);
49 
50 protected:
51 	struct HNetImpl
52 	{
53 		HINTERNET hNet;
54 		alias hNet this;
55 		~this() { if (hNet != hNet.init) InternetCloseHandle(hNet); }
56 	}
57 	alias HNet = RefCounted!HNetImpl;
58 
59 	final static HNet open(DWORD flags = 0)
60 	{
61 		auto hNet = InternetOpenA("ae.sys.net.wininet", INTERNET_OPEN_TYPE_PRECONFIG, null, null, flags)
62 			.wenforce("InternetOpen");
63 		return HNet(hNet);
64 	}
65 
66 	final static HNet openUrl(ref HNet hNet, string url)
67 	{
68 		auto hUrl = InternetOpenUrlA(hNet, url.toStringz(), null, 0xFFFFFFFF, INTERNET_FLAG_RELOAD, 0)
69 			.wenforce("InternetOpenUrl");
70 		return HNet(hUrl);
71 	}
72 
73 	final static HNet connect(ref HNet hNet, string serverName, INTERNET_PORT port)
74 	{
75 		auto hCon = InternetConnectA(hNet, serverName.toStringz(), port, null, null, INTERNET_SERVICE_HTTP, 0, 0)
76 			.wenforce("InternetConnect");
77 		return HNet(hCon);
78 	}
79 
80 	final static HNet openRequest(ref HNet hCon, string method, string resource, DWORD flags = 0)
81 	{
82 		auto hReq = HttpOpenRequestA(hCon, method.toStringz(), resource.toStringz(), null, null, null, flags, 0);
83 			.wenforce("HttpOpenRequest");
84 		return HNet(hReq);
85 	}
86 
87 	final static void sendRequest(ref HNet hReq)
88 	{
89 		HttpSendRequestA(hReq, null, 0, null, 0);
90 			.wenforce("HttpSendRequest");
91 	}
92 
93 	static ubyte[0x10000] buf = void;
94 
95 	final static void[][] httpQuery(ref HNet hUrl, uint infoLevel)
96 	{
97 		DWORD index = 0;
98 
99 		void[][] result;
100 		while (true)
101 		{
102 			DWORD size = buf.sizeof;
103 			if (HttpQueryInfoA(hUrl, infoLevel, buf.ptr, &size, &index))
104 			{
105 				if (size == buf.sizeof && (infoLevel & HTTP_QUERY_FLAG_NUMBER))
106 					size = DWORD.sizeof;
107 				result ~= buf[0..size].dup;
108 			}
109 			else
110 			if (GetLastError() == ERROR_HTTP_HEADER_NOT_FOUND)
111 				return result;
112 			else
113 				wenforce(false, "HttpQueryInfo");
114 
115 			if (index == 0)
116 				return result;
117 		}
118 	}
119 
120 	final static void[] httpQueryOne(ref HNet hUrl, uint infoLevel)
121 	{
122 		auto results = hUrl.I!httpQuery(infoLevel);
123 		enforce(results.length <= 1, "Multiple results for HTTP info query");
124 		return results.length ? results[0] : null;
125 	}
126 
127 	final static string httpQueryString(ref HNet hUrl, uint infoLevel)
128 	{
129 		return cast(string)hUrl.I!httpQueryOne(infoLevel);
130 	}
131 
132 	final static uint httpQueryNumber(ref HNet hUrl, uint infoLevel)
133 	{
134 		DWORD[] results = cast(DWORD[])hUrl.I!httpQueryOne(infoLevel | HTTP_QUERY_FLAG_NUMBER);
135 		enforce(results.length, "No result for HTTP info query");
136 		return results[0];
137 	}
138 
139 	final static void doDownload(string url, void delegate(ubyte[]) sink)
140 	{
141 		auto hNet = open();
142 		auto hUrl = hNet
143 			.I!openUrl(url);
144 
145 		// Check HTTP status code
146 		auto statusCode = hUrl.I!httpQueryNumber(HTTP_QUERY_STATUS_CODE);
147 		if (statusCode != 200)
148 		{
149 			auto statusText = hUrl.I!httpQueryString(HTTP_QUERY_STATUS_TEXT);
150 			throw new Exception("Bad HTTP status code: %d (%s)".format(statusCode, statusText));
151 		}
152 
153 		// Get total file size
154 		DWORD bytesTotal = 0;
155 		try
156 			bytesTotal = hUrl.I!httpQueryNumber(HTTP_QUERY_CONTENT_LENGTH);
157 		catch (Exception e) {}
158 
159 		DWORD bytesReadTotal;
160 
161 		while (true)
162 		{
163 			DWORD bytesRead;
164 			InternetReadFile(hUrl, buf.ptr, buf.length, &bytesRead);
165 			if (bytesRead==0)
166 				break;
167 			sink(buf[0..bytesRead]);
168 			bytesReadTotal += bytesRead;
169 		}
170 
171 		enforce(!bytesTotal || bytesReadTotal == bytesTotal,
172 			"Failed to download the whole object (got %s out of %s bytes)".format(bytesReadTotal, bytesTotal));
173 	}
174 
175 	final static DWORD urlFlags(string url)
176 	{
177 		return url.startsWith("https://") ? INTERNET_FLAG_SECURE : 0;
178 	}
179 
180 public:
181 	override void downloadFile(string url, string target)
182 	{
183 		import std.stdio;
184 		auto f = File(target, "wb");
185 		doDownload(url,
186 			(ubyte[] bytes)
187 			{
188 				f.rawWrite(bytes);
189 			}
190 		);
191 	}
192 
193 	override void[] getFile(string url)
194 	{
195 		auto result = appender!(ubyte[]);
196 		doDownload(url,
197 			(ubyte[] bytes)
198 			{
199 				result.put(bytes);
200 			}
201 		);
202 		return result.data;
203 	}
204 
205 	override bool urlOK(string url)
206 	{
207 		try
208 		{
209 			auto request = new HttpRequest(url);
210 
211 			auto hNet = open();
212 			auto hCon = hNet.I!connect(request.host, request.port);
213 			auto hReq = hCon.I!openRequest("HEAD", request.resource, urlFlags(url));
214 			hReq.I!sendRequest();
215 
216 			return hReq.I!httpQueryNumber(HTTP_QUERY_STATUS_CODE) == 200;
217 		}
218 		catch (Exception e)
219 			return false;
220 	}
221 
222 	override string resolveRedirect(string url)
223 	{
224 		auto request = new HttpRequest(url);
225 
226 		auto hNet = open(INTERNET_FLAG_NO_AUTO_REDIRECT);
227 		auto hCon = hNet.I!connect(request.host, request.port);
228 		auto hReq = hCon.I!openRequest("HEAD", request.resource, INTERNET_FLAG_NO_AUTO_REDIRECT | urlFlags(url));
229 		hReq.I!sendRequest();
230 
231 		auto location = hReq.I!httpQueryString(HTTP_QUERY_LOCATION);
232 		return location ? url.applyRelativeURL(location) : null;
233 	}
234 }
235 
236 static this()
237 {
238 	net = new WinINetNetwork();
239 }