HttpsServer.cpp 16.5 KB
Newer Older
1
2
3
4
5
6
7
8
/*
 * HttpsServer.cpp
 *
 *  Created on: 25.05.2018
 *      Author: Micha Mueller
 */

#include "HttpsServer.h"
9
#include "timestamp.h"
10

11
#include <iostream>
12
13
#include <memory>
#include <functional>
14
#include <string>
15

16
#include <boost/property_tree/ptree.hpp>
Alessio Netti's avatar
Alessio Netti committed
17
#include <boost/property_tree/json_parser.hpp>
Micha Mueller's avatar
Micha Mueller committed
18
19
#include <boost/algorithm/string/split.hpp>

20
21
HttpsServer::requestHandler::requestHandler(HttpsServer& httpsServer) :	_httpsServer(httpsServer) {}

22
void HttpsServer::requestHandler::operator()(server::request const &request, server::connection_ptr connection) {
23
	//first log some info about client
24
25
26
	server::string_type ip = source(request);
	unsigned int port = request.source_port;

27
	LOGH(info) << ip << ":" << port << " connected";
28

29
	//set appropriate default value to connection status
Micha Mueller's avatar
Micha Mueller committed
30
31
32
	connection->set_status(server::connection::internal_server_error);

	std::ostringstream data;
33
34

	boost::network::uri::uri uri("https://" + request.destination);
Micha Mueller's avatar
Micha Mueller committed
35
	server::string_type method = request.method;
36
37
38
39
40
	server::string_type path = uri.path();
	server::string_type query = uri.query();

	std::string response = "";
	std::string auth_value = "";
41
	bool json = false;
42
43
	std::vector<std::string> pathStrs;
	std::vector<std::pair<std::string, std::string>> queries;
44

Micha Mueller's avatar
Micha Mueller committed
45
46
	//first check if request is supported at all
	if (method != "GET" && method != "PUT") {
47
		LOGH(warning) << "Unsupported " << method << " request was made";
Micha Mueller's avatar
Micha Mueller committed
48
		connection->set_status(server::connection::not_supported);
49
50
		goto error;
	}
51

52
	LOGH(info) << method << " request of " << request.destination << " was made";
Micha Mueller's avatar
Micha Mueller committed
53

54
55
56
57
58
59
60
61
	//do some string processing
	//split path into its hierarchical parts
	if (path.size() >= 2) {
		if (path[0] == '/') {
			path.erase(0,1);
		}
		if (path[path.size() -1] == '/') {
			path.erase(path.size() -1);
Micha Mueller's avatar
Micha Mueller committed
62
		}
63

64
65
66
67
		boost::split(pathStrs, path, boost::is_any_of("/"), boost::token_compress_off);
	}

	//split query part into the individual queries (key-value pairs)
Micha Mueller's avatar
Micha Mueller committed
68
69
	{	//do not remove the enclosing brackets
		//need to encapsulate this code block to keep queryStrs local
Micha Mueller's avatar
Micha Mueller committed
70
71
72
73
		std::vector<std::string> queryStrs;
		boost::split(queryStrs, query, boost::is_any_of(";"), boost::token_compress_on);
		for(auto& key : queryStrs) {
			size_t pos = key.find("=");
74
			if (pos != std::string::npos) {
Micha Mueller's avatar
Micha Mueller committed
75
76
77
78
				std::string value;
				value = key.substr(pos+1);
				key.erase(pos);
				queries.push_back(std::make_pair(key, value));
79
80
			}
		}
81
82
	}
	//finished string processing
83

84
	for (auto& p : queries) {
85
		//authkey is required in every case
86
87
		if (p.first == "authkey") {
			auth_value = p.second;
88
89
90
91
		} else if (p.first == "json") {
			if (stoi(p.second) > 0) {
				json = true;
			}
92
		}
93
	}
94

95
	if (pathStrs.size() < 1) {
96
		LOGH(warning) << "Received malformed request: No first path part";
97
98
99
100
101
102
103
104
		connection->set_status(server::connection::bad_request);
		goto error;
	}

	//select code depending on request
	if (method == "GET") {
		if (pathStrs[0] == "help") {
			response = "dcdbpusher RESTful API cheatsheet:\n"
105
					   " -GET:  /help     This help message\n"
Alessio Netti's avatar
Alessio Netti committed
106
107
                       "        /analytics/help\n"
                       "                  An help message for data analytics commands\n"
108
109
110
111
112
113
114
115
116
117
118
					   "        /plugins  List of currently loaded plugins (Discovery)\n"
					   "        /[plugin]/sensors\n"
					   "                  List of currently running sensors which belong\n"
					   "                  to the specified plugin (Discovery)\n"
					   "        /[plugin]/[sensor]/avg?interval=[timeInSec]\n"
					   "                  Average of last sensor readings from the last\n"
					   "                  [interval] seconds or of all cached readings\n"
					   "                  if no interval is given\n"
					   " -PUT:  /[plugin]/[start|stop|reload]\n"
					   "                  Start/stop the sensors of the plugin or\n"
					   "                  reload the plugin configuration\n"
Alessio Netti's avatar
Alessio Netti committed
119
120
121
122
123
					   "\n";
					   //"All resources have to be prepended by host:port and need at\n"
					   //"least the query ?authkey=[token] at the end. Multiple queries\n"
					   //"need to be separated by semicolons(';')\n";
            response += _httpsServer._manager->restCheatSheet;
Micha Mueller's avatar
Micha Mueller committed
124
		} else {
125
126
127
128
			//first check permission
			if (!_httpsServer.check_authkey(auth_value, permission::GETReq)) {
				LOGH(warning) << "Provided authentication token has insufficient permissions";
				connection->set_status(server::connection::unauthorized);
129
130
				goto error;
			}
131

Alessio Netti's avatar
Alessio Netti committed
132
133
134
			//Managing REST GET commands to the data analytics framework
			if(pathStrs[0] == "analytics") {
				try {
Alessio Netti's avatar
Alessio Netti committed
135
136
137
					restResponse_t reply = _httpsServer._manager->REST(pathStrs, queries, method, _httpsServer._io);
					data << reply.data;
					response = reply.response;
Alessio Netti's avatar
Alessio Netti committed
138
139
140
141
142
143
144
145
146
147
148
149
150
				} catch(const std::invalid_argument &e) {
					LOGH(warning) << e.what();
					connection->set_status(server::connection::bad_request);
					goto error;
				} catch(const std::domain_error &e) {
					response = e.what();
					connection->set_status(server::connection::not_found);
				} catch(const std::exception &e) {
					LOGH(warning) << e.what();
					connection->set_status(server::connection::internal_server_error);
					goto error;
				}
			} else if (pathStrs[0] == "plugins") {
151
152
153
154
155
156
				if (json) {
					boost::property_tree::ptree root, plugins;
					for(auto& p : _httpsServer._plugins) {
						plugins.put(p.id, "");
					}
					root.add_child("plugins", plugins);
Alessio Netti's avatar
Alessio Netti committed
157
					boost::property_tree::write_json(data, root, true);
158
159
160
				} else {
					for(auto& p : _httpsServer._plugins) {
						data << p.id << "\n";
161
162
					}
				}
163
			} else {
164
165
166
				//do some prior checks
				if (pathStrs.size() < 2) {
					LOGH(warning) << "Received malformed request: No second path part";
Micha Mueller's avatar
Micha Mueller committed
167
					connection->set_status(server::connection::bad_request);
168
169
					goto error;
				}
170

171
172
173
				if (pathStrs[1] == "sensors") {
					response = "Plugin not found!";
					connection->set_status(server::connection::not_found);
174

175
176
177
178
					for(auto& p : _httpsServer._plugins) {
						if (p.id == pathStrs[0]) {
							if (json) {
								boost::property_tree::ptree root, sensors;
179

180
181
182
183
184
185
186
187
								for(auto g : p.configurator->getSensorGroups()) {
									boost::property_tree::ptree group;
									for(auto s : g->getSensors()) {
										group.put(s->getName(), s->getMqtt());
									}
									sensors.add_child(g->getGroupName(), group);
								}
								root.add_child(p.id, sensors);
Alessio Netti's avatar
Alessio Netti committed
188
								boost::property_tree::write_json(data, root, true);
189
190
191
192
193
194
195
196
197
198
199
							} else {
								for(auto g : p.configurator->getSensorGroups()) {
									for(auto s : g->getSensors()) {
										data << g->getGroupName() << "." << s->getName() << " " << s->getMqtt() << "\n";
									}
								}
							}
							response = "";
							connection->set_status(server::connection::ok);
							break;
						}
Micha Mueller's avatar
Micha Mueller committed
200
					}
201
				} else {
202

203
204
205
206
207
					if (pathStrs.size() < 3) {
						LOGH(warning) << "Received malformed request: No third path part";
						connection->set_status(server::connection::bad_request);
						goto error;
					}
208

209
210
211
212
213
					if (pathStrs[2] != "avg") {
						LOGH(warning) << "Unknown action " << pathStrs[2] << " requested";
						connection->set_status(server::connection::not_supported);
						goto error;
					}
214

Alessio Netti's avatar
Alessio Netti committed
215
					std::string sensor = MQTTChecker::convertTopic(pathStrs[1]);
216
217
					std::string action = pathStrs[2];
					uint64_t time = 0;
218

219
					for (auto& p : queries) {
Alessio Netti's avatar
Alessio Netti committed
220
221
						if (p.first == "interval")
							time = std::stoul(p.second);
222
					}
223

224
					//process actual request
Alessio Netti's avatar
Alessio Netti committed
225
					bool found = false;
226
227
					response = "Plugin not found!";
					connection->set_status(server::connection::not_found);
228

229
230
231
					for(auto& p : _httpsServer._plugins) {
						if (p.id == pathStrs[0]) {
							response = "Sensor not found!";
Michael Ott's avatar
Michael Ott committed
232
233
							for(const auto& g : p.configurator->getSensorGroups()) {
								for(const auto& s : g->getSensors()) {
Alessio Netti's avatar
Alessio Netti committed
234
									if (s->getName() == sensor && s->isInit()) {
Alessio Netti's avatar
Alessio Netti committed
235
										found = true;
Alessio Netti's avatar
Alessio Netti committed
236
237
238
239
240
241
242
243
244
245
246
										uint64_t avg = 0;
										try {
											avg = s->getCache()->getAverage(S_TO_NS(time));
										} catch(const std::exception& e) {
											response = "Unable to compute average: ";
											response += e.what();
											connection->set_status(server::connection::internal_server_error);
											break;
										}
										response = pathStrs[0] + "::" + sensor + " Average of last " +
												   std::to_string(time) + " seconds is " + std::to_string(avg);
247
248
249
										connection->set_status(server::connection::ok);
										break;
									}
250
251
252
								}
							}
						}
253
					}
Alessio Netti's avatar
Alessio Netti committed
254
255
256
257
258
259
260
261
262

					if(!found) {
						for(auto& p : _httpsServer._manager->getPlugins())
							if (p.id == pathStrs[0]) {
								response = "Sensor not found!";
								for(const auto& a : p.configurator->getAnalyzers())
									if(a->getStreaming())
										for(const auto& u : a->getUnits())
											for (const auto& s : u->getBaseOutputs())
Alessio Netti's avatar
Alessio Netti committed
263
												if (s->getName() == sensor && s->isInit()) {
Alessio Netti's avatar
Alessio Netti committed
264
													found = true;
Alessio Netti's avatar
Alessio Netti committed
265
266
267
268
269
270
271
272
273
274
275
													uint64_t avg = 0;
													try {
														avg = s->getCache()->getAverage(S_TO_NS(time));
													} catch(const std::exception& e) {
														response = "Unable to compute average: ";
														response += e.what();
														connection->set_status(server::connection::internal_server_error);
														break;
													}
													response = pathStrs[0] + "::" + sensor + " Average of last " +
															   std::to_string(time) + " seconds is " + std::to_string(avg);
Alessio Netti's avatar
Alessio Netti committed
276
277
278
279
280
													connection->set_status(server::connection::ok);
													break;
												}
							}
					}
281
282
283
284
				}
			}
		}
	} else if (method == "PUT") {
285

286
287
		//first check permission
		if (!_httpsServer.check_authkey(auth_value, permission::PUTReq)) {
288
			LOGH(warning) << "Provided authentication token has insufficient permissions";
289
290
291
			connection->set_status(server::connection::unauthorized);
			goto error;
		}
292

Alessio Netti's avatar
Alessio Netti committed
293
294
295
296
297
298
299
300
		//Managing REST PUT commands to the data analytics framework
		if(pathStrs[0] == "analytics") {
			if( pathStrs.back() == "reload" ) {
				_httpsServer._mqttPusher->halt();
				// Wait until MQTTPusher is paused in order to reload plugins
				while (!_httpsServer._mqttPusher->isHalted()) { sleep(1); }
			}
			try {
Alessio Netti's avatar
Alessio Netti committed
301
302
303
				restResponse_t reply = _httpsServer._manager->REST(pathStrs, queries, method, _httpsServer._io);
				data << reply.data;
				response = reply.response;
Alessio Netti's avatar
Alessio Netti committed
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
			} catch(const std::invalid_argument &e) {
				LOGH(warning) << e.what();
				connection->set_status(server::connection::bad_request);
				goto error;
			} catch(const std::domain_error &e) {
				response = e.what();
				connection->set_status(server::connection::not_found);
			} catch(const std::exception &e) {
				response = e.what();
				connection->set_status(server::connection::internal_server_error);
			}
			// Continue MQTTPusher when a reload was performed
			if( pathStrs.back() == "reload" )
				_httpsServer._mqttPusher->cont();
		} else {

			if (pathStrs.size() < 2) {
				LOGH(warning) << "Received malformed request: No second path part";
				connection->set_status(server::connection::bad_request);
				goto error;
			}
325

Alessio Netti's avatar
Alessio Netti committed
326
			std::string action = pathStrs[1];
327

Alessio Netti's avatar
Alessio Netti committed
328
329
330
			//process actual request
			response = "Plugin not found!";
			connection->set_status(server::connection::not_found);
331

Alessio Netti's avatar
Alessio Netti committed
332
333
334
335
336
337
338
339
340
341
			//switch code depending on selected action
			if (action == "start") {
				for (auto &p : _httpsServer._plugins) {
					if (p.id == pathStrs[0]) {
						for (const auto &g : p.configurator->getSensorGroups()) {
							g->start();
						}
						response = "Plugin " + pathStrs[0] + ": Sensors started";
						connection->set_status(server::connection::ok);
						break;
342
					}
343
				}
Alessio Netti's avatar
Alessio Netti committed
344
345
346
347
348
349
350
351
352
			} else if (action == "stop") {
				for (auto &p : _httpsServer._plugins) {
					if (p.id == pathStrs[0]) {
						for (const auto &g : p.configurator->getSensorGroups()) {
							g->stop();
						}
						response = "Plugin " + pathStrs[0] + ": Sensors stopped";
						connection->set_status(server::connection::ok);
						break;
353
					}
354
				}
Alessio Netti's avatar
Alessio Netti committed
355
356
357
358
359
360
361
362
363
364
			} else if (action == "reload") {
				for (auto &p : _httpsServer._plugins) {
					if (p.id == pathStrs[0]) {
						//before modifying the plugin we need to ensure that we have exclusive access
						//therefore pause the only other concurrent user (MQTTPusher)
						_httpsServer._mqttPusher->halt();
						//wait until MQTTPusher is paused
						while (!_httpsServer._mqttPusher->isHalted()) {
							sleep(1);
						}
Alessio Netti's avatar
Alessio Netti committed
365
366
						// Removing obsolete MQTT topics
						_httpsServer.removeTopics(p);
Alessio Netti's avatar
Alessio Netti committed
367
						if (p.configurator->reReadConfig()) {
Alessio Netti's avatar
Alessio Netti committed
368
369
							// Perform checks on MQTT topics
							if(!_httpsServer.checkTopics(p)) {
Alessio Netti's avatar
Alessio Netti committed
370
								response = "Plugin " + pathStrs[0] + ": problematic MQTT topics or sensor names, please check your config files!";
Alessio Netti's avatar
Alessio Netti committed
371
372
373
374
375
376
377
378
379
380
381
								connection->set_status(server::connection::internal_server_error);
								_httpsServer.removeTopics(p);
								p.configurator->clearConfig();
							} else {
								response = "Plugin " + pathStrs[0] + ": Configuration reloaded";
								connection->set_status(server::connection::ok);
								for (const auto &g : p.configurator->getSensorGroups()) {
									g->init(_httpsServer._io);
									g->start();
								}
							}
Alessio Netti's avatar
Alessio Netti committed
382
383
384
385
						} else {
							response = "Plugin " + pathStrs[0] + ": Could not reload configuration";
							connection->set_status(server::connection::internal_server_error);
						}
386

Alessio Netti's avatar
Alessio Netti committed
387
388
389
390
						//continue MQTTPusher
						_httpsServer._mqttPusher->cont();
						break;
					}
391
				}
Alessio Netti's avatar
Alessio Netti committed
392
393
394
395
			} else {
				LOGH(warning) << "Unknown action " << pathStrs[1] << " requested";
				connection->set_status(server::connection::not_supported);
				goto error;
396
			}
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
		}
		//Updating the SensorNavigator on plugin reloads
		if(pathStrs.back() == "reload") {
			QueryEngine &qEngine = QueryEngine::getInstance();
			std::shared_ptr <SensorNavigator> navigator = std::make_shared<SensorNavigator>();
			vector <std::string> names, topics;
			for (const auto &p : _httpsServer._plugins)
				for (const auto &g : p.configurator->getSensorGroups())
					for (const auto &s : g->getSensors()) {
						names.push_back(s->getName());
						topics.push_back(s->getMqtt());
					}
			navigator->buildTree(qEngine.getSensorHierarchy(), &names, &topics);
			qEngine.setNavigator(navigator);
			qEngine.triggerUpdate();
Alessio Netti's avatar
Alessio Netti committed
412
		}
413
414
	}

415
	LOGH(info) << "Responding: " << response;
416
417
418
419
420
	data << response << std::endl;

	//jump right here if an error was encountered.
	//An empty response will be send while the connections status should be set to an appropriate error state
	error:
421
	server::response_header headers[] = { {"Connection", "close"}, {"Content-Type", "text/plain"} };
422
423
424
425
426
	connection->set_headers(boost::make_iterator_range(headers, headers + 2));
	connection->write(data.str());
}

