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