1 /*
2  * Copyright 2008-2018 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 "config.h"
31 #include "Request.hxx"
32 #include "Global.hxx"
33 #include "Handler.hxx"
34 #include "event/Call.hxx"
35 #include "util/RuntimeError.hxx"
36 #include "util/StringStrip.hxx"
37 #include "util/StringView.hxx"
38 #include "util/CharUtil.hxx"
39 #include "Version.h"
40 
41 #include <curl/curl.h>
42 
43 #include <algorithm>
44 #include <cassert>
45 
46 #include <string.h>
47 
CurlRequest(CurlGlobal & _global,CurlResponseHandler & _handler)48 CurlRequest::CurlRequest(CurlGlobal &_global,
49 			 CurlResponseHandler &_handler)
50 	:global(_global), handler(_handler)
51 {
52 	error_buffer[0] = 0;
53 
54 	easy.SetPrivate((void *)this);
55 	easy.SetUserAgent("Music Player Daemon " VERSION);
56 	easy.SetHeaderFunction(_HeaderFunction, this);
57 	easy.SetWriteFunction(WriteFunction, this);
58 #if !defined(ANDROID) && !defined(_WIN32)
59 	easy.SetOption(CURLOPT_NETRC, 1L);
60 #endif
61 	easy.SetErrorBuffer(error_buffer);
62 	easy.SetNoProgress();
63 	easy.SetNoSignal();
64 	easy.SetConnectTimeout(10);
65 	easy.SetOption(CURLOPT_HTTPAUTH, (long) CURLAUTH_ANY);
66 }
67 
~CurlRequest()68 CurlRequest::~CurlRequest() noexcept
69 {
70 	FreeEasy();
71 }
72 
73 void
Start()74 CurlRequest::Start()
75 {
76 	assert(!registered);
77 
78 	global.Add(*this);
79 	registered = true;
80 }
81 
82 void
StartIndirect()83 CurlRequest::StartIndirect()
84 {
85 	BlockingCall(global.GetEventLoop(), [this](){
86 			Start();
87 		});
88 }
89 
90 void
Stop()91 CurlRequest::Stop() noexcept
92 {
93 	if (!registered)
94 		return;
95 
96 	global.Remove(*this);
97 	registered = false;
98 }
99 
100 void
StopIndirect()101 CurlRequest::StopIndirect()
102 {
103 	BlockingCall(global.GetEventLoop(), [this](){
104 			Stop();
105 		});
106 }
107 
108 void
FreeEasy()109 CurlRequest::FreeEasy() noexcept
110 {
111 	if (!easy)
112 		return;
113 
114 	Stop();
115 	easy = nullptr;
116 }
117 
118 void
Resume()119 CurlRequest::Resume() noexcept
120 {
121 	assert(registered);
122 
123 	easy.Unpause();
124 
125 	global.InvalidateSockets();
126 }
127 
128 void
FinishHeaders()129 CurlRequest::FinishHeaders()
130 {
131 	if (state != State::HEADERS)
132 		return;
133 
134 	state = State::BODY;
135 
136 	long status = 0;
137 	easy.GetInfo(CURLINFO_RESPONSE_CODE, &status);
138 
139 	handler.OnHeaders(status, std::move(headers));
140 }
141 
142 void
FinishBody()143 CurlRequest::FinishBody()
144 {
145 	FinishHeaders();
146 
147 	if (state != State::BODY)
148 		return;
149 
150 	state = State::CLOSED;
151 	handler.OnEnd();
152 }
153 
154 void
Done(CURLcode result)155 CurlRequest::Done(CURLcode result) noexcept
156 {
157 	Stop();
158 
159 	try {
160 		if (result != CURLE_OK) {
161 			StripRight(error_buffer);
162 			const char *msg = error_buffer;
163 			if (*msg == 0)
164 				msg = curl_easy_strerror(result);
165 			throw FormatRuntimeError("CURL failed: %s", msg);
166 		}
167 
168 		FinishBody();
169 	} catch (...) {
170 		state = State::CLOSED;
171 		handler.OnError(std::current_exception());
172 	}
173 }
174 
175 [[gnu::pure]]
176 static bool
IsResponseBoundaryHeader(StringView s)177 IsResponseBoundaryHeader(StringView s) noexcept
178 {
179 	return s.size > 5 && (s.StartsWith("HTTP/") ||
180 			      /* the proprietary "ICY 200 OK" is
181 				 emitted by Shoutcast */
182 			      s.StartsWith("ICY 2"));
183 }
184 
185 inline void
HeaderFunction(StringView s)186 CurlRequest::HeaderFunction(StringView s) noexcept
187 {
188 	if (state > State::HEADERS)
189 		return;
190 
191 	if (IsResponseBoundaryHeader(s)) {
192 		/* this is the boundary to a new response, for example
193 		   after a redirect */
194 		headers.clear();
195 		return;
196 	}
197 
198 	const char *header = s.data;
199 	const char *end = StripRight(header, header + s.size);
200 
201 	const char *value = s.Find(':');
202 	if (value == nullptr)
203 		return;
204 
205 	std::string name(header, value);
206 	std::transform(name.begin(), name.end(), name.begin(),
207 		       static_cast<char(*)(char)>(ToLowerASCII));
208 
209 	/* skip the colon */
210 
211 	++value;
212 
213 	/* strip the value */
214 
215 	value = StripLeft(value, end);
216 	end = StripRight(value, end);
217 
218 	headers.emplace(std::move(name), std::string(value, end));
219 }
220 
221 size_t
_HeaderFunction(char * ptr,size_t size,size_t nmemb,void * stream)222 CurlRequest::_HeaderFunction(char *ptr, size_t size, size_t nmemb,
223 			     void *stream) noexcept
224 {
225 	CurlRequest &c = *(CurlRequest *)stream;
226 
227 	size *= nmemb;
228 
229 	c.HeaderFunction({ptr, size});
230 	return size;
231 }
232 
233 inline size_t
DataReceived(const void * ptr,size_t received_size)234 CurlRequest::DataReceived(const void *ptr, size_t received_size) noexcept
235 {
236 	assert(received_size > 0);
237 
238 	try {
239 		FinishHeaders();
240 		handler.OnData({ptr, received_size});
241 		return received_size;
242 	} catch (CurlResponseHandler::Pause) {
243 		return CURL_WRITEFUNC_PAUSE;
244 	}
245 
246 }
247 
248 size_t
WriteFunction(char * ptr,size_t size,size_t nmemb,void * stream)249 CurlRequest::WriteFunction(char *ptr, size_t size, size_t nmemb,
250 			   void *stream) noexcept
251 {
252 	CurlRequest &c = *(CurlRequest *)stream;
253 
254 	size *= nmemb;
255 	if (size == 0)
256 		return 0;
257 
258 	return c.DataReceived(ptr, size);
259 }
260