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 }