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