void HttpsServer::requestHandler::log(const server::string_type& message) {
427
	LOGH(error) << message;
428
}
429

Alessio Netti's avatar
Alessio Netti committed
430
431
HttpsServer::HttpsServer(restAPISettings_t restAPISettings, pluginVector_t& plugins, MQTTPusher* mqttPusher, AnalyticsManager* manager, boost::asio::io_service& io) :
		_plugins(plugins), _mqttPusher(mqttPusher), _manager(manager), _io(io), _handler(*this) {
432

433
434
435
	std::shared_ptr<asio::ssl::context> ctx = std::make_shared<asio::ssl::context>(asio::ssl::context::sslv23);
	ctx->set_options(asio::ssl::context::default_workarounds | asio::ssl::context::no_sslv3 | asio::ssl::context::single_dh_use);

436
	// Set certificate, private key and DH parameters
437
	//ctx->set_password_callback(HttpsServer::password_callback);
438
439
440
	ctx->use_certificate_chain_file(restAPISettings.certificate);
	ctx->use_private_key_file(restAPISettings.privateKey, asio::ssl::context::pem);
	ctx->use_tmp_dh_file(restAPISettings.dhFile);
441

442
	server::options options(_handler);
443
	_server = new server(options.address(restAPISettings.restHost).port(restAPISettings.restPort).context(ctx));
444
445
446
447
448
449
}

