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 static struct Metadata 59 { 60 HTTP.StatusLine statusLine; 61 string[][string] headers; 62 } 63 64 static struct Request 65 { 66 string url; 67 HTTP.Method method = HTTP.Method.get; 68 const(void)[] data; 69 70 int maxRedirects = int.min; // choose depending or method 71 } 72 73 /*private*/ static void req(CachedCurlNetwork instance, in ref Request request, string target, string metadataPath) 74 { 75 with (instance) 76 { 77 http.clearRequestHeaders(); 78 http.method = request.method; 79 if (request.maxRedirects != int.min) 80 http.maxRedirects = request.maxRedirects; 81 else 82 if (request.method == HTTP.Method.head) 83 http.maxRedirects = uint.max; 84 else 85 http.maxRedirects = 10; 86 auto host = request.url.split("/")[2]; 87 if (cookieDir) 88 { 89 auto cookiePath = buildPath(cookieDir, host ~ cookieExt); 90 if (cookiePath.exists) 91 http.addRequestHeader("Cookie", cookiePath.readText.chomp()); 92 } 93 Metadata metadata; 94 http.onReceiveHeader = 95 (in char[] key, in char[] value) 96 { 97 metadata.headers[key.idup] ~= value.idup; 98 }; 99 http.onReceiveStatusLine = 100 (HTTP.StatusLine statusLine) 101 { 102 metadata.statusLine = statusLine; 103 }; 104 if (request.data) 105 { 106 const(void)[] data = request.data; 107 http.addRequestHeader("Content-Length", data.length.text); 108 http.onSend = (void[] buf) 109 { 110 size_t len = min(buf.length, data.length); 111 buf[0..len] = data[0..len]; 112 data = data[len..$]; 113 return len; 114 }; 115 } 116 else 117 http.onSend = null; 118 download!HTTP(request.url, target, http); 119 write(metadataPath, metadata.toJson); 120 } 121 } 122 123 static struct Response 124 { 125 string responsePath; 126 string metadataPath; 127 128 @property ubyte[] responseData() 129 { 130 checkOK(); 131 return cast(ubyte[])std.file.read(responsePath); 132 } 133 134 @property Metadata metadata() 135 { 136 return metadataPath.exists ? metadataPath.readText.jsonParse!Metadata : Metadata.init; 137 } 138 139 @property bool ok() 140 { 141 return metadata.statusLine.code / 100 == 2; 142 } 143 144 ref Response checkOK() 145 { 146 if (!ok) 147 throw new CachedCurlException(metadata); 148 return this; 149 } 150 } 151 152 static class CachedCurlException : Exception 153 { 154 Metadata metadata; 155 156 this(Metadata metadata, string fn = __FILE__, size_t ln = __LINE__) 157 { 158 this.metadata = metadata; 159 super("Request failed: " ~ metadata.statusLine.reason, fn, ln); 160 } 161 } 162 163 Response cachedReq(in ref Request request) 164 { 165 auto hash = getDigestString!MD5(request.url ~ cast(char)request.method ~ request.data); 166 auto path = buildPath(cacheDir, hash[0..2], hash); 167 ensurePathExists(path); 168 auto metadataPath = path ~ ".metadata"; 169 if (path.exists && path.timeLastModified.stdTime < epoch) 170 path.remove(); 171 cached!req(this, request, path, metadataPath); 172 return Response(path, metadataPath); 173 } 174 175 Response cachedReq(string url, HTTP.Method method, in void[] data = null) 176 { 177 auto req = Request(url, method, data); 178 return cachedReq(req); 179 } 180 181 override void downloadFile(string url, string target) 182 { 183 std.file.copy(cachedReq(url, HTTP.Method.get).checkOK.responsePath, target); 184 } 185 186 override void[] getFile(string url) 187 { 188 return cachedReq(url, HTTP.Method.get).responseData; 189 } 190 191 override bool urlOK(string url) 192 { 193 return cachedReq(url, HTTP.Method.get).ok; 194 } 195 196 override string resolveRedirect(string url) 197 { 198 return 199 url.applyRelativeURL( 200 cachedReq(url, HTTP.Method.head, null) 201 .metadata 202 .headers 203 .get("location", null) 204 .enforce("Not a redirect: " ~ url) 205 [$-1]); 206 } 207 208 override void[] post(string url, in void[] data) 209 { 210 return cachedReq(url, HTTP.Method.post, data).responseData; 211 } 212 } 213 214 alias CachedCurlException = CachedCurlNetwork.CachedCurlException; 215 216 static this() 217 { 218 net = new CachedCurlNetwork(); 219 }