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