HttpsServer::~HttpsServer() {
	delete _server;
}

450
451
452
453
454
455
456
457
bool HttpsServer::check_authkey(const std::string& authkey, permission requiredPerm) {
	authkeyMap_t::iterator it = _authkeys.find(authkey);
	if (it != _authkeys.end()) {
		return it->second[requiredPerm];
	}
	return false;
}

Alessio Netti's avatar
Alessio Netti committed
458
459
void HttpsServer::removeTopics(dl_t p) {
    MQTTChecker& mqttCheck = MQTTChecker::getInstance();
Alessio Netti's avatar
Alessio Netti committed
460
461
462
463
464
465
466
    for(const auto& g : p.configurator->getSensorGroups()) {
    	mqttCheck.removeGroup(g->getGroupName());
		for (const auto &s : g->getSensors()) {
			mqttCheck.removeTopic(s->getMqtt());
			mqttCheck.removeName(s->getName());
		}
	}
Alessio Netti's avatar
Alessio Netti committed
467
468
469
470
471
}

bool HttpsServer::checkTopics(dl_t p) {
    MQTTChecker& mqttCheck = MQTTChecker::getInstance();
    bool validTopics=true;
Alessio Netti's avatar
Alessio Netti committed
472
473
474
475
476
    for(const auto& g : p.configurator->getSensorGroups()) {
		if (!mqttCheck.checkGroup(g->getGroupName()))
			validTopics = false;
		for (const auto &s : g->getSensors())
            if (!mqttCheck.checkTopic(s->getMqtt()) || !mqttCheck.checkName(s->getName()))
Alessio Netti's avatar
Alessio Netti committed
477
                validTopics = false;
Alessio Netti's avatar
Alessio Netti committed
478
	}
Alessio Netti's avatar
Alessio Netti committed
479
480
481
    return validTopics;
}

482
483
484
485
486
/*
std::string HttpsServer::password_callback(std::size_t max_length, asio::ssl::context_base::password_purpose purpose) {
    return std::string("pwd");
}
*/