HttpsServer.cpp 10.7 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

15
16
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/info_parser.hpp>
Micha Mueller's avatar
Micha Mueller committed
17
18
#include <boost/algorithm/string/split.hpp>

19
20
#define LOGH(sev) LOG(sev) << "HttpsServer: "

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

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

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

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

	std::ostringstream data;
34
35

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

	std::string response = "";
	std::string auth_value = "";
	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
85
86
87
88
	//authkey is required in every case
	for (auto& p : queries) {
		if (p.first == "authkey") {
			auth_value = p.second;
			break;
89
		}
90
	}
91

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

	//select code depending on request
	if (method == "GET") {

		//first check permission
		if (!_httpsServer.check_authkey(auth_value, permission::GETReq)) {
103
			LOGH(warning) << "Provided authentication token has insufficient permissions";
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
			connection->set_status(server::connection::unauthorized);
			goto error;
		}

		if (pathStrs[0] == "help") {
			response = "dcdbpusher RESTful API cheatsheet:\n"
					" -GET:  /help     This help message\n"
					"        /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"
119
120
121
					" -PUT:  /[plugin]/[start|stop|reload]\n"
					"                  Start/stop the sensors of the plugin or\n"
					"                  reload the plugin configuration\n"
122
123
124
125
126
					"\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";
		} else if (pathStrs[0] == "plugins") {
127
128
129
130
131
			boost::property_tree::ptree root, plugins;
			for(auto& p : _httpsServer._plugins) {
				plugins.put(p.id, "");
			}
			root.add_child("plugins", plugins);
132
			boost::property_tree::write_info(data, root);
Micha Mueller's avatar
Micha Mueller committed
133
134
		} else {

135
136
			//do some prior checks
			if (pathStrs.size() < 2) {
137
				LOGH(warning) << "Received malformed request: No second path part";
138
139
140
141
142
				connection->set_status(server::connection::bad_request);
				goto error;
			}

			if (pathStrs[1] == "sensors") {
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
				response = "Plugin not found!";
				connection->set_status(server::connection::not_found);

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

						for(auto s : p.configurator->getSensors()) {
							sensors.put(s->getName(), "");
						}
						root.add_child(p.id, sensors);
						boost::property_tree::write_info(data, root);
						response = "";
						connection->set_status(server::connection::ok);
						break;
					}
				}
160
			} else {
Micha Mueller's avatar
Micha Mueller committed
161

162
				if (pathStrs.size() < 3) {
163
					LOGH(warning) << "Received malformed request: No third path part";
Micha Mueller's avatar
Micha Mueller committed
164
					connection->set_status(server::connection::bad_request);
165
166
					goto error;
				}
167

168
				if (pathStrs[2] != "avg") {
169
					LOGH(warning) << "Unknown action " << pathStrs[2] << " requested";
170
171
172
173
174
175
176
177
178
179
180
181
					connection->set_status(server::connection::not_supported);
					goto error;
				}

				std::string sensor = pathStrs[1];
				std::string action = pathStrs[2];
				uint64_t time = 0;

				for (auto& p : queries) {
					if (p.first == "interval") {
						time = getTimestamp();
						time -= S_TO_NS(std::stoul(p.second));
Micha Mueller's avatar
Micha Mueller committed
182
183
184
185
					}
				}

				//process actual request
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
				response = "Plugin not found!";
				connection->set_status(server::connection::not_found);

				for(auto& p : _httpsServer._plugins) {
					if (p.id == pathStrs[0]) {
						response = "Sensor not found!";
						for(auto s : p.configurator->getSensors()) {
							if (s->getName() == sensor) {
								uint64_t avg = 0;
								const reading_t * const cache = s->getCache();
								unsigned size = s->getCacheSize();

								unsigned count = 0;
								for(unsigned i = 0; i < size; i++) {
									if (cache[i].timestamp > time) {
										avg += cache[i].value;
										count++;
Micha Mueller's avatar
Micha Mueller committed
203
204
									}
								}
205
206
207
208
209
210
								if (count > 0)
									avg /= count;

								response = pathStrs[0] + "::" + sensor + " Average of last " + std::to_string(count) + " values is " + std::to_string(avg);
								connection->set_status(server::connection::ok);
								break;
211
212
213
							}
						}
					}
214
215
216
217
				}
			}
		}
	} else if (method == "PUT") {
Micha Mueller's avatar
Micha Mueller committed
218

219
220
		//first check permission
		if (!_httpsServer.check_authkey(auth_value, permission::PUTReq)) {
221
			LOGH(warning) << "Provided authentication token has insufficient permissions";
222
223
224
225
226
			connection->set_status(server::connection::unauthorized);
			goto error;
		}

		if (pathStrs.size() < 2) {
227
			LOGH(warning) << "Received malformed request: No second path part";
228
229
230
231
232
233
234
235
236
237
			connection->set_status(server::connection::bad_request);
			goto error;
		}

		std::string action = pathStrs[1];

		//process actual request
		response = "Plugin not found!";
		connection->set_status(server::connection::not_found);

238
239
240
241
		//switch code depending on selected action
		if (action == "start") {
			for(auto& p : _httpsServer._plugins) {
				if (p.id == pathStrs[0]) {
242
243
244
245
246
					for(auto s : p.configurator->getSensors()) {
						s->startPolling();
					}
					response = "Plugin " + pathStrs[0] + ": Sensors started";
					connection->set_status(server::connection::ok);
247
248
249
250
251
252
					break;
				}
			}
		} else if (action == "stop") {
			for(auto& p : _httpsServer._plugins) {
				if (p.id == pathStrs[0]) {
253
254
255
256
257
					for(auto s : p.configurator->getSensors()) {
						s->stopPolling();
					}
					response = "Plugin " + pathStrs[0] + ": Sensors stopped";
					connection->set_status(server::connection::ok);
258
					break;
259
260
				}
			}
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
		} 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);
					}

					if (p.configurator->reReadConfig()) {
						response = "Plugin " + pathStrs[0] + ": Configuration reloaded";
						connection->set_status(server::connection::ok);
					} else {
						response = "Plugin " + pathStrs[0] + ": Could not reload configuration";
						connection->set_status(server::connection::internal_server_error);
					}

					for(auto s : p.configurator->getSensors()) {
						s->init(_httpsServer._io);
						s->startPolling();
					}

					//continue MQTTPusher
					_httpsServer._mqttPusher->cont();
					break;
				}
			}
		} else {
			LOGH(warning) << "Unknown action " << pathStrs[1] << " requested";
			connection->set_status(server::connection::not_supported);
			goto error;
294
295
296
		}
	}

