1 /*
2  * Copyright 2008-2020 Max Kellermann <max.kellermann@gmail.com>
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * - Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *
11  * - Redistributions in binary form must reproduce the above copyright
12  * notice, this list of conditions and the following disclaimer in the
13  * documentation and/or other materials provided with the
14  * distribution.
15  *
16  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
19  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
20  * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
21  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
25  * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
27  * OF THE POSSIBILITY OF SUCH DAMAGE.
28  */
29 
30 #include "Global.hxx"
31 #include "Request.hxx"
32 #include "event/Loop.hxx"
33 #include "event/SocketEvent.hxx"
34 #include "util/Compiler.h"
35 
36 #include <cassert>
37 
38 /**
39  * Monitor for one socket created by CURL.
40  */
41 class CurlSocket final {
42 	CurlGlobal &global;
43 
44 	SocketEvent socket_event;
45 
46 public:
CurlSocket(CurlGlobal & _global,EventLoop & _loop,SocketDescriptor _fd)47 	CurlSocket(CurlGlobal &_global, EventLoop &_loop, SocketDescriptor _fd)
48 		:global(_global),
49 		 socket_event(_loop, BIND_THIS_METHOD(OnSocketReady), _fd) {}
50 
~CurlSocket()51 	~CurlSocket() noexcept {
52 		/* TODO: sometimes, CURL uses CURL_POLL_REMOVE after
53 		   closing the socket, and sometimes, it uses
54 		   CURL_POLL_REMOVE just to move the (still open)
55 		   connection to the pool; in the first case,
56 		   Abandon() would be most appropriate, but it breaks
57 		   the second case - is that a CURL bug?  is there a
58 		   better solution? */
59 	}
60 
61 	CurlSocket(const CurlSocket &) = delete;
62 	CurlSocket &operator=(const CurlSocket &) = delete;
63 
GetEventLoop() const64 	[[nodiscard]] auto &GetEventLoop() const noexcept {
65 		return socket_event.GetEventLoop();
66 	}
67 
68 	/**
69 	 * Callback function for CURLMOPT_SOCKETFUNCTION.
70 	 */
71 	static int SocketFunction(CURL *easy,
72 				  curl_socket_t s, int action,
73 				  void *userp, void *socketp) noexcept;
74 
75 private:
GetSocket() const76 	[[nodiscard]] SocketDescriptor GetSocket() const noexcept {
77 		return socket_event.GetSocket();
78 	}
79 
80 	void OnSocketReady(unsigned events) noexcept;
81 
FlagsToCurlCSelect(unsigned flags)82 	static constexpr int FlagsToCurlCSelect(unsigned flags) noexcept {
83 		return (flags & (SocketEvent::READ | SocketEvent::HANGUP) ? CURL_CSELECT_IN : 0) |
84 			(flags & SocketEvent::WRITE ? CURL_CSELECT_OUT : 0) |
85 			(flags & SocketEvent::ERROR ? CURL_CSELECT_ERR : 0);
86 	}
87 
88 	[[gnu::const]]
CurlPollToFlags(int action)89 	static unsigned CurlPollToFlags(int action) noexcept {
90 		switch (action) {
91 		case CURL_POLL_NONE:
92 			return 0;
93 
94 		case CURL_POLL_IN:
95 			return SocketEvent::READ;
96 
97 		case CURL_POLL_OUT:
98 			return SocketEvent::WRITE;
99 
100 		case CURL_POLL_INOUT:
101 			return SocketEvent::READ|SocketEvent::WRITE;
102 		}
103 
104 		assert(false);
105 		gcc_unreachable();
106 	}
107 };
108 
CurlGlobal(EventLoop & _loop)109 CurlGlobal::CurlGlobal(EventLoop &_loop)
110 	:defer_read_info(_loop, BIND_THIS_METHOD(ReadInfo)),
111 	 timeout_event(_loop, BIND_THIS_METHOD(OnTimeout))
112 {
113 	multi.SetOption(CURLMOPT_SOCKETFUNCTION, CurlSocket::SocketFunction);
114 	multi.SetOption(CURLMOPT_SOCKETDATA, this);
115 
116 	multi.SetOption(CURLMOPT_TIMERFUNCTION, TimerFunction);
117 	multi.SetOption(CURLMOPT_TIMERDATA, this);
118 }
119 
120 int
SocketFunction(CURL * easy,curl_socket_t s,int action,void * userp,void * socketp)121 CurlSocket::SocketFunction([[maybe_unused]] CURL *easy,
122 			   curl_socket_t s, int action,
123 			   void *userp, void *socketp) noexcept
124 {
125 	auto &global = *(CurlGlobal *)userp;
126 	auto *cs = (CurlSocket *)socketp;
127 
128 	assert(global.GetEventLoop().IsInside());
129 
130 	if (action == CURL_POLL_REMOVE) {
131 		delete cs;
132 		return 0;
133 	}
134 
135 	if (cs == nullptr) {
136 		cs = new CurlSocket(global, global.GetEventLoop(),
137 				    SocketDescriptor(s));
138 		global.Assign(s, *cs);
139 	}
140 
141 	unsigned flags = CurlPollToFlags(action);
142 	if (flags != 0)
143 		cs->socket_event.Schedule(flags);
144 	return 0;
145 }
146 
147 void
OnSocketReady(unsigned flags)148 CurlSocket::OnSocketReady(unsigned flags) noexcept
149 {
150 	assert(GetEventLoop().IsInside());
151 
152 	global.SocketAction(GetSocket().Get(), FlagsToCurlCSelect(flags));
153 }
154 
155 void
Add(CurlRequest & r)156 CurlGlobal::Add(CurlRequest &r)
157 {
158 	assert(GetEventLoop().IsInside());
159 
160 	multi.Add(r.Get());
161 
162 	InvalidateSockets();
163 }
164 
165 void
Remove(CurlRequest & r)166 CurlGlobal::Remove(CurlRequest &r) noexcept
167 {
168 	assert(GetEventLoop().IsInside());
169 
170 	multi.Remove(r.Get());
171 }
172 
173 /**
174  * Find a request by its CURL "easy" handle.
175  */
176 [[gnu::pure]]
177 static CurlRequest *
ToRequest(CURL * easy)178 ToRequest(CURL *easy) noexcept
179 {
180 	void *p;
181 	CURLcode code = curl_easy_getinfo(easy, CURLINFO_PRIVATE, &p);
182 	if (code != CURLE_OK)
183 		return nullptr;
184 
185 	return (CurlRequest *)p;
186 }
187 
188 inline void
ReadInfo()189 CurlGlobal::ReadInfo() noexcept
190 {
191 	assert(GetEventLoop().IsInside());
192 
193 	CURLMsg *msg;
194 
195 	while ((msg = multi.InfoRead()) != nullptr) {
196 		if (msg->msg == CURLMSG_DONE) {
197 			auto *request = ToRequest(msg->easy_handle);
198 			if (request != nullptr)
199 				request->Done(msg->data.result);
200 		}
201 	}
202 }
203 
204 void
SocketAction(curl_socket_t fd,int ev_bitmask)205 CurlGlobal::SocketAction(curl_socket_t fd, int ev_bitmask) noexcept
206 {
207 	int running_handles;
208 	CURLMcode mcode = curl_multi_socket_action(multi.Get(), fd, ev_bitmask,
209 						   &running_handles);
210 	(void)mcode;
211 
212 	defer_read_info.Schedule();
213 }
214 
215 inline void
UpdateTimeout(long timeout_ms)216 CurlGlobal::UpdateTimeout(long timeout_ms) noexcept
217 {
218 	if (timeout_ms < 0) {
219 		timeout_event.Cancel();
220 		return;
221 	}
222 
223 	if (timeout_ms < 1)
224 		/* CURL's threaded resolver sets a timeout of 0ms, which
225 		   means we're running in a busy loop.  Quite a bad
226 		   idea to waste so much CPU.  Let's use a lower limit
227 		   of 1ms. */
228 		timeout_ms = 1;
229 
230 	timeout_event.Schedule(std::chrono::milliseconds(timeout_ms));
231 }
232 
233 int
TimerFunction(CURLM * _multi,long timeout_ms,void * userp)234 CurlGlobal::TimerFunction([[maybe_unused]] CURLM *_multi, long timeout_ms,
235 			  void *userp) noexcept
236 {
237 	auto &global = *(CurlGlobal *)userp;
238 	assert(_multi == global.multi.Get());
239 
240 	global.UpdateTimeout(timeout_ms);
241 	return 0;
242 }
243 
244 void
OnTimeout()245 CurlGlobal::OnTimeout() noexcept
246 {
247 	SocketAction(CURL_SOCKET_TIMEOUT, 0);
248 }
249