1 /**
2  * ae.sys.net implementation using std.net.curl
3  * Note: std.net.curl requires libcurl.
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.curl;
16 
17 import etc.c.curl : CurlSeekPos, CurlSeek;
18 
19 import std.algorithm.comparison;
20 import std.file;
21 import std.net.curl;
22 import std.string;
23 
24 import ae.net.http.common;
25 import ae.net.ietf.url;
26 import ae.sys.data : Data;
27 import ae.sys.dataset : DataVec, bytes;
28 import ae.sys.net;
29 
30 /// Implementation of `Network` using libcurl.
31 class CurlNetwork : Network
32 {
33 	override void downloadFile(string url, string target)
34 	{
35 		std.file.write(target, getFile(url));
36 	} ///
37 
38 	override ubyte[] getFile(string url)
39 	{
40 		return get!(AutoProtocol, ubyte)(url);
41 	} ///
42 
43 	override ubyte[] post(string url, const(ubyte)[] data)
44 	{
45 		return .post!ubyte(url, data);
46 	} ///
47 
48 	override bool urlOK(string url)
49 	{
50 		try
51 		{
52 			auto http = HTTP(url);
53 			http.method = HTTP.Method.head;
54 			http.perform();
55 			return http.statusLine.code == 200; // OK
56 		}
57 		catch (Exception e)
58 			return false;
59 	} ///
60 
61 	override string resolveRedirect(string url)
62 	{
63 		string result = null;
64 
65 		auto http = HTTP(url);
66 		http.method = HTTP.Method.head;
67 		http.onReceiveHeader =
68 			(in char[] key, in char[] value)
69 			{
70 				if (icmp(key, "Location")==0)
71 				{
72 					result = value.idup;
73 					if (result)
74 						result = url.applyRelativeURL(result);
75 				}
76 			};
77 		http.perform();
78 
79 		return result;
80 	} ///
81 
82 	override HttpResponse httpRequest(HttpRequest request)
83 	{
84 		auto http = HTTP();
85 		http.url = request.url;
86 		switch (request.method.toUpper)
87 		{
88 			case "HEAD"   : http.method = HTTP.Method.head; break;
89 			case "GET"    : http.method = HTTP.Method.get; break;
90 			case "POST"   : http.method = HTTP.Method.post; break;
91 			case "PUT"    : http.method = HTTP.Method.put; break;
92 			case "DEL"    : http.method = HTTP.Method.del; break;
93 			case "OPTIONS": http.method = HTTP.Method.options; break;
94 			case "TRACE"  : http.method = HTTP.Method.trace; break;
95 			case "CONNECT": http.method = HTTP.Method.connect; break;
96 			case "PATCH"  : http.method = HTTP.Method.patch; break;
97 			default: throw new Exception("Unknown HTTP method: " ~ request.method);
98 		}
99 		foreach (name, value; request.headers)
100 			http.addRequestHeader(name, value);
101 
102 		DataVec remainingData; // https://issues.dlang.org/show_bug.cgi?id=14261
103 		if (request.data)
104 		{
105 			remainingData = request.data.dup;
106 			http.contentLength = request.data.bytes.length;
107 			http.onSend =
108 				(void[] buf)
109 				{
110 					size_t bytesToSend = min(buf.length, remainingData.bytes.length);
111 					if (!bytesToSend)
112 						return 0;
113 					auto dataToSend = remainingData.bytes[0 .. bytesToSend];
114 					{
115 						size_t p = 0;
116 						foreach (ref datum; dataToSend)
117 						{
118 							datum.enter((contents) {
119 								buf[p .. p + datum.length] = contents;
120 							});
121 							p += datum.length;
122 						}
123 					}
124 					remainingData = remainingData.bytes[bytesToSend .. $];
125 					return bytesToSend;
126 				};
127 			http.handle.onSeek =
128 				(long offset, CurlSeekPos mode)
129 				{
130 					switch (mode)
131 					{
132 						case CurlSeekPos.set:
133 							remainingData = request.data.bytes[cast(size_t) offset .. $];
134 							return CurlSeek.ok;
135 						default:
136 							return CurlSeek.cantseek;
137 					}
138 				};
139 		}
140 
141 		auto response = new HttpResponse;
142 		http.onReceiveStatusLine =
143 			(HTTP.StatusLine statusLine)
144 			{
145 				response.status = cast(HttpStatusCode)statusLine.code;
146 				response.statusMessage = statusLine.reason;
147 			};
148 		http.onReceiveHeader =
149 			(in char[] key, in char[] value)
150 			{
151 				response.headers.add(key.idup, value.idup);
152 			};
153 		http.onReceive =
154 			(ubyte[] data)
155 			{
156 				response.data ~= Data(data);
157 				return data.length;
158 			};
159 		http.perform();
160 		return response;
161 	} ///
162 }
163 
164 static this()
165 {
166 	net = new CurlNetwork();
167 }