297
	LOGH(info) << "Responding: " << response;
298
299
300
301
302
	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:
303
	server::response_header headers[] = { {"Connection", "close"}, {"Content-Type", "text/plain"} };
304
305
306
307
308
	connection->set_headers(boost::make_iterator_range(headers, headers + 2));
	connection->write(data.str());
}

void HttpsServer::requestHandler::log(const server::string_type& message) {
309
	LOGH(error) << message;
310
}
311

312
313
HttpsServer::HttpsServer(restAPISettings_t restAPISettings, pluginVector_t& plugins, MQTTPusher* mqttPusher, boost::asio::io_service& io) :
		_plugins(plugins), _mqttPusher(mqttPusher), _io(io), _handler(*this) {
314

315
316
317
	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);

318
	// Set certificate, private key and DH parameters
319
	//ctx->set_password_callback(HttpsServer::password_callback);
320
321
322
	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);
323

324
	server::options options(_handler);
325
	_server = new server(options.address(restAPISettings.restHost).port(restAPISettings.restPort).context(ctx));
326
327
328
329
330
331
}

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

332
333
334
335
336
337
338
339
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;
}

340
341
342
343
344
/*
std::string HttpsServer::password_callback(std::size_t max_length, asio::ssl::context_base::password_purpose purpose) {
    return std::string("pwd");
}
*/