]> granicus.if.org Git - icinga2/blob - lib/remote/httpserverconnection.cpp
2589c9d7db5fe3e088da9f1a98164761fc6d4a8e
[icinga2] / lib / remote / httpserverconnection.cpp
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
37 HttpServerConnection::HttpServerConnection(const String& identity, bool authenticated, const std::shared_ptr<AsioTlsStream>& stream)
38         : HttpServerConnection(identity, authenticated, stream, IoEngine::Get().GetIoContext())
39 {
40 }
41
42 HttpServerConnection::HttpServerConnection(const String& identity, bool authenticated, const std::shared_ptr<AsioTlsStream>& 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
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
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
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
135 bool HttpServerConnection::Disconnected()
136 {
137         return m_ShuttingDown;
138 }
139
140 static inline
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.set(http::field::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
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
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, X-HTTP-Method-Override");
261                                         response.body() = "Preflight OK";
262                                         response.set(http::field::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
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.set(http::field::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
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.set(http::field::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
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.set(http::field::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
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
492 void HttpServerConnection::ProcessMessages(boost::asio::yield_context yc)
493 {
494         namespace beast = boost::beast;
495         namespace http = beast::http;
496
497         try {
498                 beast::flat_buffer buf;
499
500                 for (;;) {
501                         m_Seen = Utility::GetTime();
502
503                         http::parser<true, http::string_body> parser;
504                         http::response<http::string_body> response;
505
506                         parser.header_limit(1024 * 1024);
507                         parser.body_limit(-1);
508
509                         response.set(http::field::server, l_ServerHeader);
510
511                         // Best practice is to always reset the buffer.
512                         buf = {};
513                         if (!EnsureValidHeaders(*m_Stream, buf, parser, response, m_ShuttingDown, yc)) {
514                                 break;
515                         }
516
517                         m_Seen = Utility::GetTime();
518
519                         auto& request (parser.get());
520
521                         {
522                                 auto method (http::string_to_verb(request["X-Http-Method-Override"]));
523
524                                 if (method != http::verb::unknown) {
525                                         request.method(method);
526                                 }
527                         }
528
529                         HandleExpect100(*m_Stream, request, yc);
530
531                         auto authenticatedUser (m_ApiUser);
532
533                         if (!authenticatedUser) {
534                                 CpuBoundWork fetchingAuthenticatedUser (yc);
535
536                                 authenticatedUser = ApiUser::GetByAuthHeader(request[http::field::authorization].to_string());
537                         }
538
539                         Log(LogInformation, "HttpServerConnection")
540                                 << "Request: " << request.method_string() << ' ' << request.target()
541                                 << " (from " << m_PeerAddress
542                                 << "), user: " << (authenticatedUser ? authenticatedUser->GetName() : "<unauthenticated>")
543                                 << ", agent: " << request[http::field::user_agent] << ")."; //operator[] - Returns the value for a field, or "" if it does not exist.
544
545
546                         if (!HandleAccessControl(*m_Stream, request, response, yc)) {
547                                 break;
548                         }
549
550                         if (!EnsureAcceptHeader(*m_Stream, request, response, yc)) {
551                                 break;
552                         }
553
554                         if (!EnsureAuthenticatedUser(*m_Stream, request, authenticatedUser, response, yc)) {
555                                 break;
556                         }
557
558                         // Best practice is to always reset the buffer.
559                         buf = {};
560                         if (!EnsureValidBody(*m_Stream, buf, parser, authenticatedUser, response, m_ShuttingDown, yc)) {
561                                 break;
562                         }
563
564                         m_Seen = std::numeric_limits<decltype(m_Seen)>::max();
565
566                         if (!ProcessRequest(*m_Stream, request, authenticatedUser, response, *this, m_HasStartedStreaming, yc)) {
567                                 break;
568                         }
569
570                         if (request.version() != 11 || request[http::field::connection] == "close") {
571                                 break;
572                         }
573                 }
574         } catch (const std::exception& ex) {
575                 if (!m_ShuttingDown) {
576                         Log(LogCritical, "HttpServerConnection")
577                                 << "Unhandled exception while processing HTTP request: " << ex.what();
578                 }
579         }
580
581         Disconnect();
582 }
583
584 void HttpServerConnection::CheckLiveness(boost::asio::yield_context yc)
585 {
586         boost::system::error_code ec;
587
588         for (;;) {
589                 m_CheckLivenessTimer.expires_from_now(boost::posix_time::seconds(5));
590                 m_CheckLivenessTimer.async_wait(yc[ec]);
591
592                 if (m_ShuttingDown) {
593                         break;
594                 }
595
596                 if (m_Seen < Utility::GetTime() - 10) {
597                         Log(LogInformation, "HttpServerConnection")
598                                 <<  "No messages for HTTP connection have been received in the last 10 seconds.";
599
600                         Disconnect();
601                         break;
602                 }
603         }
604 }