1 /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
2 
3 #include "remote/httpserverconnection.hpp"
4 #include "remote/httphandler.hpp"
5 #include "remote/httputility.hpp"
6 #include "remote/apilistener.hpp"
7 #include "remote/apifunction.hpp"
8 #include "remote/jsonrpc.hpp"
9 #include "base/application.hpp"
10 #include "base/base64.hpp"
11 #include "base/convert.hpp"
12 #include "base/configtype.hpp"
13 #include "base/defer.hpp"
14 #include "base/exception.hpp"
15 #include "base/io-engine.hpp"
16 #include "base/logger.hpp"
17 #include "base/objectlock.hpp"
18 #include "base/timer.hpp"
19 #include "base/tlsstream.hpp"
20 #include "base/utility.hpp"
21 #include <limits>
22 #include <memory>
23 #include <stdexcept>
24 #include <boost/asio/error.hpp>
25 #include <boost/asio/io_context.hpp>
26 #include <boost/asio/spawn.hpp>
27 #include <boost/beast/core.hpp>
28 #include <boost/beast/http.hpp>
29 #include <boost/system/error_code.hpp>
30 #include <boost/system/system_error.hpp>
31 #include <boost/thread/once.hpp>
32 
33 using namespace icinga;
34 
35 auto const l_ServerHeader ("Icinga/" + Application::GetAppVersion());
36 
HttpServerConnection(const String & identity,bool authenticated,const Shared<AsioTlsStream>::Ptr & stream)37 HttpServerConnection::HttpServerConnection(const String& identity, bool authenticated, const Shared<AsioTlsStream>::Ptr& stream)
38 	: HttpServerConnection(identity, authenticated, stream, IoEngine::Get().GetIoContext())
39 {
40 }
41 
HttpServerConnection(const String & identity,bool authenticated,const Shared<AsioTlsStream>::Ptr & stream,boost::asio::io_context & io)42 HttpServerConnection::HttpServerConnection(const String& identity, bool authenticated, const Shared<AsioTlsStream>::Ptr& stream, boost::asio::io_context& io)
43 	: m_Stream(stream), m_Seen(Utility::GetTime()), m_IoStrand(io), m_ShuttingDown(false), m_HasStartedStreaming(false),
44 	m_CheckLivenessTimer(io)
45 {
46 	if (authenticated) {
47 		m_ApiUser = ApiUser::GetByClientCN(identity);
48 	}
49 
50 	{
51 		std::ostringstream address;
52 		auto endpoint (stream->lowest_layer().remote_endpoint());
53 
54 		address << '[' << endpoint.address() << "]:" << endpoint.port();
55 
56 		m_PeerAddress = address.str();
57 	}
58 }
59 
Start()60 void HttpServerConnection::Start()
61 {
62 	namespace asio = boost::asio;
63 
64 	HttpServerConnection::Ptr keepAlive (this);
65 
66 	IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) { ProcessMessages(yc); });
67 	IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) { CheckLiveness(yc); });
68 }
69 
Disconnect()70 void HttpServerConnection::Disconnect()
71 {
72 	namespace asio = boost::asio;
73 
74 	HttpServerConnection::Ptr keepAlive (this);
75 
76 	IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) {
77 		if (!m_ShuttingDown) {
78 			m_ShuttingDown = true;
79 
80 			Log(LogInformation, "HttpServerConnection")
81 				<< "HTTP client disconnected (from " << m_PeerAddress << ")";
82 
83 			/*
84 			 * Do not swallow exceptions in a coroutine.
85 			 * https://github.com/Icinga/icinga2/issues/7351
86 			 * We must not catch `detail::forced_unwind exception` as
87 			 * this is used for unwinding the stack.
88 			 *
89 			 * Just use the error_code dummy here.
90 			 */
91 			boost::system::error_code ec;
92 
93 			m_CheckLivenessTimer.cancel();
94 
95 			m_Stream->lowest_layer().cancel(ec);
96 
97 			m_Stream->next_layer().async_shutdown(yc[ec]);
98 
99 			m_Stream->lowest_layer().shutdown(m_Stream->lowest_layer().shutdown_both, ec);
100 
101 			auto listener (ApiListener::GetInstance());
102 
103 			if (listener) {
104 				CpuBoundWork removeHttpClient (yc);
105 
106 				listener->RemoveHttpClient(this);
107 			}
108 		}
109 	});
110 }
111 
StartStreaming()112 void HttpServerConnection::StartStreaming()
113 {
114 	namespace asio = boost::asio;
115 
116 	m_HasStartedStreaming = true;
117 
118 	HttpServerConnection::Ptr keepAlive (this);
119 
120 	IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) {
121 		if (!m_ShuttingDown) {
122 			char buf[128];
123 			asio::mutable_buffer readBuf (buf, 128);
124 			boost::system::error_code ec;
125 
126 			do {
127 				m_Stream->async_read_some(readBuf, yc[ec]);
128 			} while (!ec);
129 
130 			Disconnect();
131 		}
132 	});
133 }
134 
Disconnected()135 bool HttpServerConnection::Disconnected()
136 {
137 	return m_ShuttingDown;
138 }
139 
140 static inline
EnsureValidHeaders(AsioTlsStream & stream,boost::beast::flat_buffer & buf,boost::beast::http::parser<true,boost::beast::http::string_body> & parser,boost::beast::http::response<boost::beast::http::string_body> & response,bool & shuttingDown,boost::asio::yield_context & yc)141 bool EnsureValidHeaders(
142 	AsioTlsStream& stream,
143 	boost::beast::flat_buffer& buf,
144 	boost::beast::http::parser<true, boost::beast::http::string_body>& parser,
145 	boost::beast::http::response<boost::beast::http::string_body>& response,
146 	bool& shuttingDown,
147 	boost::asio::yield_context& yc
148 )
149 {
150 	namespace http = boost::beast::http;
151 
152 	if (shuttingDown)
153 		return false;
154 
155 	bool httpError = false;
156 	String errorMsg;
157 
158 	boost::system::error_code ec;
159 
160 	http::async_read_header(stream, buf, parser, yc[ec]);
161 
162 	if (ec) {
163 		if (ec == boost::asio::error::operation_aborted)
164 			return false;
165 
166 		errorMsg = ec.message();
167 		httpError = true;
168 	} else {
169 		switch (parser.get().version()) {
170 		case 10:
171 		case 11:
172 			break;
173 		default:
174 			errorMsg = "Unsupported HTTP version";
175 		}
176 	}
177 
178 	if (!errorMsg.IsEmpty() || httpError) {
179 		response.result(http::status::bad_request);
180 
181 		if (!httpError && parser.get()[http::field::accept] == "application/json") {
182 			HttpUtility::SendJsonBody(response, nullptr, new Dictionary({
183 				{ "error", 400 },
184 				{ "status", String("Bad Request: ") + errorMsg }
185 			}));
186 		} else {
187 			response.set(http::field::content_type, "text/html");
188 			response.body() = String("<h1>Bad Request</h1><p><pre>") + errorMsg + "</pre></p>";
189 			response.content_length(response.body().size());
190 		}
191 
192 		response.set(http::field::connection, "close");
193 
194 		boost::system::error_code ec;
195 
196 		http::async_write(stream, response, yc[ec]);
197 		stream.async_flush(yc[ec]);
198 
199 		return false;
200 	}
201 
202 	return true;
203 }
204 
205 static inline
HandleExpect100(AsioTlsStream & stream,boost::beast::http::request<boost::beast::http::string_body> & request,boost::asio::yield_context & yc)206 void HandleExpect100(
207 	AsioTlsStream& stream,
208 	boost::beast::http::request<boost::beast::http::string_body>& request,
209 	boost::asio::yield_context& yc
210 )
211 {
212 	namespace http = boost::beast::http;
213 
214 	if (request[http::field::expect] == "100-continue") {
215 		http::response<http::string_body> response;
216 
217 		response.result(http::status::continue_);
218 
219 		boost::system::error_code ec;
220 
221 		http::async_write(stream, response, yc[ec]);
222 		stream.async_flush(yc[ec]);
223 	}
224 }
225 
226 static inline
HandleAccessControl(AsioTlsStream & stream,boost::beast::http::request<boost::beast::http::string_body> & request,boost::beast::http::response<boost::beast::http::string_body> & response,boost::asio::yield_context & yc)227 bool HandleAccessControl(
228 	AsioTlsStream& stream,
229 	boost::beast::http::request<boost::beast::http::string_body>& request,
230 	boost::beast::http::response<boost::beast::http::string_body>& response,
231 	boost::asio::yield_context& yc
232 )
233 {
234 	namespace http = boost::beast::http;
235 
236 	auto listener (ApiListener::GetInstance());
237 
238 	if (listener) {
239 		auto headerAllowOrigin (listener->GetAccessControlAllowOrigin());
240 
241 		if (headerAllowOrigin) {
242 			CpuBoundWork allowOriginHeader (yc);
243 
244 			auto allowedOrigins (headerAllowOrigin->ToSet<String>());
245 
246 			if (!allowedOrigins.empty()) {
247 				auto& origin (request[http::field::origin]);
248 
249 				if (allowedOrigins.find(origin.to_string()) != allowedOrigins.end()) {
250 					response.set(http::field::access_control_allow_origin, origin);
251 				}
252 
253 				allowOriginHeader.Done();
254 
255 				response.set(http::field::access_control_allow_credentials, "true");
256 
257 				if (request.method() == http::verb::options && !request[http::field::access_control_request_method].empty()) {
258 					response.result(http::status::ok);
259 					response.set(http::field::access_control_allow_methods, "GET, POST, PUT, DELETE");
260 					response.set(http::field::access_control_allow_headers, "Authorization, Content-Type, X-HTTP-Method-Override");
261 					response.body() = "Preflight OK";
262 					response.content_length(response.body().size());
263 					response.set(http::field::connection, "close");
264 
265 					boost::system::error_code ec;
266 
267 					http::async_write(stream, response, yc[ec]);
268 					stream.async_flush(yc[ec]);
269 
270 					return false;
271 				}
272 			}
273 		}
274 	}
275 
276 	return true;
277 }
278 
279 static inline
EnsureAcceptHeader(AsioTlsStream & stream,boost::beast::http::request<boost::beast::http::string_body> & request,boost::beast::http::response<boost::beast::http::string_body> & response,boost::asio::yield_context & yc)280 bool EnsureAcceptHeader(
281 	AsioTlsStream& stream,
282 	boost::beast::http::request<boost::beast::http::string_body>& request,
283 	boost::beast::http::response<boost::beast::http::string_body>& response,
284 	boost::asio::yield_context& yc
285 )
286 {
287 	namespace http = boost::beast::http;
288 
289 	if (request.method() != http::verb::get && request[http::field::accept] != "application/json") {
290 		response.result(http::status::bad_request);
291 		response.set(http::field::content_type, "text/html");
292 		response.body() = "<h1>Accept header is missing or not set to 'application/json'.</h1>";
293 		response.content_length(response.body().size());
294 		response.set(http::field::connection, "close");
295 
296 		boost::system::error_code ec;
297 
298 		http::async_write(stream, response, yc[ec]);
299 		stream.async_flush(yc[ec]);
300 
301 		return false;
302 	}
303 
304 	return true;
305 }
306 
307 static inline
EnsureAuthenticatedUser(AsioTlsStream & stream,boost::beast::http::request<boost::beast::http::string_body> & request,ApiUser::Ptr & authenticatedUser,boost::beast::http::response<boost::beast::http::string_body> & response,boost::asio::yield_context & yc)308 bool EnsureAuthenticatedUser(
309 	AsioTlsStream& stream,
310 	boost::beast::http::request<boost::beast::http::string_body>& request,
311 	ApiUser::Ptr& authenticatedUser,
312 	boost::beast::http::response<boost::beast::http::string_body>& response,
313 	boost::asio::yield_context& yc
314 )
315 {
316 	namespace http = boost::beast::http;
317 
318 	if (!authenticatedUser) {
319 		Log(LogWarning, "HttpServerConnection")
320 			<< "Unauthorized request: " << request.method_string() << ' ' << request.target();
321 
322 		response.result(http::status::unauthorized);
323 		response.set(http::field::www_authenticate, "Basic realm=\"Icinga 2\"");
324 		response.set(http::field::connection, "close");
325 
326 		if (request[http::field::accept] == "application/json") {
327 			HttpUtility::SendJsonBody(response, nullptr, new Dictionary({
328 				{ "error", 401 },
329 				{ "status", "Unauthorized. Please check your user credentials." }
330 			}));
331 		} else {
332 			response.set(http::field::content_type, "text/html");
333 			response.body() = "<h1>Unauthorized. Please check your user credentials.</h1>";
334 			response.content_length(response.body().size());
335 		}
336 
337 		boost::system::error_code ec;
338 
339 		http::async_write(stream, response, yc[ec]);
340 		stream.async_flush(yc[ec]);
341 
342 		return false;
343 	}
344 
345 	return true;
346 }
347 
348 static inline
EnsureValidBody(AsioTlsStream & stream,boost::beast::flat_buffer & buf,boost::beast::http::parser<true,boost::beast::http::string_body> & parser,ApiUser::Ptr & authenticatedUser,boost::beast::http::response<boost::beast::http::string_body> & response,bool & shuttingDown,boost::asio::yield_context & yc)349 bool EnsureValidBody(
350 	AsioTlsStream& stream,
351 	boost::beast::flat_buffer& buf,
352 	boost::beast::http::parser<true, boost::beast::http::string_body>& parser,
353 	ApiUser::Ptr& authenticatedUser,
354 	boost::beast::http::response<boost::beast::http::string_body>& response,
355 	bool& shuttingDown,
356 	boost::asio::yield_context& yc
357 )
358 {
359 	namespace http = boost::beast::http;
360 
361 	{
362 		size_t maxSize = 1024 * 1024;
363 		Array::Ptr permissions = authenticatedUser->GetPermissions();
364 
365 		if (permissions) {
366 			CpuBoundWork evalPermissions (yc);
367 
368 			ObjectLock olock(permissions);
369 
370 			for (const Value& permissionInfo : permissions) {
371 				String permission;
372 
373 				if (permissionInfo.IsObjectType<Dictionary>()) {
374 					permission = static_cast<Dictionary::Ptr>(permissionInfo)->Get("permission");
375 				} else {
376 					permission = permissionInfo;
377 				}
378 
379 				static std::vector<std::pair<String, size_t>> specialContentLengthLimits {
380 					 { "config/modify", 512 * 1024 * 1024 }
381 				};
382 
383 				for (const auto& limitInfo : specialContentLengthLimits) {
384 					if (limitInfo.second <= maxSize) {
385 						continue;
386 					}
387 
388 					if (Utility::Match(permission, limitInfo.first)) {
389 						maxSize = limitInfo.second;
390 					}
391 				}
392 			}
393 		}
394 
395 		parser.body_limit(maxSize);
396 	}
397 
398 	if (shuttingDown)
399 		return false;
400 
401 	boost::system::error_code ec;
402 
403 	http::async_read(stream, buf, parser, yc[ec]);
404 
405 	if (ec) {
406 		if (ec == boost::asio::error::operation_aborted)
407 			return false;
408 
409 		/**
410 		 * Unfortunately there's no way to tell an HTTP protocol error
411 		 * from an error on a lower layer:
412 		 *
413 		 * <https://github.com/boostorg/beast/issues/643>
414 		 */
415 
416 		response.result(http::status::bad_request);
417 
418 		if (parser.get()[http::field::accept] == "application/json") {
419 			HttpUtility::SendJsonBody(response, nullptr, new Dictionary({
420 				{ "error", 400 },
421 				{ "status", String("Bad Request: ") + ec.message() }
422 			}));
423 		} else {
424 			response.set(http::field::content_type, "text/html");
425 			response.body() = String("<h1>Bad Request</h1><p><pre>") + ec.message() + "</pre></p>";
426 			response.content_length(response.body().size());
427 		}
428 
429 		response.set(http::field::connection, "close");
430 
431 		http::async_write(stream, response, yc[ec]);
432 		stream.async_flush(yc[ec]);
433 
434 		return false;
435 	}
436 
437 	return true;
438 }
439 
440 static inline
ProcessRequest(AsioTlsStream & stream,boost::beast::http::request<boost::beast::http::string_body> & request,ApiUser::Ptr & authenticatedUser,boost::beast::http::response<boost::beast::http::string_body> & response,HttpServerConnection & server,bool & hasStartedStreaming,boost::asio::yield_context & yc)441 bool ProcessRequest(
442 	AsioTlsStream& stream,
443 	boost::beast::http::request<boost::beast::http::string_body>& request,
444 	ApiUser::Ptr& authenticatedUser,
445 	boost::beast::http::response<boost::beast::http::string_body>& response,
446 	HttpServerConnection& server,
447 	bool& hasStartedStreaming,
448 	boost::asio::yield_context& yc
449 )
450 {
451 	namespace http = boost::beast::http;
452 
453 	try {
454 		CpuBoundWork handlingRequest (yc);
455 
456 		HttpHandler::ProcessRequest(stream, authenticatedUser, request, response, yc, server);
457 	} catch (const std::exception& ex) {
458 		if (hasStartedStreaming) {
459 			return false;
460 		}
461 
462 		auto sysErr (dynamic_cast<const boost::system::system_error*>(&ex));
463 
464 		if (sysErr && sysErr->code() == boost::asio::error::operation_aborted) {
465 			throw;
466 		}
467 
468 		http::response<http::string_body> response;
469 
470 		HttpUtility::SendJsonError(response, nullptr, 500, "Unhandled exception" , DiagnosticInformation(ex));
471 
472 		boost::system::error_code ec;
473 
474 		http::async_write(stream, response, yc[ec]);
475 		stream.async_flush(yc[ec]);
476 
477 		return true;
478 	}
479 
480 	if (hasStartedStreaming) {
481 		return false;
482 	}
483 
484 	boost::system::error_code ec;
485 
486 	http::async_write(stream, response, yc[ec]);
487 	stream.async_flush(yc[ec]);
488 
489 	return true;
490 }
491 
ProcessMessages(boost::asio::yield_context yc)492 void HttpServerConnection::ProcessMessages(boost::asio::yield_context yc)
493 {
494 	namespace beast = boost::beast;
495 	namespace http = beast::http;
496 
497 	try {
498 		/* Do not reset the buffer in the state machine.
499 		 * EnsureValidHeaders already reads from the stream into the buffer,
500 		 * EnsureValidBody continues. ProcessRequest() actually handles the request
501 		 * and needs the full buffer.
502 		 */
503 		beast::flat_buffer buf;
504 
505 		for (;;) {
506 			m_Seen = Utility::GetTime();
507 
508 			http::parser<true, http::string_body> parser;
509 			http::response<http::string_body> response;
510 
511 			parser.header_limit(1024 * 1024);
512 			parser.body_limit(-1);
513 
514 			response.set(http::field::server, l_ServerHeader);
515 
516 			if (!EnsureValidHeaders(*m_Stream, buf, parser, response, m_ShuttingDown, yc)) {
517 				break;
518 			}
519 
520 			m_Seen = Utility::GetTime();
521 
522 			auto& request (parser.get());
523 
524 			{
525 				auto method (http::string_to_verb(request["X-Http-Method-Override"]));
526 
527 				if (method != http::verb::unknown) {
528 					request.method(method);
529 				}
530 			}
531 
532 			HandleExpect100(*m_Stream, request, yc);
533 
534 			auto authenticatedUser (m_ApiUser);
535 
536 			if (!authenticatedUser) {
537 				CpuBoundWork fetchingAuthenticatedUser (yc);
538 
539 				authenticatedUser = ApiUser::GetByAuthHeader(request[http::field::authorization].to_string());
540 			}
541 
542 			Log logMsg (LogInformation, "HttpServerConnection");
543 
544 			logMsg << "Request: " << request.method_string() << ' ' << request.target()
545 				<< " (from " << m_PeerAddress
546 				<< "), user: " << (authenticatedUser ? authenticatedUser->GetName() : "<unauthenticated>")
547 				<< ", agent: " << request[http::field::user_agent]; //operator[] - Returns the value for a field, or "" if it does not exist.
548 
549 			Defer addRespCode ([&response, &logMsg]() {
550 				logMsg << ", status: " << response.result() << ").";
551 			});
552 
553 			if (!HandleAccessControl(*m_Stream, request, response, yc)) {
554 				break;
555 			}
556 
557 			if (!EnsureAcceptHeader(*m_Stream, request, response, yc)) {
558 				break;
559 			}
560 
561 			if (!EnsureAuthenticatedUser(*m_Stream, request, authenticatedUser, response, yc)) {
562 				break;
563 			}
564 
565 			if (!EnsureValidBody(*m_Stream, buf, parser, authenticatedUser, response, m_ShuttingDown, yc)) {
566 				break;
567 			}
568 
569 			m_Seen = std::numeric_limits<decltype(m_Seen)>::max();
570 
571 			if (!ProcessRequest(*m_Stream, request, authenticatedUser, response, *this, m_HasStartedStreaming, yc)) {
572 				break;
573 			}
574 
575 			if (request.version() != 11 || request[http::field::connection] == "close") {
576 				break;
577 			}
578 		}
579 	} catch (const std::exception& ex) {
580 		if (!m_ShuttingDown) {
581 			Log(LogCritical, "HttpServerConnection")
582 				<< "Unhandled exception while processing HTTP request: " << ex.what();
583 		}
584 	}
585 
586 	Disconnect();
587 }
588 
CheckLiveness(boost::asio::yield_context yc)589 void HttpServerConnection::CheckLiveness(boost::asio::yield_context yc)
590 {
591 	boost::system::error_code ec;
592 
593 	for (;;) {
594 		m_CheckLivenessTimer.expires_from_now(boost::posix_time::seconds(5));
595 		m_CheckLivenessTimer.async_wait(yc[ec]);
596 
597 		if (m_ShuttingDown) {
598 			break;
599 		}
600 
601 		if (m_Seen < Utility::GetTime() - 10) {
602 			Log(LogInformation, "HttpServerConnection")
603 				<<  "No messages for HTTP connection have been received in the last 10 seconds.";
604 
605 			Disconnect();
606 			break;
607 		}
608 	}
609 }
610