1 /** 2 * ae.sys.net implementation for HTTP using Curl, 3 * with caching and cookie support 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.cachedcurl; 16 17 // TODO: refactor into an abstract Cached!Network wrapper? 18 19 import std.algorithm.comparison; 20 import std.conv; 21 import std.exception; 22 import std.file; 23 import std.net.curl; 24 import std.path; 25 import std.string; 26 import std.typecons; 27 28 import ae.net.ietf.url; 29 import ae.sys.file; 30 import ae.sys.net; 31 import ae.utils.digest; 32 import ae.utils.json; 33 import ae.utils.time; 34 35 class CachedCurlNetwork : Network 36 { 37 /// Curl HTTP object 38 /// Can be customized after construction. 39 HTTP http; 40 41 /// Directory for caching responses 42 string cacheDir = "cache"; 43 44 /// Ignore cache entries older than the given time 45 StdTime epoch = 0; 46 47 /// Directory for reading cookies. 48 /// May be moved to a lambda in the future. 49 /// Format is one file per host, with hostname ~ cookieExt being the file name. 50 /// Contents is one line for the entire HTTP "Cookie" header. 51 string cookieDir, cookieExt; 52 53 this() 54 { 55 http = HTTP(); 56 } 57 58 private struct Metadata 59 { 60 HTTP.StatusLine statusLine; 61 string[][string] headers; 62 } 63 64 /*private*/ static void req(CachedCurlNetwork instance, string url, HTTP.Method method, const(void)[] data, string target, string metadataPath) 65 { 66 with (instance) 67 { 68 http.clearRequestHeaders(); 69 http.method = method; 70 if (method == HTTP.Method.head) 71 http.maxRedirects = uint.max; 72 else 73 http.maxRedirects = 10; 74 auto host = url.split("/")[2]; 75 if (cookieDir) 76 { 77 auto cookiePath = buildPath(cookieDir, host ~ cookieExt); 78 if (cookiePath.exists) 79 http.addRequestHeader("Cookie", cookiePath.readText.chomp()); 80 } 81 Metadata metadata; 82 http.onReceiveHeader = 83 (in char[] key, in char[] value) 84 { 85 metadata.headers[key.idup] ~= value.idup; 86 }; 87 http.onReceiveStatusLine = 88 (HTTP.StatusLine statusLine) 89 { 90 metadata.statusLine = statusLine; 91 }; 92 if (data) 93 { 94 http.addRequestHeader("Content-Length", data.length.text); 95 http.onSend = (void[] buf) 96 { 97 size_t len = min(buf.length, data.length); 98 buf[0..len] = data[0..len]; 99 data = data[len..$]; 100 return len; 101 }; 102 } 103 else 104 http.onSend = null; 105 download!HTTP(url, target, http); 106 write(metadataPath, metadata.toJson); 107 } 108 } 109 110 private struct Response 111 { 112 string responsePath; 113 string metadataPath; 114 115 @property ubyte[] responseData() 116 { 117 checkOK(); 118 return cast(ubyte[])std.file.read(responsePath); 119 } 120 121 @property Metadata metadata() 122 { 123 return metadataPath.exists ? metadataPath.readText.jsonParse!Metadata : Metadata.init; 124 } 125 126 @property bool ok() 127 { 128 return metadata.statusLine.code / 100 == 2; 129 } 130 131 ref Response checkOK() 132 { 133 if (!ok) 134 throw new CachedCurlException(metadata); 135 return this; 136 } 137 } 138 139 static class CachedCurlException : Exception 140 { 141 Metadata metadata; 142 143 this(Metadata metadata, string fn = __FILE__, size_t ln = __LINE__) 144 { 145 this.metadata = metadata; 146 super("Request failed: " ~ metadata.statusLine.reason, fn, ln); 147 } 148 } 149 150 private Response cachedReq(string url, HTTP.Method method, in void[] data = null) 151 { 152 auto hash = getDigestString!MD5(url ~ cast(char)method ~ data); 153 auto path = buildPath(cacheDir, hash[0..2], hash); 154 ensurePathExists(path); 155 auto metadataPath = path ~ ".metadata"; 156 if (path.exists && path.timeLastModified.stdTime < epoch) 157 path.remove(); 158 cached!req(this, url, method, data, path, metadataPath); 159 return Response(path, metadataPath); 160 } 161 162 override void downloadFile(string url, string target) 163 { 164 std.file.copy(cachedReq(url, HTTP.Method.get).checkOK.responsePath, target); 165 } 166 167 override void[] getFile(string url) 168 { 169 return cachedReq(url, HTTP.Method.get).responseData; 170 } 171 172 override bool urlOK(string url) 173 { 174 return cachedReq(url, HTTP.Method.get).ok; 175 } 176 177 override string resolveRedirect(string url) 178 { 179 return 180 url.applyRelativeURL( 181 cachedReq(url, HTTP.Method.head, null) 182 .metadata 183 .headers 184 .get("location", null) 185 .enforce("Not a redirect: " ~ url) 186 [$-1]); 187 } 188 189 override void[] post(string url, in void[] data) 190 { 191 return cachedReq(url, HTTP.Method.post, data).responseData; 192 } 193 } 194 195 alias CachedCurlException = CachedCurlNetwork.CachedCurlException; 196 197 static this() 198 { 199 net = new CachedCurlNetwork(); 200 }